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