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.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}