diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /container-core/src/test/java/com/yahoo |
Publish
Diffstat (limited to 'container-core/src/test/java/com/yahoo')
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; + } +} |