// Copyright Yahoo. 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 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.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* @author Einar M R Rosenvinge
*/
public class AsynchronousSectionedRendererTest {
private static final Charset CHARSET = Utf8.getCharset();
@Test
public void testRenderersOfTheSamePrototypeUseTheSameExecutor() {
TestRenderer rendererPrototype = new TestRenderer();
TestRenderer rendererCopy1 = (TestRenderer)rendererPrototype.clone();
rendererCopy1.init();
assertTrue(rendererPrototype.getRenderingExecutor() == rendererCopy1.getRenderingExecutor());
}
@Test
public void testRenderersOfDifferentPrototypesUseDifferentExecutors() {
TestRenderer rendererPrototype1 = new TestRenderer();
TestRenderer rendererCopy1 = (TestRenderer)rendererPrototype1.clone();
rendererCopy1.init();
TestRenderer rendererPrototype2 = new TestRenderer();
TestRenderer rendererCopy2 = (TestRenderer)rendererPrototype2.clone();
rendererCopy2.init();
assertTrue(rendererPrototype1.getRenderingExecutor() != rendererCopy2.getRenderingExecutor());
}
@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", "removal"})
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 {
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 {
private final ArrayList 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 asList() {
return list;
}
@SuppressWarnings("unchecked")
@Override
public IncomingData incoming() {
return incomingData;
}
@Override
public void addDataListener(Runnable runnable) {
throw new RuntimeException("Not supported");
}
@Override
public ListenableFuture