org.eclipse.gemini.blueprint.test.AbstractOsgiTests.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.gemini.blueprint.test.AbstractOsgiTests.java

Source

/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.test;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLDecoder;

import junit.framework.Protectable;
import junit.framework.TestCase;
import junit.framework.TestResult;

import org.eclipse.gemini.blueprint.io.OsgiBundleResourceLoader;
import org.eclipse.gemini.blueprint.test.internal.holder.OsgiTestInfoHolder;
import org.eclipse.gemini.blueprint.test.internal.util.TestUtils;
import org.eclipse.gemini.blueprint.test.platform.OsgiPlatform;
import org.eclipse.gemini.blueprint.util.OsgiBundleUtils;
import org.eclipse.gemini.blueprint.util.OsgiPlatformDetector;
import org.eclipse.gemini.blueprint.util.OsgiStringUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.ServiceReference;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
 * Base test for OSGi environments. Takes care of configuring the chosen OSGi platform, starting it, installing a number
 * of bundles and delegating the test execution to a test copy that runs inside OSGi.
 * 
 * @author Costin Leau
 */
public abstract class AbstractOsgiTests extends AbstractOptionalDependencyInjectionTests {

    private static final String UTF_8_CHARSET = "UTF-8";

    // JVM shutdown hook
    private static Thread shutdownHook;

    // the OSGi fixture
    private static OsgiPlatform osgiPlatform;

    // OsgiPlatform bundle context
    private static BundleContext platformContext;

    // JUnit Service
    private static Object service;

    // JUnitService trigger
    private static Method serviceTrigger;

    // the test results used by the triggering test runner
    private TestResult originalResult;

    // OsgiResourceLoader
    protected ResourceLoader resourceLoader;

    /**
     * Hook for JUnit infrastructures which can't reuse this class hierarchy. This instance represents the test which
     * will be executed by AbstractOsgiTests & co.
     */
    private TestCase osgiJUnitTest = this;

    private static final String ACTIVATOR_REFERENCE = "org.eclipse.gemini.blueprint.test.JUnitTestActivator";

    /**
     * Default constructor. Constructs a new <code>AbstractOsgiTests</code> instance.
     */
    public AbstractOsgiTests() {
        super();
    }

    /**
     * Constructs a new <code>AbstractOsgiTests</code> instance.
     * 
     * @param name test name
     */
    public AbstractOsgiTests(String name) {
        super(name);
    }

    /**
     * Returns the test framework bundles (part of the test setup). Used by the test infrastructure. Override this
     * method <i>only</i> if you want to change the jars used by default, by the testing infrastructure.
     * 
     * User subclasses should use {@link #getTestBundles()} instead.
     * 
     * @return the array of test framework bundle resources
     */
    protected abstract Resource[] getTestFrameworkBundles();

    /**
     * Returns the bundles required for the test execution.
     * 
     * @return the array of bundles to install
     */
    protected abstract Resource[] getTestBundles();

    /**
     * Creates (and configures) the OSGi platform.
     * 
     * @return OSGi platform instance
     * @throws Exception if the platform creation fails
     */
    protected abstract OsgiPlatform createPlatform() throws Exception;

    /**
     * Pre-processes the bundle context. This call back gives access to the platform bundle context before any bundles
     * have been installed. The method is invoked <b>after</b> starting the OSGi environment but <b>before</b> any
     * bundles are installed in the OSGi framework.
     * 
     * <p/> Normally, this method is called only once during the lifecycle of a test suite.
     * 
     * @param platformBundleContext the platform bundle context
     * @throws Exception if processing the bundle context fails
     * @see #postProcessBundleContext(BundleContext)
     * 
     */
    protected void preProcessBundleContext(BundleContext platformBundleContext) throws Exception {
    }

    /**
     * Post-processes the bundle context. This call back gives access to the platform bundle context after the critical
     * test infrastructure bundles have been installed and started. The method is invoked <b>after</b> preparing the
     * OSGi environment for the test execution but <b>before</b> any test is executed.
     * 
     * The given <code>BundleContext</code> belongs to the underlying OSGi framework.
     * 
     * <p/> Normally, this method is called only one during the lifecycle of a test suite.
     * 
     * @param platformBundleContext the platform bundle context
     * @see #preProcessBundleContext(BundleContext)
     */
    protected void postProcessBundleContext(BundleContext platformBundleContext) throws Exception {
    }

    //
    // JUnit overridden methods.
    //

    /**
     * {@inheritDoc}
     * 
     * <p/> Replacement run method. Gets a hold of the TestRunner used for running this test so it can populate it with
     * the results retrieved from OSGi.
     */
    public final void run(TestResult result) {

        // get a hold of the test result
        originalResult = result;

        result.startTest(osgiJUnitTest);
        result.runProtected(osgiJUnitTest, new Protectable() {

            public void protect() throws Throwable {
                AbstractOsgiTests.this.runBare();
            }
        });
        result.endTest(osgiJUnitTest);

        // super.run(result);
    }

    public void runBare() throws Throwable {
        // add ConditionalTestCase behaviour

        // getName will return the name of the method being run
        if (isDisabledInThisEnvironment(getName())) {
            recordDisabled();
            logger.warn("**** " + getClass().getName() + "." + getName() + " disabled in this environment: "
                    + "Total disabled tests=" + getDisabledTestCount());
            return;
        } else {
            prepareTestExecution();
            try {
                // invoke OSGi test run
                invokeOSGiTestExecution();
                readTestResult();
            } finally {
                // nothing to clean up
            }
        }
    }

    //
    // OSGi testing infrastructure setup.
    //

    /**
     * Starts the OSGi platform and install/start the bundles (happens once for the all test runs)
     * 
     * @throws Exception
     */
    private void startup() throws Exception {
        if (osgiPlatform == null) {

            boolean debug = logger.isDebugEnabled();

            // make sure the platform is closed properly
            registerShutdownHook();

            osgiPlatform = createPlatform();
            // start platform
            if (debug)
                logger.debug("About to start " + osgiPlatform);
            osgiPlatform.start();
            // platform context
            platformContext = osgiPlatform.getBundleContext();

            // log platform name and version
            logPlatformInfo(platformContext);

            // hook before the OSGi platform is setup but right after is has
            // been started
            preProcessBundleContext(platformContext);

            // install bundles (from the local system/classpath)
            Resource[] bundleResources = locateBundles();

            Bundle[] bundles = new Bundle[bundleResources.length];
            for (int i = 0; i < bundles.length; i++) {
                bundles[i] = installBundle(bundleResources[i]);
            }

            // start bundles
            for (int i = 0; i < bundles.length; i++) {
                startBundle(bundles[i]);
            }

            // hook after the OSGi platform has been setup
            postProcessBundleContext(platformContext);

            initializeServiceRunnerInvocationMethods();
        }
    }

    // concatenate bundles to install
    private Resource[] locateBundles() {
        Resource[] testFrameworkBundles = getTestFrameworkBundles();
        Resource[] testBundles = getTestBundles();

        if (testFrameworkBundles == null)
            testFrameworkBundles = new Resource[0];
        if (testBundles == null)
            testBundles = new Resource[0];

        Resource[] allBundles = new Resource[testFrameworkBundles.length + testBundles.length];
        System.arraycopy(testFrameworkBundles, 0, allBundles, 0, testFrameworkBundles.length);
        System.arraycopy(testBundles, 0, allBundles, testFrameworkBundles.length, testBundles.length);
        return allBundles;
    }

    /**
     * Logs the underlying OSGi information (which can be tricky).
     * 
     */
    private void logPlatformInfo(BundleContext context) {
        StringBuilder platformInfo = new StringBuilder();

        // add platform information
        platformInfo.append(osgiPlatform);
        platformInfo.append(" [");
        // Version
        platformInfo.append(OsgiPlatformDetector.getVersion(context));
        platformInfo.append("]");
        logger.info(platformInfo + " started");
    }

    /**
     * Installs an OSGi bundle from the given location.
     * 
     * @param location
     * @return
     * @throws Exception
     */
    private Bundle installBundle(Resource location) throws Exception {
        Assert.notNull(platformContext, "the OSGi platform is not set");
        Assert.notNull(location, "cannot install from a null location");
        if (logger.isDebugEnabled())
            logger.debug("Installing bundle from location " + location.getDescription());

        String bundleLocation;

        try {
            bundleLocation = URLDecoder.decode(location.getURL().toExternalForm(), UTF_8_CHARSET);
        } catch (Exception ex) {
            // the URL cannot be created, fall back to the description
            bundleLocation = location.getDescription();
        }

        return platformContext.installBundle(bundleLocation, location.getInputStream());
    }

    /**
     * Starts a bundle and prints a nice logging message in case of failure.
     * 
     * @param bundle
     * @return
     * @throws BundleException
     */
    private void startBundle(Bundle bundle) throws BundleException {
        boolean debug = logger.isDebugEnabled();
        String info = "[" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "|" + bundle.getLocation() + "]";

        if (!OsgiBundleUtils.isFragment(bundle)) {
            if (debug)
                logger.debug("Starting " + info);
            try {
                bundle.start();
            } catch (BundleException ex) {
                logger.error("cannot start bundle " + info, ex);
                throw ex;
            }
        } else {
            //         if (!OsgiBundleUtils.isBundleResolved(bundle)) {
            //            logger.error("fragment not resolved: " + info);
            //            throw new BundleException("Unable to resolve fragment: " + info);
            //         } else if (debug)
            logger.debug(info + " is a fragment; start not invoked");
        }
    }

    //
    // Delegation methods for OSGi execution and initialization
    //

    // runs outside OSGi
    /**
     * Prepares test execution - the OSGi platform will be started (if needed) and cached for the test suite execution.
     */
    private void prepareTestExecution() throws Exception {

        if (getName() == null)
            throw new IllegalArgumentException("no test specified");

        // clear test results
        OsgiTestInfoHolder.INSTANCE.clearResults();
        // set test class
        OsgiTestInfoHolder.INSTANCE.setTestClassName(osgiJUnitTest.getClass().getName());

        // start OSGi platform (the caching is done inside the method).
        try {
            startup();
        } catch (Exception e) {
            logger.debug("Caught exception starting up", e);
            throw e;
        }

        if (logger.isTraceEnabled())
            logger.trace("Writing test name [" + getName() + "] to OSGi");

        // write test name to OSGi
        // set test method name
        OsgiTestInfoHolder.INSTANCE.setTestMethodName(getName());
    }

    /**
     * Delegates the test execution to the OSGi copy.
     * 
     * @throws Exception
     */
    private void invokeOSGiTestExecution() throws Exception {
        Assert.notNull(serviceTrigger, "no executeTest() method found on: " + service.getClass());
        try {
            serviceTrigger.invoke(service, null);
        } catch (InvocationTargetException ex) {
            Throwable th = ex.getCause();
            if (th instanceof Exception)
                throw ((Exception) th);
            else
                throw ((Error) th);
        }
    }

    /**
     * Determines through reflection the methods used for invoking the TestRunnerService.
     * 
     * @throws Exception
     */
    private void initializeServiceRunnerInvocationMethods() throws Exception {
        // get JUnit test service reference
        // this is a loose reference - update it if the JUnitTestActivator
        // class is
        // changed.

        BundleContext ctx = getRuntimeBundleContext();

        ServiceReference reference = ctx.getServiceReference(ACTIVATOR_REFERENCE);
        Assert.notNull(reference, "no OSGi service reference found at " + ACTIVATOR_REFERENCE);

        service = ctx.getService(reference);
        Assert.notNull(service, "no service found for reference: " + reference);

        serviceTrigger = service.getClass().getDeclaredMethod("executeTest", null);
        ReflectionUtils.makeAccessible(serviceTrigger);
        Assert.notNull(serviceTrigger, "no executeTest() method found on: " + service.getClass());
    }

    /**
     * Tries to get the bundle context for spring-osgi-test-support bundle. This is useful on platform where the
     * platformContext or system BundleContext doesn't behave like a normal context.
     * 
     * Will fallback to {@link #platformContext}.
     * 
     * @return
     */
    private BundleContext getRuntimeBundleContext() {

        // read test bundle id property
        Long id = OsgiTestInfoHolder.INSTANCE.getTestBundleId();

        BundleContext ctx = null;
        if (id != null)
            try {
                ctx = OsgiBundleUtils.getBundleContext(platformContext.getBundle(id.longValue()));
            } catch (RuntimeException ex) {
                logger.trace("cannot determine bundle context for bundle " + id, ex);
            }

        return (ctx == null ? platformContext : ctx);
    }

    // runs outside OSGi
    private void readTestResult() {
        if (logger.isTraceEnabled())
            logger.trace("Reading OSGi results for test [" + getName() + "]");

        // copy results from OSGi into existing test result
        TestUtils.cloneTestResults(OsgiTestInfoHolder.INSTANCE, originalResult, osgiJUnitTest);

        if (logger.isTraceEnabled())
            logger.debug("Test[" + getName() + "]'s result read");
    }

    /**
     * Special shutdown hook.
     */
    private void registerShutdownHook() {
        if (shutdownHook == null) {
            // No shutdown hook registered yet.
            shutdownHook = new Thread() {

                public void run() {
                    shutdownTest();
                }
            };
            Runtime.getRuntime().addShutdownHook(shutdownHook);
        }
    }

    /**
     * Cleanup for the test suite.
     */
    private void shutdownTest() {
        logger.info("Shutting down OSGi platform");
        if (osgiPlatform != null) {
            try {
                osgiPlatform.stop();
            } catch (Exception ex) {
                // swallow
                logger.warn("Shutdown procedure threw exception " + ex);
            }
            osgiPlatform = null;
        }
    }

    //
    // OsgiJUnitTest execution hooks. Used by the test framework.
    //

    /**
     * Set the bundle context to be used by this test.
     * 
     * <p/> This method is called automatically by the test infrastructure after the OSGi platform is being setup.
     */
    private void injectBundleContext(BundleContext bundleContext) {
        this.bundleContext = bundleContext;
        // instantiate ResourceLoader
        this.resourceLoader = new OsgiBundleResourceLoader(bundleContext.getBundle());
    }

    /**
     * Set the underlying OsgiJUnitTest used for the test delegation.
     * 
     * <p/> This method is called automatically by the test infrastructure after the OSGi platform is being setup.
     * 
     * @param test
     */
    private void injectOsgiJUnitTest(TestCase test) {
        this.osgiJUnitTest = test;
    }

    /**
     * the setUp version for the OSGi environment.
     * 
     * @throws Exception
     */
    private void osgiSetUp() throws Exception {
        // call the normal onSetUp
        setUp();
    }

    private void osgiTearDown() throws Exception {
        // call the normal tearDown
        tearDown();
    }

    /**
     * Actual test execution (delegates to the superclass implementation).
     * 
     * @throws Throwable
     */
    private void osgiRunTest() throws Throwable {
        super.runTest();
    }

}