/* * Copyright 2009 Toni Menzel. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. * * See the License for the specific language governing permissions and * limitations under the License. */ package org.ops4j.pax.exam.nat.internal; import static org.ops4j.pax.exam.Constants.EXAM_FAIL_ON_UNRESOLVED_KEY; import static org.ops4j.pax.exam.Constants.EXAM_SERVICE_TIMEOUT_DEFAULT; import static org.ops4j.pax.exam.Constants.EXAM_SERVICE_TIMEOUT_KEY; import static org.ops4j.pax.exam.Constants.START_LEVEL_TEST_BUNDLE; import static org.ops4j.pax.exam.CoreOptions.systemPackage; import static org.ops4j.pax.exam.CoreOptions.systemProperty; import static org.osgi.framework.Constants.FRAMEWORK_BOOTDELEGATION; import static org.osgi.framework.Constants.FRAMEWORK_STORAGE; import static org.osgi.framework.Constants.FRAMEWORK_STORAGE_CLEAN; import static org.osgi.framework.Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT; import static org.osgi.framework.Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.Stack; import java.util.TreeMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.ops4j.pax.exam.ConfigurationManager; import org.ops4j.pax.exam.Constants; import org.ops4j.pax.exam.ExamSystem; import org.ops4j.pax.exam.FrameworkEventUtils; import org.ops4j.pax.exam.Info; import org.ops4j.pax.exam.Option; import org.ops4j.pax.exam.ProbeInvoker; import org.ops4j.pax.exam.TestAddress; import org.ops4j.pax.exam.TestContainer; import org.ops4j.pax.exam.TestContainerException; import org.ops4j.pax.exam.options.BootDelegationOption; import org.ops4j.pax.exam.options.FrameworkPropertyOption; import org.ops4j.pax.exam.options.FrameworkStartLevelOption; import org.ops4j.pax.exam.options.ProvisionOption; import org.ops4j.pax.exam.options.SystemPackageOption; import org.ops4j.pax.exam.options.SystemPropertyOption; import org.ops4j.pax.exam.options.ValueOption; import org.ops4j.pax.exam.options.extra.CleanCachesOption; import org.ops4j.pax.exam.options.extra.RepositoryOption; import org.ops4j.pax.swissbox.tracker.ServiceLookup; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleException; import org.osgi.framework.FrameworkEvent; import org.osgi.framework.FrameworkListener; import org.osgi.framework.launch.Framework; import org.osgi.framework.launch.FrameworkFactory; import org.osgi.framework.startlevel.BundleStartLevel; import org.osgi.framework.startlevel.FrameworkStartLevel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The Native Test Container starts an OSGi framework using {@link FrameworkFactory} and provisions * the bundles configured in the Exam system. * <p> * When the framework has reached the configured start level, the container checks that all bundles * are resolved and throws an exception otherwise. * * @author Toni Menzel * @author Harald Wellmann * @since Jan 7, 2010 */ public class NativeTestContainer implements TestContainer { private static final Logger LOG = LoggerFactory.getLogger(NativeTestContainer.class); private static final String PROBE_SIGNATURE_KEY = "Probe-Signature"; private final Stack<Long> installed = new Stack<Long>(); private Long probeId; private final FrameworkFactory frameworkFactory; private ExamSystem system; private volatile Framework framework; public NativeTestContainer(ExamSystem system, FrameworkFactory frameworkFactory) throws IOException { this.frameworkFactory = frameworkFactory; this.system = system; } @Override public synchronized void call(TestAddress address) { Map<String, String> props = new HashMap<String, String>(); props.put(PROBE_SIGNATURE_KEY, address.root().identifier()); BundleContext bundleContext = framework.getBundleContext(); ProbeInvoker probeInvokerService; probeInvokerService = ServiceLookup.getService(bundleContext, ProbeInvoker.class, determineExamServiceTimeout(), props); probeInvokerService.call(address.arguments()); } private long determineExamServiceTimeout() { String timeoutProp = System.getProperty(EXAM_SERVICE_TIMEOUT_KEY, EXAM_SERVICE_TIMEOUT_DEFAULT); try { return Long.valueOf(timeoutProp); } catch (NumberFormatException exc) { LOG.warn("Invalid timeout value {}, falling back to default"); return Long.valueOf(EXAM_SERVICE_TIMEOUT_DEFAULT); } } @Override public synchronized long install(String location, InputStream stream) { try { Bundle b = framework.getBundleContext().installBundle(location, stream); installed.push(b.getBundleId()); LOG.debug("Installed bundle " + b.getSymbolicName() + " as Bundle ID " + b.getBundleId()); setBundleStartLevel(b.getBundleId(), Constants.START_LEVEL_TEST_BUNDLE); b.start(); return b.getBundleId(); } catch (BundleException e) { e.printStackTrace(); } return -1; } @Override public synchronized long install(InputStream stream) { return install("local", stream); } public synchronized void cleanup() { while ((!installed.isEmpty())) { try { Long id = installed.pop(); Bundle bundle = framework.getBundleContext().getBundle(id); bundle.uninstall(); LOG.debug("Uninstalled bundle " + id); } catch (BundleException e) { // Sometimes bundles go mad when install + uninstall happens too // fast. } } } public Bundle getSystemBundle() { return framework; } public void setBundleStartLevel(long bundleId, int startLevel) { Bundle bundle = framework.getBundleContext().getBundle(bundleId); BundleStartLevel sl = bundle.adapt(BundleStartLevel.class); sl.setStartLevel(startLevel); } @Override public TestContainer start() { try { system = system.fork(new Option[] { systemPackage("org.ops4j.pax.exam;version=" + skipSnapshotFlag(Info.getPaxExamVersion())), systemPackage("org.ops4j.pax.exam.options;version=" + skipSnapshotFlag(Info.getPaxExamVersion())), systemPackage("org.ops4j.pax.exam.util;version=" + skipSnapshotFlag(Info.getPaxExamVersion())), systemProperty("java.protocol.handler.pkgs").value("org.ops4j.pax.url") }); Map<String, String> p = createFrameworkProperties(); if (LOG.isDebugEnabled()) { logFrameworkProperties(p); logSystemProperties(); } framework = frameworkFactory.newFramework(p); framework.init(); framework.getBundleContext().addFrameworkListener(new FrameworkListener() { @Override public void frameworkEvent(FrameworkEvent frameworkEvent) { if (frameworkEvent.getType() == FrameworkEvent.ERROR) { LOG.error( String.format("Framework ERROR event %s", frameworkEvent.toString()), frameworkEvent.getThrowable()); } else { LOG.debug(String.format("Framework event type %s: %s", FrameworkEventUtils.getFrameworkEventString(frameworkEvent.getType()), frameworkEvent.toString()), frameworkEvent.getThrowable()); } } }); installAndStartBundles(framework.getBundleContext()); } catch (BundleException e) { throw new TestContainerException("Problem starting test container.", e); } catch (IOException e) { throw new TestContainerException("Problem starting test container.", e); } return this; } private void logFrameworkProperties(Map<String, String> p) { LOG.debug("==== Framework properties:"); for (String key : p.keySet()) { LOG.debug("{} = {}", key, p.get(key)); } } private void logSystemProperties() { LOG.debug("==== System properties:"); SortedMap<Object, Object> map = new TreeMap<Object, Object>(System.getProperties()); for (Map.Entry<Object, Object> entry : map.entrySet()) { LOG.debug("{} = {}", entry.getKey(), entry.getValue()); } } @Override public TestContainer stop() { if (framework != null) { try { cleanup(); stopOrAbort(); framework = null; system.clear(); } catch (BundleException e) { LOG.warn("Problem during stopping fw.", e); } catch (InterruptedException e) { LOG.warn("InterruptedException during stopping fw.", e); } } else { LOG.warn("Framework does not exist. Called start() before ? "); } return this; } private void stopOrAbort() throws BundleException, InterruptedException { framework.stop(); long timeout = system.getTimeout().getValue(); Thread stopper = new Stopper(timeout); stopper.start(); stopper.join(timeout + 500); // If the framework is not stopped, then we're in trouble anyway, so we do not worry // about stopping the worker thread. if (framework.getState() != Bundle.RESOLVED) { String message = "Framework has not yet stopped after " + timeout + " ms. waitForStop did not return"; throw new TestContainerException(message); } } private Map<String, String> createFrameworkProperties() throws IOException { final Map<String, String> p = new HashMap<String, String>(); CleanCachesOption cleanCaches = system.getSingleOption(CleanCachesOption.class); if (cleanCaches != null && cleanCaches.getValue() != null && cleanCaches.getValue()) { p.put(FRAMEWORK_STORAGE_CLEAN, FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); } p.put(FRAMEWORK_STORAGE, system.getTempFolder().getAbsolutePath()); p.put(FRAMEWORK_SYSTEMPACKAGES_EXTRA, buildString(system.getOptions(SystemPackageOption.class))); p.put(FRAMEWORK_BOOTDELEGATION, buildString(system.getOptions(BootDelegationOption.class))); for (FrameworkPropertyOption option : system.getOptions(FrameworkPropertyOption.class)) { p.put(option.getKey(), (String) option.getValue()); } for (SystemPropertyOption option : system.getOptions(SystemPropertyOption.class)) { System.setProperty(option.getKey(), option.getValue()); } String repositories = buildString(system.getOptions(RepositoryOption.class)); if (!repositories.isEmpty()) { System.setProperty("org.ops4j.pax.url.mvn.repositories", repositories); } return p; } private String buildString(ValueOption<?>[] options) { return buildString(new String[0], options, new String[0]); } @SuppressWarnings("unused") private String buildString(String[] prepend, ValueOption<?>[] options) { return buildString(prepend, options, new String[0]); } @SuppressWarnings("unused") private String buildString(ValueOption<?>[] options, String[] append) { return buildString(new String[0], options, append); } private String buildString(String[] prepend, ValueOption<?>[] options, String[] append) { StringBuilder builder = new StringBuilder(); for (String a : prepend) { builder.append(a); builder.append(","); } for (ValueOption<?> option : options) { builder.append(option.getValue()); builder.append(","); } for (String a : append) { builder.append(a); builder.append(","); } if (builder.length() > 0) { return builder.substring(0, builder.length() - 1); } else { return ""; } } private void installAndStartBundles(BundleContext context) throws BundleException { List<Bundle> bundles = new ArrayList<Bundle>(); for (ProvisionOption<?> bundle : system.getOptions(ProvisionOption.class)) { Bundle b = context.installBundle(bundle.getURL()); bundles.add(b); int startLevel = getStartLevel(bundle); BundleStartLevel sl = b.adapt(BundleStartLevel.class); sl.setStartLevel(startLevel); if (bundle.shouldStart()) { try { b.start(); } catch (BundleException e) { throw new BundleException("Error starting bundle " + b.getSymbolicName() + ". " + e.getMessage(), e); } LOG.debug("+ Install (start@{}) {}", startLevel, bundle); } else { LOG.debug("+ Install (no start) {}", bundle); } } // All bundles are installed, we can now start the framework... framework.start(); FrameworkStartLevel fsl = framework.adapt(FrameworkStartLevel.class); setFrameworkStartLevel(context, fsl); verifyThatBundlesAreResolved(bundles); } private void setFrameworkStartLevel(BundleContext context, final FrameworkStartLevel sl) { FrameworkStartLevelOption startLevelOption = system .getSingleOption(FrameworkStartLevelOption.class); final int startLevel = startLevelOption == null ? START_LEVEL_TEST_BUNDLE : startLevelOption.getStartLevel(); LOG.debug("Jump to startlevel: " + startLevel); final CountDownLatch latch = new CountDownLatch(1); context.addFrameworkListener(new FrameworkListener() { @Override public void frameworkEvent(FrameworkEvent frameworkEvent) { if (frameworkEvent.getType() == FrameworkEvent.STARTLEVEL_CHANGED) { if (sl.getStartLevel() == startLevel) { latch.countDown(); } } } }); sl.setStartLevel(startLevel); // Check the current start level before starting to wait. if (sl.getStartLevel() == startLevel) { LOG.debug("requested start level reached"); return; } else { LOG.debug("start level {} requested, current start level is {}", startLevel, sl.getStartLevel()); } try { long timeout = system.getTimeout().getValue(); if (!latch.await(timeout, TimeUnit.MILLISECONDS)) { // Before throwing an exception, do a last check if (startLevel != sl.getStartLevel()) { String msg = String.format("start level %d has not been reached within %d ms", startLevel, timeout); throw new TestContainerException(msg); } else { // We reached the requested start level. LOG.debug("requested start level reached"); } } } catch (InterruptedException e) { throw new TestContainerException(e); } } private void verifyThatBundlesAreResolved(List<Bundle> bundles) { boolean hasUnresolvedBundles = false; for (Bundle bundle : bundles) { if (bundle.getState() == Bundle.INSTALLED) { LOG.error("Bundle [{}] is not resolved", bundle); hasUnresolvedBundles = true; } } ConfigurationManager cm = new ConfigurationManager(); boolean failOnUnresolved = Boolean.parseBoolean(cm.getProperty(EXAM_FAIL_ON_UNRESOLVED_KEY, "false")); if (hasUnresolvedBundles && failOnUnresolved) { throw new TestContainerException( "There are unresolved bundles. See previous ERROR log messages for details."); } } private int getStartLevel(ProvisionOption<?> bundle) { Integer start = bundle.getStartLevel(); if (start == null) { start = Constants.START_LEVEL_DEFAULT_PROVISION; } return start; } private String skipSnapshotFlag(String version) { int idx = version.indexOf("-"); if (idx >= 0) { return version.substring(0, idx); } else { return version; } } @Override public String toString() { return "Native:" + frameworkFactory.getClass().getSimpleName(); } /** * Worker thread for shutting down the framework. We'd expect Framework.waitForStop(timeout) to * return after the given timeout, but this is not the case with Equinox (tested on 3.6.2 and * 3.7.0), so we use this worker thread to avoid blocking the main thread. * * @author Harald Wellmann */ private class Stopper extends Thread { private final long timeout; private Stopper(long timeout) { this.timeout = timeout; } @Override public void run() { try { FrameworkEvent frameworkEvent = framework.waitForStop(timeout); if (frameworkEvent.getType() != FrameworkEvent.STOPPED) { LOG.error("Framework has not yet stopped after {} ms. " + "waitForStop returned: {}", timeout, frameworkEvent); } } catch (InterruptedException exc) { LOG.error("Stopper thread was interrupted"); } } } @Override public synchronized long installProbe(InputStream stream) { probeId = install(stream); installed.pop(); return probeId; } @Override public synchronized void uninstallProbe() { Bundle bundle = framework.getBundleContext().getBundle(probeId); try { bundle.uninstall(); probeId = null; } catch (BundleException exc) { throw new TestContainerException(exc); } } }