aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/test/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 /container-core/src/test/java
Publish
Diffstat (limited to 'container-core/src/test/java')
-rw-r--r--container-core/src/test/java/com/yahoo/component/chain/dependencies/.gitignore0
-rw-r--r--container-core/src/test/java/com/yahoo/component/provider/test/ComponentClassTestCase.java191
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/AccessLogRequestHandlerTest.java47
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/ThreadPoolProviderTestCase.java134
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/VipStatusHandlerTestCase.java220
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/VipStatusTestCase.java42
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/test/MockServiceTest.java88
-rw-r--r--container-core/src/test/java/com/yahoo/container/handler/test/test.txt6
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/ExtendedResponseTestCase.java89
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/HttpRequestTestCase.java104
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/HttpResponseTestCase.java82
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/LoggingRequestHandlerTestCase.java224
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/LoggingTestCase.java109
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/RequestBuilderTestCase.java46
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/ThreadedRequestHandlerTestCase.java317
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/MetricConsumerProviders.java21
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/MetricSnapshotTest.java23
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java409
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/StateMonitorBenchmarkTest.java80
-rw-r--r--container-core/src/test/java/com/yahoo/container/messagebus/cfg-disabled/.gitignore0
-rw-r--r--container-core/src/test/java/com/yahoo/container/xml/bind/JAXBContextFactoryTest.java45
-rw-r--r--container-core/src/test/java/com/yahoo/container/xml/providers/XMLProviderTest.java96
-rw-r--r--container-core/src/test/java/com/yahoo/osgi/provider/model/ComponentModelTest.java47
-rw-r--r--container-core/src/test/java/com/yahoo/processing/handler/ProcessingHandlerTestCase.java844
-rw-r--r--container-core/src/test/java/com/yahoo/processing/processors/MockUserDatabaseClient.java136
-rw-r--r--container-core/src/test/java/com/yahoo/processing/processors/MockUserDatabaseClientTest.java94
-rw-r--r--container-core/src/test/java/com/yahoo/processing/rendering/AsynchronousSectionedRendererTest.java435
-rw-r--r--container-core/src/test/java/com/yahoo/processing/rendering/TestContentChannel.java42
28 files changed, 3971 insertions, 0 deletions
diff --git a/container-core/src/test/java/com/yahoo/component/chain/dependencies/.gitignore b/container-core/src/test/java/com/yahoo/component/chain/dependencies/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/component/chain/dependencies/.gitignore
diff --git a/container-core/src/test/java/com/yahoo/component/provider/test/ComponentClassTestCase.java b/container-core/src/test/java/com/yahoo/component/provider/test/ComponentClassTestCase.java
new file mode 100644
index 00000000000..ed68afa834f
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/component/provider/test/ComponentClassTestCase.java
@@ -0,0 +1,191 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.yahoo.component.AbstractComponent;
+import org.junit.Test;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.Version;
+import com.yahoo.component.provider.ComponentClass;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.core.IntConfig;
+import com.yahoo.config.core.StringConfig;
+import com.yahoo.vespa.config.ConfigKey;
+
+/**
+ * @author <a href="gv@yahoo-inc.com">G. Voldengen</a>
+ */
+@SuppressWarnings("unused")
+public class ComponentClassTestCase {
+
+ @Test
+ public void testComponentConstructor() throws NoSuchMethodException {
+ ComponentClass<A> a = new ComponentClass<>(A.class);
+ assertEquals(A.preferred(), a.getPreferredConstructor().getConstructor());
+
+ ComponentClass<B> b = new ComponentClass<>(B.class);
+ assertEquals(B.preferred(), b.getPreferredConstructor().getConstructor());
+
+ ComponentClass<C> c = new ComponentClass<>(C.class);
+ assertEquals(C.preferred(), c.getPreferredConstructor().getConstructor());
+
+ ComponentClass<E> e = new ComponentClass<>(E.class);
+ assertEquals(E.preferred(), e.getPreferredConstructor().getConstructor());
+
+ ComponentClass<G> g = new ComponentClass<>(G.class);
+ assertEquals(G.preferred(), g.getPreferredConstructor().getConstructor());
+
+ try {
+ ComponentClass<H> h = new ComponentClass<>(H.class);
+ fail("Expected exception due to no legal public constructors.");
+ } catch (IllegalArgumentException expected) {
+ assertTrue(expected.getMessage().contains("must have at least one public constructor with an optional " +
+ "component ID followed by an optional FileAcquirer and zero or more config arguments"));
+ }
+
+ try {
+ ComponentClass<I> i = new ComponentClass<>(I.class);
+ fail("Expected exception due to no public constructors.");
+ } catch (RuntimeException expected) {
+ assertTrue(expected.getMessage().contains("Class has no public constructors"));
+ }
+
+ try {
+ ComponentClass<J> j = new ComponentClass<>(J.class);
+ fail("Expected exception due to no public constructors.");
+ } catch (RuntimeException expected) {
+ assertTrue(expected.getMessage().contains("Class has no public constructors"));
+ }
+
+ ComponentClass<K> k = new ComponentClass<>(K.class);
+ assertEquals(K.preferred(), k.getPreferredConstructor().getConstructor());
+
+ ComponentClass<L> l = new ComponentClass<>(L.class);
+ assertEquals(L.preferred(), l.getPreferredConstructor().getConstructor());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testCreateComponent() throws NoSuchMethodException {
+ Map<ConfigKey, ConfigInstance> availableConfigs = new HashMap<>();
+ String configId = "testConfigId";
+ availableConfigs.put(new ConfigKey(StringConfig.class, configId), new StringConfig(new StringConfig.Builder()));
+ availableConfigs.put(new ConfigKey(IntConfig.class, configId), new IntConfig(new IntConfig.Builder()));
+
+ ComponentClass<TestComponent> testClass = new ComponentClass<>(TestComponent.class);
+ TestComponent component = testClass.
+ createComponent(new ComponentId("test", new Version(1)), availableConfigs, configId);
+ assertEquals("test", component.getId().getName());
+ assertEquals(1, component.getId().getVersion().getMajor());
+ assertEquals(1, component.intVal);
+ assertEquals("_default_", component.stringVal);
+ }
+
+ /**
+ * Verifies that ComponentClass sets the ComponentId when a component that takes a ComponentId as
+ * constructor argument fails to call super(id).
+ */
+ @Test
+ public void testNullIdComponent() throws NoSuchMethodException {
+ ComponentClass<NullIdComponent> testClass = new ComponentClass<>(NullIdComponent.class);
+ NullIdComponent component = testClass.createComponent(new ComponentId("null-test", new Version(1)), new HashMap<ConfigKey, ConfigInstance>(), null);
+ assertEquals("null-test", component.getId().getName());
+ assertEquals(1, component.getId().getVersion().getMajor());
+ }
+
+ public static class TestComponent extends AbstractComponent {
+ private int intVal = 0;
+ private String stringVal = "";
+ public TestComponent(ComponentId id, IntConfig intConfig, StringConfig stringConfig) {
+ super(id);
+ intVal = intConfig.intVal();
+ stringVal = stringConfig.stringVal();
+ }
+ }
+
+ /**
+ * This component takes a ComponentId as constructor arg, but "forgets" to call super(id).
+ */
+ public static class NullIdComponent extends AbstractComponent {
+ private int intVal = 0;
+ private String stringVal = "";
+ public NullIdComponent(ComponentId id) {
+ }
+ }
+
+ private static class A extends AbstractComponent {
+ public A(IntConfig intConfig) { }
+ public A(IntConfig intConfig, StringConfig stringConfig) { }
+ static Constructor<A> preferred() throws NoSuchMethodException{
+ return A.class.getConstructor(IntConfig.class, StringConfig.class);
+ }
+ }
+
+ private static class B extends AbstractComponent {
+ public B(ComponentId id, IntConfig intConfig) { }
+ public B(IntConfig intConfig) { }
+ static Constructor<B> preferred() throws NoSuchMethodException{
+ return B.class.getConstructor(ComponentId.class, IntConfig.class);
+ }
+ }
+
+ private static class C extends AbstractComponent {
+ public C(IntConfig intConfig, ComponentId id) { }
+ public C(String id, IntConfig intConfig) { }
+ static Constructor<C> preferred() throws NoSuchMethodException{
+ return C.class.getConstructor(IntConfig.class, ComponentId.class);
+ }
+ }
+
+ private static class E extends AbstractComponent {
+ public E(IntConfig intConfig) { }
+ public E(String id, String illegal, IntConfig intConfig, StringConfig stringConfig) { }
+ static Constructor<E> preferred() throws NoSuchMethodException{
+ return E.class.getConstructor(IntConfig.class);
+ }
+ }
+
+ private static class G extends AbstractComponent {
+ public G(ComponentId id) { }
+ public G(String id) { }
+ static Constructor<G> preferred() throws NoSuchMethodException{
+ return G.class.getConstructor(ComponentId.class);
+ }
+ }
+
+ private static class H extends AbstractComponent {
+ public H(ComponentId id, String illegal) { }
+ public H(String id, String illegal) { }
+ }
+
+ private static class I extends AbstractComponent {
+ protected I(ComponentId id) { }
+ }
+
+ private static class J extends AbstractComponent {
+ }
+
+ private static class K extends AbstractComponent {
+ public K() { }
+ public K(ComponentId id, String illegal) { }
+ static Constructor<K> preferred() throws NoSuchMethodException{
+ return K.class.getConstructor();
+ }
+ }
+
+ private static class L extends AbstractComponent {
+ public L(long l, long ll, long lll) { }
+ public L(ComponentId id, IntConfig intConfig) { }
+ static Constructor<L> preferred() throws NoSuchMethodException{
+ return L.class.getConstructor(ComponentId.class, IntConfig.class);
+ }
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/handler/AccessLogRequestHandlerTest.java b/container-core/src/test/java/com/yahoo/container/handler/AccessLogRequestHandlerTest.java
new file mode 100644
index 00000000000..ad266a4a87f
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/handler/AccessLogRequestHandlerTest.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.handler;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.CircularArrayAccessLogKeeper;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class AccessLogRequestHandlerTest {
+
+ private final CircularArrayAccessLogKeeper keeper = new CircularArrayAccessLogKeeper();
+ private final Executor executor = mock(Executor.class);
+ private final AccessLogRequestHandler handler = new AccessLogRequestHandler(executor, keeper);
+ private final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ @Test
+ public void testOneLogLine() throws IOException {
+ keeper.addUri("foo");
+ HttpResponse response = handler.handle(null);
+ response.render(out);
+ assertThat(out.toString(), is("{\"entries\":[{\"url\":\"foo\"}]}"));
+ }
+
+ @Test
+ public void testEmpty() throws IOException {
+ HttpResponse response = handler.handle(null);
+ response.render(out);
+ assertThat(out.toString(), is("{\"entries\":[]}"));
+ }
+
+ @Test
+ public void testManyLogLines() throws IOException {
+ keeper.addUri("foo");
+ keeper.addUri("foo");
+ HttpResponse response = handler.handle(null);
+ response.render(out);
+ assertThat(out.toString(), is("{\"entries\":[{\"url\":\"foo\"},{\"url\":\"foo\"}]}"));
+ }
+
+} \ No newline at end of file
diff --git a/container-core/src/test/java/com/yahoo/container/handler/ThreadPoolProviderTestCase.java b/container-core/src/test/java/com/yahoo/container/handler/ThreadPoolProviderTestCase.java
new file mode 100644
index 00000000000..95ce73b4414
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/handler/ThreadPoolProviderTestCase.java
@@ -0,0 +1,134 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.handler;
+
+import static org.junit.Assert.fail;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+import com.yahoo.container.protect.ProcessTerminator;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.yahoo.concurrent.Receiver;
+import com.yahoo.concurrent.Receiver.MessageState;
+import com.yahoo.collections.Tuple2;
+import com.yahoo.jdisc.Metric;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Check threadpool provider accepts tasks and shuts down properly.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class ThreadPoolProviderTestCase {
+
+ @Test
+ public final void testThreadPoolProvider() throws InterruptedException {
+ ThreadpoolConfig config = new ThreadpoolConfig(new ThreadpoolConfig.Builder().maxthreads(1));
+ ThreadPoolProvider provider = new ThreadPoolProvider(config, Mockito.mock(Metric.class));
+ Executor exec = provider.get();
+ Tuple2<MessageState, Boolean> reply;
+ FlipIt command = new FlipIt();
+ for (boolean done = false; !done;) {
+ try {
+ exec.execute(command);
+ done = true;
+ } catch (RejectedExecutionException e) {
+ // just try again
+ }
+ }
+ reply = command.didItRun.get(5 * 60 * 1000);
+ if (reply.first != MessageState.VALID) {
+ fail("Executor task probably timed out, five minutes should be enough to flip a boolean.");
+ }
+ if (reply.second != Boolean.TRUE) {
+ fail("Executor task seemed to run, but did not get correct value.");
+ }
+ provider.deconstruct();
+ command = new FlipIt();
+ try {
+ exec.execute(command);
+ } catch (final RejectedExecutionException e) {
+ // this is what should happen
+ return;
+ }
+ fail("Pool did not reject tasks after shutdown.");
+ }
+
+ private class FlipIt implements Runnable {
+ public final Receiver<Boolean> didItRun = new Receiver<>();
+
+ @Override
+ public void run() {
+ didItRun.put(Boolean.TRUE);
+ }
+ }
+
+ @Test
+ @Ignore
+ public final void testThreadPoolProviderTerminationOnBreakdown() throws InterruptedException {
+ ThreadpoolConfig config = new ThreadpoolConfig(new ThreadpoolConfig.Builder().maxthreads(2)
+ .maxThreadExecutionTimeSeconds(1));
+ MockProcessTerminator terminator = new MockProcessTerminator();
+ ThreadPoolProvider provider = new ThreadPoolProvider(config, Mockito.mock(Metric.class), terminator);
+
+ // No dying when threads hang shorter than max thread execution time
+ provider.get().execute(new Hang(500));
+ provider.get().execute(new Hang(500));
+ assertEquals(0, terminator.dieRequests);
+ assertRejected(provider, new Hang(500)); // no more threads
+ assertEquals(0, terminator.dieRequests); // ... but not for long enough yet
+ try { Thread.sleep(1500); } catch (InterruptedException e) {}
+ provider.get().execute(new Hang(1));
+ assertEquals(0, terminator.dieRequests);
+ try { Thread.sleep(50); } catch (InterruptedException e) {} // Make sure both threads are available
+
+ // Dying when hanging both thread pool threads for longer than max thread execution time
+ provider.get().execute(new Hang(2000));
+ provider.get().execute(new Hang(2000));
+ assertEquals(0, terminator.dieRequests);
+ assertRejected(provider, new Hang(2000)); // no more threads
+ assertEquals(0, terminator.dieRequests); // ... but not for long enough yet
+ try { Thread.sleep(1500); } catch (InterruptedException e) {}
+ assertRejected(provider, new Hang(2000)); // no more threads
+ assertEquals(1, terminator.dieRequests); // ... for longer than maxThreadExecutionTime
+ }
+
+ private void assertRejected(ThreadPoolProvider provider, Runnable task) {
+ try {
+ provider.get().execute(task);
+ fail("Expected execution rejected");
+ } catch (final RejectedExecutionException expected) {
+ }
+ }
+
+ private class Hang implements Runnable {
+
+ private final long hangMillis;
+
+ public Hang(int hangMillis) {
+ this.hangMillis = hangMillis;
+ }
+
+ @Override
+ public void run() {
+ try { Thread.sleep(hangMillis); } catch (InterruptedException e) {}
+ }
+
+ }
+
+ private static class MockProcessTerminator extends ProcessTerminator {
+
+ public volatile int dieRequests = 0;
+
+ @Override
+ public void logAndDie(String message, boolean dumpThreads) {
+ dieRequests++;
+ }
+
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/handler/VipStatusHandlerTestCase.java b/container-core/src/test/java/com/yahoo/container/handler/VipStatusHandlerTestCase.java
new file mode 100644
index 00000000000..a4f7bec07f6
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/handler/VipStatusHandlerTestCase.java
@@ -0,0 +1,220 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.handler;
+
+import com.google.inject.Key;
+import com.yahoo.container.core.VipStatusConfig;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.handler.BufferedContentChannel;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ReadableContentChannel;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.text.Utf8;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Check semantics of VIP status handler. Do note this handler does not need to
+ * care about the incoming URI, that's 100% handled in JDIsc by the binding
+ * pattern.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class VipStatusHandlerTestCase {
+
+ public static final class MockResponseHandler implements ResponseHandler {
+ final ReadableContentChannel channel = new ReadableContentChannel();
+
+ @Override
+ public ContentChannel handleResponse(
+ final com.yahoo.jdisc.Response response) {
+ return channel;
+ }
+ }
+
+ Metric metric = Mockito.mock(Metric.class);
+
+ @Test
+ public final void testHandleRequest() {
+ final VipStatusConfig config = new VipStatusConfig(new VipStatusConfig.Builder().accessdisk(false)
+ .noSearchBackendsImpliesOutOfService(false));
+ final VipStatusHandler handler = new VipStatusHandler(Executors.newCachedThreadPool(), config, metric);
+ final MockResponseHandler responseHandler = new MockResponseHandler();
+ final HttpRequest request = createRequest();
+ final BufferedContentChannel requestContent = createChannel();
+ handler.handleRequest(request, requestContent, responseHandler);
+ final ByteBuffer b = responseHandler.channel.read();
+ final byte[] asBytes = new byte[b.remaining()];
+ b.get(asBytes);
+ assertEquals(VipStatusHandler.OK_MESSAGE, Utf8.toString(asBytes));
+ }
+
+ public static final class NotFoundResponseHandler implements
+ ResponseHandler {
+ final ReadableContentChannel channel = new ReadableContentChannel();
+
+ @Override
+ public ContentChannel handleResponse(
+ final com.yahoo.jdisc.Response response) {
+ assertEquals(com.yahoo.jdisc.Response.Status.NOT_FOUND,
+ response.getStatus());
+ return channel;
+ }
+ }
+
+ @Test
+ public final void testFileNotFound() {
+ final VipStatusConfig config = new VipStatusConfig(new VipStatusConfig.Builder().accessdisk(true)
+ .statusfile("/VipStatusHandlerTestCaseFileThatReallyReallyShouldNotExist")
+ .noSearchBackendsImpliesOutOfService(false));
+ final VipStatusHandler handler = new VipStatusHandler(Executors.newCachedThreadPool(), config, metric);
+ final NotFoundResponseHandler responseHandler = new NotFoundResponseHandler();
+ final HttpRequest request = createRequest();
+ final BufferedContentChannel requestContent = createChannel();
+ handler.handleRequest(request, requestContent, responseHandler);
+ final ByteBuffer b = responseHandler.channel.read();
+ final byte[] asBytes = new byte[b.remaining()];
+ b.get(asBytes);
+ assertEquals(
+ VipStatusHandler.StatusResponse.COULD_NOT_FIND_STATUS_FILE,
+ Utf8.toString(asBytes));
+ }
+
+ @Test
+ public final void testFileFound() throws IOException {
+ final File statusFile = File.createTempFile("VipStatusHandlerTestCase",
+ null);
+ try {
+ final FileWriter writer = new FileWriter(statusFile);
+ final String OK = "OK\n";
+ writer.write(OK);
+ writer.close();
+ final VipStatusConfig config = new VipStatusConfig(new VipStatusConfig.Builder().accessdisk(true)
+ .statusfile(statusFile.getAbsolutePath()).noSearchBackendsImpliesOutOfService(false));
+ final VipStatusHandler handler = new VipStatusHandler(Executors.newCachedThreadPool(), config, metric);
+ final MockResponseHandler responseHandler = new MockResponseHandler();
+ final HttpRequest request = createRequest();
+ final BufferedContentChannel requestContent = createChannel();
+ handler.handleRequest(request, requestContent, responseHandler);
+ final ByteBuffer b = responseHandler.channel.read();
+ final byte[] asBytes = new byte[b.remaining()];
+ b.get(asBytes);
+ assertEquals(OK, Utf8.toString(asBytes));
+ } finally {
+ statusFile.delete();
+ }
+ }
+
+ @Test
+ public final void testProgrammaticallyRemovedFromRotation() throws IOException {
+ VipStatus vipStatus = new VipStatus();
+ final VipStatusConfig config = new VipStatusConfig(new VipStatusConfig.Builder().accessdisk(false)
+ .noSearchBackendsImpliesOutOfService(true));
+ final VipStatusHandler handler = new VipStatusHandler(Executors.newCachedThreadPool(), config, metric, vipStatus);
+
+ vipStatus.removeFromRotation(this);
+
+ {
+ final MockResponseHandler responseHandler = new MockResponseHandler();
+ final HttpRequest request = createRequest();
+ final BufferedContentChannel requestContent = createChannel();
+ handler.handleRequest(request, requestContent, responseHandler);
+ final ByteBuffer b = responseHandler.channel.read();
+ final byte[] asBytes = new byte[b.remaining()];
+ b.get(asBytes);
+ assertEquals(VipStatusHandler.StatusResponse.NO_SEARCH_BACKENDS, Utf8.toString(asBytes));
+ }
+
+ vipStatus.addToRotation(this);
+
+ {
+ final MockResponseHandler responseHandler = new MockResponseHandler();
+ final HttpRequest request = createRequest();
+ final BufferedContentChannel requestContent = createChannel();
+ handler.handleRequest(request, requestContent, responseHandler);
+ final ByteBuffer b = responseHandler.channel.read();
+ final byte[] asBytes = new byte[b.remaining()];
+ b.get(asBytes);
+ assertEquals(VipStatusHandler.OK_MESSAGE, Utf8.toString(asBytes));
+ }
+ }
+
+ public static HttpRequest createRequest() {
+ return createRequest("http://localhost/search/?query=geewhiz");
+ }
+
+ public static HttpRequest createRequest(String uri) {
+ HttpRequest request = null;
+ try {
+ request = HttpRequest.newClientRequest(new com.yahoo.jdisc.Request(
+ new MockCurrentContainer(), new URI(uri)), new URI(uri),
+ HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1);
+ request.setRemoteAddress(new InetSocketAddress(0));
+ } catch (URISyntaxException e) {
+ fail("Illegal URI string in test?");
+ }
+ return request;
+ }
+
+ public static BufferedContentChannel createChannel() {
+ BufferedContentChannel channel = new BufferedContentChannel();
+ channel.close(null);
+ return channel;
+ }
+
+ private static class MockCurrentContainer implements CurrentContainer {
+ @Override
+ public Container newReference(java.net.URI uri) {
+ return new Container() {
+
+ @Override
+ public RequestHandler resolveHandler(com.yahoo.jdisc.Request request) {
+ return null;
+ }
+
+ @Override
+ public <T> T getInstance(Key<T> tKey) {
+ return null;
+ }
+
+ @Override
+ public <T> T getInstance(Class<T> tClass) {
+ return null;
+ }
+
+ @Override
+ public ResourceReference refer() {
+ return References.NOOP_REFERENCE;
+ }
+
+ @Override
+ public void release() {
+ // NOP
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return 0;
+ }
+ };
+ }
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/handler/VipStatusTestCase.java b/container-core/src/test/java/com/yahoo/container/handler/VipStatusTestCase.java
new file mode 100644
index 00000000000..740f15622bc
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/handler/VipStatusTestCase.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.handler;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * Smoke test that VipStatus has the right basic logic.
+ *
+ * @author steinar
+ */
+public class VipStatusTestCase {
+
+ @Test
+ public final void testSmoke() {
+ Object cluster1 = new Object();
+ Object cluster2 = new Object();
+ Object cluster3 = new Object();
+ VipStatus v = new VipStatus();
+ // initial state
+ assertTrue(v.isInRotation());
+ // all clusters down
+ v.removeFromRotation(cluster1);
+ v.removeFromRotation(cluster2);
+ v.removeFromRotation(cluster3);
+ assertFalse(v.isInRotation());
+ // some clusters down
+ v.addToRotation(cluster2);
+ assertTrue(v.isInRotation());
+ // all clusters up
+ v.addToRotation(cluster1);
+ v.addToRotation(cluster3);
+ assertTrue(v.isInRotation());
+ // and down again
+ v.removeFromRotation(cluster1);
+ v.removeFromRotation(cluster2);
+ v.removeFromRotation(cluster3);
+ assertFalse(v.isInRotation());
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/handler/test/MockServiceTest.java b/container-core/src/test/java/com/yahoo/container/handler/test/MockServiceTest.java
new file mode 100644
index 00000000000..d582566cb03
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/handler/test/MockServiceTest.java
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.handler.test;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.filedistribution.fileacquirer.MockFileAcquirer;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ */
+public class MockServiceTest {
+
+ private final File testFile = new File("src/test/java/com/yahoo/container/handler/test/test.txt");
+
+ @Test
+ public void testHandlerTextFormat() throws InterruptedException, IOException {
+ HttpResponse response = runHandler(com.yahoo.jdisc.http.HttpRequest.Method.GET, "/foo/bar");
+ assertResponse(response, 200, "Hello\nThere!");
+
+ response = runHandler(com.yahoo.jdisc.http.HttpRequest.Method.GET, "http://my.host:8080/foo/bar?key1=foo&key2=bar");
+ assertResponse(response, 200, "With params!");
+
+ response = runHandler(com.yahoo.jdisc.http.HttpRequest.Method.PUT, "/bar");
+ assertResponse(response, 301, "My data is on a single line");
+ }
+
+ @Test
+ public void testNoHandlerFound() throws InterruptedException, IOException {
+ HttpResponse response = runHandler(com.yahoo.jdisc.http.HttpRequest.Method.DELETE, "/foo/bar");
+ assertThat(response.getStatus(), is(404));
+ assertResponseContents(response, "DELETE:/foo/bar was not found");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testUnknownFileType() throws InterruptedException, IOException {
+ runHandlerWithFile(com.yahoo.jdisc.http.HttpRequest.Method.GET, "", new File("nonexistant"));
+ }
+
+ @Test(expected = FileNotFoundException.class)
+ public void testExceptionResponse() throws InterruptedException, IOException {
+ runHandlerWithFile(com.yahoo.jdisc.http.HttpRequest.Method.GET, "", new File("nonexistant.txt"));
+ }
+
+ private void assertResponse(HttpResponse response, int expectedCode, String expectedMessage) throws IOException {
+ assertThat(response.getStatus(), is(expectedCode));
+ assertResponseContents(response, expectedMessage);
+ }
+
+ private void assertResponseContents(HttpResponse response, String expected) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ response.render(baos);
+ assertThat(baos.toString(), is(expected));
+ }
+
+ private void assertResponseOk(HttpResponse response) {
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getContentType(), is("text/plain"));
+ }
+
+ private HttpResponse runHandler(com.yahoo.jdisc.http.HttpRequest.Method method, String path) throws InterruptedException, IOException {
+ return runHandlerWithFile(method, path, testFile);
+ }
+
+ private HttpResponse runHandlerWithFile(com.yahoo.jdisc.http.HttpRequest.Method method, String path, File file) throws InterruptedException, IOException {
+ MockserviceConfig.Builder builder = new MockserviceConfig.Builder();
+ builder.file(file.getPath());
+ MockService handler = new MockService(new MockExecutor(), AccessLog.voidAccessLog(), MockFileAcquirer.returnFile(file), new MockserviceConfig(builder), null);
+ return handler.handle(HttpRequest.createTestRequest(path, method));
+ }
+
+ private static class MockExecutor implements Executor {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/handler/test/test.txt b/container-core/src/test/java/com/yahoo/container/handler/test/test.txt
new file mode 100644
index 00000000000..baca20fbbdc
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/handler/test/test.txt
@@ -0,0 +1,6 @@
+GET:/foo/bar:200:Hello
+There!
+
+PUT:/bar:301:My data is on a single line
+
+GET:/foo/bar?key1=foo&key2=bar:200:With params!
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/ExtendedResponseTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/ExtendedResponseTestCase.java
new file mode 100644
index 00000000000..78424b2f4e0
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/ExtendedResponseTestCase.java
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.text.Utf8;
+
+/**
+ * API test for ExtendedResponse.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class ExtendedResponseTestCase {
+
+ private static final String COM_YAHOO_CONTAINER_JDISC_EXTENDED_RESPONSE_TEST_CASE = "com.yahoo.container.jdisc.ExtendedResponseTestCase";
+ ExtendedResponse r;
+
+ private static class TestResponse extends ExtendedResponse {
+
+ public TestResponse(int status) {
+ super(status);
+ }
+
+
+ @Override
+ public void render(OutputStream output, ContentChannel networkChannel,
+ CompletionHandler handler) throws IOException {
+ // yes, this is sync rendering, so sue me :p
+ try {
+ output.write(Utf8.toBytes(COM_YAHOO_CONTAINER_JDISC_EXTENDED_RESPONSE_TEST_CASE));
+ } finally {
+ if (networkChannel != null) {
+ networkChannel.close(handler);
+ }
+ }
+ }
+ }
+
+
+ @Before
+ public void setUp() throws Exception {
+ r = new TestResponse(Response.Status.OK);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ r = null;
+ }
+
+ @Test
+ public final void testRenderOutputStreamContentChannelCompletionHandler() throws IOException {
+ ByteArrayOutputStream b = new ByteArrayOutputStream();
+ r.render(b, null, null);
+ assertEquals(COM_YAHOO_CONTAINER_JDISC_EXTENDED_RESPONSE_TEST_CASE, Utf8.toString(b.toByteArray()));
+ }
+
+
+ @Test
+ public final void testGetParsedQuery() {
+ assertNull(r.getParsedQuery());
+ }
+
+ @Test
+ public final void testGetTiming() {
+ assertNull(r.getTiming());
+ }
+
+ @Test
+ public final void testGetCoverage() {
+ assertNull(r.getCoverage());
+ }
+
+ @Test
+ public final void testGetHitCounts() {
+ assertNull(r.getHitCounts());
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/HttpRequestTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/HttpRequestTestCase.java
new file mode 100644
index 00000000000..6434c1c3e7e
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/HttpRequestTestCase.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.text.Utf8;
+
+/**
+ * API control of HttpRequest.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class HttpRequestTestCase {
+ private static final String X_RAY_YANKEE_ZULU = "x-ray yankee zulu";
+ private static final String HTTP_MAILHOST_25_ALPHA_BRAVO_CHARLIE_DELTA = "http://mailhost:25/alpha?bravo=charlie&falseboolean=false&trueboolean=true";
+ HttpRequest r;
+ InputStream requestData;
+
+ @Before
+ public void setUp() throws Exception {
+ requestData = new ByteArrayInputStream(Utf8.toBytes(X_RAY_YANKEE_ZULU));
+ r = HttpRequest.createTestRequest(HTTP_MAILHOST_25_ALPHA_BRAVO_CHARLIE_DELTA, Method.GET, requestData, Collections.singletonMap("foxtrot", "golf"));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ r = null;
+ }
+
+ @Test
+ public final void testGetMethod() {
+ assertSame(Method.GET, r.getMethod());
+ }
+
+ @Test
+ public final void testGetUri() throws URISyntaxException {
+ assertEquals(new URI(HTTP_MAILHOST_25_ALPHA_BRAVO_CHARLIE_DELTA), r.getUri());
+ }
+
+ @Test
+ public final void testGetJDiscRequest() throws URISyntaxException {
+ assertEquals(new URI(HTTP_MAILHOST_25_ALPHA_BRAVO_CHARLIE_DELTA), r.getJDiscRequest().getUri());
+ }
+
+ @Test
+ public final void testGetProperty() {
+ assertEquals("charlie", r.getProperty("bravo"));
+ assertEquals("golf", r.getProperty("foxtrot"));
+ assertNull(r.getProperty("zulu"));
+ }
+
+ @Test
+ public final void testPropertyMap() {
+ assertEquals(4, r.propertyMap().size());
+ }
+
+ @Test
+ public final void testGetBooleanProperty() {
+ assertTrue(r.getBooleanProperty("trueboolean"));
+ assertFalse(r.getBooleanProperty("falseboolean"));
+ assertFalse(r.getBooleanProperty("bravo"));
+ }
+
+ @Test
+ public final void testHasProperty() {
+ assertFalse(r.hasProperty("alpha"));
+ assertTrue(r.hasProperty("bravo"));
+ }
+
+ @Test
+ public final void testGetHeader() {
+ assertNull(r.getHeader("SyntheticHeaderFor-com.yahoo.container.jdisc.HttpRequestTestCase"));
+ }
+
+ @Test
+ public final void testGetHost() {
+ assertEquals("mailhost", r.getHost());
+ }
+
+ @Test
+ public final void testGetPort() {
+ assertEquals(25, r.getPort());
+ }
+
+ @Test
+ public final void testGetData() throws IOException {
+ byte[] b = new byte[X_RAY_YANKEE_ZULU.length()];
+ r.getData().read(b);
+ assertEquals(X_RAY_YANKEE_ZULU, Utf8.toString(b));
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/HttpResponseTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/HttpResponseTestCase.java
new file mode 100644
index 00000000000..6349da6e771
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/HttpResponseTestCase.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.jdisc.Response;
+import com.yahoo.text.Utf8;
+
+/**
+ * API test for HttpResponse.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class HttpResponseTestCase {
+
+ private static final String COM_YAHOO_CONTAINER_JDISC_HTTP_RESPONSE_TEST_CASE_TEST_RESPONSE = "com.yahoo.container.jdisc.HttpResponseTestCase.TestResponse";
+
+ private static class TestResponse extends HttpResponse {
+
+ public TestResponse(int status) {
+ super(status);
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(Utf8.toBytes(COM_YAHOO_CONTAINER_JDISC_HTTP_RESPONSE_TEST_CASE_TEST_RESPONSE));
+ }
+ }
+
+ HttpResponse r;
+
+ @Before
+ public void setUp() throws Exception {
+ r = new TestResponse(Response.Status.OK);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ r = null;
+ }
+
+ @Test
+ public final void testRender() throws IOException {
+ ByteArrayOutputStream o = new ByteArrayOutputStream(1024);
+ r.render(o);
+ assertEquals(COM_YAHOO_CONTAINER_JDISC_HTTP_RESPONSE_TEST_CASE_TEST_RESPONSE, Utf8.toString(o.toByteArray()));
+ }
+
+ @Test
+ public final void testGetStatus() {
+ assertEquals(Response.Status.OK, r.getStatus());
+ }
+
+ @Test
+ public final void testHeaders() {
+ assertNotNull(r.headers());
+ }
+
+ @Test
+ public final void testGetJdiscResponse() {
+ assertNotNull(r.getJdiscResponse());
+ }
+
+ @Test
+ public final void testGetContentType() {
+ assertEquals(HttpResponse.DEFAULT_MIME_TYPE, r.getContentType());
+ }
+
+ @Test
+ public final void testGetCharacterEncoding() {
+ assertEquals(HttpResponse.DEFAULT_CHARACTER_ENCODING, r.getCharacterEncoding());
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/LoggingRequestHandlerTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/LoggingRequestHandlerTestCase.java
new file mode 100644
index 00000000000..d2d74502102
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/LoggingRequestHandlerTestCase.java
@@ -0,0 +1,224 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.google.inject.Key;
+import com.yahoo.container.logging.HitCounts;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.service.CurrentContainer;
+import java.net.URI;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.container.handler.Coverage;
+import com.yahoo.container.handler.Timing;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.container.logging.AccessLogEntry;
+import com.yahoo.container.logging.AccessLogInterface;
+import com.yahoo.jdisc.handler.BufferedContentChannel;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+/**
+ * Test contracts in LoggingRequestHandler.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class LoggingRequestHandlerTestCase {
+
+ StartTimePusher accessLogging;
+ AccessLogTestHandler handler;
+ ExecutorService executor;
+
+ public static final class NoTimingResponse extends ExtendedResponse {
+
+ public NoTimingResponse() {
+ super(200);
+ }
+
+
+ @Override
+ public HitCounts getHitCounts() {
+ return new HitCounts(1, 1, 1, 1, 1);
+ }
+
+ @Override
+ public Timing getTiming() {
+ return null;
+ }
+
+ @Override
+ public Coverage getCoverage() {
+ return new Coverage(1, 1, true);
+ }
+
+
+ @Override
+ public void render(OutputStream output, ContentChannel networkChannel,
+ CompletionHandler handler) throws IOException {
+ networkChannel.close(handler);
+ }
+ }
+
+ static class CloseableContentChannel implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ }
+
+ public static final class MockResponseHandler implements ResponseHandler {
+ public final ContentChannel channel = new CloseableContentChannel();
+
+ @Override
+ public ContentChannel handleResponse(
+ final com.yahoo.jdisc.Response response) {
+ return channel;
+ }
+ }
+
+ static final class AccessLogTestHandler extends LoggingRequestHandler {
+
+ public AccessLogTestHandler(Executor executor, AccessLog accessLog) {
+ super(executor, accessLog);
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ return new NoTimingResponse();
+ }
+
+ }
+
+ static final class StartTimePusher implements AccessLogInterface {
+
+ public final ArrayBlockingQueue<Long> starts = new ArrayBlockingQueue<>(1);
+
+ @Override
+ public void log(final AccessLogEntry accessLogEntry) {
+ starts.offer(Long.valueOf(accessLogEntry.getTimeStampMillis()));
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ accessLogging = new StartTimePusher();
+ ComponentRegistry<AccessLogInterface> implementers = new ComponentRegistry<>();
+ implementers.register(new ComponentId("nalle"), accessLogging);
+ implementers.freeze();
+ executor = Executors.newCachedThreadPool();
+ handler = new AccessLogTestHandler(executor, new AccessLog(implementers));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ accessLogging = null;
+ handler = null;
+ executor.shutdown();
+ executor = null;
+ }
+
+ @Test
+ public final void checkStartIsNotZeroWithoutTimingInstance() throws InterruptedException {
+ Long startTime;
+
+ MockResponseHandler responseHandler = new MockResponseHandler();
+ com.yahoo.jdisc.http.HttpRequest request = createRequest();
+ BufferedContentChannel requestContent = new BufferedContentChannel();
+ requestContent.close(null);
+ handler.handleRequest(request, requestContent, responseHandler);
+ startTime = accessLogging.starts.poll(5, TimeUnit.MINUTES);
+ if (startTime == null) {
+ // test timed out, ignoring
+ } else {
+ assertFalse(
+ "Start time was 0, that should never happen after the first millisecond of 1970.",
+ startTime.longValue() == 0L);
+ }
+ }
+
+ public static com.yahoo.jdisc.http.HttpRequest createRequest() {
+ return createRequest("http://localhost/search/?query=geewhiz");
+ }
+
+ public static com.yahoo.jdisc.http.HttpRequest createRequest(String uri) {
+ com.yahoo.jdisc.http.HttpRequest request = null;
+ try {
+ request = com.yahoo.jdisc.http.HttpRequest.newClientRequest(new com.yahoo.jdisc.Request(new MockCurrentContainer(), new URI(uri)), new URI(uri),
+ com.yahoo.jdisc.http.HttpRequest.Method.GET, com.yahoo.jdisc.http.HttpRequest.Version.HTTP_1_1);
+ request.setRemoteAddress(new InetSocketAddress(0));
+ } catch (URISyntaxException e) {
+ fail("Illegal URI string in test?");
+ }
+ return request;
+ }
+
+ private static class MockCurrentContainer implements CurrentContainer {
+ @Override
+ public Container newReference(java.net.URI uri) {
+ return new Container() {
+
+ @Override
+ public RequestHandler resolveHandler(com.yahoo.jdisc.Request request) {
+ return null;
+ }
+
+ @Override
+ public <T> T getInstance(Key<T> tKey) {
+ return null;
+ }
+
+ @Override
+ public <T> T getInstance(Class<T> tClass) {
+ return null;
+ }
+
+ @Override
+ public ResourceReference refer() {
+ return References.NOOP_REFERENCE;
+ }
+
+ @Override
+ public void release() {
+ // NOP
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return 37;
+ }
+ };
+ }
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/LoggingTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/LoggingTestCase.java
new file mode 100644
index 00000000000..6fc93c2eea4
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/LoggingTestCase.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.log.LogLevel;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Check error logging from ContentChannelOutputStream is sane.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class LoggingTestCase {
+
+ Logger logger = Logger.getLogger(ContentChannelOutputStream.class.getName());
+ boolean initUseParentHandlers = logger.getUseParentHandlers();
+ LogCheckHandler logChecker;
+ Level initLevel;
+
+ private static class FailingContentChannel implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ handler.failed(new RuntimeException());
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ // NOP
+
+ }
+ }
+
+ private class LogCheckHandler extends Handler {
+ Map<Level, Integer> errorCounter = new HashMap<>();
+
+ @Override
+ public void publish(LogRecord record) {
+ synchronized (errorCounter) {
+ Integer count = errorCounter.get(record.getLevel());
+ if (count == null) {
+ count = Integer.valueOf(0);
+ }
+ errorCounter.put(record.getLevel(),
+ Integer.valueOf(count.intValue() + 1));
+ }
+ }
+
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public void close() throws SecurityException {
+ }
+ }
+
+ ContentChannelOutputStream stream;
+
+ @Before
+ public void setUp() throws Exception {
+ stream = new ContentChannelOutputStream(new FailingContentChannel());
+ logger = Logger.getLogger(ContentChannelOutputStream.class.getName());
+ initUseParentHandlers = logger.getUseParentHandlers();
+ logger.setUseParentHandlers(false);
+ logger.setLevel(Level.ALL);
+ logChecker = new LogCheckHandler();
+ logger.addHandler(logChecker);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ logger.removeHandler(logChecker);
+ logger.setUseParentHandlers(initUseParentHandlers);
+ logger.setLevel(initLevel);
+ }
+
+ private ByteBuffer createData() {
+ ByteBuffer b = ByteBuffer.allocate(10);
+ return b;
+ }
+
+ @Test
+ public final void testFailed() throws IOException, InterruptedException {
+ stream.send(createData());
+ stream.send(createData());
+ stream.send(createData());
+ stream.flush();
+ assertNull(logChecker.errorCounter.get(LogLevel.INFO));
+ assertEquals(1, logChecker.errorCounter.get(LogLevel.DEBUG).intValue());
+ assertEquals(2, logChecker.errorCounter.get(LogLevel.SPAM).intValue());
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/RequestBuilderTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/RequestBuilderTestCase.java
new file mode 100644
index 00000000000..375e7fe39f5
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/RequestBuilderTestCase.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.jdisc.http.HttpRequest.Method;
+
+/**
+ * API check for HttpRequest.Builder.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class RequestBuilderTestCase {
+ HttpRequest.Builder b;
+
+ @Before
+ public void setUp() throws Exception {
+ HttpRequest r = HttpRequest.createTestRequest("http://ssh:22/alpha?bravo=charlie", Method.GET);
+ b = new HttpRequest.Builder(r);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ b = null;
+ }
+
+ @Test
+ public final void testBasic() {
+ HttpRequest r = b.put("delta", "echo").createDirectRequest();
+ assertEquals("charlie", r.getProperty("bravo"));
+ assertEquals("echo", r.getProperty("delta"));
+ }
+
+ @Test
+ public void testRemove() {
+ HttpRequest orig = b.put("delta", "echo").createDirectRequest();
+
+ HttpRequest child = new HttpRequest.Builder(orig).removeProperty("delta").createDirectRequest();
+ assertFalse(child.propertyMap().containsKey("delta"));
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/ThreadedRequestHandlerTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/ThreadedRequestHandlerTestCase.java
new file mode 100644
index 00000000000..400cb507620
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/ThreadedRequestHandlerTestCase.java
@@ -0,0 +1,317 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.*;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ThreadedRequestHandlerTestCase {
+
+ @Test
+ public void requireThatNullExecutorThrowsException() {
+ try {
+ new ThreadedRequestHandler(null) {
+
+ @Override
+ public void handleRequest(Request request, BufferedContentChannel content, ResponseHandler handler) {
+
+ }
+ };
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatHandlerSetsRequestTimeout() throws InterruptedException {
+ Executor executor = Executors.newSingleThreadExecutor();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance(executor);
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ driver.dispatchRequest("http://localhost/", responseHandler);
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertNull(requestHandler.content.read());
+ assertNotNull(requestHandler.request.getTimeout(TimeUnit.MILLISECONDS));
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestAndResponseReachHandlers() throws InterruptedException {
+ Executor executor = Executors.newSingleThreadExecutor();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance(executor);
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ Request request = new Request(driver, URI.create("http://localhost/"));
+ ContentChannel requestContent = request.connect(responseHandler);
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ requestContent.write(buf, null);
+ requestContent.close(null);
+ request.release();
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertSame(request, requestHandler.request);
+ assertSame(buf, requestHandler.content.read());
+ assertNull(requestHandler.content.read());
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertSame(requestHandler.response, responseHandler.response);
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRejectedExecutionIsHandledGracefully() throws Exception {
+ // Instrumentation.
+ final Executor executor = new Executor() {
+ @Override
+ public void execute(final Runnable command) {
+ throw new RejectedExecutionException("Deliberately thrown; simulating overloaded executor");
+ }
+ };
+ final RequestHandler requestHandler = new ThreadedRequestHandler(executor) {
+ @Override
+ protected void handleRequest(Request request, BufferedContentChannel requestContent, ResponseHandler responseHandler) {
+ throw new AssertionError("Should never get here");
+ }
+ };
+
+ // Setup.
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ final ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+ final MyResponseHandler responseHandler = new MyResponseHandler();
+
+ // Execution.
+ try {
+ driver.dispatchRequest("http://localhost/", responseHandler);
+ fail("Above statement should throw exception");
+ } catch (OverloadException e) {
+ // As expected.
+ }
+
+ // Verification.
+ assertEquals("Response handler should be invoked synchronously in this case.", 0, responseHandler.latch.getCount());
+ assertEquals(Response.Status.SERVICE_UNAVAILABLE, responseHandler.response.getStatus());
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentIsClosedIfHandlerIgnoresIt() throws InterruptedException {
+ Executor executor = Executors.newSingleThreadExecutor();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler requestHandler = MyRequestHandler.newIgnoreContent(executor);
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ ContentChannel content = driver.connectRequest("http://localhost/", responseHandler);
+ MyCompletion writeCompletion = new MyCompletion();
+ content.write(ByteBuffer.allocate(69), writeCompletion);
+ MyCompletion closeCompletion = new MyCompletion();
+ content.close(closeCompletion);
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.completed);
+ assertTrue(closeCompletion.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.completed);
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertSame(requestHandler.response, responseHandler.response);
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatResponseIsDispatchedIfHandlerIgnoresIt() throws InterruptedException {
+ Executor executor = Executors.newSingleThreadExecutor();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler requestHandler = MyRequestHandler.newIgnoreResponse(executor);
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ driver.dispatchRequest("http://localhost/", responseHandler);
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertNull(requestHandler.content.read());
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertEquals(Response.Status.INTERNAL_SERVER_ERROR, responseHandler.response.getStatus());
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentIsClosedAndResponseIsDispatchedIfHandlerIgnoresIt()
+ throws InterruptedException
+ {
+ Executor executor = Executors.newSingleThreadExecutor();
+ assertThatRequestContentIsClosedAndResponseIsDispatchedIfHandlerIgnoresIt(
+ MyRequestHandler.newIgnoreAll(executor));
+ assertThatRequestContentIsClosedAndResponseIsDispatchedIfHandlerIgnoresIt(
+ MyRequestHandler.newThrowException(executor));
+ }
+
+ private static void assertThatRequestContentIsClosedAndResponseIsDispatchedIfHandlerIgnoresIt(
+ MyRequestHandler requestHandler)
+ throws InterruptedException
+ {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ ContentChannel content = driver.connectRequest("http://localhost/", responseHandler);
+ MyCompletion writeCompletion = new MyCompletion();
+ content.write(ByteBuffer.allocate(69), writeCompletion);
+ MyCompletion closeCompletion = new MyCompletion();
+ content.close(closeCompletion);
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.completed);
+ assertTrue(closeCompletion.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.completed);
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertEquals(Response.Status.INTERNAL_SERVER_ERROR, responseHandler.response.getStatus());
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ private static class MyRequestHandler extends ThreadedRequestHandler {
+
+ final CountDownLatch entryLatch = new CountDownLatch(1);
+ final CountDownLatch exitLatch = new CountDownLatch(1);
+ final ReadableContentChannel content = new ReadableContentChannel();
+ final boolean consumeContent;
+ final boolean createResponse;
+ final boolean throwException;
+ Response response = null;
+ Request request = null;
+
+ MyRequestHandler(Executor executor, boolean consumeContent, boolean createResponse, boolean throwException) {
+ super(executor);
+ this.consumeContent = consumeContent;
+ this.createResponse = createResponse;
+ this.throwException = throwException;
+ }
+
+ @Override
+ public void handleRequest(Request request, BufferedContentChannel content, ResponseHandler handler) {
+ try {
+ if (!entryLatch.await(60, TimeUnit.SECONDS)) {
+ return;
+ }
+ if (throwException) {
+ throw new RuntimeException();
+ }
+ this.request = request;
+ if (consumeContent) {
+ content.connectTo(this.content);
+ }
+ if (createResponse) {
+ response = new Response(Response.Status.OK);
+ handler.handleResponse(response).close(null);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ exitLatch.countDown();
+ }
+ }
+
+ static MyRequestHandler newInstance(Executor executor) {
+ return new MyRequestHandler(executor, true, true, false);
+ }
+
+ static MyRequestHandler newThrowException(Executor executor) {
+ return new MyRequestHandler(executor, true, true, true);
+ }
+
+ static MyRequestHandler newIgnoreContent(Executor executor) {
+ return new MyRequestHandler(executor, false, true, false);
+ }
+
+ static MyRequestHandler newIgnoreResponse(Executor executor) {
+ return new MyRequestHandler(executor, true, false, false);
+ }
+
+ static MyRequestHandler newIgnoreAll(Executor executor) {
+ return new MyRequestHandler(executor, false, false, false);
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ReadableContentChannel content = new ReadableContentChannel();
+ Response response = null;
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ this.response = response;
+ latch.countDown();
+
+ BufferedContentChannel content = new BufferedContentChannel();
+ content.connectTo(this.content);
+ return content;
+ }
+ }
+
+ private static class MyCompletion implements CompletionHandler {
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ boolean completed;
+
+ @Override
+ public void completed() {
+ completed = true;
+ latch.countDown();
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ latch.countDown();
+ }
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricConsumerProviders.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricConsumerProviders.java
new file mode 100644
index 00000000000..ff62f1f4078
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricConsumerProviders.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc.state;
+
+import com.google.inject.Provider;
+import com.yahoo.jdisc.application.MetricConsumer;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class MetricConsumerProviders {
+
+ public static Provider<MetricConsumer> wrap(final StateMonitor statetMonitor) {
+ return new Provider<MetricConsumer>() {
+
+ @Override
+ public MetricConsumer get() {
+ return statetMonitor.newMetricConsumer();
+ }
+ };
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricSnapshotTest.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricSnapshotTest.java
new file mode 100644
index 00000000000..9dc9379e585
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricSnapshotTest.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc.state;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class MetricSnapshotTest {
+ /**
+ * Aggregate metrics are not cloned into new snapshot. In turn, a metric
+ * set with only aggregates will be added as an empty set if we do not
+ * filter these away at clone time. This test ensures that we do just that.
+ * If/when we start carrying aggregates across snapshots, this test will
+ * most likely be deprecated.
+ */
+ @Test
+ public void emptyMetricSetNotAddedToClonedSnapshot() {
+ final StateMetricContext ctx = StateMetricContext.newInstance(null);
+ MetricSnapshot snapshot = new MetricSnapshot();
+ snapshot.add(ctx, "foo", 1234);
+ MetricSnapshot newSnapshot = snapshot.createSnapshot();
+ assertFalse(newSnapshot.iterator().hasNext());
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java
new file mode 100644
index 00000000000..49e97fc9d06
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java
@@ -0,0 +1,409 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc.state;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.AbstractModule;
+import com.yahoo.container.core.ApplicationMetadataConfig;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.Timer;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.MetricConsumer;
+import com.yahoo.jdisc.handler.BufferedContentChannel;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.test.TestDriver;
+import com.yahoo.vespa.defaults.Defaults;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class StateHandlerTest {
+
+ private final static long SNAPSHOT_INTERVAL = TimeUnit.SECONDS.toMillis(300);
+ private final static long META_GENERATION = 69;
+ private final TestDriver driver;
+ private final StateMonitor monitor;
+ private final Metric metric;
+ private volatile long currentTimeMillis = 0;
+
+ public StateHandlerTest() {
+ driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(Timer.class).toInstance(new Timer() {
+
+ @Override
+ public long currentTimeMillis() {
+ return currentTimeMillis;
+ }
+ });
+ }
+ });
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.guiceModules().install(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(HealthMonitorConfig.class)
+ .toInstance(new HealthMonitorConfig(new HealthMonitorConfig.Builder().snapshot_interval(
+ TimeUnit.MILLISECONDS.toSeconds(SNAPSHOT_INTERVAL))));
+ }
+ });
+ monitor = builder.guiceModules().getInstance(StateMonitor.class);
+ builder.guiceModules().install(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(StateMonitor.class).toInstance(monitor);
+ bind(MetricConsumer.class).toProvider(MetricConsumerProviders.wrap(monitor));
+ bind(ApplicationMetadataConfig.class).toInstance(new ApplicationMetadataConfig(
+ new ApplicationMetadataConfig.Builder().generation(META_GENERATION)));
+ }
+ });
+ builder.serverBindings().bind("http://*/*", builder.getInstance(StateHandler.class));
+ driver.activateContainer(builder);
+ metric = builder.getInstance(Metric.class);
+ }
+
+ @After
+ public void closeTestDriver() {
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void testReportPriorToFirstSnapshot() throws Exception {
+ metric.add("foo", 1, null);
+ metric.set("bar", 4, null);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertFalse(json.toString(), json.get("metrics").has("values"));
+ }
+
+ @Test
+ public void testReportIncludesMetricsAfterSnapshot() throws Exception {
+ metric.add("foo", 1, null);
+ metric.set("bar", 4, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 2, json.get("metrics").get("values").size());
+ }
+
+ /**
+ * Tests that we restart an metric when it changes type from gauge to counter or back.
+ * This may happen in practice on config reloads.
+ */
+ @Test
+ public void testMetricTypeChangeIsAllowed() {
+ String metricName = "myMetric";
+ Metric.Context metricContext = null;
+
+ {
+ // Add a count metric
+ metric.add(metricName, 1, metricContext);
+ metric.add(metricName, 2, metricContext);
+ // Change it to a gauge metric
+ metric.set(metricName, 9, metricContext);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ MetricValue resultingMetric = monitor.snapshot().iterator().next().getValue().get(metricName);
+ assertEquals(GaugeMetric.class, resultingMetric.getClass());
+ assertEquals("Value was reset and produces the last gauge value",
+ 9.0, ((GaugeMetric) resultingMetric).getLast(), 0.000001);
+ }
+
+ {
+ // Add a gauge metric
+ metric.set(metricName, 9, metricContext);
+ // Change it to a count metric
+ metric.add(metricName, 1, metricContext);
+ metric.add(metricName, 2, metricContext);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ MetricValue resultingMetric = monitor.snapshot().iterator().next().getValue().get(metricName);
+ assertEquals(CountMetric.class, resultingMetric.getClass());
+ assertEquals("Value was reset, and changed to add semantics giving 1+2",
+ 3, ((CountMetric) resultingMetric).getCount());
+ }
+ }
+
+ @Test
+ public void testAverageAggregationOfValues() throws Exception {
+ metric.set("bar", 4, null);
+ metric.set("bar", 5, null);
+ metric.set("bar", 7, null);
+ metric.set("bar", 2, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 1, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), 4.5,
+ json.get("metrics").get("values").get(0).get("values").get("average").asDouble(), 0.001);
+ }
+
+ @Test
+ public void testSumAggregationOfCounts() throws Exception {
+ metric.add("foo", 1, null);
+ metric.add("foo", 1, null);
+ metric.add("foo", 2, null);
+ metric.add("foo", 1, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 1, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), 5,
+ json.get("metrics").get("values").get(0).get("values").get("count").asDouble(), 0.001);
+ }
+
+ @Test
+ public void testReadabilityOfJsonReport() throws Exception {
+ metric.add("foo", 1, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ assertEquals("{\n" +
+ " \"metrics\": {\n" +
+ " \"snapshot\": {\n" +
+ " \"from\": 0,\n" +
+ " \"to\": 300\n" +
+ " },\n" +
+ " \"values\": [{\n" +
+ " \"name\": \"foo\",\n" +
+ " \"values\": {\n" +
+ " \"count\": 1,\n" +
+ " \"rate\": 0.0033333333333333335\n" +
+ " }\n" +
+ " }]\n" +
+ " },\n" +
+ " \"status\": {\"code\": \"up\"},\n" +
+ " \"time\": 300000\n" +
+ "}",
+ requestAsString("http://localhost/state/v1/all"));
+
+ Metric.Context ctx = metric.createContext(Collections.singletonMap("component", "test"));
+ metric.set("bar", 2, ctx);
+ metric.set("bar", 3, ctx);
+ metric.set("bar", 4, ctx);
+ metric.set("bar", 5, ctx);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ assertEquals("{\n" +
+ " \"metrics\": {\n" +
+ " \"snapshot\": {\n" +
+ " \"from\": 300,\n" +
+ " \"to\": 600\n" +
+ " },\n" +
+ " \"values\": [\n" +
+ " {\n" +
+ " \"name\": \"foo\",\n" +
+ " \"values\": {\n" +
+ " \"count\": 0,\n" +
+ " \"rate\": 0\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dimensions\": {\"component\": \"test\"},\n" +
+ " \"name\": \"bar\",\n" +
+ " \"values\": {\n" +
+ " \"average\": 3.5,\n" +
+ " \"count\": 4,\n" +
+ " \"last\": 5,\n" +
+ " \"max\": 5,\n" +
+ " \"min\": 2,\n" +
+ " \"rate\": 0.013333333333333334\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " \"status\": {\"code\": \"up\"},\n" +
+ " \"time\": 600000\n" +
+ "}",
+ requestAsString("http://localhost/state/v1/all"));
+ }
+
+ @Test
+ public void testNotAggregatingCountsBeyondSnapshots() throws Exception {
+ metric.add("foo", 1, null);
+ metric.add("foo", 1, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ metric.add("foo", 2, null);
+ metric.add("foo", 1, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 1, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), 3,
+ json.get("metrics").get("values").get(0).get("values").get("count").asDouble(), 0.001);
+ }
+
+ @Test
+ public void testSnapshottingTimes() throws Exception {
+ metric.add("foo", 1, null);
+ metric.set("bar", 3, null);
+ // At this time we should not have done any snapshotting
+ incrementCurrentTime(SNAPSHOT_INTERVAL - 1);
+ {
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertFalse(json.toString(), json.get("metrics").has("snapshot"));
+ }
+ // At this time first snapshot should have been generated
+ incrementCurrentTime(1);
+ {
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertTrue(json.toString(), json.get("metrics").has("snapshot"));
+ assertEquals(0.0, json.get("metrics").get("snapshot").get("from").asDouble(), 0.00001);
+ assertEquals(300.0, json.get("metrics").get("snapshot").get("to").asDouble(), 0.00001);
+ }
+ // No new snapshot at this time
+ incrementCurrentTime(SNAPSHOT_INTERVAL - 1);
+ {
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertTrue(json.toString(), json.get("metrics").has("snapshot"));
+ assertEquals(0.0, json.get("metrics").get("snapshot").get("from").asDouble(), 0.00001);
+ assertEquals(300.0, json.get("metrics").get("snapshot").get("to").asDouble(), 0.00001);
+ }
+ // A new snapshot
+ incrementCurrentTime(1);
+ {
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertTrue(json.toString(), json.get("metrics").has("snapshot"));
+ assertEquals(300.0, json.get("metrics").get("snapshot").get("from").asDouble(), 0.00001);
+ assertEquals(600.0, json.get("metrics").get("snapshot").get("to").asDouble(), 0.00001);
+ }
+ }
+
+ @Test
+ public void testFreshStartOfValuesBeyondSnapshot() throws Exception {
+ metric.set("bar", 4, null);
+ metric.set("bar", 5, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ metric.set("bar", 4, null);
+ metric.set("bar", 2, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 1, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), 3,
+ json.get("metrics").get("values").get(0).get("values").get("average").asDouble(), 0.001);
+ }
+
+ @Test
+ public void snapshotsPreserveLastGaugeValue() throws Exception {
+ metric.set("bar", 4, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ JsonNode metricValues = getFirstMetricValueNode(json);
+ assertEquals(json.toString(), 4, metricValues.get("last").asDouble(), 0.001);
+ // Use 'last' as avg/min/max when none has been set explicitly during snapshot period
+ assertEquals(json.toString(), 4, metricValues.get("average").asDouble(), 0.001);
+ assertEquals(json.toString(), 4, metricValues.get("min").asDouble(), 0.001);
+ assertEquals(json.toString(), 4, metricValues.get("max").asDouble(), 0.001);
+ // Count is tracked per period.
+ assertEquals(json.toString(), 0, metricValues.get("count").asInt());
+ }
+
+ private JsonNode getFirstMetricValueNode(JsonNode root) {
+ assertEquals(root.toString(), 1, root.get("metrics").get("values").size());
+ JsonNode metricValues = root.get("metrics").get("values").get(0).get("values");
+ assertTrue(root.toString(), metricValues.has("last"));
+ return metricValues;
+ }
+
+ @Test
+ public void gaugeSnapshotsTracksCountMinMaxAvgPerPeriod() throws Exception {
+ metric.set("bar", 10000, null); // Ensure any cross-snapshot noise is visible
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ metric.set("bar", 20, null);
+ metric.set("bar", 40, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ JsonNode metricValues = getFirstMetricValueNode(json);
+ assertEquals(json.toString(), 40, metricValues.get("last").asDouble(), 0.001);
+ // Last snapshot had explicit values set
+ assertEquals(json.toString(), 30, metricValues.get("average").asDouble(), 0.001);
+ assertEquals(json.toString(), 20, metricValues.get("min").asDouble(), 0.001);
+ assertEquals(json.toString(), 40, metricValues.get("max").asDouble(), 0.001);
+ assertEquals(json.toString(), 2, metricValues.get("count").asInt());
+ }
+
+ @Test
+ public void testHealthAggregation() throws Exception {
+ Map<String, String> dimensions1 = new TreeMap<>();
+ dimensions1.put("port", String.valueOf(Defaults.getDefaults().vespaWebServicePort()));
+ Metric.Context context1 = metric.createContext(dimensions1);
+ Map<String, String> dimensions2 = new TreeMap<>();
+ dimensions2.put("port", "80");
+ Metric.Context context2 = metric.createContext(dimensions2);
+
+ metric.add("serverNumSuccessfulResponses", 4, context1);
+ metric.add("serverNumSuccessfulResponses", 2, context2);
+ metric.set("serverTotalSuccessfulResponseLatency", 20, context1);
+ metric.set("serverTotalSuccessfulResponseLatency", 40, context2);
+ metric.add("random", 3, context1);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/health");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 2, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), "requestsPerSecond",
+ json.get("metrics").get("values").get(0).get("name").asText());
+ assertEquals(json.toString(), 6,
+ json.get("metrics").get("values").get(0).get("values").get("count").asDouble(), 0.001);
+ assertEquals(json.toString(), "latencySeconds",
+ json.get("metrics").get("values").get(1).get("name").asText());
+ assertEquals(json.toString(), 0.03,
+ json.get("metrics").get("values").get(1).get("values").get("average").asDouble(), 0.001);
+ }
+
+ @Test
+ public void testStateConfig() throws Exception {
+ JsonNode root = requestAsJson("http://localhost/state/v1/config");
+
+ JsonNode config = root.get("config");
+ JsonNode container = config.get("container");
+ assertEquals(META_GENERATION, container.get("generation").asLong());
+ }
+
+ private void incrementCurrentTime(long val) {
+ currentTimeMillis += val;
+ monitor.checkTime();
+ }
+
+ private String requestAsString(String requestUri) throws Exception {
+ final BufferedContentChannel content = new BufferedContentChannel();
+ Response response = driver.dispatchRequest(requestUri, new ResponseHandler() {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return content;
+ }
+ }).get(60, TimeUnit.SECONDS);
+ assertNotNull(response);
+ assertEquals(Response.Status.OK, response.getStatus());
+ StringBuilder str = new StringBuilder();
+ Reader in = new InputStreamReader(content.toStream(), StandardCharsets.UTF_8);
+ for (int c; (c = in.read()) != -1; ) {
+ str.append((char)c);
+ }
+ return str.toString();
+ }
+
+ private JsonNode requestAsJson(String requestUri) throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ return mapper.readTree(mapper.getFactory().createParser(requestAsString(requestUri)));
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/StateMonitorBenchmarkTest.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateMonitorBenchmarkTest.java
new file mode 100644
index 00000000000..103d22afe6d
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateMonitorBenchmarkTest.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc.state;
+
+import com.google.inject.Provider;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.application.ContainerThread;
+import com.yahoo.jdisc.application.MetricConsumer;
+import com.yahoo.jdisc.application.MetricProvider;
+import com.yahoo.jdisc.core.SystemTimer;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class StateMonitorBenchmarkTest {
+
+ private final static int NUM_THREADS = 32;
+ private final static int NUM_UPDATES = 1000;//0000;
+
+ @Test
+ public void requireThatHealthMonitorDoesNotBlockMetricThreads() throws Exception {
+ StateMonitor monitor = new StateMonitor(new HealthMonitorConfig(new HealthMonitorConfig.Builder()),
+ new SystemTimer());
+ Provider<MetricConsumer> provider = MetricConsumerProviders.wrap(monitor);
+ performUpdates(provider, 8);
+ for (int i = 1; i <= NUM_THREADS; i *= 2) {
+ long millis = performUpdates(provider, i);
+ System.err.format("%2d threads, %5d millis => %9d ups\n",
+ i, millis, (int)((i * NUM_UPDATES) / (millis / 1000.0)));
+ }
+ monitor.deconstruct();
+ }
+
+ private long performUpdates(Provider<MetricConsumer> metricProvider, int numThreads) throws Exception {
+ ThreadFactory threadFactory = new ContainerThread.Factory(metricProvider);
+ ExecutorService executor = Executors.newFixedThreadPool(numThreads, threadFactory);
+ List<Callable<Boolean>> tasks = new ArrayList<>(numThreads);
+ for (int i = 0; i < numThreads; ++i) {
+ tasks.add(new UpdateTask(new MetricProvider(metricProvider).get()));
+ }
+ long before = System.nanoTime();
+ List<Future<Boolean>> results = executor.invokeAll(tasks);
+ long after = System.nanoTime();
+ for (Future<Boolean> result : results) {
+ assertTrue(result.get());
+ }
+ return TimeUnit.NANOSECONDS.toMillis(after - before);
+ }
+
+ public static class UpdateTask implements Callable<Boolean> {
+
+ final Metric metric;
+
+ UpdateTask(Metric metric) {
+ this.metric = metric;
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ Metric.Context ctx = metric.createContext(Collections.<String, Object>emptyMap());
+ for (int i = 0; i < NUM_UPDATES; ++i) {
+ metric.add("foo", 69L, ctx);
+ }
+ return true;
+ }
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/messagebus/cfg-disabled/.gitignore b/container-core/src/test/java/com/yahoo/container/messagebus/cfg-disabled/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/messagebus/cfg-disabled/.gitignore
diff --git a/container-core/src/test/java/com/yahoo/container/xml/bind/JAXBContextFactoryTest.java b/container-core/src/test/java/com/yahoo/container/xml/bind/JAXBContextFactoryTest.java
new file mode 100644
index 00000000000..c027574a208
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/xml/bind/JAXBContextFactoryTest.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.xml.bind;
+
+import com.yahoo.container.xml.providers.JAXBContextFactoryProvider;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * @author einarmr
+ * @author gjoranv
+ * @since 5.3
+ */
+public class JAXBContextFactoryTest {
+ @Test
+ public void testInstantiationAndDestruction() {
+
+ JAXBContextFactoryProvider provider = new JAXBContextFactoryProvider();
+ JAXBContextFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(JAXBContextFactoryProvider.FACTORY_CLASS));
+
+ try {
+ JAXBContextFactory.getContextPath((Class) null);
+ fail("Should have failed with null classes.");
+ } catch (Exception e) { }
+
+ try {
+ JAXBContextFactory.getContextPath();
+ fail("Should have failed with empty list.");
+ } catch (Exception e) { }
+
+ assertThat(JAXBContextFactory.getContextPath(this.getClass()),
+ equalTo(this.getClass().getPackage().getName()));
+
+ assertThat(JAXBContextFactory.getContextPath(this.getClass(),
+ String.class),
+ equalTo(this.getClass().getPackage().getName() + ":" +
+ String.class.getPackage().getName()));
+
+ provider.deconstruct();
+
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/xml/providers/XMLProviderTest.java b/container-core/src/test/java/com/yahoo/container/xml/providers/XMLProviderTest.java
new file mode 100644
index 00000000000..2c38d71a6f2
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/xml/providers/XMLProviderTest.java
@@ -0,0 +1,96 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.xml.providers;
+
+import com.yahoo.container.Server;
+import com.yahoo.container.xml.bind.JAXBContextFactory;
+import com.yahoo.container.xml.providers.DatatypeFactoryProvider;
+import com.yahoo.container.xml.providers.DocumentBuilderFactoryProvider;
+import com.yahoo.container.xml.providers.JAXBContextFactoryProvider;
+import com.yahoo.container.xml.providers.SAXParserFactoryProvider;
+import com.yahoo.container.xml.providers.SchemaFactoryProvider;
+import com.yahoo.container.xml.providers.TransformerFactoryProvider;
+import com.yahoo.container.xml.providers.XMLEventFactoryProvider;
+import com.yahoo.container.xml.providers.XMLInputFactoryProvider;
+import com.yahoo.container.xml.providers.XMLOutputFactoryProvider;
+import com.yahoo.container.xml.providers.XPathFactoryProvider;
+import org.junit.Test;
+
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.xpath.XPathFactory;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.1.29
+ */
+public class XMLProviderTest {
+
+ @Test
+ public void testInstantiationAndDestruction() {
+ {
+ DatatypeFactoryProvider provider = new DatatypeFactoryProvider();
+ DatatypeFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(DatatypeFactoryProvider.FACTORY_CLASS));
+ provider.deconstruct();
+ }
+ {
+ DocumentBuilderFactoryProvider provider = new DocumentBuilderFactoryProvider();
+ DocumentBuilderFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(DocumentBuilderFactoryProvider.FACTORY_CLASS));
+ provider.deconstruct();
+ }
+ {
+ SAXParserFactoryProvider provider = new SAXParserFactoryProvider();
+ SAXParserFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(SAXParserFactoryProvider.FACTORY_CLASS));
+ provider.deconstruct();
+ }
+ {
+ SchemaFactoryProvider provider = new SchemaFactoryProvider();
+ SchemaFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(SchemaFactoryProvider.FACTORY_CLASS));
+ provider.deconstruct();
+ }
+ {
+ TransformerFactoryProvider provider = new TransformerFactoryProvider();
+ TransformerFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(TransformerFactoryProvider.FACTORY_CLASS));
+ provider.deconstruct();
+ }
+ {
+ XMLEventFactoryProvider provider = new XMLEventFactoryProvider();
+ XMLEventFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(XMLEventFactoryProvider.FACTORY_CLASS));
+ provider.deconstruct();
+ }
+ {
+ XMLInputFactoryProvider provider = new XMLInputFactoryProvider();
+ XMLInputFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(XMLInputFactoryProvider.FACTORY_CLASS));
+ provider.deconstruct();
+ }
+ {
+ XMLOutputFactoryProvider provider = new XMLOutputFactoryProvider();
+ XMLOutputFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(XMLOutputFactoryProvider.FACTORY_CLASS));
+ provider.deconstruct();
+ }
+ {
+ XPathFactoryProvider provider = new XPathFactoryProvider();
+ XPathFactory factory = provider.get();
+ assertThat(factory.getClass().getName(), equalTo(XPathFactoryProvider.FACTORY_CLASS));
+ provider.deconstruct();
+ }
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/osgi/provider/model/ComponentModelTest.java b/container-core/src/test/java/com/yahoo/osgi/provider/model/ComponentModelTest.java
new file mode 100644
index 00000000000..865d8c5a788
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/osgi/provider/model/ComponentModelTest.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.osgi.provider.model;
+
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ */
+public class ComponentModelTest {
+
+ @Test
+ public void create_from_instantiation_spec() throws Exception {
+ ComponentModel model = new ComponentModel(
+ BundleInstantiationSpecification.getFromStrings("id", "class", "bundle"));
+ verifyBundleSpec(model);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void require_exception_upon_null_instantiation_spec() throws Exception {
+ ComponentModel model = new ComponentModel(null);
+ }
+
+ @Test
+ public void create_from_instantiation_spec_and_config_id() throws Exception {
+ ComponentModel model = new ComponentModel(
+ BundleInstantiationSpecification.getFromStrings("id", "class", "bundle"), "configId");
+ verifyBundleSpec(model);
+ assertThat(model.configId, is("configId"));
+ }
+
+ @Test
+ public void create_from_strings() throws Exception {
+ ComponentModel model = new ComponentModel("id", "class", "bundle", "configId");
+ verifyBundleSpec(model);
+ assertThat(model.configId, is("configId"));
+ }
+
+ private void verifyBundleSpec(ComponentModel model) {
+ assertThat(model.getComponentId().stringValue(), is("id"));
+ assertThat(model.getClassId().stringValue(), is("class"));
+ assertThat(model.bundleInstantiationSpec.bundle.stringValue(), is("bundle"));
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/processing/handler/ProcessingHandlerTestCase.java b/container-core/src/test/java/com/yahoo/processing/handler/ProcessingHandlerTestCase.java
new file mode 100644
index 00000000000..e76a7f5b20d
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/processing/handler/ProcessingHandlerTestCase.java
@@ -0,0 +1,844 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.processing.handler;
+
+import com.google.common.util.concurrent.SettableFuture;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Chain;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.container.jdisc.RequestHandlerTestDriver;
+import com.yahoo.container.logging.AccessLogEntry;
+import com.yahoo.container.logging.AccessLogInterface;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.processing.Processor;
+import com.yahoo.processing.execution.Execution;
+import com.yahoo.processing.processors.RequestPropertyTracer;
+import com.yahoo.processing.rendering.ProcessingRenderer;
+import com.yahoo.processing.rendering.Renderer;
+import com.yahoo.processing.request.ErrorMessage;
+import com.yahoo.processing.response.Data;
+import com.yahoo.processing.test.ProcessorLibrary;
+import com.yahoo.container.protect.Error;
+import org.junit.After;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+
+import static com.yahoo.processing.test.ProcessorLibrary.MapData;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.ExecutionException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.notNull;
+import static org.mockito.Mockito.times;
+
+/**
+ * Tests processing handler scenarios end to end.
+ *
+ * @author bratseth
+ * @author gjoranv
+ * @author tonytv
+ */
+public class ProcessingHandlerTestCase {
+
+ private static final String LOG_KEY = "Log-Key";
+ private static final String LOG_VALUE = "Log-Value";
+
+ private ProcessingTestDriver driver;
+
+ private final Chain<Processor> defaultChain =
+ new Chain<Processor>("default",
+ new ProcessorLibrary.StringDataListAdder("Item1", "Item2"),
+ new ProcessorLibrary.Trace("TraceMessage", 1),
+ new ProcessorLibrary.StringDataAdder("StringData.toString()"));
+
+ private final Chain<Processor> simpleChain =
+ new Chain<Processor>("simple",
+ new ProcessorLibrary.StringDataAdder("StringData.toString()"));
+
+ private final Chain<Processor> logValueChain =
+ new Chain<Processor>("log-value",
+ new ProcessorLibrary.LogValueAdder(LOG_KEY, LOG_VALUE));
+
+ @After
+ public void shutDown() {
+ driver.close();
+ }
+
+ @Test
+ public void processing_handler_stores_trace_log_values_in_the_access_log_entry() throws InterruptedException {
+ ArgumentCaptor<AccessLogEntry> accessLogEntryCaptor = ArgumentCaptor.forClass(AccessLogEntry.class);
+ AccessLogInterface accessLog = Mockito.mock(AccessLogInterface.class);
+
+ driver = new ProcessingTestDriver(logValueChain, accessLog);
+ driver.sendRequest("http://localhost/?chain=log-value").readAll();
+
+ Mockito.verify(accessLog, times(1)).log(accessLogEntryCaptor.capture());
+
+ AccessLogEntry entry = accessLogEntryCaptor.getValue();
+ assertNotNull(entry);
+ assertThat(entry.getKeyValues().get(LOG_KEY), is(Collections.singletonList(LOG_VALUE)));
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T notNull() {
+ return (T)Matchers.notNull();
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T any() {
+ return (T)Matchers.any();
+ }
+
+ @Test
+ public void testProcessingHandlerResolvesChains() throws Exception {
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(defaultChain);
+ chains.add(simpleChain);
+ driver = new ProcessingTestDriver(chains);
+
+ assertEquals(simpleChainResponse, driver.sendRequest("http://localhost/?chain=simple").readAll());
+ assertEquals(defaultChainResponse, driver.sendRequest("http://localhost/?chain=default").readAll());
+ }
+
+ @Test
+ public void testProcessingHandlerPropagatesRequestParametersAndContext() throws InterruptedException {
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(new Chain<Processor>("default", new RequestPropertyTracer()));
+ driver = new ProcessingTestDriver(chains);
+ assertTrue("JDisc request context is propagated to properties()",
+ driver.sendRequest("http://localhost/?chain=default&tracelevel=4").readAll().contains("context.contextVariable: '37'"));
+ }
+
+ @Test
+ public void testProcessingHandlerOutputsTrace() throws Exception {
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(defaultChain);
+ driver = new ProcessingTestDriver(chains);
+
+ assertEquals(trace1, driver.sendRequest("http://localhost/?tracelevel=1").readAll().substring(0, trace1.length()));
+ assertEquals(trace1WithFullResult, driver.sendRequest("http://localhost/?tracelevel=1").readAll());
+ assertEquals(trace4, driver.sendRequest("http://localhost/?tracelevel=4").readAll().substring(0, trace4.length()));
+ assertEquals(trace5, driver.censorDigits(driver.sendRequest("http://localhost/?tracelevel=5").readAll().substring(0, trace5.length())));
+ assertEquals(trace6, driver.censorDigits(driver.sendRequest("http://localhost/?tracelevel=6").readAll().substring(0, trace6.length())));
+ }
+
+ @Test
+ public void testProcessingHandlerTransfersErrorsToHttpStatusCodesNoData() throws Exception {
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(simpleChain);
+ chains.add(new Chain<Processor>("moved_permanently", new ProcessorLibrary.ErrorAdder(new ErrorMessage(301,"Message"))));
+ chains.add(new Chain<Processor>("unauthorized", new ProcessorLibrary.ErrorAdder(new ErrorMessage(401,"Message"))));
+ chains.add(new Chain<Processor>("unauthorized_mapped", new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.UNAUTHORIZED.code,"Message"))));
+ chains.add(new Chain<Processor>("forbidden", new ProcessorLibrary.ErrorAdder(new ErrorMessage(403,"Message"))));
+ chains.add(new Chain<Processor>("forbidden_mapped", new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.FORBIDDEN.code,"Message"))));
+ chains.add(new Chain<Processor>("not_found", new ProcessorLibrary.ErrorAdder(new ErrorMessage(404,"Message"))));
+ chains.add(new Chain<Processor>("not_found_mapped", new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.NOT_FOUND.code,"Message"))));
+ chains.add(new Chain<Processor>("too_many_requests", new ProcessorLibrary.ErrorAdder(new ErrorMessage(429,"Message"))));
+ chains.add(new Chain<Processor>("bad_request", new ProcessorLibrary.ErrorAdder(new ErrorMessage(400,"Message"))));
+ chains.add(new Chain<Processor>("bad_request_mapped", new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.BAD_REQUEST.code,"Message"))));
+ chains.add(new Chain<Processor>("internal_server_error", new ProcessorLibrary.ErrorAdder(new ErrorMessage(500,"Message"))));
+ chains.add(new Chain<Processor>("internal_server_error_mapped", new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.INTERNAL_SERVER_ERROR.code,"Message"))));
+ chains.add(new Chain<Processor>("service_unavailable", new ProcessorLibrary.ErrorAdder(new ErrorMessage(503,"Message"))));
+ chains.add(new Chain<Processor>("service_unavailable_mapped", new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.NO_BACKENDS_IN_SERVICE.code,"Message"))));
+ chains.add(new Chain<Processor>("gateway_timeout", new ProcessorLibrary.ErrorAdder(new ErrorMessage(504,"Message"))));
+ chains.add(new Chain<Processor>("gateway_timeout_mapped", new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.TIMEOUT.code,"Message"))));
+ chains.add(new Chain<Processor>("bad_gateway", new ProcessorLibrary.ErrorAdder(new ErrorMessage(502,"Message"))));
+ chains.add(new Chain<Processor>("bad_gateway_mapped", new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.BACKEND_COMMUNICATION_ERROR.code,"Message"))));
+ chains.add(new Chain<Processor>("unknown_code", new ProcessorLibrary.ErrorAdder(new ErrorMessage(1234567,"Message"))));
+ driver = new ProcessingTestDriver(chains);
+ assertEqualStatus(200, "http://localhost/?chain=simple");
+ assertEqualStatus(301, "http://localhost/?chain=moved_permanently");
+ assertEqualStatus(401, "http://localhost/?chain=unauthorized");
+ assertEqualStatus(401, "http://localhost/?chain=unauthorized_mapped");
+ assertEqualStatus(403, "http://localhost/?chain=forbidden");
+ assertEqualStatus(403, "http://localhost/?chain=forbidden_mapped");
+ assertEqualStatus(404, "http://localhost/?chain=not_found");
+ assertEqualStatus(404, "http://localhost/?chain=not_found_mapped");
+ assertEqualStatus(429, "http://localhost/?chain=too_many_requests");
+ assertEqualStatus(400, "http://localhost/?chain=bad_request");
+ assertEqualStatus(400, "http://localhost/?chain=bad_request_mapped");
+ assertEqualStatus(500, "http://localhost/?chain=internal_server_error");
+ assertEqualStatus(500, "http://localhost/?chain=internal_server_error_mapped");
+ assertEqualStatus(503, "http://localhost/?chain=service_unavailable");
+ assertEqualStatus(503, "http://localhost/?chain=service_unavailable_mapped");
+ assertEqualStatus(504, "http://localhost/?chain=gateway_timeout");
+ assertEqualStatus(504, "http://localhost/?chain=gateway_timeout_mapped");
+ assertEqualStatus(502, "http://localhost/?chain=bad_gateway");
+ assertEqualStatus(503, "http://localhost/?chain=bad_gateway_mapped");
+ assertEqualStatus(500, "http://localhost/?chain=unknown_code");
+ }
+
+ @Test
+ public void testProcessingHandlerTransfersErrorsToHttpStatusCodesWithData() throws Exception {
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(simpleChain);
+ chains.add(new Chain<Processor>("moved_permanently", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(301,"Message"))));
+ chains.add(new Chain<Processor>("unauthorized", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(401,"Message"))));
+ chains.add(new Chain<Processor>("unauthorized_mapped", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.UNAUTHORIZED.code,"Message"))));
+ chains.add(new Chain<Processor>("forbidden", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(403,"Message"))));
+ chains.add(new Chain<Processor>("forbidden_mapped", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.FORBIDDEN.code,"Message"))));
+ chains.add(new Chain<Processor>("not_found", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(404,"Message"))));
+ chains.add(new Chain<Processor>("not_found_mapped", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.NOT_FOUND.code,"Message"))));
+ chains.add(new Chain<Processor>("too_many_requests", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(429,"Message"))));
+ chains.add(new Chain<Processor>("bad_request", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(400,"Message"))));
+ chains.add(new Chain<Processor>("bad_request_mapped", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.BAD_REQUEST.code,"Message"))));
+ chains.add(new Chain<Processor>("internal_server_error", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(500,"Message"))));
+ chains.add(new Chain<Processor>("internal_server_error_mapped", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.INTERNAL_SERVER_ERROR.code,"Message"))));
+ chains.add(new Chain<Processor>("service_unavailable", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(503,"Message"))));
+ chains.add(new Chain<Processor>("service_unavailable_mapped", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.NO_BACKENDS_IN_SERVICE.code,"Message"))));
+ chains.add(new Chain<Processor>("gateway_timeout", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(504,"Message"))));
+ chains.add(new Chain<Processor>("gateway_timeout_mapped", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.TIMEOUT.code,"Message"))));
+ chains.add(new Chain<Processor>("bad_gateway", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(502,"Message"))));
+ chains.add(new Chain<Processor>("bad_gateway_mapped", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.BACKEND_COMMUNICATION_ERROR.code,"Message"))));
+ chains.add(new Chain<Processor>("unknown_code", new ProcessorLibrary.StringDataAdder("Hello"), new ProcessorLibrary.ErrorAdder(new ErrorMessage(1234567,"Message"))));
+ driver = new ProcessingTestDriver(chains);
+ assertEqualStatus(200, "http://localhost/?chain=simple");
+ assertEqualStatus(301, "http://localhost/?chain=moved_permanently");
+ assertEqualStatus(401, "http://localhost/?chain=unauthorized");
+ assertEqualStatus(401, "http://localhost/?chain=unauthorized_mapped");
+ assertEqualStatus(403, "http://localhost/?chain=forbidden");
+ assertEqualStatus(403, "http://localhost/?chain=forbidden_mapped");
+ assertEqualStatus(404, "http://localhost/?chain=not_found");
+ assertEqualStatus(404, "http://localhost/?chain=not_found_mapped");
+ assertEqualStatus(429, "http://localhost/?chain=too_many_requests");
+ assertEqualStatus(400, "http://localhost/?chain=bad_request");
+ assertEqualStatus(400, "http://localhost/?chain=bad_request_mapped");
+ assertEqualStatus(500, "http://localhost/?chain=internal_server_error");
+ assertEqualStatus(500, "http://localhost/?chain=internal_server_error_mapped");
+ assertEqualStatus(503, "http://localhost/?chain=service_unavailable");
+ assertEqualStatus(200, "http://localhost/?chain=service_unavailable_mapped"); // as this didn't fail and this isn't a web service mapped code
+ assertEqualStatus(504, "http://localhost/?chain=gateway_timeout");
+ assertEqualStatus(200, "http://localhost/?chain=gateway_timeout_mapped"); // as this didn't fail and this isn't a web service mapped code
+ assertEqualStatus(502, "http://localhost/?chain=bad_gateway");
+ assertEqualStatus(200, "http://localhost/?chain=bad_gateway_mapped"); // as this didn't fail and this isn't a web service mapped code
+ assertEqualStatus(200, "http://localhost/?chain=unknown_code"); // as this didn't fail and this isn't a web service mapped code
+ }
+
+ @Test
+ public void testProcessorSetsResponseHeaders() throws InterruptedException {
+ ProcessingTestDriver.MockResponseHandler responseHandler = null;
+ try {
+ Map<String,List<String>> responseHeaders = new HashMap<>();
+ responseHeaders.put("foo", Collections.singletonList("fooValue"));
+ responseHeaders.put("bar", Arrays.asList(new String[] { "barValue", "bazValue"}));
+
+ Map<String,List<String>> otherResponseHeaders = new HashMap<>();
+ otherResponseHeaders.put("foo", Collections.singletonList("fooValue2"));
+ otherResponseHeaders.put("bax", Collections.singletonList("baxValue"));
+
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(new Chain<Processor>("default",new ResponseHeaderSetter(responseHeaders),
+ new ResponseHeaderSetter(otherResponseHeaders)));
+ driver = new ProcessingTestDriver(chains);
+ responseHandler = driver.sendRequest("http://localhost/?chain=default").awaitResponse();
+ Response response = responseHandler.getResponse();
+ assertEquals("[fooValue2, fooValue]",response.headers().get("foo").toString());
+ assertEquals("[barValue, bazValue]", response.headers().get("bar").toString());
+ assertEquals("[baxValue]", response.headers().get("bax").toString());
+ assertEquals("ResponseHeaders are not rendered", "{\"datalist\":[]}", responseHandler.read());
+ }
+ finally {
+ if (responseHandler != null)
+ responseHandler.readAll();
+ }
+ }
+
+ @Test
+ public void testResponseDataStatus() throws InterruptedException {
+ ProcessingTestDriver.MockResponseHandler responseHandler = null;
+ try {
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(new Chain<Processor>("default", new ResponseStatusSetter(429)));
+ driver = new ProcessingTestDriver(chains);
+ responseHandler = driver.sendRequest("http://localhost/?chain=default").awaitResponse();
+ Response response = responseHandler.getResponse();
+ assertEquals(429, response.getStatus());
+ assertEquals("ResponseHeaders are not rendered", "{\"datalist\":[]}", responseHandler.read());
+ }
+ finally {
+ if (responseHandler != null)
+ responseHandler.readAll();
+ }
+ }
+
+ /** Tests that the ResponseStatus takes precedence over errors */
+ @Test
+ public void testResponseDataStatusOverridesErrors() throws InterruptedException {
+ ProcessingTestDriver.MockResponseHandler responseHandler = null;
+ try {
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(new Chain<Processor>("default", new ResponseStatusSetter(200),
+ new ProcessorLibrary.StringDataAdder("Hello"),
+ new ProcessorLibrary.ErrorAdder(new ErrorMessage(Error.FORBIDDEN.code,"Message"))));
+ driver = new ProcessingTestDriver(chains);
+ responseHandler = driver.sendRequest("http://localhost/?chain=default").awaitResponse();
+ Response response = responseHandler.getResponse();
+ assertEquals(200, response.getStatus());
+ }
+ finally {
+ if (responseHandler != null)
+ responseHandler.readAll();
+ }
+ }
+
+ private void assertEqualStatus(int statusCode,String uri) {
+ ProcessingTestDriver.MockResponseHandler response = null;
+ try {
+ response = driver.sendRequest(uri).awaitResponse();
+ assertEquals(statusCode, response.getStatus());
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ if (response != null) {
+ response.readAll();
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testProcessingHandlerSupportsAsyncRendering() throws Exception {
+ // Set up
+ ProcessorLibrary.FutureDataSource futureDataSource = new ProcessorLibrary.FutureDataSource();
+ Chain<Processor> asyncCompletionChain = new Chain<Processor>("asyncCompletion", new ProcessorLibrary.DataCounter("async"));
+ Chain<Processor> chain =
+ new Chain<Processor>("federation", new ProcessorLibrary.DataCounter("sync"),
+ new ProcessorLibrary.Federator(new Chain<Processor>(new ProcessorLibrary.DataSource()),
+ new Chain<Processor>(new ProcessorLibrary.AsyncDataProcessingInitiator(asyncCompletionChain),futureDataSource)));
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(chain);
+ driver = new ProcessingTestDriver(chains);
+
+ ProcessingTestDriver.MockResponseHandler responseHandler = driver.sendRequest("http://localhost/?chain=federation");
+ String synchronousResponse = responseHandler.read();
+ assertEquals(
+ "{\"datalist\":[" +
+ "{\"datalist\":[" +
+ "{\"data\":\"first.null\"}," +
+ "{\"data\":\"second.null\"}," +
+ "{\"data\":\"third.null\"}" +
+ "]}",
+ synchronousResponse);
+ assertEquals("No more data is available at this point", 0, responseHandler.available());
+
+ // Now, complete async data
+ futureDataSource.incomingData.get(0).add(new ProcessorLibrary.StringData(null, "d1"));
+ assertEquals(
+ "," +
+ "{\"datalist\":[" +
+ "{\"data\":\"d1\"}",
+ responseHandler.read());
+ futureDataSource.incomingData.get(0).addLast(new ProcessorLibrary.StringData(null, "d2"));
+
+ // ... which leads to the rest of the response becoming available
+ assertEquals(
+ "," +
+ "{\"data\":\"d2\"}," +
+ "{\"data\":\"[async] Data count: 2\"}" +
+ "]}",
+ responseHandler.read());
+ assertEquals(",{\"data\":\"[sync] Data count: 3\"}" + // Async items not counted as they arrive after chain completion
+ "]}",
+ responseHandler.read());
+ assertTrue("Transmission completed", null == responseHandler.read());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testProcessingHandlerSupportsAsyncUnorderedRendering() throws Exception {
+ // Set up
+ ProcessorLibrary.FutureDataSource futureDataSource1 = new ProcessorLibrary.FutureDataSource();
+ ProcessorLibrary.FutureDataSource futureDataSource2 = new ProcessorLibrary.FutureDataSource();
+ Chain<Processor> chain =
+ new Chain<Processor>("federation",
+ new ProcessorLibrary.Federator(false,new Chain<Processor>(futureDataSource1),
+ new Chain<Processor>(futureDataSource2)));
+ List<Chain<Processor>> chains = new ArrayList<>();
+ chains.add(chain);
+ driver = new ProcessingTestDriver(chains);
+
+ ProcessingTestDriver.MockResponseHandler responseHandler = driver.sendRequest("http://localhost/?chain=federation");
+ assertEquals(
+ "{\"datalist\":[",
+ responseHandler.read());
+ assertEquals("No more data is available at this point", 0, responseHandler.available());
+
+ // Complete second async data first
+ futureDataSource2.incomingData.get(0).addLast(new ProcessorLibrary.StringData(null, "d2"));
+ assertEquals(
+ "{\"datalist\":[" +
+ "{\"data\":\"d2\"}"+
+ "]}",
+ responseHandler.read());
+
+ // Now complete first async data (which is therefore rendered last)
+ futureDataSource1.incomingData.get(0).addLast(new ProcessorLibrary.StringData(null, "d1"));
+ assertEquals(
+ "," +
+ "{\"datalist\":[" +
+ "{\"data\":\"d1\"}"+
+ "]}",
+ responseHandler.read());
+ assertEquals(
+ "]}",
+ responseHandler.read());
+
+ assertTrue("Transmission completed", responseHandler.read()==null);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testAsyncOnlyRendering() throws Exception {
+ // Set up
+ ProcessorLibrary.ListenableFutureDataSource futureDataSource = new ProcessorLibrary.ListenableFutureDataSource();
+ Chain<Processor> chain = new Chain<>("main", Collections.<Processor>singletonList(futureDataSource));
+ driver = new ProcessingTestDriver(chain);
+
+ ProcessingTestDriver.MockResponseHandler responseHandler = driver.sendRequest("http://localhost/?chain=main");
+ assertEquals("No data is available at this point", 0, responseHandler.available());
+
+ futureDataSource.incomingData.get().add(new ProcessorLibrary.StringData(null, "d1"));
+ assertEquals(
+ "{\"datalist\":[" +
+ "{\"data\":\"d1\"}",
+ responseHandler.read());
+ futureDataSource.incomingData.get().addLast(new ProcessorLibrary.StringData(null, "d2"));
+
+ assertEquals(
+ "," +
+ "{\"data\":\"d2\"}" +
+ "]}",
+ responseHandler.read());
+
+ assertEquals(200, responseHandler.getStatus());
+ assertTrue("Transmission completed", null == responseHandler.read());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testAsyncRenderingWithClientClose() throws Exception {
+ // Set up
+ ProcessorLibrary.ListenableFutureDataSource futureDataSource = new ProcessorLibrary.ListenableFutureDataSource();
+ Chain<Processor> chain = new Chain<>("main", Collections.<Processor>singletonList(futureDataSource));
+ driver = new ProcessingTestDriver(chain);
+
+ ProcessingTestDriver.MockResponseHandler responseHandler = driver.sendRequest("http://localhost/?chain=main");
+ assertEquals("No data is available at this point", 0, responseHandler.available());
+
+ futureDataSource.incomingData.get().add(new ProcessorLibrary.StringData(null, "d1"));
+ assertEquals(
+ "{\"datalist\":[" +
+ "{\"data\":\"d1\"}",
+ responseHandler.read());
+ responseHandler.clientClose();
+ futureDataSource.incomingData.get().addLast(new ProcessorLibrary.StringData(null, "d2"));
+
+ assertNull(responseHandler.read());
+
+ assertEquals(200, responseHandler.getStatus());
+ assertTrue("Transmission completed", null == responseHandler.read());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testAsyncOnlyRenderingWithAsyncPostData() throws Exception {
+ // Set up
+ ProcessorLibrary.ListenableFutureDataSource futureDataSource = new ProcessorLibrary.ListenableFutureDataSource();
+ PostReader postReader = new PostReader();
+ Chain<Processor> chain = new Chain<>("main",
+ new ProcessorLibrary.AsyncDataProcessingInitiator(new Chain<>(postReader)),
+ futureDataSource);
+ driver = new ProcessingTestDriver(chain);
+ RequestHandlerTestDriver.MockResponseHandler responseHandler =
+ driver.sendRequest("http://localhost/?chain=main", HttpRequest.Method.POST, "Hello, world!");
+
+ assertFalse("Post data is read later, on async completion", postReader.bodyDataFuture.isDone());
+ assertEquals("No data is available at this point", 0, responseHandler.available());
+
+ futureDataSource.incomingData.get().add(new ProcessorLibrary.StringData(null, "d1"));
+ assertEquals(
+ "{\"datalist\":[" +
+ "{\"data\":\"d1\"}",
+ responseHandler.read()
+ );
+ futureDataSource.incomingData.get().addLast(new ProcessorLibrary.StringData(null, "d2"));
+
+ assertEquals(
+ "," +
+ "{\"data\":\"d2\"}" +
+ "]}",
+ responseHandler.read()
+ );
+ assertEquals("Data is completed, so post data is read", "Hello, world!", postReader.bodyDataFuture.get().trim());
+
+ assertEquals(200, responseHandler.getStatus());
+ assertTrue("Transmission completed", null == responseHandler.read());
+ }
+
+ private static class PostReader extends Processor {
+
+ SettableFuture<String> bodyDataFuture = SettableFuture.create();
+
+ @Override
+ public com.yahoo.processing.Response process(com.yahoo.processing.Request request, Execution execution) {
+ try {
+ InputStream stream = ((com.yahoo.container.jdisc.HttpRequest)request.properties().get(com.yahoo.processing.Request.JDISC_REQUEST)).getData();
+ StringBuilder b = new StringBuilder();
+ int nextRead;
+ while (-1 != (nextRead = stream.read()))
+ b.appendCodePoint(nextRead);
+ bodyDataFuture.set(b.toString());
+ return execution.process(request);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testStatusAndHeadersCanBeSetAsynchronously() throws Exception {
+ Map<String,List<String>> responseHeaders = new HashMap<>();
+ responseHeaders.put("foo", Collections.singletonList("fooValue"));
+ responseHeaders.put("bar", Arrays.asList(new String[] { "barValue", "bazValue"}));
+
+ // Set up
+ ProcessorLibrary.ListenableFutureDataSource futureDataSource = new ProcessorLibrary.ListenableFutureDataSource(true, false);
+ Chain<Processor> chain = new Chain<Processor>("main", new ProcessorLibrary.AsyncDataProcessingInitiator(new Chain<Processor>("async", new ProcessorLibrary.StatusSetter(500), new ResponseHeaderSetter(responseHeaders))), futureDataSource);
+ driver = new ProcessingTestDriver(chain);
+
+ ProcessingTestDriver.MockResponseHandler responseHandler = driver.sendRequest("http://localhost/?chain=main");
+ assertEquals("No data is available at this point", 0, responseHandler.available());
+
+ com.yahoo.processing.Request request = futureDataSource.incomingData.get().getOwner().request();
+ futureDataSource.incomingData.get().addLast(new ProcessorLibrary.StringData(request, "d1"));
+ //assertEquals("{\"datalist\":[{\"data\":\"d1\"}]}", consumeFrom(responseHandler.content));
+ assertEquals("{\"errors\":[\"500: \"],\"datalist\":[{\"data\":\"d1\"}]}", responseHandler.read());
+
+ assertEquals(500, responseHandler.getStatus());
+ assertEquals("[fooValue]", responseHandler.getResponse().headers().get("foo").toString());
+ assertEquals("[barValue, bazValue]", responseHandler.getResponse().headers().get("bar").toString());
+ assertTrue("Transmission completed", null == responseHandler.read());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testAsyncRenderingDoesNotHoldThreads() throws Exception {
+ // Set up
+ ProcessorLibrary.FutureDataSource futureDataSource = new ProcessorLibrary.FutureDataSource();
+ // Add some sync data as well to cause rendering to start before async data is added.
+ // This allows us to wait on return data rather than having to wait for the 100 requests
+ // to be done, which is cumbersome.
+ Chain<Processor> chain = new Chain<Processor>("main", new ProcessorLibrary.Federator(new Chain<Processor>(new ProcessorLibrary.DataSource()), new Chain<Processor>(futureDataSource)));
+ driver = new ProcessingTestDriver(chain);
+
+ int requestCount = 1000;
+ ProcessingTestDriver.MockResponseHandler[] responseHandler = new ProcessingTestDriver.MockResponseHandler[requestCount];
+ for (int i = 0; i < requestCount; i++) {
+ responseHandler[i] = driver.sendRequest("http://localhost/?chain=main");
+ assertEquals("Sync data is available",
+ "{\"datalist\":[{\"datalist\":[{\"data\":\"first.null\"},{\"data\":\"second.null\"},{\"data\":\"third.null\"}]}",
+ responseHandler[i].read());
+ }
+ assertEquals("All requests was processed", requestCount, futureDataSource.incomingData.size());
+
+ // Complete all
+ for (int i = 0; i < requestCount; i++) {
+ futureDataSource.incomingData.get(i).add(new ProcessorLibrary.StringData(null, "d1"));
+ assertEquals(",{\"datalist\":[{\"data\":\"d1\"}", responseHandler[i].read());
+ futureDataSource.incomingData.get(i).addLast(new ProcessorLibrary.StringData(null, "d2"));
+ assertEquals(",{\"data\":\"d2\"}]}", responseHandler[i].read());
+ assertEquals("]}", responseHandler[i].read());
+ assertTrue("Transmission completed", null == responseHandler[i].read());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testStreamedRendering() throws Exception {
+ // Set up
+ Chain<Processor> streamChain = new Chain<Processor>(new StreamProcessor());
+
+ ProcessorLibrary.ListenableFutureDataSource futureDataSource = new ProcessorLibrary.ListenableFutureDataSource();
+ Chain<Processor> mainChain = new Chain<Processor>("main", new ProcessorLibrary.StreamProcessingInitiator(streamChain), futureDataSource);
+ driver = new ProcessingTestDriver(mainChain);
+
+ ProcessingTestDriver.MockResponseHandler responseHandler = driver.sendRequest("http://localhost/?chain=main");
+
+ // Add one data element
+ futureDataSource.incomingData.get().add(new MapData(null));
+ assertEquals(
+ "{\"datalist\":[" +
+ "{\"data\":\"map data: {streamProcessed=true}\"}",
+ responseHandler.read()
+ );
+ // add another
+ futureDataSource.incomingData.get().add(new MapData(null));
+ assertEquals(
+ ",{\"data\":\"map data: {streamProcessed=true}\"}",
+ responseHandler.read());
+
+ // add last
+ futureDataSource.incomingData.get().addLast(new MapData(null));
+ assertEquals(
+ ",{\"data\":\"map data: {streamProcessed=true}\"}]}",
+ responseHandler.read());
+
+ assertTrue("Transmission completed", null == responseHandler.read());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testEagerStreamedRenderingOnFreeze() throws Exception {
+ FreezingDataSource source = new FreezingDataSource();
+ Chain<Processor> mainChain = new Chain<Processor>("main", source);
+ driver = new ProcessingTestDriver(mainChain);
+ ProcessingTestDriver.MockResponseHandler responseHandler = driver.sendRequest("http://localhost/?chain=main");
+ assertEquals("No data is available at this point", 0, responseHandler.available());
+ source.freeze.set(true);
+ assertEquals("{\"datalist\":[{\"data\":\"d1\"}", responseHandler.read());
+ source.addLastData.set(true); // signal completion
+ assertEquals(",{\"data\":\"d2\"}]}", responseHandler.read());
+ assertTrue("Transmission completed", null == responseHandler.read());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ @Ignore // TODO
+ public void testNestedEagerStreamedRenderingOnFreeze() throws Exception {
+ try {
+ FreezingDataSource source1 = new FreezingDataSource("s1");
+ FreezingDataSource source2 = new FreezingDataSource("s2");
+ FreezingDataSource source3 = new FreezingDataSource("s3");
+ Chain<Processor> mainChain = new Chain<Processor>("main",
+ new ProcessorLibrary.StringDataAdder("main-data"),
+ new ProcessorLibrary.EagerReturnFederator(true,
+ new Chain<Processor>(source1),
+ new Chain<Processor>(source2),
+ new Chain<Processor>(source3)));
+ driver = new ProcessingTestDriver(mainChain);
+ ProcessingTestDriver.MockResponseHandler responseHandler = driver.sendRequest("http://localhost/?chain=main");
+ assertEquals("No data is available at this point", 0, responseHandler.available());
+ source1.freeze.set(true);
+ assertEquals("Received because the parent list and source1 list is frozen",
+ "{\"datalist\":[{\"datalist\":[{\"data\":\"s1d1\"}", responseHandler.read());
+
+ source2.addLastData.set(true); // No effect as we are working on source1, which is not completed yet
+ assertEquals("{\"datalist\":[{\"data\":\"s1d1\"}", responseHandler.read());
+ source1.addLastData.set(true); // Make source 1 and 2 available
+ assertEquals(",{\"data\":\"d2\"}]}", responseHandler.read());
+ assertTrue("Transmission completed", null == responseHandler.read());
+ }
+ catch (Throwable t) {
+ t.printStackTrace();
+ throw t;
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRetrievingNonExistingRendererThrows() throws Exception {
+ driver = new ProcessingTestDriver(Collections.<Chain<Processor>>emptyList());
+ driver.processingHandler().getRendererCopy(ComponentSpecification.fromString("non-existent"));
+ }
+
+ @Test
+ public void testDefaultRendererIsAddedToRegistryWhenNoneIsGivenByUser() throws Exception {
+ String defaultId = AbstractProcessingHandler.DEFAULT_RENDERER_ID;
+
+ driver = new ProcessingTestDriver(Collections.<Chain<Processor>>emptyList());
+ Renderer defaultRenderer = driver.processingHandler().getRenderers().getComponent(defaultId);
+ assertThat(defaultRenderer, notNullValue());
+
+ }
+
+ @Test
+ public void testUserSpecifiedDefaultRendererIsNotReplacedInRegistry() throws Exception {
+ String defaultId = AbstractProcessingHandler.DEFAULT_RENDERER_ID;
+ Renderer myDefaultRenderer = new ProcessingRenderer();
+ ComponentRegistry<Renderer> renderers = new ComponentRegistry<>();
+ renderers.register(ComponentId.fromString(defaultId), myDefaultRenderer);
+
+ driver = new ProcessingTestDriver(Collections.<Chain<Processor>>emptyList(), renderers);
+ Renderer defaultRenderer = driver.processingHandler().getRenderers().getComponent(defaultId);
+ assertThat(defaultRenderer, sameInstance(myDefaultRenderer));
+
+ }
+
+ private static class FreezingDataSource extends Processor {
+
+ final SettableFuture<Boolean> freeze = SettableFuture.create();
+ final SettableFuture<Boolean> addLastData = SettableFuture.create();
+
+ private final String stringDataPrefix;
+
+ public FreezingDataSource() {
+ this("");
+ }
+
+ public FreezingDataSource(String stringDataPrefix) {
+ this.stringDataPrefix = stringDataPrefix;
+ }
+
+ @Override
+ public com.yahoo.processing.Response process(com.yahoo.processing.Request request, Execution execution) {
+ try {
+ com.yahoo.processing.Response response = execution.process(request);
+ response.data().add(new ProcessorLibrary.StringData(request, stringDataPrefix + "d1"));
+ freeze.get();
+ response.data().freeze();
+ // wait for permission from test driver to add more data
+ addLastData.get();
+ response.data().add(new ProcessorLibrary.StringData(request, stringDataPrefix + "d2"));
+ return response;
+ }
+ catch (InterruptedException | ConcurrentModificationException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ }
+
+ @SuppressWarnings("unchecked")
+ private static class StreamProcessor extends Processor {
+
+ @Override
+ public com.yahoo.processing.Response process(com.yahoo.processing.Request request, Execution execution) {
+ com.yahoo.processing.Response response = execution.process(request);
+ List<Data> dataList = response.data().asList();
+ for (Data data : dataList) {
+ if ( ! (data instanceof MapData)) continue;
+ MapData mapData = (MapData)data;
+ mapData.map().put("streamProcessed",Boolean.TRUE);
+ }
+ return response;
+ }
+
+ }
+
+ private String defaultChainResponse =
+ "{\"datalist\":[" +
+ "{\"data\":\"StringData.toString()\"}," +
+ "{\"datalist\":[" +
+ "{\"data\":\"Item1\"}," +
+ "{\"data\":\"Item2\"}]" +
+ "}]" +
+ "}";
+
+ private String simpleChainResponse =
+ "{\"datalist\":[" +
+ "{\"data\":\"StringData.toString()\"}]" +
+ "}";
+
+ private String trace1 =
+ "{\"trace\":[" +
+ "\"TraceMessage\"" +
+ "],";
+
+ private String trace1WithFullResult =
+ "{\"trace\":[" +
+ "\"TraceMessage\"" +
+ "]," +
+ "\"datalist\":[" +
+ "{\"data\":\"StringData.toString()\"}," +
+ "{\"datalist\":[" +
+ "{\"data\":\"Item1\"}," +
+ "{\"data\":\"Item2\"}" +
+ "]}" +
+ "]}";
+
+ private String trace4 =
+ "{\"trace\":[" +
+ "\"Invoke '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataListAdder'\"," +
+ "\"Invoke '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$Trace'\"," +
+ "\"TraceMessage\"," +
+ "\"Invoke '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataAdder'\"," +
+ "\"Return '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataAdder'\"," +
+ "\"Return '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$Trace'\"," +
+ "\"Return '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataListAdder'\"" +
+ "],";
+
+ private String trace5 =
+ "{\"trace\":[" +
+ "\"Invoke '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataListAdder'\"," +
+ "\"Invoke '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$Trace'\"," +
+ "\"TraceMessage\"," +
+ "\"Invoke '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataAdder'\"," +
+ "\"Return '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataAdder'\"," +
+ "\"Return '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$Trace'\"," +
+ "\"Return '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataListAdder'\"" +
+ "],";
+
+ private String trace6 =
+ "{\"trace\":[" +
+ "{\"timestamp\":ddddddddddddd,\"message\":\"Invoke '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataListAdder'\"}," +
+ "{\"timestamp\":ddddddddddddd,\"message\":\"Invoke '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$Trace'\"}," +
+ "\"TraceMessage\"," +
+ "{\"timestamp\":ddddddddddddd,\"message\":\"Invoke '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataAdder'\"}," +
+ "{\"timestamp\":ddddddddddddd,\"message\":\"Return '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataAdder'\"}," +
+ "{\"timestamp\":ddddddddddddd,\"message\":\"Return '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$Trace'\"}," +
+ "{\"timestamp\":ddddddddddddd,\"message\":\"Return '(anonymous)' of class 'com.yahoo.processing.test.ProcessorLibrary$StringDataListAdder'\"}" +
+ "],";
+
+ /** Adds a set of headers to every passing response */
+ @SuppressWarnings("unchecked")
+ public static class ResponseHeaderSetter extends Processor {
+
+ private final Map<String,List<String>> responseHeaders;
+
+ public ResponseHeaderSetter(Map<String,List<String>> responseHeaders) {
+ this.responseHeaders = Collections.unmodifiableMap(responseHeaders);
+ }
+
+ @Override
+ public com.yahoo.processing.Response process(com.yahoo.processing.Request request, Execution execution) {
+ com.yahoo.processing.Response response = execution.process(request);
+ response.data().add(new ResponseHeaders(responseHeaders, request));
+ return response;
+ }
+
+ }
+
+ /** Adds a HTTP status to every passing response */
+ @SuppressWarnings("unchecked")
+ public static class ResponseStatusSetter extends Processor {
+
+ private final int code;
+
+ public ResponseStatusSetter(int code) {
+ this.code = code;
+ }
+
+ @Override
+ public com.yahoo.processing.Response process(com.yahoo.processing.Request request, Execution execution) {
+ com.yahoo.processing.Response response = execution.process(request);
+ response.data().add(new ResponseStatus(code, request));
+ return response;
+ }
+
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/processing/processors/MockUserDatabaseClient.java b/container-core/src/test/java/com/yahoo/processing/processors/MockUserDatabaseClient.java
new file mode 100644
index 00000000000..7e39ef3359f
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/processing/processors/MockUserDatabaseClient.java
@@ -0,0 +1,136 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.processing.processors;
+
+import com.yahoo.component.chain.dependencies.Provides;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.handler.*;
+import com.yahoo.processing.Processor;
+import com.yahoo.processing.Request;
+import com.yahoo.processing.Response;
+import com.yahoo.processing.execution.Execution;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@Provides("User")
+public class MockUserDatabaseClient extends Processor {
+
+ @Override
+ public Response process(Request request, Execution execution) {
+ try {
+ Dispatch.CompleteResponse response =
+ new Dispatch("pio://endpoint/parameters",request).get(request.properties().getInteger("timeout"), TimeUnit.MILLISECONDS);
+ User user = decodeResponseToUser(response);
+ request.properties().set("User", user);
+ return execution.process(request);
+ }
+ catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new RuntimeException("Exception waiting for database", e);
+ }
+ }
+
+ private User decodeResponseToUser(Dispatch.CompleteResponse response) {
+ // Just a mock implementation ...
+ String responseData = response.nextString();
+ if ( ! responseData.startsWith("id="))
+ throw new IllegalArgumentException("Unexpected response " + responseData);
+ int newLine = responseData.indexOf("\n");
+ if (newLine<0)
+ throw new IllegalArgumentException("Unexpected response " + responseData);
+ String id = responseData.substring(3,newLine);
+
+ // Make sure to always consume all
+ while ( (responseData=response.nextString()) !=null) { }
+
+ return new User(id);
+ }
+
+ // TODO: Move this to a top-level class
+ public static class User {
+
+ // TODO: OO model of users
+
+ private String id;
+
+ public User(String id) {
+ this.id = id;
+ }
+
+ public String getId() { return id; }
+
+ }
+
+ private static class Dispatch {
+
+ private final SimpleRequestDispatch requestDispatch;
+
+ public Dispatch(String requestUri,Request request) {
+ this.requestDispatch = new SimpleRequestDispatch(requestUri, request);
+ }
+
+ public CompleteResponse get(int timeout, TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException {
+ return new CompleteResponse(requestDispatch.get(timeout, timeUnit),
+ requestDispatch.getResponseContent());
+ }
+
+ public static class CompleteResponse {
+
+ private final com.yahoo.jdisc.Response response;
+ private final ReadableContentChannel responseData;
+
+ public CompleteResponse(com.yahoo.jdisc.Response response, ReadableContentChannel responseData) {
+ this.response = response;
+ this.responseData = responseData;
+ }
+
+ public com.yahoo.jdisc.Response response() { return response; }
+
+ public ReadableContentChannel responseData() { return responseData; }
+
+ /**
+ * Convenience which returns the next piece of content from the response data of this as a string, or
+ * null if there is no more data. The channel must always be consumed until there is no more data.
+ */
+ private String nextString() {
+ ByteBuffer nextBuffer = responseData.read();
+ if (nextBuffer == null) return null; // end of transmission
+ return Charset.forName("utf-8").decode(nextBuffer).toString();
+ }
+
+ }
+
+ private static class SimpleRequestDispatch extends RequestDispatch {
+
+ private final URI requestUri;
+ private final com.yahoo.jdisc.Request parentRequest;
+ private final ReadableContentChannel responseData = new ReadableContentChannel();
+
+ public SimpleRequestDispatch(String requestUri,Request request) {
+ this.requestUri = URI.create(requestUri);
+ this.parentRequest = ((HttpRequest)request.properties().get("jdisc.request")).getJDiscRequest();
+ dispatch();
+ }
+
+ @Override
+ protected com.yahoo.jdisc.Request newRequest() {
+ return new com.yahoo.jdisc.Request(parentRequest, requestUri);
+ }
+
+ @Override
+ public ContentChannel handleResponse(com.yahoo.jdisc.Response response) {
+ return responseData;
+ }
+
+ public ReadableContentChannel getResponseContent() {
+ return responseData;
+ }
+
+ }
+
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/processing/processors/MockUserDatabaseClientTest.java b/container-core/src/test/java/com/yahoo/processing/processors/MockUserDatabaseClientTest.java
new file mode 100644
index 00000000000..f1d32fe09c5
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/processing/processors/MockUserDatabaseClientTest.java
@@ -0,0 +1,94 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.processing.processors;
+
+import com.yahoo.component.chain.Chain;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.*;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.test.TestDriver;
+import com.yahoo.processing.Processor;
+import com.yahoo.processing.Request;
+import com.yahoo.processing.Response;
+import com.yahoo.processing.execution.Execution;
+import com.yahoo.processing.execution.chain.ChainRegistry;
+import org.junit.After;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.junit.Assert.assertTrue;
+
+public class MockUserDatabaseClientTest {
+
+ private TestDriver driver;
+
+ @Test
+ public void testClientExampleProcessor() {
+ Request request=null;
+ try {
+ Chain<Processor> chain = new Chain<>("default",new MockUserDatabaseClient());
+ setupJDisc(Collections.singletonList(chain));
+ request = createRequest();
+ Response response = Execution.createRoot(chain, 0, Execution.Environment.createEmpty()).process(request);
+ MockUserDatabaseClient.User user = (MockUserDatabaseClient.User)response.data().request().properties().get("User");
+ assertNotNull(user);
+ assertEquals("foo", user.getId());
+ }
+ finally {
+ release(request);
+ }
+ }
+
+ /** Creates a request which has an underlying jdisc request, which is needed to make the outgoing request */
+ private Request createRequest() {
+ com.yahoo.jdisc.http.HttpRequest jdiscRequest = HttpRequest.newServerRequest(driver, URI.create("http://localhost/"));
+ com.yahoo.container.jdisc.HttpRequest containerRequest = new com.yahoo.container.jdisc.HttpRequest(jdiscRequest,new ByteArrayInputStream(new byte[0]));
+
+ Request request = new Request();
+ request.properties().set("jdisc.request",containerRequest);
+ request.properties().set("timeout",1000);
+ return request;
+ }
+
+ private void release(Request request) {
+ if (request==null) return;
+ ((com.yahoo.container.jdisc.HttpRequest)request.properties().get("jdisc.request")).getJDiscRequest().release();
+ }
+
+ private void setupJDisc(Collection<Chain<Processor>> chains) {
+ driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+
+ ChainRegistry<Processor> registry = new ChainRegistry<>();
+ for (Chain<Processor> chain : chains)
+ registry.register(chain.getId(), chain);
+
+ builder.clientBindings().bind("pio://endpoint/*", new MockUserDatabaseRequestHandler());
+ driver.activateContainer(builder);
+ }
+
+ @After
+ public void shutDownDisc() {
+ assertTrue(driver.close());
+ }
+
+ private static class MockUserDatabaseRequestHandler extends AbstractRequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(com.yahoo.jdisc.Request request, ResponseHandler responseHandler) {
+ FastContentWriter writer = new FastContentWriter(ResponseDispatch.newInstance(com.yahoo.jdisc.Response.Status.OK).connect(responseHandler));
+ try {
+ writer.write("id=foo\n");
+ } finally {
+ writer.close();
+ }
+ return null;
+ }
+
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/processing/rendering/AsynchronousSectionedRendererTest.java b/container-core/src/test/java/com/yahoo/processing/rendering/AsynchronousSectionedRendererTest.java
new file mode 100644
index 00000000000..b8efd8562b1
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/processing/rendering/AsynchronousSectionedRendererTest.java
@@ -0,0 +1,435 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.processing.rendering;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.yahoo.component.provider.ListenableFreezableClass;
+import com.yahoo.container.jdisc.ContentChannelOutputStream;
+import com.yahoo.processing.Processor;
+import com.yahoo.processing.Request;
+import com.yahoo.processing.Response;
+import com.yahoo.processing.execution.Execution;
+import com.yahoo.processing.request.ErrorMessage;
+import com.yahoo.processing.response.ArrayDataList;
+import com.yahoo.processing.response.Data;
+import com.yahoo.processing.response.DataList;
+import com.yahoo.processing.response.IncomingData;
+import com.yahoo.text.Utf8;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class AsynchronousSectionedRendererTest {
+
+ private static final Charset CHARSET = Utf8.getCharset();
+
+ @Test
+ public void testAsyncSectionedRenderer() throws IOException, InterruptedException {
+ StringDataList dataList = createDataListWithStrangeStrings();
+
+ TestRenderer renderer = new TestRenderer();
+ renderer.init();
+
+ String str = render(renderer, dataList);
+
+ assertThat(str,
+ equalTo(" beginResponse beginList[f\\o\"o, [b/a\br, f\f\no\ro\tbar\u0005]] dataf\\o\"o beginList[b/a\br, " +
+ "f\f\no\ro\tbar\u0005] datab/a\br dataf\f\no\ro\tbar\u0005 endList[b/a\br, f\f\no\ro\tbar\u0005] endList[f\\o\"o, [b/a\br, f\f\no\ro\tbar\u0005]] endResponse"));
+ }
+
+ @Test
+ public void testEmptyProcessingRendering() throws IOException, InterruptedException {
+ Request request = new Request();
+ DataList dataList = ArrayDataList.create(request);
+
+ assertThat(render(dataList),
+ equalTo("{\"datalist\":[" +
+ "]}"));
+ }
+
+ @Test
+ public void testProcessingRendering() throws IOException, InterruptedException {
+ StringDataList dataList = createDataListWithStrangeStrings();
+
+ assertThat(render(dataList),
+ equalTo("{\"datalist\":[" +
+ "{\"data\":\"f\\\\o\\\"o\"}," +
+ "{\"datalist\":[" +
+ "{\"data\":\"b/a\\br\"}," +
+ "{\"data\":\"f\\f\\no\\ro\\tbar\\u0005\"}" +
+ "]}" +
+ "]}"));
+ }
+
+ @Test
+ public void testProcessingRenderingWithErrors() throws IOException, InterruptedException {
+ StringDataList dataList = createDataList();
+
+ // Add errors
+ dataList.request().errors().add(new ErrorMessage("m1","d1"));
+ dataList.request().errors().add(new ErrorMessage("m2","d2"));
+
+ assertThat(render(dataList),
+ equalTo("{\"errors\":[" +
+ "\"m1: d1\"," +
+ "\"m2: d2\"" +
+ "]," +
+ "\"datalist\":[" +
+ "{\"data\":\"l1\"}," +
+ "{\"datalist\":[" +
+ "{\"data\":\"l11\"}," +
+ "{\"data\":\"l12\"}" +
+ "]}" +
+ "]}"));
+ }
+
+ @Test
+ public void testProcessingRenderingWithStackTraces() throws IOException, InterruptedException {
+ Exception exception=null;
+ // Create thrown exception
+ try {
+ throw new RuntimeException("Thrown");
+ }
+ catch (RuntimeException e) {
+ exception=e;
+ }
+
+ StringDataList dataList = createDataList();
+
+ // Add errors
+ dataList.request().errors().add(new ErrorMessage("m1","d1",exception));
+ dataList.request().errors().add(new ErrorMessage("m2","d2"));
+
+ assertEquals(
+ "{\"errors\":[" +
+ "{" +
+ "\"error\":\"m1: d1: Thrown\"," +
+ "\"stacktrace\":\"java.lang.RuntimeException: Thrown\\n\\tat com.yahoo.processing.rendering.AsynchronousSectionedRendererTest.",
+ render(dataList).substring(0,157));
+ }
+
+ @Test
+ public void testProcessingRenderingWithClonedErrorRequest() throws IOException, InterruptedException {
+ StringDataList dataList = createDataList();
+
+ // Add errors
+ dataList.request().errors().add(new ErrorMessage("m1","d1"));
+ dataList.request().errors().add(new ErrorMessage("m2","d2"));
+ dataList.add(new StringDataList(dataList.request().clone())); // Cloning a request which contains errors
+ // ... should not cause repetition of those errors
+
+ assertThat(render(dataList),
+ equalTo("{\"errors\":[" +
+ "\"m1: d1\"," +
+ "\"m2: d2\"" +
+ "]," +
+ "\"datalist\":[" +
+ "{\"data\":\"l1\"}," +
+ "{\"datalist\":[" +
+ "{\"data\":\"l11\"}," +
+ "{\"data\":\"l12\"}" +
+ "]}," +
+ "{\"datalist\":[]}" +
+ "]}"));
+ }
+
+ @Test
+ public void testProcessingRenderingWithClonedErrorRequestContainingNewErrors() throws IOException, InterruptedException {
+ StringDataList dataList = createDataList();
+
+ // Add errors
+ dataList.request().errors().add(new ErrorMessage("m1","d1"));
+ dataList.request().errors().add(new ErrorMessage("m2","d2"));
+ dataList.add(new StringDataList(dataList.request().clone())); // Cloning a request containing errors
+ // and adding new errors to it
+ dataList.asList().get(2).request().errors().add(new ErrorMessage("m3","d3"));
+
+ assertThat(render(dataList),
+ equalTo("{\"errors\":[" +
+ "\"m1: d1\"," +
+ "\"m2: d2\"" +
+ "]," +
+ "\"datalist\":[" +
+ "{\"data\":\"l1\"}," +
+ "{\"datalist\":[" +
+ "{\"data\":\"l11\"}," +
+ "{\"data\":\"l12\"}" +
+ "]}," +
+ "{\"errors\":[" +
+ "\"m3: d3\"" +
+ "]," +
+ "\"datalist\":[]}" +
+ "]}"));
+ }
+
+ public StringDataList createDataList() {
+ Request request = new Request();
+ StringDataList dataList = new StringDataList(request);
+ dataList.add(new StringDataItem(request, "l1"));
+ StringDataList secondLevel = new StringDataList(request);
+ secondLevel.add(new StringDataItem(request, "l11"));
+ secondLevel.add(new StringDataItem(request, "l12"));
+ dataList.add(secondLevel);
+ return dataList;
+ }
+
+ public StringDataList createDataListWithStrangeStrings() {
+ Request request = new Request();
+ StringDataList dataList = new StringDataList(request);
+ dataList.add(new StringDataItem(request, "f\\o\"o"));
+ StringDataList secondLevel = new StringDataList(request);
+ secondLevel.add(new StringDataItem(request, "b/a\br"));
+ secondLevel.add(new StringDataItem(request, "f\f\no\ro\tbar\u0005"));
+ dataList.add(secondLevel);
+ return dataList;
+ }
+
+ public String render(DataList data) throws InterruptedException, IOException {
+ ProcessingRenderer renderer = new ProcessingRenderer();
+ renderer.init();
+ return render(renderer, data);
+ }
+
+ @SuppressWarnings("unchecked")
+ public String render(Renderer renderer, DataList data) throws InterruptedException, IOException {
+ TestContentChannel contentChannel = new TestContentChannel();
+
+ Execution execution = Execution.createRoot(new NoopProcessor(), 0, null);
+
+ final ContentChannelOutputStream stream = new ContentChannelOutputStream(contentChannel);
+ ListenableFuture result = renderer.render(stream, new Response(data), execution, null);
+
+ int waitCounter = 1000;
+ while (!result.isDone()) {
+ Thread.sleep(60);
+ --waitCounter;
+ if (waitCounter < 0) {
+ throw new IllegalStateException();
+ }
+ }
+
+ stream.close();
+ contentChannel.close(null);
+
+ String str = "";
+ for (ByteBuffer buf : contentChannel.getBuffers()) {
+ str += Utf8.toString(buf);
+ }
+ return str;
+ }
+
+ private static class TestRenderer extends AsynchronousSectionedRenderer<Response> {
+
+ private OutputStream stream;
+
+ private StringDataList checkInstanceList(DataList<?> list) {
+ if (!(list instanceof StringDataList)) {
+ throw new IllegalArgumentException();
+ }
+ return (StringDataList) list;
+ }
+
+ private StringData checkInstanceData(Data data) {
+ if (!(data instanceof StringData)) {
+ throw new IllegalArgumentException();
+ }
+ return (StringData) data;
+ }
+
+
+ @Override
+ public void beginResponse(OutputStream stream) {
+ this.stream = stream;
+ try {
+ stream.write(" beginResponse".getBytes(CHARSET));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void beginList(DataList<?> list) {
+ StringDataList stringDataList = checkInstanceList(list);
+ try {
+ stream.write((" beginList" + stringDataList.getString()).getBytes(CHARSET));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void data(Data data) {
+ StringData stringData = checkInstanceData(data);
+ try {
+ stream.write((" data" + stringData.getString()).getBytes(CHARSET));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void endList(DataList<?> list) {
+ StringDataList stringDataList = checkInstanceList(list);
+ try {
+ stream.write((" endList" + stringDataList.getString()).getBytes(CHARSET));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void endResponse() {
+ try {
+ stream.write(" endResponse".getBytes(CHARSET));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String getEncoding() {
+ return CHARSET.name();
+ }
+
+ @Override
+ public String getMimeType() {
+ return "text/plain";
+ }
+ }
+
+ private abstract class StringData extends ListenableFreezableClass implements Data {
+ private final Request request;
+
+ private StringData(Request request) {
+ this.request = request;
+ }
+
+ @Override
+ public Request request() {
+ return request;
+ }
+
+ public abstract String getString();
+
+ @Override
+ public String toString() {
+ return getString();
+ }
+ }
+
+ private class StringDataItem extends StringData {
+
+ private final String string;
+
+ private StringDataItem(Request request, String string) {
+ super(request);
+ this.string = string;
+ }
+
+ @Override
+ public String getString() {
+ return string;
+ }
+ }
+
+ private class StringDataList extends StringData implements DataList<StringData> {
+
+ private final ArrayList<StringData> list = new ArrayList<>();
+
+ private final IncomingData incomingData;
+
+ private StringDataList(Request request) {
+ super(request);
+ incomingData = new IncomingData.NullIncomingData<>(this);
+ }
+
+ @Override
+ public StringData add(StringData data) {
+ list.add(data);
+ return data;
+ }
+
+ @Override
+ public StringData get(int index) {
+ return list.get(index);
+ }
+
+ @Override
+ public List<StringData> asList() {
+ return list;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public IncomingData<StringData> incoming() {
+ return incomingData;
+ }
+
+ @Override
+ public void addDataListener(Runnable runnable) {
+ throw new RuntimeException("Not supported");
+ }
+
+ @Override
+ public ListenableFuture<DataList<StringData>> complete() {
+ return new ListenableFuture<DataList<StringData>>() {
+ @Override
+ public void addListener(Runnable runnable, Executor executor) {
+ }
+
+ @Override
+ public boolean cancel(boolean b) {
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public DataList<StringData> get() throws InterruptedException, ExecutionException {
+ return StringDataList.this;
+ }
+
+ @Override
+ public DataList<StringData> get(long l, TimeUnit timeUnit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return StringDataList.this;
+ }
+ };
+ }
+
+ @Override
+ public String getString() {
+ return list.toString();
+ }
+ }
+
+ private static class NoopProcessor extends Processor {
+
+ @Override
+ public Response process(Request request, Execution execution) {
+ return execution.process(request);
+ }
+
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/processing/rendering/TestContentChannel.java b/container-core/src/test/java/com/yahoo/processing/rendering/TestContentChannel.java
new file mode 100644
index 00000000000..1e684b816aa
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/processing/rendering/TestContentChannel.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.processing.rendering;
+
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+* @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+* @since 5.1.9
+*/
+class TestContentChannel implements ContentChannel {
+ private final List<ByteBuffer> buffers = new ArrayList<>();
+ private boolean closed = false;
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ buffers.add(buf);
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ closed = true;
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ public List<ByteBuffer> getBuffers() {
+ return buffers;
+ }
+
+ public boolean isClosed() {
+ return closed;
+ }
+}