summaryrefslogtreecommitdiffstats
path: root/jdisc_core/src/main/java/com/yahoo/jdisc/test/TestDriver.java
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /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.java402
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&nbsp;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&nbsp;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() {
+
+ }
+ }
+}