001/* 002 * (C) Copyright 2006-2015 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 */ 019 020package org.nuxeo.runtime.test; 021 022import static org.junit.Assert.assertEquals; 023import static org.junit.Assert.assertNotNull; 024import static org.junit.Assert.fail; 025 026import java.io.File; 027import java.io.IOException; 028import java.lang.reflect.InvocationTargetException; 029import java.lang.reflect.Method; 030import java.net.MalformedURLException; 031import java.net.URI; 032import java.net.URISyntaxException; 033import java.net.URL; 034import java.net.URLClassLoader; 035import java.util.ArrayList; 036import java.util.Enumeration; 037import java.util.HashMap; 038import java.util.HashSet; 039import java.util.List; 040import java.util.Map; 041import java.util.Properties; 042import java.util.Set; 043import java.util.jar.Attributes; 044import java.util.jar.JarFile; 045import java.util.jar.Manifest; 046 047import org.apache.commons.io.FileUtils; 048import org.apache.commons.logging.Log; 049import org.apache.commons.logging.LogFactory; 050import org.jmock.Mockery; 051import org.jmock.integration.junit4.JUnit4Mockery; 052import org.junit.After; 053import org.junit.Before; 054import org.junit.Ignore; 055import org.junit.runner.RunWith; 056import org.nuxeo.common.Environment; 057import org.nuxeo.osgi.BundleFile; 058import org.nuxeo.osgi.BundleImpl; 059import org.nuxeo.osgi.DirectoryBundleFile; 060import org.nuxeo.osgi.JarBundleFile; 061import org.nuxeo.osgi.OSGiAdapter; 062import org.nuxeo.osgi.SystemBundle; 063import org.nuxeo.osgi.SystemBundleFile; 064import org.nuxeo.osgi.application.StandaloneBundleLoader; 065import org.nuxeo.runtime.AbstractRuntimeService; 066import org.nuxeo.runtime.api.Framework; 067import org.nuxeo.runtime.model.RuntimeContext; 068import org.nuxeo.runtime.osgi.OSGiRuntimeContext; 069import org.nuxeo.runtime.osgi.OSGiRuntimeService; 070import org.nuxeo.runtime.test.runner.ConditionalIgnoreRule; 071import org.nuxeo.runtime.test.runner.Features; 072import org.nuxeo.runtime.test.runner.FeaturesRunner; 073import org.nuxeo.runtime.test.runner.MDCFeature; 074import org.nuxeo.runtime.test.runner.RandomBug; 075import org.nuxeo.runtime.test.runner.RuntimeHarness; 076import org.nuxeo.runtime.transaction.TransactionHelper; 077import org.osgi.framework.Bundle; 078import org.osgi.framework.FrameworkEvent; 079 080/** 081 * Abstract base class for test cases that require a test runtime service. 082 * <p> 083 * The runtime service itself is conveniently available as the <code>runtime</code> instance variable in derived 084 * classes. 085 * 086 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 087 */ 088// Make sure this class is kept in sync with with RuntimeHarness 089@RunWith(FeaturesRunner.class) 090@Features({ MDCFeature.class, ConditionalIgnoreRule.Feature.class, RandomBug.Feature.class }) 091@Ignore 092public class NXRuntimeTestCase implements RuntimeHarness { 093 094 protected Mockery jmcontext = new JUnit4Mockery(); 095 096 static { 097 // jul to jcl redirection may pose problems (infinite loops) in some 098 // environment 099 // where slf4j to jul, and jcl over slf4j is deployed 100 System.setProperty(AbstractRuntimeService.REDIRECT_JUL, "false"); 101 } 102 103 private static final Log log = LogFactory.getLog(NXRuntimeTestCase.class); 104 105 protected OSGiRuntimeService runtime; 106 107 protected URL[] urls; // classpath urls, used for bundles lookup 108 109 protected File workingDir; 110 111 protected StandaloneBundleLoader bundleLoader; 112 113 private Set<URI> readUris; 114 115 protected Map<String, BundleFile> bundles; 116 117 protected boolean restart = false; 118 119 @Override 120 public boolean isRestart() { 121 return restart; 122 } 123 124 protected OSGiAdapter osgi; 125 126 protected Bundle runtimeBundle; 127 128 protected final List<WorkingDirectoryConfigurator> wdConfigs = new ArrayList<>(); 129 130 protected final TargetResourceLocator targetResourceLocator; 131 132 public NXRuntimeTestCase() { 133 targetResourceLocator = new TargetResourceLocator(this.getClass()); 134 } 135 136 public NXRuntimeTestCase(String name) { 137 this(); 138 } 139 140 public NXRuntimeTestCase(Class<?> clazz) { 141 targetResourceLocator = new TargetResourceLocator(clazz); 142 } 143 144 @Override 145 public void addWorkingDirectoryConfigurator(WorkingDirectoryConfigurator config) { 146 wdConfigs.add(config); 147 } 148 149 @Override 150 public File getWorkingDir() { 151 return workingDir; 152 } 153 154 /** 155 * Restarts the runtime and preserve homes directory. 156 */ 157 @Override 158 public void restart() throws Exception { 159 restart = true; 160 try { 161 tearDown(); 162 setUp(); 163 } finally { 164 restart = false; 165 } 166 } 167 168 @Override 169 public void start() throws Exception { 170 setUp(); 171 } 172 173 @Before 174 public void setUp() throws Exception { 175 System.setProperty("org.nuxeo.runtime.testing", "true"); 176 // super.setUp(); 177 wipeRuntime(); 178 initUrls(); 179 if (urls == null) { 180 throw new UnsupportedOperationException("no bundles available"); 181 } 182 initOsgiRuntime(); 183 } 184 185 /** 186 * Fire the event {@code FrameworkEvent.STARTED}. 187 */ 188 @Override 189 public void fireFrameworkStarted() throws Exception { 190 boolean txStarted = !TransactionHelper.isTransactionActiveOrMarkedRollback() && TransactionHelper.startTransaction(); 191 boolean txFinished = false; 192 try { 193 osgi.fireFrameworkEvent(new FrameworkEvent(FrameworkEvent.STARTED, runtimeBundle, null)); 194 txFinished = true; 195 } finally { 196 if (!txFinished) { 197 TransactionHelper.setTransactionRollbackOnly(); 198 } 199 if (txStarted) { 200 TransactionHelper.commitOrRollbackTransaction(); 201 } 202 } 203 } 204 205 @After 206 public void tearDown() throws Exception { 207 wipeRuntime(); 208 if (workingDir != null) { 209 if (!restart) { 210 if (workingDir.exists() && !FileUtils.deleteQuietly(workingDir)) { 211 log.warn("Cannot delete " + workingDir); 212 } 213 workingDir = null; 214 } 215 } 216 readUris = null; 217 bundles = null; 218 } 219 220 @Override 221 public void stop() throws Exception { 222 tearDown(); 223 } 224 225 @Override 226 public boolean isStarted() { 227 return runtime != null; 228 } 229 230 protected void initOsgiRuntime() throws Exception { 231 try { 232 if (!restart) { 233 Environment.setDefault(null); 234 if (System.getProperties().remove("nuxeo.home") != null) { 235 log.warn("Removed System property nuxeo.home."); 236 } 237 workingDir = File.createTempFile("nxruntime-" + Thread.currentThread().getName() + "-", null, new File( 238 "target")); 239 workingDir.delete(); 240 } 241 } catch (IOException e) { 242 log.error("Could not init working directory", e); 243 throw e; 244 } 245 osgi = new OSGiAdapter(workingDir); 246 BundleFile bf = new SystemBundleFile(workingDir); 247 bundleLoader = new StandaloneBundleLoader(osgi, NXRuntimeTestCase.class.getClassLoader()); 248 SystemBundle systemBundle = new SystemBundle(osgi, bf, bundleLoader.getSharedClassLoader().getLoader()); 249 osgi.setSystemBundle(systemBundle); 250 Thread.currentThread().setContextClassLoader(bundleLoader.getSharedClassLoader().getLoader()); 251 252 for (WorkingDirectoryConfigurator cfg : wdConfigs) { 253 cfg.configure(this, workingDir); 254 } 255 256 bundleLoader.setScanForNestedJARs(false); // for now 257 bundleLoader.setExtractNestedJARs(false); 258 259 BundleFile bundleFile = lookupBundle("org.nuxeo.runtime"); 260 runtimeBundle = new RootRuntimeBundle(osgi, bundleFile, bundleLoader.getClass().getClassLoader(), true); 261 runtimeBundle.start(); 262 263 runtime = handleNewRuntime((OSGiRuntimeService) Framework.getRuntime()); 264 265 assertNotNull(runtime); 266 } 267 268 protected OSGiRuntimeService handleNewRuntime(OSGiRuntimeService aRuntime) { 269 return aRuntime; 270 } 271 272 public static URL[] introspectClasspath(ClassLoader loader) { 273 // normal case 274 if (loader instanceof URLClassLoader) { 275 return ((URLClassLoader) loader).getURLs(); 276 } 277 // surefire suite runner 278 final Class<? extends ClassLoader> loaderClass = loader.getClass(); 279 if (loaderClass.getName().equals("org.apache.tools.ant.AntClassLoader")) { 280 try { 281 Method method = loaderClass.getMethod("getClasspath"); 282 String cp = (String) method.invoke(loader); 283 String[] paths = cp.split(File.pathSeparator); 284 URL[] urls = new URL[paths.length]; 285 for (int i = 0; i < paths.length; i++) { 286 urls[i] = new URL("file:" + paths[i]); 287 } 288 return urls; 289 } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException 290 | InvocationTargetException | MalformedURLException cause) { 291 throw new AssertionError("Cannot introspect mavent class loader", cause); 292 } 293 } 294 // try getURLs method 295 try { 296 Method m = loaderClass.getMethod("getURLs"); 297 return (URL[]) m.invoke(loader); 298 } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException 299 | InvocationTargetException cause) { 300 throw new AssertionError("Unsupported classloader type: " + loaderClass.getName() 301 + "\nWon't be able to load OSGI bundles"); 302 } 303 } 304 305 protected void initUrls() throws Exception { 306 ClassLoader classLoader = NXRuntimeTestCase.class.getClassLoader(); 307 urls = introspectClasspath(classLoader); 308 // special cases such as Surefire with useManifestOnlyJar or Jacoco 309 // Look for nuxeo-runtime 310 boolean found = false; 311 JarFile surefirebooterJar = null; 312 for (URL url : urls) { 313 URI uri = url.toURI(); 314 if (uri.getPath().matches(".*/nuxeo-runtime-[^/]*\\.jar")) { 315 found = true; 316 break; 317 } else if (uri.getScheme().equals("file") && uri.getPath().contains("surefirebooter")) { 318 surefirebooterJar = new JarFile(new File(uri)); 319 } 320 } 321 if (!found && surefirebooterJar != null) { 322 try { 323 String cp = surefirebooterJar.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH); 324 if (cp != null) { 325 String[] cpe = cp.split(" "); 326 URL[] newUrls = new URL[cpe.length]; 327 for (int i = 0; i < cpe.length; i++) { 328 // Don't need to add 'file:' with maven surefire 329 // >= 2.4.2 330 String newUrl = cpe[i].startsWith("file:") ? cpe[i] : "file:" + cpe[i]; 331 newUrls[i] = new URL(newUrl); 332 } 333 urls = newUrls; 334 } 335 } catch (Exception e) { 336 // skip 337 } finally { 338 surefirebooterJar.close(); 339 } 340 } 341 if (log.isDebugEnabled()) { 342 StringBuilder sb = new StringBuilder(); 343 sb.append("URLs on the classpath: "); 344 for (URL url : urls) { 345 sb.append(url.toString()); 346 sb.append('\n'); 347 } 348 log.debug(sb.toString()); 349 } 350 readUris = new HashSet<>(); 351 bundles = new HashMap<>(); 352 } 353 354 /** 355 * Makes sure there is no previous runtime hanging around. 356 * <p> 357 * This happens for instance if a previous test had errors in its <code>setUp()</code>, because 358 * <code>tearDown()</code> has not been called. 359 */ 360 protected void wipeRuntime() throws Exception { 361 // Make sure there is no active runtime (this might happen if an 362 // exception is raised during a previous setUp -> tearDown is not called 363 // afterwards). 364 runtime = null; 365 if (Framework.getRuntime() != null) { 366 Framework.shutdown(); 367 } 368 } 369 370 public static URL getResource(String name) { 371 final ClassLoader loader = Thread.currentThread().getContextClassLoader(); 372 String callerName = Thread.currentThread().getStackTrace()[2].getClassName(); 373 final String relativePath = callerName.replace('.', '/').concat(".class"); 374 final String fullPath = loader.getResource(relativePath).getPath(); 375 final String basePath = fullPath.substring(0, fullPath.indexOf(relativePath)); 376 Enumeration<URL> resources; 377 try { 378 resources = loader.getResources(name); 379 while (resources.hasMoreElements()) { 380 URL resource = resources.nextElement(); 381 if (resource.getPath().startsWith(basePath)) { 382 return resource; 383 } 384 } 385 } catch (IOException e) { 386 return null; 387 } 388 return loader.getResource(name); 389 } 390 391 /** 392 * @deprecated use <code>deployContrib()</code> instead 393 */ 394 @Override 395 @Deprecated 396 public void deploy(String contrib) { 397 deployContrib(contrib); 398 } 399 400 protected void deployContrib(URL url) { 401 assertEquals(runtime, Framework.getRuntime()); 402 log.info("Deploying contribution from " + url.toString()); 403 try { 404 runtime.getContext().deploy(url); 405 } catch (Exception e) { 406 fail("Failed to deploy contrib " + url.toString()); 407 } 408 } 409 410 /** 411 * Deploys a contribution file by looking for it in the class loader. 412 * <p> 413 * The first contribution file found by the class loader will be used. You have no guarantee in case of name 414 * collisions. 415 * 416 * @deprecated use the less ambiguous {@link #deployContrib(String, String)} 417 * @param contrib the relative path to the contribution file 418 */ 419 @Override 420 @Deprecated 421 public void deployContrib(String contrib) { 422 URL url = getResource(contrib); 423 assertNotNull("Test contribution not found: " + contrib, url); 424 deployContrib(url); 425 } 426 427 /** 428 * Deploys a contribution from a given bundle. 429 * <p> 430 * The path will be relative to the bundle root. Example: <code> 431 * deployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml") 432 * </code> 433 * <p> 434 * For compatibility reasons the name of the bundle may be a jar name, but this use is discouraged and deprecated. 435 * 436 * @param name the name of the bundle to peek the contrib in 437 * @param contrib the path to contrib in the bundle. 438 */ 439 @Override 440 public void deployContrib(String name, String contrib) throws Exception { 441 RuntimeContext context = runtime.getContext(name); 442 if (context == null) { 443 context = runtime.getContext(); 444 BundleFile file = lookupBundle(name); 445 URL location = file.getEntry(contrib); 446 if (location == null) { 447 throw new AssertionError("Cannot locate " + contrib + " in " + name); 448 } 449 context.deploy(location); 450 return; 451 } 452 context.deploy(contrib); 453 } 454 455 /** 456 * Deploy an XML contribution from outside a bundle. 457 * <p> 458 * This should be used by tests wiling to deploy test contribution as part of a real bundle. 459 * <p> 460 * The bundle owner is important since the contribution may depend on resources deployed in that bundle. 461 * <p> 462 * Note that the owner bundle MUST be an already deployed bundle. 463 * 464 * @param bundle the bundle that becomes the contribution owner 465 * @param contrib the contribution to deploy as part of the given bundle 466 */ 467 @Override 468 public RuntimeContext deployTestContrib(String bundle, String contrib) throws Exception { 469 URL url = targetResourceLocator.getTargetTestResource(contrib); 470 return deployTestContrib(bundle, url); 471 } 472 473 @Override 474 public RuntimeContext deployTestContrib(String bundle, URL contrib) throws Exception { 475 Bundle b = bundleLoader.getOSGi().getRegistry().getBundle(bundle); 476 if (b == null) { 477 b = osgi.getSystemBundle(); 478 } 479 OSGiRuntimeContext ctx = new OSGiRuntimeContext(runtime, b); 480 ctx.deploy(contrib); 481 return ctx; 482 } 483 484 /** 485 * @deprecated use {@link #undeployContrib(String, String)} instead 486 */ 487 @Override 488 @Deprecated 489 public void undeploy(String contrib) { 490 undeployContrib(contrib); 491 } 492 493 /** 494 * @deprecated use {@link #undeployContrib(String, String)} instead 495 */ 496 @Override 497 @Deprecated 498 public void undeployContrib(String contrib) { 499 URL url = getResource(contrib); 500 assertNotNull("Test contribution not found: " + contrib, url); 501 try { 502 runtime.getContext().undeploy(url); 503 } catch (Exception e) { 504 fail("Failed to undeploy contrib " + url.toString()); 505 } 506 } 507 508 /** 509 * Undeploys a contribution from a given bundle. 510 * <p> 511 * The path will be relative to the bundle root. Example: <code> 512 * undeployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml") 513 * </code> 514 * 515 * @param name the bundle 516 * @param contrib the contribution 517 */ 518 @Override 519 public void undeployContrib(String name, String contrib) throws Exception { 520 RuntimeContext context = runtime.getContext(name); 521 if (context == null) { 522 context = runtime.getContext(); 523 } 524 context.undeploy(contrib); 525 } 526 527 protected static boolean isVersionSuffix(String s) { 528 if (s.length() == 0) { 529 return true; 530 } 531 return s.matches("-(\\d+\\.?)+(-SNAPSHOT)?(\\.\\w+)?"); 532 } 533 534 /** 535 * Resolves an URL for bundle deployment code. 536 * <p> 537 * TODO: Implementation could be finer... 538 * 539 * @return the resolved url 540 */ 541 protected URL lookupBundleUrl(String bundle) { 542 for (URL url : urls) { 543 String[] pathElts = url.getPath().split("/"); 544 for (int i = 0; i < pathElts.length; i++) { 545 if (pathElts[i].startsWith(bundle) && isVersionSuffix(pathElts[i].substring(bundle.length()))) { 546 // we want the main version of the bundle 547 boolean isTestVersion = false; 548 for (int j = i + 1; j < pathElts.length; j++) { 549 // ok for Eclipse (/test) and Maven (/test-classes) 550 if (pathElts[j].startsWith("test")) { 551 isTestVersion = true; 552 break; 553 } 554 } 555 if (!isTestVersion) { 556 log.info("Resolved " + bundle + " as " + url.toString()); 557 return url; 558 } 559 } 560 } 561 } 562 throw new RuntimeException("Could not resolve bundle " + bundle); 563 } 564 565 /** 566 * Deploys a whole OSGI bundle. 567 * <p> 568 * The lookup is first done on symbolic name, as set in <code>MANIFEST.MF</code> and then falls back to the bundle 569 * url (e.g., <code>nuxeo-platform-search-api</code>) for backwards compatibility. 570 * 571 * @param name the symbolic name 572 */ 573 @Override 574 public void deployBundle(String name) throws Exception { 575 // install only if not yet installed 576 BundleImpl bundle = bundleLoader.getOSGi().getRegistry().getBundle(name); 577 if (bundle == null) { 578 BundleFile bundleFile = lookupBundle(name); 579 bundleLoader.loadBundle(bundleFile); 580 bundleLoader.installBundle(bundleFile); 581 bundle = bundleLoader.getOSGi().getRegistry().getBundle(name); 582 } 583 if (runtime.getContext(bundle) == null) { 584 runtime.createContext(bundle); 585 } 586 } 587 588 protected String readSymbolicName(BundleFile bf) { 589 Manifest manifest = bf.getManifest(); 590 if (manifest == null) { 591 return null; 592 } 593 Attributes attrs = manifest.getMainAttributes(); 594 String name = attrs.getValue("Bundle-SymbolicName"); 595 if (name == null) { 596 return null; 597 } 598 String[] sp = name.split(";", 2); 599 return sp[0]; 600 } 601 602 public BundleFile lookupBundle(String bundleName) throws Exception { 603 BundleFile bundleFile = bundles.get(bundleName); 604 if (bundleFile != null) { 605 return bundleFile; 606 } 607 for (URL url : urls) { 608 URI uri = url.toURI(); 609 if (readUris.contains(uri)) { 610 continue; 611 } 612 File file = new File(uri); 613 readUris.add(uri); 614 try { 615 if (file.isDirectory()) { 616 bundleFile = new DirectoryBundleFile(file); 617 } else { 618 bundleFile = new JarBundleFile(file); 619 } 620 } catch (IOException e) { 621 // no manifest => not a bundle 622 continue; 623 } 624 String symbolicName = readSymbolicName(bundleFile); 625 if (symbolicName != null) { 626 log.info(String.format("Bundle '%s' has URL %s", symbolicName, url)); 627 bundles.put(symbolicName, bundleFile); 628 } 629 if (bundleName.equals(symbolicName)) { 630 return bundleFile; 631 } 632 } 633 log.warn(String.format("No bundle with symbolic name '%s'; Falling back to deprecated url lookup scheme", 634 bundleName)); 635 return oldLookupBundle(bundleName); 636 } 637 638 @Deprecated 639 protected BundleFile oldLookupBundle(String bundle) throws Exception { 640 URL url = lookupBundleUrl(bundle); 641 File file = new File(url.toURI()); 642 BundleFile bundleFile; 643 if (file.isDirectory()) { 644 bundleFile = new DirectoryBundleFile(file); 645 } else { 646 bundleFile = new JarBundleFile(file); 647 } 648 log.warn(String.format( 649 "URL-based bundle lookup is deprecated. Please use the symbolic name from MANIFEST (%s) instead", 650 readSymbolicName(bundleFile))); 651 return bundleFile; 652 } 653 654 @Override 655 public void deployFolder(File folder, ClassLoader loader) throws Exception { 656 DirectoryBundleFile bf = new DirectoryBundleFile(folder); 657 BundleImpl bundle = new BundleImpl(osgi, bf, loader); 658 osgi.install(bundle); 659 } 660 661 @Override 662 public Properties getProperties() { 663 return runtime.getProperties(); 664 } 665 666 @Override 667 public RuntimeContext getContext() { 668 return runtime.getContext(); 669 } 670 671 @Override 672 public OSGiAdapter getOSGiAdapter() { 673 return osgi; 674 } 675 676 /* 677 * (non-Javadoc) 678 * @see org.nuxeo.runtime.test.runner.RuntimeHarness#getClassLoaderFiles() 679 */ 680 @Override 681 public List<String> getClassLoaderFiles() throws URISyntaxException { 682 List<String> files = new ArrayList<>(urls.length); 683 for (URL url : urls) { 684 files.add(url.toURI().getPath()); 685 } 686 return files; 687 } 688 689}