001/* 002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Nuxeo - initial API and implementation 018 */ 019package org.nuxeo.runtime.test; 020 021import static org.junit.Assert.assertEquals; 022import static org.junit.Assert.assertNotNull; 023import static org.junit.Assert.fail; 024 025import java.io.File; 026import java.io.IOException; 027import java.net.MalformedURLException; 028import java.net.URI; 029import java.net.URISyntaxException; 030import java.net.URL; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Enumeration; 034import java.util.HashMap; 035import java.util.HashSet; 036import java.util.List; 037import java.util.Map; 038import java.util.Objects; 039import java.util.Properties; 040import java.util.Set; 041import java.util.jar.Attributes; 042import java.util.jar.Manifest; 043import java.util.stream.Collectors; 044import java.util.stream.Stream; 045 046import org.apache.commons.io.FileUtils; 047import org.apache.commons.logging.Log; 048import org.apache.commons.logging.LogFactory; 049import org.jmock.Mockery; 050import org.jmock.integration.junit4.JUnit4Mockery; 051import org.junit.After; 052import org.junit.Before; 053import org.junit.Ignore; 054import org.junit.runner.RunWith; 055import org.nuxeo.common.Environment; 056import org.nuxeo.osgi.BundleFile; 057import org.nuxeo.osgi.BundleImpl; 058import org.nuxeo.osgi.DirectoryBundleFile; 059import org.nuxeo.osgi.JarBundleFile; 060import org.nuxeo.osgi.OSGiAdapter; 061import org.nuxeo.osgi.SystemBundle; 062import org.nuxeo.osgi.SystemBundleFile; 063import org.nuxeo.osgi.application.StandaloneBundleLoader; 064import org.nuxeo.runtime.AbstractRuntimeService; 065import org.nuxeo.runtime.RuntimeServiceException; 066import org.nuxeo.runtime.api.Framework; 067import org.nuxeo.runtime.model.Extension; 068import org.nuxeo.runtime.model.RegistrationInfo; 069import org.nuxeo.runtime.model.RuntimeContext; 070import org.nuxeo.runtime.model.StreamRef; 071import org.nuxeo.runtime.model.URLStreamRef; 072import org.nuxeo.runtime.model.impl.DefaultRuntimeContext; 073import org.nuxeo.runtime.osgi.OSGiRuntimeContext; 074import org.nuxeo.runtime.osgi.OSGiRuntimeService; 075import org.nuxeo.runtime.test.protocols.inline.InlineURLFactory; 076import org.nuxeo.runtime.test.runner.ConditionalIgnoreRule; 077import org.nuxeo.runtime.test.runner.Features; 078import org.nuxeo.runtime.test.runner.FeaturesRunner; 079import org.nuxeo.runtime.test.runner.MDCFeature; 080import org.nuxeo.runtime.test.runner.RandomBug; 081import org.nuxeo.runtime.test.runner.RuntimeHarness; 082import org.nuxeo.runtime.test.runner.TargetExtensions; 083import org.nuxeo.runtime.transaction.TransactionHelper; 084import org.osgi.framework.Bundle; 085import org.osgi.framework.FrameworkEvent; 086 087import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; 088 089/** 090 * Abstract base class for test cases that require a test runtime service. 091 * <p> 092 * The runtime service itself is conveniently available as the <code>runtime</code> instance variable in derived 093 * classes. 094 * 095 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 096 */ 097// Make sure this class is kept in sync with with RuntimeHarness 098@RunWith(FeaturesRunner.class) 099@Features({ MDCFeature.class, ConditionalIgnoreRule.Feature.class, RandomBug.Feature.class }) 100@Ignore 101public class NXRuntimeTestCase implements RuntimeHarness { 102 103 protected Mockery jmcontext = new JUnit4Mockery(); 104 105 static { 106 // jul to jcl redirection may pose problems (infinite loops) in some 107 // environment 108 // where slf4j to jul, and jcl over slf4j is deployed 109 System.setProperty(AbstractRuntimeService.REDIRECT_JUL, "false"); 110 } 111 112 private static final Log log = LogFactory.getLog(NXRuntimeTestCase.class); 113 114 protected OSGiRuntimeService runtime; 115 116 protected URL[] urls; // classpath urls, used for bundles lookup 117 118 protected File workingDir; 119 120 protected StandaloneBundleLoader bundleLoader; 121 122 private Set<URI> readUris; 123 124 protected Map<String, BundleFile> bundles; 125 126 protected boolean restart = false; 127 128 @Override 129 public boolean isRestart() { 130 return restart; 131 } 132 133 protected OSGiAdapter osgi; 134 135 protected Bundle runtimeBundle; 136 137 protected final List<WorkingDirectoryConfigurator> wdConfigs = new ArrayList<>(); 138 139 protected final TargetResourceLocator targetResourceLocator; 140 141 public NXRuntimeTestCase() { 142 targetResourceLocator = new TargetResourceLocator(this.getClass()); 143 } 144 145 public NXRuntimeTestCase(String name) { 146 this(); 147 } 148 149 public NXRuntimeTestCase(Class<?> clazz) { 150 targetResourceLocator = new TargetResourceLocator(clazz); 151 } 152 153 @Override 154 public void addWorkingDirectoryConfigurator(WorkingDirectoryConfigurator config) { 155 wdConfigs.add(config); 156 } 157 158 @Override 159 public File getWorkingDir() { 160 return workingDir; 161 } 162 163 /** 164 * Restarts the runtime and preserve homes directory. 165 */ 166 @Override 167 public void restart() throws Exception { 168 restart = true; 169 try { 170 tearDown(); 171 setUp(); 172 } finally { 173 restart = false; 174 } 175 } 176 177 @Override 178 public void start() throws Exception { 179 setUp(); 180 } 181 182 @Before 183 public void setUp() throws Exception { 184 System.setProperty("org.nuxeo.runtime.testing", "true"); 185 // super.setUp(); 186 wipeRuntime(); 187 initUrls(); 188 if (urls == null) { 189 throw new UnsupportedOperationException("no bundles available"); 190 } 191 initOsgiRuntime(); 192 } 193 194 /** 195 * Fire the event {@code FrameworkEvent.STARTED}. 196 */ 197 @Override 198 public void fireFrameworkStarted() throws Exception { 199 boolean txStarted = !TransactionHelper.isTransactionActiveOrMarkedRollback() 200 && TransactionHelper.startTransaction(); 201 boolean txFinished = false; 202 try { 203 osgi.fireFrameworkEvent(new FrameworkEvent(FrameworkEvent.STARTED, runtimeBundle, null)); 204 txFinished = true; 205 } finally { 206 if (!txFinished) { 207 TransactionHelper.setTransactionRollbackOnly(); 208 } 209 if (txStarted) { 210 TransactionHelper.commitOrRollbackTransaction(); 211 } 212 } 213 } 214 215 @After 216 public void tearDown() throws Exception { 217 wipeRuntime(); 218 if (workingDir != null) { 219 if (!restart) { 220 if (workingDir.exists() && !FileUtils.deleteQuietly(workingDir)) { 221 log.warn("Cannot delete " + workingDir); 222 } 223 workingDir = null; 224 } 225 } 226 readUris = null; 227 bundles = null; 228 } 229 230 @Override 231 public void stop() throws Exception { 232 tearDown(); 233 } 234 235 @Override 236 public boolean isStarted() { 237 return runtime != null; 238 } 239 240 protected void initOsgiRuntime() throws Exception { 241 try { 242 if (!restart) { 243 Environment.setDefault(null); 244 if (System.getProperties().remove("nuxeo.home") != null) { 245 log.warn("Removed System property nuxeo.home."); 246 } 247 workingDir = File.createTempFile("nxruntime-" + Thread.currentThread().getName() + "-", null, 248 new File("target")); 249 workingDir.delete(); 250 } 251 } catch (IOException e) { 252 log.error("Could not init working directory", e); 253 throw e; 254 } 255 osgi = new OSGiAdapter(workingDir); 256 BundleFile bf = new SystemBundleFile(workingDir); 257 bundleLoader = new StandaloneBundleLoader(osgi, NXRuntimeTestCase.class.getClassLoader()); 258 SystemBundle systemBundle = new SystemBundle(osgi, bf, bundleLoader.getSharedClassLoader().getLoader()); 259 osgi.setSystemBundle(systemBundle); 260 Thread.currentThread().setContextClassLoader(bundleLoader.getSharedClassLoader().getLoader()); 261 262 for (WorkingDirectoryConfigurator cfg : wdConfigs) { 263 cfg.configure(this, workingDir); 264 } 265 266 bundleLoader.setScanForNestedJARs(false); // for now 267 bundleLoader.setExtractNestedJARs(false); 268 269 BundleFile bundleFile = lookupBundle("org.nuxeo.runtime"); 270 runtimeBundle = new RootRuntimeBundle(osgi, bundleFile, bundleLoader.getClass().getClassLoader(), true); 271 runtimeBundle.start(); 272 273 runtime = handleNewRuntime((OSGiRuntimeService) Framework.getRuntime()); 274 275 assertNotNull(runtime); 276 } 277 278 protected OSGiRuntimeService handleNewRuntime(OSGiRuntimeService aRuntime) { 279 return aRuntime; 280 } 281 282 public static URL[] introspectClasspath(ClassLoader loader) { 283 return new FastClasspathScanner().getUniqueClasspathElements().stream().map(file -> { 284 try { 285 return file.toURI().toURL(); 286 } catch (MalformedURLException cause) { 287 throw new Error("Could not get URL from " + file, cause); 288 } 289 }).toArray(URL[]::new); 290 } 291 292 protected void initUrls() throws Exception { 293 ClassLoader classLoader = NXRuntimeTestCase.class.getClassLoader(); 294 urls = introspectClasspath(classLoader); 295 if (log.isDebugEnabled()) { 296 StringBuilder sb = new StringBuilder(); 297 sb.append("URLs on the classpath: "); 298 for (URL url : urls) { 299 sb.append(url.toString()); 300 sb.append('\n'); 301 } 302 log.debug(sb.toString()); 303 } 304 readUris = new HashSet<>(); 305 bundles = new HashMap<>(); 306 } 307 308 /** 309 * Makes sure there is no previous runtime hanging around. 310 * <p> 311 * This happens for instance if a previous test had errors in its <code>setUp()</code>, because 312 * <code>tearDown()</code> has not been called. 313 */ 314 protected void wipeRuntime() throws Exception { 315 // Make sure there is no active runtime (this might happen if an 316 // exception is raised during a previous setUp -> tearDown is not called 317 // afterwards). 318 runtime = null; 319 if (Framework.getRuntime() != null) { 320 Framework.shutdown(); 321 } 322 } 323 324 public static URL getResource(String name) { 325 final ClassLoader loader = Thread.currentThread().getContextClassLoader(); 326 String callerName = Thread.currentThread().getStackTrace()[2].getClassName(); 327 final String relativePath = callerName.replace('.', '/').concat(".class"); 328 final String fullPath = loader.getResource(relativePath).getPath(); 329 final String basePath = fullPath.substring(0, fullPath.indexOf(relativePath)); 330 Enumeration<URL> resources; 331 try { 332 resources = loader.getResources(name); 333 while (resources.hasMoreElements()) { 334 URL resource = resources.nextElement(); 335 if (resource.getPath().startsWith(basePath)) { 336 return resource; 337 } 338 } 339 } catch (IOException e) { 340 return null; 341 } 342 return loader.getResource(name); 343 } 344 345 protected void deployContrib(URL url) { 346 assertEquals(runtime, Framework.getRuntime()); 347 log.info("Deploying contribution from " + url.toString()); 348 try { 349 runtime.getContext().deploy(url); 350 } catch (Exception e) { 351 fail("Failed to deploy contrib " + url.toString()); 352 } 353 } 354 355 /** 356 * Deploys a contribution from a given bundle. 357 * <p> 358 * The path will be relative to the bundle root. Example: <code> 359 * deployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml") 360 * </code> 361 * <p> 362 * For compatibility reasons the name of the bundle may be a jar name, but this use is discouraged and deprecated. 363 * 364 * @param name the name of the bundle to peek the contrib in 365 * @param contrib the path to contrib in the bundle. 366 */ 367 @Override 368 public void deployContrib(String name, String contrib) throws Exception { 369 RuntimeContext context = runtime.getContext(name); 370 if (context == null) { 371 context = runtime.getContext(); 372 BundleFile file = lookupBundle(name); 373 URL location = file.getEntry(contrib); 374 if (location == null) { 375 throw new AssertionError("Cannot locate " + contrib + " in " + name); 376 } 377 context.deploy(location); 378 return; 379 } 380 context.deploy(contrib); 381 } 382 383 /** 384 * Deploy an XML contribution from outside a bundle. 385 * <p> 386 * This should be used by tests wiling to deploy test contribution as part of a real bundle. 387 * <p> 388 * The bundle owner is important since the contribution may depend on resources deployed in that bundle. 389 * <p> 390 * Note that the owner bundle MUST be an already deployed bundle. 391 * 392 * @param bundle the bundle that becomes the contribution owner 393 * @param contrib the contribution to deploy as part of the given bundle 394 */ 395 @Override 396 public RuntimeContext deployTestContrib(String bundle, String contrib) throws Exception { 397 URL url = targetResourceLocator.getTargetTestResource(contrib); 398 return deployTestContrib(bundle, url); 399 } 400 401 @Override 402 public RuntimeContext deployTestContrib(String bundle, URL contrib) throws Exception { 403 Bundle b = bundleLoader.getOSGi().getRegistry().getBundle(bundle); 404 if (b == null) { 405 b = osgi.getSystemBundle(); 406 } 407 OSGiRuntimeContext ctx = new OSGiRuntimeContext(runtime, b); 408 ctx.deploy(contrib); 409 return ctx; 410 } 411 412 @Override 413 public RuntimeContext deployPartial(String name, Set<TargetExtensions> targetExtensions) throws Exception { 414 // Do not install bundle; we only need the Object to list his components 415 Bundle bundle = new BundleImpl(osgi, lookupBundle(name), null); 416 417 RuntimeContext ctx = new OSGiRuntimeContext(runtime, bundle); 418 listBundleComponents(bundle).map(URLStreamRef::new).forEach(component -> { 419 try { 420 this.deployPartialComponent(ctx, targetExtensions, component); 421 } catch (IOException e) { 422 log.error("PartialBundle: " + name + " failed to load: " + component, e); 423 } 424 }); 425 return ctx; 426 } 427 428 /** 429 * Read a component from his StreamRef and create a new component (suffixed with `-partial`, and the base component 430 * name aliased) with only matching contributions of the extensionPoints parameter. 431 * 432 * @param ctx RuntimeContext in which the new component will be deployed 433 * @param extensionPoints Set of white listed TargetExtensions 434 * @param component Reference to the original component 435 */ 436 protected void deployPartialComponent(RuntimeContext ctx, Set<TargetExtensions> extensionPoints, 437 StreamRef component) throws IOException { 438 RegistrationInfo ri = ((DefaultRuntimeContext) ctx).createRegistrationInfo(component); 439 String name = ri.getName().getName() + "-partial"; 440 441 // Flatten Target Extension Points 442 Set<String> targets = extensionPoints.stream() 443 .map(TargetExtensions::getTargetExtensions) 444 .flatMap(Set::stream) 445 .collect(Collectors.toSet()); 446 447 String ext = Arrays.stream(ri.getExtensions()) 448 .filter(e -> targets.contains(TargetExtensions.newTargetExtension( 449 e.getTargetComponent().getName(), e.getExtensionPoint()))) 450 .map(Extension::toXML) 451 .collect(Collectors.joining()); 452 453 InlineURLFactory.install(); 454 ctx.deploy(new InlineRef(name, String.format("<component name=\"%s\">%s</component>", name, ext))); 455 } 456 457 /** 458 * Listing component's urls of a bundle. Inspired from org.nuxeo.runtime.osgi.OSGiRuntimeService#loadComponents but 459 * without deploying anything. 460 * 461 * @param bundle Bundle to be read 462 */ 463 protected Stream<URL> listBundleComponents(Bundle bundle) { 464 String list = OSGiRuntimeService.getComponentsList(bundle); 465 String name = bundle.getSymbolicName(); 466 log.debug("PartialBundle: " + name + " components: " + list); 467 if (list == null) { 468 return null; 469 } 470 471 return Arrays.stream(list.split("[, \t\n\r\f]")).map(s -> bundle.getEntry((String) s)).filter(Objects::nonNull); 472 } 473 474 /** 475 * Undeploys a contribution from a given bundle. 476 * <p> 477 * The path will be relative to the bundle root. Example: <code> 478 * undeployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml") 479 * </code> 480 * 481 * @param name the bundle 482 * @param contrib the contribution 483 */ 484 @Override 485 public void undeployContrib(String name, String contrib) throws Exception { 486 RuntimeContext context = runtime.getContext(name); 487 if (context == null) { 488 context = runtime.getContext(); 489 } 490 context.undeploy(contrib); 491 } 492 493 protected static boolean isVersionSuffix(String s) { 494 if (s.length() == 0) { 495 return true; 496 } 497 return s.matches("-(\\d+\\.?)+(-SNAPSHOT)?(\\.\\w+)?"); 498 } 499 500 /** 501 * Resolves an URL for bundle deployment code. 502 * <p> 503 * TODO: Implementation could be finer... 504 * 505 * @return the resolved url 506 */ 507 protected URL lookupBundleUrl(String bundle) { 508 for (URL url : urls) { 509 String[] pathElts = url.getPath().split("/"); 510 for (int i = 0; i < pathElts.length; i++) { 511 if (pathElts[i].startsWith(bundle) && isVersionSuffix(pathElts[i].substring(bundle.length()))) { 512 // we want the main version of the bundle 513 boolean isTestVersion = false; 514 for (int j = i + 1; j < pathElts.length; j++) { 515 // ok for Eclipse (/test) and Maven (/test-classes) 516 if (pathElts[j].startsWith("test")) { 517 isTestVersion = true; 518 break; 519 } 520 } 521 if (!isTestVersion) { 522 log.info("Resolved " + bundle + " as " + url.toString()); 523 return url; 524 } 525 } 526 } 527 } 528 throw new RuntimeException("Could not resolve bundle " + bundle); 529 } 530 531 /** 532 * Deploys a whole OSGI bundle. 533 * <p> 534 * The lookup is first done on symbolic name, as set in <code>MANIFEST.MF</code> and then falls back to the bundle 535 * url (e.g., <code>nuxeo-platform-search-api</code>) for backwards compatibility. 536 * 537 * @param name the symbolic name 538 */ 539 @Override 540 public void deployBundle(String name) throws Exception { 541 // install only if not yet installed 542 Bundle bundle = bundleLoader.getOSGi().getRegistry().getBundle(name); 543 if (bundle == null) { 544 BundleFile bundleFile = lookupBundle(name); 545 bundleLoader.loadBundle(bundleFile); 546 bundleLoader.installBundle(bundleFile); 547 bundle = bundleLoader.getOSGi().getRegistry().getBundle(name); 548 } 549 if (runtime.getContext(bundle) == null) { 550 runtime.createContext(bundle); 551 } 552 } 553 554 protected String readSymbolicName(BundleFile bf) { 555 Manifest manifest = bf.getManifest(); 556 if (manifest == null) { 557 return null; 558 } 559 Attributes attrs = manifest.getMainAttributes(); 560 String name = attrs.getValue("Bundle-SymbolicName"); 561 if (name == null) { 562 return null; 563 } 564 String[] sp = name.split(";", 2); 565 return sp[0]; 566 } 567 568 public BundleFile lookupBundle(String bundleName) throws Exception { 569 BundleFile bundleFile = bundles.get(bundleName); 570 if (bundleFile != null) { 571 return bundleFile; 572 } 573 for (URL url : urls) { 574 URI uri = url.toURI(); 575 if (readUris.contains(uri)) { 576 continue; 577 } 578 File file = new File(uri); 579 readUris.add(uri); 580 try { 581 if (file.isDirectory()) { 582 bundleFile = new DirectoryBundleFile(file); 583 } else { 584 bundleFile = new JarBundleFile(file); 585 } 586 } catch (IOException e) { 587 // no manifest => not a bundle 588 continue; 589 } 590 String symbolicName = readSymbolicName(bundleFile); 591 if (symbolicName != null) { 592 log.info(String.format("Bundle '%s' has URL %s", symbolicName, url)); 593 bundles.put(symbolicName, bundleFile); 594 } 595 if (bundleName.equals(symbolicName)) { 596 return bundleFile; 597 } 598 } 599 throw new RuntimeServiceException(String.format("No bundle with symbolic name '%s';", bundleName)); 600 } 601 602 @Override 603 public void deployFolder(File folder, ClassLoader loader) throws Exception { 604 DirectoryBundleFile bf = new DirectoryBundleFile(folder); 605 BundleImpl bundle = new BundleImpl(osgi, bf, loader); 606 osgi.install(bundle); 607 } 608 609 @Override 610 public Properties getProperties() { 611 return runtime.getProperties(); 612 } 613 614 @Override 615 public RuntimeContext getContext() { 616 return runtime.getContext(); 617 } 618 619 @Override 620 public OSGiAdapter getOSGiAdapter() { 621 return osgi; 622 } 623 624 /* 625 * (non-Javadoc) 626 * @see org.nuxeo.runtime.test.runner.RuntimeHarness#getClassLoaderFiles() 627 */ 628 @Override 629 public List<String> getClassLoaderFiles() throws URISyntaxException { 630 List<String> files = new ArrayList<>(urls.length); 631 for (URL url : urls) { 632 files.add(url.toURI().getPath()); 633 } 634 return files; 635 } 636 637}