/****************************************************************************** * 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(); } }