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