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