001/*
002 * (C) Copyright 2006-2018 Nuxeo (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 */
019package org.nuxeo.runtime.test;
020
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.fail;
023
024import java.io.IOException;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.util.ArrayList;
028import java.util.Enumeration;
029import java.util.List;
030
031import org.apache.commons.io.FileUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.jmock.Mockery;
035import org.jmock.integration.junit4.JUnit4Mockery;
036import org.junit.After;
037import org.junit.Before;
038import org.junit.Ignore;
039import org.junit.runner.RunWith;
040import org.nuxeo.runtime.AbstractRuntimeService;
041import org.nuxeo.runtime.RuntimeServiceException;
042import org.nuxeo.runtime.api.Framework;
043import org.nuxeo.runtime.osgi.OSGiRuntimeService;
044import org.nuxeo.runtime.test.runner.ConditionalIgnoreRule;
045import org.nuxeo.runtime.test.runner.Features;
046import org.nuxeo.runtime.test.runner.FeaturesRunner;
047import org.nuxeo.runtime.test.runner.MDCFeature;
048import org.nuxeo.runtime.test.runner.RandomBug;
049
050import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;
051
052/**
053 * Abstract base class for test cases that require a test runtime service.
054 * <p>
055 * The runtime service itself is conveniently available as the <code>runtime</code> instance variable in derived
056 * classes.
057 * <p>
058 * <b>Warning:</b> NXRuntimeTestCase subclasses <b>must</b>
059 * <ul>
060 * <li>not declare they own @Before and @After.
061 * <li>override doSetUp and doTearDown (and postSetUp if needed) instead of setUp and tearDown.
062 * <li>never call deployXXX methods outside the doSetUp method.
063 *
064 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
065 * @deprecated since 10.2 this class <b>must</b> not be subclassed anymore, for RuntimeHarness implementation use
066 *             {@code RuntimeHarnessImpl}
067 */
068// Make sure this class is kept in sync with with RuntimeHarness
069@RunWith(FeaturesRunner.class)
070@Features({ MDCFeature.class, ConditionalIgnoreRule.Feature.class, RandomBug.Feature.class })
071@Ignore
072@Deprecated
073public class NXRuntimeTestCase extends RuntimeHarnessImpl {
074
075    protected Mockery jmcontext = new JUnit4Mockery();
076
077    static {
078        // jul to jcl redirection may pose problems (infinite loops) in some
079        // environment
080        // where slf4j to jul, and jcl over slf4j is deployed
081        System.setProperty(AbstractRuntimeService.REDIRECT_JUL, "false");
082    }
083
084    private static final Log log = LogFactory.getLog(NXRuntimeTestCase.class);
085
086    protected boolean restart = false;
087
088    protected List<String[]> deploymentStack = new ArrayList<>();
089
090    /**
091     * Set to true when the instance of this class is a JUnit test case. Set to false when the instance of this class is
092     * instantiated by the FeaturesRunner to manage the framework If the class is a JUnit test case then the runtime
093     * components will be started at the end of the setUp method
094     */
095    protected final boolean isTestUnit;
096
097    /**
098     * Used when subclassing to create standalone test cases
099     */
100    public NXRuntimeTestCase() {
101        super();
102        isTestUnit = true;
103    }
104
105    /**
106     * Used by the features runner to manage the Nuxeo framework
107     */
108    public NXRuntimeTestCase(Class<?> clazz) {
109        super(clazz);
110        isTestUnit = false;
111    }
112
113    /**
114     * Restarts the runtime and preserve homes directory.
115     */
116    @Override
117    public void restart() throws Exception {
118        restart = true;
119        try {
120            tearDown();
121            setUp();
122        } finally {
123            restart = false;
124        }
125    }
126
127    @Override
128    public void start() throws Exception {
129        startRuntime();
130    }
131
132    @Before
133    public void startRuntime() throws Exception {
134        System.setProperty("org.nuxeo.runtime.testing", "true");
135        wipeRuntime();
136        initUrls();
137        if (urls == null) {
138            throw new UnsupportedOperationException("no bundles available");
139        }
140        initOsgiRuntime();
141        setUp(); // let a chance to the subclasses to contribute bundles and/or components
142        if (isTestUnit) { // if this class is running as a test case start the runtime components
143            fireFrameworkStarted();
144        }
145        postSetUp();
146    }
147
148    /**
149     * Implementors should override this method to setup tests and not the {@link #startRuntime()} method. This method
150     * should contain all the bundle or component deployments needed by the tests. At the time this method is called the
151     * components are not yet started. If you need to perform component/service lookups use instead the
152     * {@link #postSetUp()} method
153     */
154    protected void setUp() throws Exception { // NOSONAR
155    }
156
157    /**
158     * Implementors should override this method to implement any specific test tear down and not the
159     * {@link #stopRuntime()} method
160     *
161     * @throws Exception
162     */
163    protected void tearDown() throws Exception { // NOSONAR
164        deploymentStack = new ArrayList<>();
165    }
166
167    /**
168     * Called after framework was started (at the end of setUp). Implementors may use this to use deployed services to
169     * initialize fields etc.
170     */
171    protected void postSetUp() throws Exception { // NOSONAR
172    }
173
174    @After
175    public void stopRuntime() throws Exception {
176        tearDown();
177        wipeRuntime();
178        if (workingDir != null && !restart) {
179            if (workingDir.exists() && !FileUtils.deleteQuietly(workingDir)) {
180                log.warn("Cannot delete " + workingDir);
181            }
182            workingDir = null;
183        }
184        readUris = null;
185        bundles = null;
186    }
187
188    @Override
189    public void stop() throws Exception {
190        stopRuntime();
191    }
192
193    protected OSGiRuntimeService handleNewRuntime(OSGiRuntimeService aRuntime) {
194        return aRuntime;
195    }
196
197    public static URL[] introspectClasspath(ClassLoader loader) {
198        return new FastClasspathScanner().getUniqueClasspathElements().stream().map(file -> {
199            try {
200                return file.toURI().toURL();
201            } catch (MalformedURLException cause) {
202                throw new Error("Could not get URL from " + file, cause);
203            }
204        }).toArray(URL[]::new);
205    }
206
207    public static URL getResource(String name) {
208        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
209        String callerName = Thread.currentThread().getStackTrace()[2].getClassName();
210        final String relativePath = callerName.replace('.', '/').concat(".class");
211        final String fullPath = loader.getResource(relativePath).getPath();
212        final String basePath = fullPath.substring(0, fullPath.indexOf(relativePath));
213        Enumeration<URL> resources;
214        try {
215            resources = loader.getResources(name);
216            while (resources.hasMoreElements()) {
217                URL resource = resources.nextElement();
218                if (resource.getPath().startsWith(basePath)) {
219                    return resource;
220                }
221            }
222        } catch (IOException e) {
223            return null;
224        }
225        return loader.getResource(name);
226    }
227
228    protected void deployContrib(URL url) {
229        assertEquals(runtime, Framework.getRuntime());
230        log.info("Deploying contribution from " + url.toString());
231        try {
232            runtime.getContext().deploy(url);
233        } catch (Exception e) {
234            fail("Failed to deploy contrib " + url.toString());
235        }
236    }
237
238    /**
239     * Deploy a contribution specified as a "bundleName:path" uri
240     */
241    public void deployContrib(String uri) throws Exception {
242        int i = uri.indexOf(':');
243        if (i == -1) {
244            throw new IllegalArgumentException(
245                    "Invalid deployment URI: " + uri + ". Must be of the form bundleSymbolicName:pathInBundleJar");
246        }
247        deployContrib(uri.substring(0, i), uri.substring(i + 1));
248    }
249
250    public void undeployContrib(String uri) throws Exception {
251        int i = uri.indexOf(':');
252        if (i == -1) {
253            throw new IllegalArgumentException(
254                    "Invalid deployment URI: " + uri + ". Must be of the form bundleSymbolicName:pathInBundleJar");
255        }
256        undeployContrib(uri.substring(0, i), uri.substring(i + 1));
257    }
258
259    protected static boolean isVersionSuffix(String s) {
260        if (s.length() == 0) {
261            return true;
262        }
263        return s.matches("-(\\d+\\.?)+(-SNAPSHOT)?(\\.\\w+)?");
264    }
265
266    /**
267     * Resolves an URL for bundle deployment code.
268     * <p>
269     * TODO: Implementation could be finer...
270     *
271     * @return the resolved url
272     */
273    protected URL lookupBundleUrl(String bundle) { // NOSONAR
274        for (URL url : urls) {
275            String[] pathElts = url.getPath().split("/");
276            for (int i = 0; i < pathElts.length; i++) {
277                if (pathElts[i].startsWith(bundle) && isVersionSuffix(pathElts[i].substring(bundle.length()))) {
278                    // we want the main version of the bundle
279                    boolean isTestVersion = false;
280                    for (int j = i + 1; j < pathElts.length; j++) {
281                        // ok for Eclipse (/test) and Maven (/test-classes)
282                        if (pathElts[j].startsWith("test")) {
283                            isTestVersion = true;
284                            break;
285                        }
286                    }
287                    if (!isTestVersion) {
288                        log.info("Resolved " + bundle + " as " + url.toString());
289                        return url;
290                    }
291                }
292            }
293        }
294        throw new RuntimeServiceException("Could not resolve bundle " + bundle);
295    }
296
297    /**
298     * Should be called by subclasses after one or more inline deployments are made inside a test method. Without
299     * calling this the inline deployment(s) will not have any effects.
300     * <p />
301     * <b>Be Warned</b> that if you reference runtime services or components you should lookup them again after calling
302     * this method!
303     * <p />
304     * This method also calls {@link #postSetUp()} for convenience.
305     */
306    protected void applyInlineDeployments() throws Exception {
307        runtime.getComponentManager().refresh(false);
308        runtime.getComponentManager().start(); // make sure components are started
309        postSetUp();
310    }
311
312    /**
313     * Should be called by subclasses to remove any inline deployments made in the current test method.
314     * <p />
315     * <b>Be Warned</b> that if you reference runtime services or components you should lookup them again after calling
316     * this method!
317     * <p />
318     * This method also calls {@link #postSetUp()} for convenience.
319     */
320    protected void removeInlineDeployments() throws Exception {
321        runtime.getComponentManager().reset();
322        runtime.getComponentManager().start();
323        postSetUp();
324    }
325
326    /**
327     * Hot deploy the given components (identified by an URI). All the started components are stopped, the new ones are
328     * registered and then all components are started. You can undeploy these components by calling
329     * {@link #popInlineDeployments()}
330     * <p>
331     * A component URI is of the form: bundleSymbolicName:pathToComponentXmlInBundle
332     */
333    public void pushInlineDeployments(String... deploymentUris) throws Exception {
334        deploymentStack.add(deploymentUris);
335        for (String uri : deploymentUris) {
336            deployContrib(uri);
337        }
338        applyInlineDeployments();
339    }
340
341    /**
342     * Remove the latest deployed components using {@link #pushInlineDeployments(String...)}.
343     */
344    public void popInlineDeployments() throws Exception {
345        if (deploymentStack.isEmpty()) {
346            throw new IllegalStateException("deployment stack is empty");
347        }
348        popInlineDeployments(deploymentStack.size() - 1);
349    }
350
351    public void popInlineDeployments(int index) throws Exception {
352        if (index < 0 || index > deploymentStack.size() - 1) {
353            throw new IllegalStateException("deployment stack index is invalid: " + index);
354        }
355        deploymentStack.remove(index);
356
357        runtime.getComponentManager().reset();
358        for (String[] ar : deploymentStack) {
359            for (String element : ar) {
360                deployContrib(element);
361            }
362        }
363        applyInlineDeployments();
364    }
365
366}