diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java |
Publish
Diffstat (limited to 'jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java')
-rw-r--r-- | jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java b/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java new file mode 100644 index 00000000000..f5a0f83f4ba --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java @@ -0,0 +1,402 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.test; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.binder.AnnotatedBindingBuilder; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.*; +import com.yahoo.jdisc.core.ApplicationLoader; +import com.yahoo.jdisc.core.BootstrapLoader; +import com.yahoo.jdisc.core.FelixFramework; +import com.yahoo.jdisc.core.FelixParams; +import com.yahoo.jdisc.handler.*; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.net.URI; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * <p>This class provides a unified way to set up and run unit tests on jDISC components. In short, it is a programmable + * {@link BootstrapLoader} that provides convenient access to the {@link ContainerActivator} and {@link + * CurrentContainer} interfaces. A typical test case using this class looks as follows:</p> + * <pre> + * {@literal @}Test + * public void requireThatMyComponentIsWellBehaved() { + * TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + * ContainerBuilder builder = driver.newContainerBuilder(); + * (... configure builder ...) + * driver.activateContainer(builder); + * (... run tests ...) + * assertTrue(driver.close()); + * } + * </pre> + * <p>One of the most important things to remember when using this class is to always call {@link #close()} at the end + * of your test case. This ensures that the tested configuration does not prevent graceful shutdown. If close() returns + * FALSE, it means that either your components or the test case itself does not conform to the reference counting + * requirements of {@link Request}, {@link RequestHandler}, {@link ContentChannel}, or {@link CompletionHandler}.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TestDriver implements ContainerActivator, CurrentContainer { + + private static final AtomicInteger testId = new AtomicInteger(0); + private final FutureTask<Boolean> closeTask = new FutureTask<>(new CloseTask()); + private final ApplicationLoader loader; + + private TestDriver(ApplicationLoader loader) { + this.loader = loader; + } + + @Override + public ContainerBuilder newContainerBuilder() { + return loader.newContainerBuilder(); + } + + @Override + public DeactivatedContainer activateContainer(ContainerBuilder builder) { + return loader.activateContainer(builder); + } + + @Override + public Container newReference(URI uri) { + return loader.newReference(uri); + } + + /** + * <p>Returns the {@link BootstrapLoader} used by this TestDriver. Use caution when invoking methods on the + * BootstrapLoader directly, since the lifecycle management done by this TestDriver may become corrupt.</p> + * + * @return The BootstrapLoader. + */ + public BootstrapLoader bootstrapLoader() { + return loader; + } + + /** + * <p>Returns the {@link Application} loaded by this TestDriver. Until {@link #close()} is called, this method will + * never return null.</p> + * + * @return The loaded Application. + */ + public Application application() { + return loader.application(); + } + + /** + * <p>Returns the {@link OsgiFramework} created by this TestDriver. Although this method will never return null, it + * might return a {@link NonWorkingOsgiFramework} depending on the factory method used to instantiate it.</p> + * + * @return The OSGi framework. + */ + public OsgiFramework osgiFramework() { + return loader.osgiFramework(); + } + + /** + * <p>Convenience method to create and {@link Request#connect(ResponseHandler)} a {@link Request} on the {@link + * CurrentContainer}. This method will either return the corresponding {@link ContentChannel} or throw the + * appropriate exception (see {@link Request#connect(ResponseHandler)}).</p> + * + * @param requestUri The URI string to parse and pass to the Request constructor. + * @param responseHandler The ResponseHandler to pass to {@link Request#connect(ResponseHandler)}. + * @return The ContentChannel returned by {@link Request#connect(ResponseHandler)}. + * @throws NullPointerException If the URI string or the {@link ResponseHandler} is null. + * @throws IllegalArgumentException If the URI string violates RFC 2396. + * @throws BindingNotFoundException If the corresponding call to {@link Container#resolveHandler(Request)} + * returns null. + * @throws RequestDeniedException If the corresponding call to {@link RequestHandler#handleRequest(Request, + * ResponseHandler)} returns null. + */ + public ContentChannel connectRequest(String requestUri, ResponseHandler responseHandler) { + return newRequestDispatch(requestUri, responseHandler).connect(); + } + + /** + * <p>Convenience method to create a {@link Request}, connect it to a {@link RequestHandler}, and close the returned + * {@link ContentChannel}. This is the same as calling:</p> + * <pre> + * connectRequest(uri, responseHandler).close(null); + * </pre> + * + * @param requestUri The URI string to parse and pass to the Request constructor. + * @param responseHandler The ResponseHandler to pass to {@link Request#connect(ResponseHandler)}. + * @return A waitable Future that provides access to the corresponding {@link Response}. + * @throws NullPointerException If the URI string or the {@link ResponseHandler} is null. + * @throws IllegalArgumentException If the URI string violates RFC 2396. + * @throws BindingNotFoundException If the corresponding call to {@link Container#resolveHandler(Request)} + * returns null. + * @throws RequestDeniedException If the corresponding call to {@link RequestHandler#handleRequest(Request, + * ResponseHandler)} returns null. + */ + public Future<Response> dispatchRequest(String requestUri, ResponseHandler responseHandler) { + return newRequestDispatch(requestUri, responseHandler).dispatch(); + } + + /** + * <p>Initiates the shut down of this TestDriver in another thread. By doing this in a separate thread, it allows + * other code to monitor its progress. Unless you need the added monitoring capability, you should use {@link + * #close()} instead.</p> + * + * @see #awaitClose(long, TimeUnit) + */ + public void scheduleClose() { + new Thread(closeTask, "TestDriver.Closer").start(); + } + + /** + * <p>Waits for shut down of this TestDriver to complete. This call must be preceded by a call to {@link + * #scheduleClose()}.</p> + * + * @param timeout The maximum time to wait. + * @param unit The time unit of the timeout argument. + * @return True if shut down completed within the allocated time. + */ + public boolean awaitClose(long timeout, TimeUnit unit) { + try { + closeTask.get(timeout, unit); + return true; + } catch (TimeoutException e) { + return false; + } catch (Exception e) { + throw e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e); + } + } + + /** + * <p>Initiatiates shut down of this TestDriver and waits for it to complete. If shut down fails to complete within + * 60 seconds, this method throws an exception.</p> + * + * @return True if shut down completed within the allocated time. + * @throws IllegalStateException If shut down failed to complete within the allocated time. + */ + public boolean close() { + scheduleClose(); + if ( ! awaitClose(600, TimeUnit.SECONDS)) { + throw new IllegalStateException("Application failed to terminate within allocated time."); + } + return true; + } + + /** + * <p>Creates a new {@link RequestDispatch} that dispatches a {@link Request} with the given URI and {@link + * ResponseHandler}.</p> + * + * @param requestUri The uri of the Request to create. + * @param responseHandler The ResponseHandler to use for the dispather. + * @return The created RequestDispatch. + */ + public RequestDispatch newRequestDispatch(final String requestUri, final ResponseHandler responseHandler) { + return new RequestDispatch() { + + @Override + protected Request newRequest() { + return new Request(loader, URI.create(requestUri)); + } + + @Override + public ContentChannel handleResponse(Response response) { + return responseHandler.handleResponse(response); + } + }; + } + + /** + * <p>Creates a new TestDriver with an injected {@link Application}.</p> + * + * @param appClass The Application class to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newInjectedApplicationInstance(Class<? extends Application> appClass, + Module... guiceModules) { + return newInstance(newOsgiFramework(), null, false, + newModuleList(null, appClass, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with an injected {@link Application}, but without OSGi support.</p> + * + * @param appClass The Application class to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + * @see #newInjectedApplicationInstance(Class, Module...) + * @see #newNonWorkingOsgiFramework() + */ + public static TestDriver newInjectedApplicationInstanceWithoutOsgi(Class<? extends Application> appClass, + Module... guiceModules) { + return newInstance(newNonWorkingOsgiFramework(), null, false, + newModuleList(null, appClass, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with an injected {@link Application}.</p> + * + * @param app The Application to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newInjectedApplicationInstance(Application app, Module... guiceModules) { + return newInstance(newOsgiFramework(), null, false, newModuleList(app, null, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with an injected {@link Application}, but without OSGi support.</p> + * + * @param app The Application to inject. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + * @see #newInjectedApplicationInstance(Application, Module...) + * @see #newNonWorkingOsgiFramework() + */ + public static TestDriver newInjectedApplicationInstanceWithoutOsgi(Application app, Module... guiceModules) { + return newInstance(newNonWorkingOsgiFramework(), null, false, newModuleList(app, null, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with a predefined {@link Application} implementation. The injected Application class + * implements nothing but the bare minimum to conform to the Application interface.</p> + * + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newSimpleApplicationInstance(Module... guiceModules) { + return newInstance(newOsgiFramework(), null, false, + newModuleList(null, SimpleApplication.class, guiceModules)); + } + + /** + * <p>Creates a new TestDriver with a predefined {@link Application} implementation, but without OSGi support. The + * injected Application class implements nothing but the bare minimum to conform to the Application interface.</p> + * + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + * @see #newSimpleApplicationInstance(Module...) + * @see #newNonWorkingOsgiFramework() + */ + public static TestDriver newSimpleApplicationInstanceWithoutOsgi(Module... guiceModules) { + return newInstance(newNonWorkingOsgiFramework(), null, false, + newModuleList(null, SimpleApplication.class, guiceModules)); + } + + /** + * <p>Creates a new TestDriver from an application bundle. This runs the same code path as the actual jDISC startup + * code. Note that the named bundle must have a "X-JDisc-Application" bundle instruction, or setup will fail.</p> + * + * @param bundleLocation The location of the application bundle to load. + * @param privileged Whether or not privileges should be marked as available to the application bundle. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newApplicationBundleInstance(String bundleLocation, boolean privileged, + Module... guiceModules) { + return newInstance(newOsgiFramework(), bundleLocation, privileged, Arrays.asList(guiceModules)); + } + + /** + * <p>Creates a new TestDriver with the given parameters. This is the factory method that all other factory methods + * call. It allows you to specify all parts of the TestDriver manually.</p> + * + * @param osgiFramework The {@link OsgiFramework} to assign to the created TestDriver. + * @param bundleLocation The location of the application bundle to load, may be null. + * @param privileged Whether or not privileges should be marked as available to the application bundle. + * @param guiceModules The Guice {@link Module Modules} to install prior to startup. + * @return The created TestDriver. + */ + public static TestDriver newInstance(OsgiFramework osgiFramework, String bundleLocation, boolean privileged, + Module... guiceModules) { + return newInstance(osgiFramework, bundleLocation, privileged, Arrays.asList(guiceModules)); + } + + /** + * <p>Factory method to create a working {@link OsgiFramework}. This method is used by all {@link TestDriver} + * factories that DO NOT have the "WithoutOsgi" suffix.</p> + * + * @return A working OsgiFramework. + */ + public static FelixFramework newOsgiFramework() { + return new FelixFramework(new FelixParams().setCachePath("target/bundlecache" + testId.getAndIncrement())); + } + + /** + * <p>Factory method to create a light-weight {@link OsgiFramework} that throws {@link + * UnsupportedOperationException} if {@link OsgiFramework#installBundle(String)} or {@link + * OsgiFramework#startBundles(List, boolean)} is called. This allows for unit testing without the footprint of OSGi + * support. This method is used by {@link TestDriver} factories that have the "WithoutOsgi" suffix.</p> + * + * @return A non-working OsgiFramework. + */ + public static OsgiFramework newNonWorkingOsgiFramework() { + return new NonWorkingOsgiFramework(); + } + + private class CloseTask implements Callable<Boolean> { + + @Override + public Boolean call() throws Exception { + loader.stop(); + loader.destroy(); + return true; + } + } + + private static TestDriver newInstance(OsgiFramework osgiFramework, String bundleLocation, boolean privileged, + Iterable<? extends Module> guiceModules) { + ApplicationLoader loader = new ApplicationLoader(osgiFramework, guiceModules); + try { + loader.init(bundleLocation, privileged); + } catch (Exception e) { + throw e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e); + } + try { + loader.start(); + } catch (Exception e) { + loader.destroy(); + throw e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e); + } + return new TestDriver(loader); + } + + private static List<Module> newModuleList(final Application app, final Class<? extends Application> appClass, + Module... guiceModules) { + List<Module> lst = new LinkedList<>(); + lst.addAll(Arrays.asList(guiceModules)); + lst.add(new AbstractModule() { + + @Override + public void configure() { + AnnotatedBindingBuilder<Application> builder = bind(Application.class); + if (app != null) { + builder.toInstance(app); + } else { + builder.to(appClass); + } + } + }); + return lst; + } + + private static class SimpleApplication implements Application { + + @Override + public void start() { + + } + + @Override + public void stop() { + + } + + @Override + public void destroy() { + + } + } +} |