/* * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Nuxeo - initial API and implementation */ package org.nuxeo.runtime.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jmock.Mockery; import org.jmock.integration.junit4.JUnit4Mockery; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.runner.RunWith; import org.nuxeo.common.Environment; import org.nuxeo.osgi.BundleFile; import org.nuxeo.osgi.BundleImpl; import org.nuxeo.osgi.DirectoryBundleFile; import org.nuxeo.osgi.JarBundleFile; import org.nuxeo.osgi.OSGiAdapter; import org.nuxeo.osgi.SystemBundle; import org.nuxeo.osgi.SystemBundleFile; import org.nuxeo.osgi.application.StandaloneBundleLoader; import org.nuxeo.runtime.AbstractRuntimeService; import org.nuxeo.runtime.RuntimeServiceException; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.Extension; import org.nuxeo.runtime.model.RegistrationInfo; import org.nuxeo.runtime.model.RuntimeContext; import org.nuxeo.runtime.model.StreamRef; import org.nuxeo.runtime.model.URLStreamRef; import org.nuxeo.runtime.model.impl.DefaultRuntimeContext; import org.nuxeo.runtime.osgi.OSGiRuntimeContext; import org.nuxeo.runtime.osgi.OSGiRuntimeService; import org.nuxeo.runtime.test.protocols.inline.InlineURLFactory; import org.nuxeo.runtime.test.runner.ConditionalIgnoreRule; import org.nuxeo.runtime.test.runner.Features; import org.nuxeo.runtime.test.runner.FeaturesRunner; import org.nuxeo.runtime.test.runner.MDCFeature; import org.nuxeo.runtime.test.runner.RandomBug; import org.nuxeo.runtime.test.runner.RuntimeHarness; import org.nuxeo.runtime.test.runner.TargetExtensions; import org.nuxeo.runtime.transaction.TransactionHelper; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkEvent; import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; /** * Abstract base class for test cases that require a test runtime service. * <p> * The runtime service itself is conveniently available as the <code>runtime</code> instance variable in derived * classes. * * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> */ // Make sure this class is kept in sync with with RuntimeHarness @RunWith(FeaturesRunner.class) @Features({ MDCFeature.class, ConditionalIgnoreRule.Feature.class, RandomBug.Feature.class }) @Ignore public class NXRuntimeTestCase implements RuntimeHarness { protected Mockery jmcontext = new JUnit4Mockery(); static { // jul to jcl redirection may pose problems (infinite loops) in some // environment // where slf4j to jul, and jcl over slf4j is deployed System.setProperty(AbstractRuntimeService.REDIRECT_JUL, "false"); } private static final Log log = LogFactory.getLog(NXRuntimeTestCase.class); protected OSGiRuntimeService runtime; protected URL[] urls; // classpath urls, used for bundles lookup protected File workingDir; protected StandaloneBundleLoader bundleLoader; private Set<URI> readUris; protected Map<String, BundleFile> bundles; protected boolean restart = false; @Override public boolean isRestart() { return restart; } protected OSGiAdapter osgi; protected Bundle runtimeBundle; protected final List<WorkingDirectoryConfigurator> wdConfigs = new ArrayList<>(); protected final TargetResourceLocator targetResourceLocator; public NXRuntimeTestCase() { targetResourceLocator = new TargetResourceLocator(this.getClass()); } public NXRuntimeTestCase(String name) { this(); } public NXRuntimeTestCase(Class<?> clazz) { targetResourceLocator = new TargetResourceLocator(clazz); } @Override public void addWorkingDirectoryConfigurator(WorkingDirectoryConfigurator config) { wdConfigs.add(config); } @Override public File getWorkingDir() { return workingDir; } /** * Restarts the runtime and preserve homes directory. */ @Override public void restart() throws Exception { restart = true; try { tearDown(); setUp(); } finally { restart = false; } } @Override public void start() throws Exception { setUp(); } @Before public void setUp() throws Exception { System.setProperty("org.nuxeo.runtime.testing", "true"); // super.setUp(); wipeRuntime(); initUrls(); if (urls == null) { throw new UnsupportedOperationException("no bundles available"); } initOsgiRuntime(); } @Override public void fireFrameworkStarted() throws Exception { boolean txStarted = !TransactionHelper.isTransactionActiveOrMarkedRollback() && TransactionHelper.startTransaction(); boolean txFinished = false; try { osgi.fireFrameworkEvent(new FrameworkEvent(FrameworkEvent.STARTED, runtimeBundle, null)); txFinished = true; } finally { if (!txFinished) { TransactionHelper.setTransactionRollbackOnly(); } if (txStarted) { TransactionHelper.commitOrRollbackTransaction(); } } } @Override public void standby(Duration delay) throws Exception { Framework.getRuntime().standby(Instant.now().plus(delay)); } @After public void tearDown() throws Exception { wipeRuntime(); if (workingDir != null) { if (!restart) { if (workingDir.exists() && !FileUtils.deleteQuietly(workingDir)) { log.warn("Cannot delete " + workingDir); } workingDir = null; } } readUris = null; bundles = null; } @Override public void stop() throws Exception { tearDown(); } @Override public boolean isStarted() { return runtime != null; } protected void initOsgiRuntime() throws Exception { try { if (!restart) { Environment.setDefault(null); if (System.getProperties().remove("nuxeo.home") != null) { log.warn("Removed System property nuxeo.home."); } workingDir = File.createTempFile("nxruntime-" + Thread.currentThread().getName() + "-", null, new File("target")); workingDir.delete(); } } catch (IOException e) { log.error("Could not init working directory", e); throw e; } osgi = new OSGiAdapter(workingDir); BundleFile bf = new SystemBundleFile(workingDir); bundleLoader = new StandaloneBundleLoader(osgi, NXRuntimeTestCase.class.getClassLoader()); SystemBundle systemBundle = new SystemBundle(osgi, bf, bundleLoader.getSharedClassLoader().getLoader()); osgi.setSystemBundle(systemBundle); Thread.currentThread().setContextClassLoader(bundleLoader.getSharedClassLoader().getLoader()); for (WorkingDirectoryConfigurator cfg : wdConfigs) { cfg.configure(this, workingDir); } bundleLoader.setScanForNestedJARs(false); // for now bundleLoader.setExtractNestedJARs(false); BundleFile bundleFile = lookupBundle("org.nuxeo.runtime"); runtimeBundle = new RootRuntimeBundle(osgi, bundleFile, bundleLoader.getClass().getClassLoader(), true); runtimeBundle.start(); runtime = handleNewRuntime((OSGiRuntimeService) Framework.getRuntime()); assertNotNull(runtime); } protected OSGiRuntimeService handleNewRuntime(OSGiRuntimeService aRuntime) { return aRuntime; } public static URL[] introspectClasspath(ClassLoader loader) { return new FastClasspathScanner().getUniqueClasspathElements().stream().map(file -> { try { return file.toURI().toURL(); } catch (MalformedURLException cause) { throw new Error("Could not get URL from " + file, cause); } }).toArray(URL[]::new); } protected void initUrls() throws Exception { ClassLoader classLoader = NXRuntimeTestCase.class.getClassLoader(); urls = introspectClasspath(classLoader); if (log.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append("URLs on the classpath: "); for (URL url : urls) { sb.append(url.toString()); sb.append('\n'); } log.debug(sb.toString()); } readUris = new HashSet<>(); bundles = new HashMap<>(); } /** * Makes sure there is no previous runtime hanging around. * <p> * This happens for instance if a previous test had errors in its <code>setUp()</code>, because * <code>tearDown()</code> has not been called. */ protected void wipeRuntime() throws Exception { // Make sure there is no active runtime (this might happen if an // exception is raised during a previous setUp -> tearDown is not called // afterwards). runtime = null; if (Framework.getRuntime() != null) { Framework.shutdown(); } } public static URL getResource(String name) { final ClassLoader loader = Thread.currentThread().getContextClassLoader(); String callerName = Thread.currentThread().getStackTrace()[2].getClassName(); final String relativePath = callerName.replace('.', '/').concat(".class"); final String fullPath = loader.getResource(relativePath).getPath(); final String basePath = fullPath.substring(0, fullPath.indexOf(relativePath)); Enumeration<URL> resources; try { resources = loader.getResources(name); while (resources.hasMoreElements()) { URL resource = resources.nextElement(); if (resource.getPath().startsWith(basePath)) { return resource; } } } catch (IOException e) { return null; } return loader.getResource(name); } protected void deployContrib(URL url) { assertEquals(runtime, Framework.getRuntime()); log.info("Deploying contribution from " + url.toString()); try { runtime.getContext().deploy(url); } catch (Exception e) { fail("Failed to deploy contrib " + url.toString()); } } /** * Deploys a contribution from a given bundle. * <p> * The path will be relative to the bundle root. Example: <code> * deployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml") * </code> * <p> * For compatibility reasons the name of the bundle may be a jar name, but this use is discouraged and deprecated. * * @param name the name of the bundle to peek the contrib in * @param contrib the path to contrib in the bundle. */ @Override public void deployContrib(String name, String contrib) throws Exception { RuntimeContext context = runtime.getContext(name); if (context == null) { context = runtime.getContext(); BundleFile file = lookupBundle(name); URL location = file.getEntry(contrib); if (location == null) { throw new AssertionError("Cannot locate " + contrib + " in " + name); } context.deploy(location); return; } context.deploy(contrib); } /** * Deploy an XML contribution from outside a bundle. * <p> * This should be used by tests wiling to deploy test contribution as part of a real bundle. * <p> * The bundle owner is important since the contribution may depend on resources deployed in that bundle. * <p> * Note that the owner bundle MUST be an already deployed bundle. * * @param bundle the bundle that becomes the contribution owner * @param contrib the contribution to deploy as part of the given bundle */ @Override public RuntimeContext deployTestContrib(String bundle, String contrib) throws Exception { URL url = targetResourceLocator.getTargetTestResource(contrib); return deployTestContrib(bundle, url); } @Override public RuntimeContext deployTestContrib(String bundle, URL contrib) throws Exception { Bundle b = bundleLoader.getOSGi().getRegistry().getBundle(bundle); if (b == null) { b = osgi.getSystemBundle(); } OSGiRuntimeContext ctx = new OSGiRuntimeContext(runtime, b); ctx.deploy(contrib); return ctx; } @Override public RuntimeContext deployPartial(String name, Set<TargetExtensions> targetExtensions) throws Exception { // Do not install bundle; we only need the Object to list his components Bundle bundle = new BundleImpl(osgi, lookupBundle(name), null); RuntimeContext ctx = new OSGiRuntimeContext(runtime, bundle); listBundleComponents(bundle).map(URLStreamRef::new).forEach(component -> { try { deployPartialComponent(ctx, targetExtensions, component); } catch (IOException e) { log.error("PartialBundle: " + name + " failed to load: " + component, e); } }); return ctx; } /** * Read a component from his StreamRef and create a new component (suffixed with `-partial`, and the base component * name aliased) with only matching contributions of the extensionPoints parameter. * * @param ctx RuntimeContext in which the new component will be deployed * @param extensionPoints Set of white listed TargetExtensions * @param component Reference to the original component */ protected void deployPartialComponent(RuntimeContext ctx, Set<TargetExtensions> extensionPoints, StreamRef component) throws IOException { RegistrationInfo ri = ((DefaultRuntimeContext) ctx).createRegistrationInfo(component); String name = ri.getName().getName() + "-partial"; // Flatten Target Extension Points Set<String> targets = extensionPoints.stream() .map(TargetExtensions::getTargetExtensions) .flatMap(Set::stream) .collect(Collectors.toSet()); String ext = Arrays.stream(ri.getExtensions()) .filter(e -> targets.contains(TargetExtensions.newTargetExtension( e.getTargetComponent().getName(), e.getExtensionPoint()))) .map(Extension::toXML) .collect(Collectors.joining()); InlineURLFactory.install(); ctx.deploy(new InlineRef(name, String.format("<component name=\"%s\">%s</component>", name, ext))); } /** * Listing component's urls of a bundle. Inspired from org.nuxeo.runtime.osgi.OSGiRuntimeService#loadComponents but * without deploying anything. * * @param bundle Bundle to be read */ protected Stream<URL> listBundleComponents(Bundle bundle) { String list = OSGiRuntimeService.getComponentsList(bundle); String name = bundle.getSymbolicName(); log.debug("PartialBundle: " + name + " components: " + list); if (list == null) { return null; } return Arrays.stream(list.split("[, \t\n\r\f]")).map(s -> bundle.getEntry(s)).filter(Objects::nonNull); } /** * Undeploys a contribution from a given bundle. * <p> * The path will be relative to the bundle root. Example: <code> * undeployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml") * </code> * * @param name the bundle * @param contrib the contribution */ @Override public void undeployContrib(String name, String contrib) throws Exception { RuntimeContext context = runtime.getContext(name); if (context == null) { context = runtime.getContext(); } context.undeploy(contrib); } protected static boolean isVersionSuffix(String s) { if (s.length() == 0) { return true; } return s.matches("-(\\d+\\.?)+(-SNAPSHOT)?(\\.\\w+)?"); } /** * Resolves an URL for bundle deployment code. * <p> * TODO: Implementation could be finer... * * @return the resolved url */ protected URL lookupBundleUrl(String bundle) { for (URL url : urls) { String[] pathElts = url.getPath().split("/"); for (int i = 0; i < pathElts.length; i++) { if (pathElts[i].startsWith(bundle) && isVersionSuffix(pathElts[i].substring(bundle.length()))) { // we want the main version of the bundle boolean isTestVersion = false; for (int j = i + 1; j < pathElts.length; j++) { // ok for Eclipse (/test) and Maven (/test-classes) if (pathElts[j].startsWith("test")) { isTestVersion = true; break; } } if (!isTestVersion) { log.info("Resolved " + bundle + " as " + url.toString()); return url; } } } } throw new RuntimeException("Could not resolve bundle " + bundle); } /** * Deploys a whole OSGI bundle. * <p> * The lookup is first done on symbolic name, as set in <code>MANIFEST.MF</code> and then falls back to the bundle * url (e.g., <code>nuxeo-platform-search-api</code>) for backwards compatibility. * * @param name the symbolic name */ @Override public void deployBundle(String name) throws Exception { // install only if not yet installed Bundle bundle = bundleLoader.getOSGi().getRegistry().getBundle(name); if (bundle == null) { BundleFile bundleFile = lookupBundle(name); bundleLoader.loadBundle(bundleFile); bundleLoader.installBundle(bundleFile); bundle = bundleLoader.getOSGi().getRegistry().getBundle(name); } if (runtime.getContext(bundle) == null) { runtime.createContext(bundle); } } protected String readSymbolicName(BundleFile bf) { Manifest manifest = bf.getManifest(); if (manifest == null) { return null; } Attributes attrs = manifest.getMainAttributes(); String name = attrs.getValue("Bundle-SymbolicName"); if (name == null) { return null; } String[] sp = name.split(";", 2); return sp[0]; } public BundleFile lookupBundle(String bundleName) throws Exception { BundleFile bundleFile = bundles.get(bundleName); if (bundleFile != null) { return bundleFile; } for (URL url : urls) { URI uri = url.toURI(); if (readUris.contains(uri)) { continue; } File file = new File(uri); readUris.add(uri); try { if (file.isDirectory()) { bundleFile = new DirectoryBundleFile(file); } else { bundleFile = new JarBundleFile(file); } } catch (IOException e) { // no manifest => not a bundle continue; } String symbolicName = readSymbolicName(bundleFile); if (symbolicName != null) { log.info(String.format("Bundle '%s' has URL %s", symbolicName, url)); bundles.put(symbolicName, bundleFile); } if (bundleName.equals(symbolicName)) { return bundleFile; } } throw new RuntimeServiceException(String.format("No bundle with symbolic name '%s';", bundleName)); } @Override public void deployFolder(File folder, ClassLoader loader) throws Exception { DirectoryBundleFile bf = new DirectoryBundleFile(folder); BundleImpl bundle = new BundleImpl(osgi, bf, loader); osgi.install(bundle); } @Override public Properties getProperties() { return runtime.getProperties(); } @Override public RuntimeContext getContext() { return runtime.getContext(); } @Override public OSGiAdapter getOSGiAdapter() { return osgi; } /* * (non-Javadoc) * @see org.nuxeo.runtime.test.runner.RuntimeHarness#getClassLoaderFiles() */ @Override public List<String> getClassLoaderFiles() throws URISyntaxException { List<String> files = new ArrayList<>(urls.length); for (URL url : urls) { files.add(url.toURI().getPath()); } return files; } }