| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.apache.ace.it; |
| |
| import static java.util.concurrent.TimeUnit.SECONDS; |
| import static org.apache.ace.test.utils.Util.properties; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.reflect.Method; |
| import java.net.ConnectException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| import junit.framework.TestCase; |
| |
| import org.apache.ace.test.constants.TestConstants; |
| import org.apache.felix.dm.Component; |
| import org.apache.felix.dm.ComponentDependencyDeclaration; |
| import org.apache.felix.dm.ComponentState; |
| import org.apache.felix.dm.ComponentStateListener; |
| import org.apache.felix.dm.DependencyManager; |
| import org.apache.felix.dm.ServiceDependency; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.FrameworkUtil; |
| import org.osgi.framework.InvalidSyntaxException; |
| import org.osgi.framework.ServiceReference; |
| import org.osgi.service.cm.Configuration; |
| import org.osgi.service.cm.ConfigurationAdmin; |
| import org.osgi.service.event.Event; |
| import org.osgi.service.event.EventConstants; |
| import org.osgi.service.event.EventHandler; |
| import org.osgi.service.log.LogService; |
| import org.osgi.util.tracker.ServiceTracker; |
| |
| /** |
| * Base class for integration tests. There is no technical reason to use this, but it might make your life easier.<br> |
| * <br> |
| * {@link org.apache.ace.it.ExampleTest} shows a minimal example of an integration test. |
| * |
| */ |
| public class IntegrationTestBase extends TestCase { |
| private static class ComponentCounter implements ComponentStateListener { |
| private final List<Component> m_components = new ArrayList<Component>(); |
| private final CountDownLatch m_latch; |
| |
| public ComponentCounter(Component[] components) { |
| m_components.addAll(Arrays.asList(components)); |
| m_latch = new CountDownLatch(components.length); |
| } |
| |
| public String componentsString() { |
| StringBuilder result = new StringBuilder(); |
| for (Component component : m_components) { |
| result.append(component).append('\n'); |
| for (ComponentDependencyDeclaration dependency : component.getComponentDeclaration().getComponentDependencies()) { |
| result.append(" ") |
| .append(dependency.toString()) |
| .append(" ") |
| .append(ComponentDependencyDeclaration.STATE_NAMES[dependency.getState()]) |
| .append('\n'); |
| } |
| result.append('\n'); |
| } |
| return result.toString(); |
| } |
| |
| // public void started(Component component) { |
| // m_components.remove(component); |
| // m_latch.countDown(); |
| // } |
| // |
| // public void starting(Component component) { |
| // } |
| // |
| // public void stopped(Component component) { |
| // } |
| // |
| // public void stopping(Component component) { |
| // } |
| |
| public boolean waitForEmpty(long timeout, TimeUnit unit) throws InterruptedException { |
| return m_latch.await(timeout, unit); |
| } |
| |
| @Override |
| public void changed(Component component, ComponentState state) { |
| if (state == ComponentState.TRACKING_OPTIONAL) { |
| m_components.remove(component); |
| m_latch.countDown(); |
| } |
| } |
| } |
| |
| /** |
| * If we have to wait for a service, wait this amount of seconds. |
| */ |
| private static final int SERVICE_TIMEOUT = 15; |
| |
| private final Map<String, ServiceTracker> m_trackedServices = new HashMap<String, ServiceTracker>(); |
| private final List<Configuration> m_trackedConfigurations = new ArrayList<Configuration>(); |
| |
| private boolean m_cleanConfigurations = true; |
| private boolean m_closeServiceTrackers = true; |
| |
| protected BundleContext m_bundleContext; |
| protected DependencyManager m_dependencyManager; |
| protected Component m_eventLoggingComponent; |
| protected Component m_loggingComponent; |
| |
| /** |
| * Overridden to ensure that our {@link #tearDown()} method is always called, even when {@link #setUp()} fails with |
| * an exception (by default, JUnit does not call this method when the set up fails). |
| * |
| * @see junit.framework.TestCase#runBare() |
| */ |
| @Override |
| public final void runBare() throws Throwable { |
| Throwable exception = null; |
| try { |
| setUp(); |
| |
| runTest(); |
| } |
| catch (Throwable running) { |
| exception = running; |
| } |
| finally { |
| try { |
| tearDown(); |
| } |
| catch (Throwable tearingDown) { |
| if (exception == null) |
| exception = tearingDown; |
| } |
| } |
| if (exception != null) |
| throw exception; |
| } |
| |
| /** |
| * Write configuration for a single service. For example, |
| * |
| * <pre> |
| * configure("org.apache.felix.http", |
| * "org.osgi.service.http.port", "1234"); |
| * </pre> |
| * |
| * @param pid |
| * the configuration PID to configure; |
| * @param configuration |
| * the configuration key/values (as pairs). |
| */ |
| protected void configure(String pid, String... configuration) throws IOException { |
| Properties props = properties(configuration); |
| Configuration config = getConfiguration(pid); |
| config.update(props); |
| m_trackedConfigurations.add(config); |
| } |
| |
| /** |
| * The 'after' callback will be called after all components from {@link #getDependencies} have been started.<br> |
| * <br> |
| * The {@link #after} callback is most useful for configuring additional services after all mandatory services are |
| * resolved. |
| */ |
| protected void configureAdditionalServices() throws Exception { |
| } |
| |
| /** |
| * Creates a factory configuration with the given properties, just like {@link #configure}. |
| * |
| * @return The PID of newly created configuration. |
| */ |
| protected String configureFactory(String factoryPid, String... configuration) throws IOException { |
| Properties props = properties(configuration); |
| Configuration config = createFactoryConfiguration(factoryPid); |
| config.update(props); |
| m_trackedConfigurations.add(config); |
| return config.getPid(); |
| } |
| |
| /** |
| * Configures the "org.apache.felix.http" and waits until the service is actually ready to process requests. |
| * <p> |
| * The reason that this method exists is that configuring the Felix HTTP bundle causes it to actually stop and |
| * restart, which is done asynchronously. This means that we cannot be sure that depending code is always able to |
| * directly use the HTTP service after its been configured. |
| * </p> |
| * |
| * @param port |
| * the new port to run the HTTP service on; |
| * @param configuration |
| * the extra (optional) configuration key/values (as pairs). |
| * @see #configure(String, String...) |
| */ |
| protected void configureHttpService(int port, String... configuration) throws IOException, InterruptedException { |
| final String httpPID = "org.apache.felix.http"; |
| final String portProperty = "org.osgi.service.http.port"; |
| final String expectedPort = Integer.toString(port); |
| |
| // Do not track this configuration (yet)... |
| Properties props = properties(configuration); |
| props.put(portProperty, expectedPort); |
| |
| Configuration config = getConfiguration(httpPID); |
| config.update(props); |
| |
| // This ugly warth is necessary as Felix HTTP currently brings the entire service down & up if it gets |
| // reconfigured. There is no other way for us to tell whether the server is ready to accept calls... |
| URL url = new URL(String.format("http://localhost:%d/", port)); |
| int tries = 50; |
| boolean ready = false; |
| do { |
| Thread.sleep(50); |
| |
| try { |
| InputStream is = url.openStream(); |
| is.close(); |
| ready = true; |
| } |
| catch (ConnectException exception) { |
| // Not there yet... |
| } |
| catch (FileNotFoundException exception) { |
| // Ok; expected... |
| ready = true; |
| } |
| } |
| while (!ready && tries-- > 0); |
| |
| if (tries == 0) { |
| throw new IOException("Failed waiting on HTTP service?!"); |
| } |
| } |
| |
| /** |
| * The 'before' callback will be called after the components from {@link #getDependencies} have been added, but you |
| * cannot necessarily rely on injected members here. You can use the {@link #configure} and |
| * {@link #configureFactory} methods, as well as the {@link #getService} methods.<br> |
| * <br> |
| * The {@link #before} callback is most useful for configuring services that have been provisioned in the |
| * 'configuration' method. |
| */ |
| protected void configureProvisionedServices() throws Exception { |
| } |
| |
| /** |
| * Bridge method for dependency manager. |
| * |
| * @return a new {@link Component}. |
| */ |
| protected Component createComponent() { |
| return m_dependencyManager.createComponent(); |
| } |
| |
| /** |
| * Creates a new factory configuration. |
| * |
| * @param factoryPid |
| * the PID of the factory to create a new configuration for. |
| * @return a new {@link Configuration} object, never <code>null</code>. |
| * @throws IOException |
| * if access to the persistent storage failed. |
| */ |
| protected Configuration createFactoryConfiguration(String factoryPid) throws IOException { |
| ConfigurationAdmin admin = getService(ConfigurationAdmin.class); |
| Configuration config = admin.createFactoryConfiguration(factoryPid, null); |
| m_trackedConfigurations.add(config); |
| return config; |
| } |
| |
| /** |
| * Bridge method for dependency manager. |
| * |
| * @return a new {@link ServiceDependency}. |
| */ |
| protected ServiceDependency createServiceDependency() { |
| return m_dependencyManager.createServiceDependency(); |
| } |
| |
| /** |
| * Disables logging to the console. |
| */ |
| protected synchronized void disableEventLogging() { |
| if (m_eventLoggingComponent != null) { |
| DependencyManager dm = m_dependencyManager; |
| dm.remove(m_eventLoggingComponent); |
| m_eventLoggingComponent = null; |
| } |
| } |
| |
| /** |
| * Disables logging to the console. |
| */ |
| protected synchronized void disableLogging() { |
| if (m_loggingComponent != null) { |
| DependencyManager dm = m_dependencyManager; |
| dm.remove(m_loggingComponent); |
| m_loggingComponent = null; |
| } |
| } |
| |
| protected void doTearDown() throws Exception { |
| // Nop |
| } |
| |
| /** |
| * Enables logging events to the console. Mainly useful when debugging tests. |
| */ |
| protected synchronized void enableEventLogging() { |
| DependencyManager dm = m_dependencyManager; |
| m_eventLoggingComponent = dm.createComponent() |
| .setInterface(EventHandler.class.getName(), new Properties() { |
| { |
| put(EventConstants.EVENT_TOPIC, "*"); |
| } |
| }) |
| .setImplementation(new EventHandler() { |
| @Override |
| public void handleEvent(Event event) { |
| System.out.print("[EVENT] " + event.getTopic()); |
| for (String key : event.getPropertyNames()) { |
| System.out.print(" " + key + "=" + event.getProperty(key)); |
| } |
| System.out.println(); |
| } |
| }); |
| dm.add(m_eventLoggingComponent); |
| } |
| |
| /** |
| * Enables logging to the console. Mainly useful when debugging tests. |
| */ |
| protected synchronized void enableLogging() { |
| if (m_loggingComponent == null) { |
| DependencyManager dm = m_dependencyManager; |
| m_loggingComponent = dm.createComponent() |
| .setInterface(LogService.class.getName(), new Properties() { |
| { |
| put(Constants.SERVICE_RANKING, Integer.valueOf(1000)); |
| } |
| }) |
| .setImplementation(new LogService() { |
| @Override |
| public void log(int level, String message) { |
| log(null, level, message, null); |
| } |
| |
| @Override |
| public void log(int level, String message, Throwable exception) { |
| log(null, level, message, exception); |
| } |
| |
| @Override |
| public void log(ServiceReference sr, int level, String message) { |
| log(sr, level, message, null); |
| } |
| |
| @Override |
| public void log(ServiceReference sr, int level, String message, Throwable exception) { |
| System.out.println("[LOG] " + |
| (sr == null ? "" : sr + " ") + |
| level + " " + |
| message + " " + |
| (exception == null ? "" : exception)); |
| } |
| }); |
| dm.add(m_loggingComponent); |
| } |
| } |
| |
| /** |
| * Gets an existing configuration or creates a new one, in case it does not exist. |
| * |
| * @param pid |
| * the PID of the configuration to return. |
| * @return a {@link Configuration} instance, never <code>null</code>. |
| * @throws IOException |
| * if access to the persistent storage failed. |
| */ |
| protected Configuration getConfiguration(String pid) throws IOException { |
| ConfigurationAdmin admin = getService(ConfigurationAdmin.class); |
| Configuration configuration = admin.getConfiguration(pid, null); |
| m_trackedConfigurations.add(configuration); |
| return configuration; |
| } |
| |
| /** |
| * Gets a list of components that must be started before the test is started; this useful to (a) add additional |
| * services, e.g. services that should be picked up by the service under test, or (b) to declare 'this' as a |
| * component, and get services injected. |
| */ |
| protected Component[] getDependencies() { |
| return new Component[0]; |
| } |
| |
| /** |
| * Returns a list of strings representing the result of the given request URL. |
| * |
| * @param requestURL |
| * the URL to access and return the response as strings. |
| * @return a list of strings, never <code>null</code>. |
| * @throws IOException |
| * in case accessing the requested URL failed. |
| */ |
| protected List<String> getResponse(String requestURL) throws IOException { |
| return getResponse(new URL(requestURL)); |
| } |
| |
| /** |
| * Returns a list of strings representing the result of the given request URL. |
| * |
| * @param requestURL |
| * the URL to access and return the response as strings. |
| * @return a list of strings, never <code>null</code>. |
| * @throws IOException |
| * in case accessing the requested URL failed. |
| */ |
| protected List<String> getResponse(URL requestURL) throws IOException { |
| List<String> result = new ArrayList<String>(); |
| InputStream in = null; |
| try { |
| in = requestURL.openConnection().getInputStream(); |
| |
| final StringBuilder element = new StringBuilder(); |
| int b; |
| while ((b = in.read()) > 0) { |
| switch (b) { |
| case '\n': |
| result.add(element.toString()); |
| element.setLength(0); |
| break; |
| default: |
| element.append((char) b); |
| } |
| } |
| if (element.length() > 0) { |
| result.add(element.toString()); |
| } |
| } |
| finally { |
| try { |
| in.close(); |
| } |
| catch (Exception e) { |
| // no problem. |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Convenience method to return an OSGi service. |
| * |
| * @param serviceClass |
| * the service class to return. |
| * @return a service instance, can be <code>null</code>. |
| */ |
| protected <T> T getService(Class<T> serviceClass) { |
| try { |
| return getService(serviceClass, null); |
| } |
| catch (InvalidSyntaxException e) { |
| return null; |
| // Will not happen, since we don't pass in a filter. |
| } |
| } |
| |
| /** |
| * Convenience method to return an OSGi service. |
| * |
| * @param serviceClass |
| * the service class to return; |
| * @param filterString |
| * the (optional) filter string, can be <code>null</code>. |
| * @return a service instance, can be <code>null</code>. |
| */ |
| @SuppressWarnings("unchecked") |
| protected <T> T getService(Class<T> serviceClass, String filterString) throws InvalidSyntaxException { |
| T serviceInstance = null; |
| |
| if (filterString != null && !"".equals(filterString)) { |
| filterString = String.format("(&(%s=%s)%s)", Constants.OBJECTCLASS, serviceClass.getName(), filterString); |
| } |
| else { |
| filterString = String.format("(%s=%s)", Constants.OBJECTCLASS, serviceClass.getName()); |
| } |
| |
| ServiceTracker serviceTracker = m_trackedServices.get(filterString); |
| if (serviceTracker == null) { |
| serviceTracker = new ServiceTracker(m_bundleContext, FrameworkUtil.createFilter(filterString), null); |
| serviceTracker.open(); |
| |
| m_trackedServices.put(filterString, serviceTracker); |
| } |
| |
| try { |
| serviceInstance = (T) serviceTracker.waitForService(SERVICE_TIMEOUT * 1000); |
| |
| if (serviceInstance == null) { |
| fail(serviceClass + " service not found."); |
| } |
| |
| return serviceInstance; |
| } |
| catch (InterruptedException e) { |
| e.printStackTrace(); |
| serviceTracker.close(); |
| fail(serviceClass + " service not available: " + e.toString()); |
| } |
| |
| return serviceInstance; |
| } |
| |
| /** |
| * Utility method to determine the number of test cases in the implementing class. |
| * <p> |
| * Test cases are considered <em>public</em> methods starting their name with "test". |
| * </p> |
| * |
| * @return a test count, >= 0. |
| */ |
| protected final int getTestCount() { |
| int count = 0; |
| |
| for (Method m : getClass().getMethods()) { |
| if (m.getName().startsWith("test")) { |
| count++; |
| } |
| } |
| |
| return count; |
| } |
| |
| /** |
| * @param filter |
| * @return an array of configurations, can be <code>null</code>. |
| */ |
| protected Configuration[] listConfigurations(String filter) throws IOException, InvalidSyntaxException { |
| ConfigurationAdmin admin = getService(ConfigurationAdmin.class); |
| return admin.listConfigurations(filter); |
| } |
| |
| /** |
| * Sets whether or not any of the tracked configurations should be automatically be deleted when ending a test. |
| * |
| * @param aClean |
| * <code>true</code> (the default) to clean configurations, <code>false</code> to disable this behaviour. |
| */ |
| protected void setAutoDeleteTrackedConfigurations(boolean aClean) { |
| m_cleanConfigurations = aClean; |
| } |
| |
| /** |
| * Set up of this test case. |
| */ |
| protected final void setUp() throws Exception { |
| m_bundleContext = FrameworkUtil.getBundle(getClass()).getBundleContext(); |
| m_dependencyManager = new DependencyManager(m_bundleContext); |
| |
| Component[] components = getDependencies(); |
| ComponentCounter listener = new ComponentCounter(components); |
| |
| // Register our listener for all the services... |
| for (Component component : components) { |
| component.add(listener); |
| } |
| |
| // Then give them to the dependency manager... |
| for (Component component : components) { |
| m_dependencyManager.add(component); |
| } |
| |
| System.setProperty("org.apache.ace.server.port", Integer.toString(TestConstants.PORT)); |
| |
| // Ensure the HTTP service is running on the port we expect... |
| int port = Integer.getInteger("org.osgi.service.http.port", 8080); |
| if (port != TestConstants.PORT) { |
| configureHttpService(TestConstants.PORT); |
| } |
| |
| // Call back the implementation... |
| configureProvisionedServices(); |
| |
| // And wait for all components to come online. |
| try { |
| if (!listener.waitForEmpty(SERVICE_TIMEOUT, SECONDS)) { |
| fail("Not all components were started. Still missing the following:\n" + listener.componentsString()); |
| } |
| |
| configureAdditionalServices(); |
| } |
| catch (InterruptedException e) { |
| fail("Interrupted while waiting for services to get started."); |
| } |
| } |
| |
| @Override |
| protected final void tearDown() throws Exception { |
| try { |
| doTearDown(); |
| } |
| finally { |
| if (m_cleanConfigurations) { |
| for (Configuration c : m_trackedConfigurations) { |
| try { |
| c.delete(); |
| } |
| catch (Exception exception) { |
| // Ignore... |
| } |
| } |
| m_trackedConfigurations.clear(); |
| } |
| if (m_closeServiceTrackers) { |
| for (ServiceTracker st : m_trackedServices.values()) { |
| try { |
| st.close(); |
| } |
| catch (Exception exception) { |
| // Ignore... |
| } |
| } |
| m_trackedServices.clear(); |
| } |
| } |
| } |
| } |