summaryrefslogtreecommitdiffstats
path: root/container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java
diff options
context:
space:
mode:
Diffstat (limited to 'container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java')
-rw-r--r--container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java1111
1 files changed, 1111 insertions, 0 deletions
diff --git a/container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java b/container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java
new file mode 100644
index 00000000000..4b26187c9d3
--- /dev/null
+++ b/container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java
@@ -0,0 +1,1111 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.rendering;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.times;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import com.yahoo.document.datatypes.TensorFieldValue;
+import com.yahoo.document.predicate.Predicate;
+
+import com.yahoo.tensor.MapTensor;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.yahoo.component.chain.Chain;
+import com.yahoo.data.access.slime.SlimeAdapter;
+import com.yahoo.document.DataType;
+import com.yahoo.document.DocumentId;
+import com.yahoo.document.Field;
+import com.yahoo.document.StructDataType;
+import com.yahoo.document.datatypes.StringFieldValue;
+import com.yahoo.document.datatypes.Struct;
+import com.yahoo.prelude.fastsearch.FastHit;
+import com.yahoo.prelude.hitfield.JSONString;
+import com.yahoo.search.Query;
+import com.yahoo.search.Result;
+import com.yahoo.search.Searcher;
+import com.yahoo.search.grouping.Continuation;
+import com.yahoo.search.grouping.result.DoubleBucketId;
+import com.yahoo.search.grouping.result.Group;
+import com.yahoo.search.grouping.result.GroupList;
+import com.yahoo.search.grouping.result.RootGroup;
+import com.yahoo.search.grouping.result.StringId;
+import com.yahoo.search.result.Coverage;
+import com.yahoo.search.result.ErrorMessage;
+import com.yahoo.search.result.Hit;
+import com.yahoo.search.result.HitGroup;
+import com.yahoo.search.result.NanNumber;
+import com.yahoo.search.result.Relevance;
+import com.yahoo.search.result.StructuredData;
+import com.yahoo.search.searchchain.Execution;
+import com.yahoo.search.statistics.ElapsedTimeTestCase;
+import com.yahoo.search.statistics.TimeTracker;
+import com.yahoo.search.statistics.ElapsedTimeTestCase.CreativeTimeSource;
+import com.yahoo.search.statistics.ElapsedTimeTestCase.UselessSearcher;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.text.Utf8;
+import com.yahoo.yolean.trace.TraceNode;
+
+import org.mockito.Mockito;
+
+/**
+ * Functional testing of {@link JsonRenderer}.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class JsonRendererTestCase {
+
+ JsonRenderer originalRenderer;
+ JsonRenderer renderer;
+
+ public JsonRendererTestCase() {
+ originalRenderer = new JsonRenderer();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ // Do the same dance as in production
+ renderer = (JsonRenderer) originalRenderer.clone();
+ renderer.init();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ renderer = null;
+ }
+
+ private static final class Thingie {
+ @Override
+ public String toString() {
+ return "thingie";
+ }
+ }
+
+ @Test
+ public final void testDocumentId() throws IOException, InterruptedException, ExecutionException, JSONException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"documentid\": \"id:unittest:smoke::whee\"\n"
+ + " },\n"
+ + " \"id\": \"id:unittest:smoke::whee\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ Hit h = new Hit("docIdTest");
+ h.setField("documentid", new DocumentId("id:unittest:smoke::whee"));
+ r.hits().add(h);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ private Result newEmptyResult() {
+ Query q = new Query("/?query=a");
+ Result r = new Result(q);
+ return r;
+ }
+
+ @Test
+ public final void testDataTypes() throws IOException, InterruptedException, ExecutionException, JSONException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"double\": 0.00390625,\n"
+ + " \"float\": 14.29,\n"
+ + " \"integer\": 1,\n"
+ + " \"long\": 4398046511104,\n"
+ + " \"object\": \"thingie\",\n"
+ + " \"string\": \"stuff\",\n"
+ + " \"predicate\": \"a in [b]\",\n"
+ + " \"tensor\": { \"dimensions\": [\"x\"], \n"
+ + " \"cells\": [ { \"address\": {\"x\": \"a\"}, \"value\":2.0 } ] }\n"
+ + " },\n"
+ + " \"id\": \"datatypestuff\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ Hit h = new Hit("datatypestuff");
+ // the floating point values are chosen to get a deterministic string representation
+ h.setField("double", Double.valueOf(0.00390625d));
+ h.setField("float", Float.valueOf(14.29f));
+ h.setField("integer", Integer.valueOf(1));
+ h.setField("long", Long.valueOf(4398046511104L));
+ h.setField("string", "stuff");
+ h.setField("predicate", Predicate.fromString("a in [b]"));
+ h.setField("tensor", new TensorFieldValue(MapTensor.from("{ {x:a}: 2.0}")));
+ h.setField("object", new Thingie());
+ r.hits().add(h);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+
+ @Test
+ public final void testTracing() throws JsonGenerationException, IOException, InterruptedException, ExecutionException {
+ // which clearly shows a trace child is created once too often...
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " },\n"
+ + " \"trace\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"No query profile is used\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"something\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"message\": \"something else\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"yellow\"\n"
+ + " }\n"
+ + " ]\n"
+ + " },\n"
+ + " {\n"
+ + " \"message\": \"marker\"\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + "}\n";
+ Query q = new Query("/?query=a&tracelevel=1");
+ Execution execution = new Execution(
+ Execution.Context.createContextStub());
+ Result r = new Result(q);
+
+ execution.search(q);
+ q.trace("something", 1);
+ q.trace("something else", 1);
+ Execution e2 = new Execution(new Chain<Searcher>(), execution.context());
+ Query subQuery = new Query("/?query=b&tracelevel=1");
+ e2.search(subQuery);
+ subQuery.trace("yellow", 1);
+ q.trace("marker", 1);
+ String summary = render(execution, r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public final void testEmptyTracing() throws JsonGenerationException, IOException, InterruptedException, ExecutionException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Query q = new Query("/?query=a&tracelevel=0");
+ Execution execution = new Execution(
+ Execution.Context.createContextStub());
+ Result r = new Result(q);
+
+ execution.search(q);
+ Execution e2 = new Execution(new Chain<Searcher>(), execution.context());
+ Query subQuery = new Query("/?query=b&tracelevel=0");
+ e2.search(subQuery);
+ subQuery.trace("yellow", 1);
+ q.trace("marker", 1);
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ ListenableFuture<Boolean> f = renderer.render(bs, r, execution, null);
+ assertTrue(f.get());
+ String summary = Utf8.toString(bs.toByteArray());
+ assertEqualJson(expected, summary);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public final void testTracingWithEmptySubtree() throws IOException, InterruptedException, ExecutionException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " },\n"
+ + " \"trace\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"No query profile is used\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"message\": \"Resolved properties:\\ntracelevel=10 (value from request)\\nquery=a (value from request)\\n\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"timestamp\": 42\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + "}";
+ Query q = new Query("/?query=a&tracelevel=10");
+ Execution execution = new Execution(Execution.Context.createContextStub());
+ Result r = new Result(q);
+
+ execution.search(q);
+ new Execution(new Chain<Searcher>(), execution.context());
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ ListenableFuture<Boolean> f = renderer.render(bs, r, execution, null);
+ assertTrue(f.get());
+ String summary = Utf8.toString(bs.toByteArray());
+ ObjectMapper m = new ObjectMapper();
+
+ Map<String, Object> exp = m.readValue(expected, Map.class);
+ Map<String, Object> gen = m.readValue(summary, Map.class);
+ {
+ // nuke timestamp and check it's there
+ Map<String, Object> trace = (Map<String, Object>) gen.get("trace");
+ List<Object> children1 = (List<Object>) trace.get("children");
+ Map<String, Object> subtrace = (Map<String, Object>) children1.get(2);
+ List<Object> children2 = (List<Object>) subtrace.get("children");
+ Map<String, Object> traceElement = (Map<String, Object>) children2.get(0);
+ traceElement.put("timestamp", Integer.valueOf(42));
+ }
+ assertEquals(exp, gen);
+ }
+
+
+ @Test
+ public final void testHalfEmptyTracing() throws JsonGenerationException, IOException, InterruptedException, ExecutionException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " },\n"
+ + " \"trace\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"green\""
+ + " }"
+ + " ]"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + "}\n";
+ Query q = new Query("/?query=a&tracelevel=0");
+ Execution execution = new Execution(
+ Execution.Context.createContextStub());
+ Result r = new Result(q);
+
+ execution.search(q);
+ subExecution(execution, "red", 0);
+ subExecution(execution, "green", 1);
+ subExecution(execution, "blue", 0);
+ q.trace("marker", 1);
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ ListenableFuture<Boolean> f = renderer.render(bs, r, execution, null);
+ assertTrue(f.get());
+ String summary = Utf8.toString(bs.toByteArray());
+ assertEqualJson(expected, summary);
+ }
+
+ private void subExecution(Execution execution, String color, int traceLevel) {
+ Execution e2 = new Execution(new Chain<Searcher>(), execution.context());
+ Query subQuery = new Query("/?query=b&tracelevel=" + traceLevel);
+ e2.search(subQuery);
+ subQuery.trace(color, 1);
+ }
+
+ @Test
+ public final void testTracingOfNodesWithBothChildrenAndData() throws JsonGenerationException, IOException, InterruptedException, ExecutionException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " },\n"
+ + " \"trace\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"No query profile is used\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"string payload\",\n"
+ + " \"children\": ["
+ + " {\n"
+ + " \"message\": \"leafnode\""
+ + " }\n"
+ + " ]\n"
+ + " },\n"
+ + " {\n"
+ + " \"message\": \"something\"\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + "}\n";
+ Query q = new Query("/?query=a&tracelevel=1");
+ Execution execution = new Execution(
+ Execution.Context.createContextStub());
+ Result r = new Result(q);
+ execution.search(q);
+ final TraceNode child = new TraceNode("string payload", 0L);
+ child.add(new TraceNode("leafnode", 0L));
+ execution.trace().traceNode().add(child);
+ q.trace("something", 1);
+ String summary = render(execution, r);
+ assertEqualJson(expected, summary);
+ }
+
+
+ @Test
+ public final void testTracingOfNodesWithBothChildrenAndDataAndEmptySubnode() throws JsonGenerationException, IOException, InterruptedException, ExecutionException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " },\n"
+ + " \"trace\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"No query profile is used\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"string payload\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"message\": \"something\"\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + "}\n";
+ Query q = new Query("/?query=a&tracelevel=1");
+ Execution execution = new Execution(
+ Execution.Context.createContextStub());
+ Result r = new Result(q);
+ execution.search(q);
+ final TraceNode child = new TraceNode("string payload", 0L);
+ child.add(new TraceNode(null, 0L));
+ execution.trace().traceNode().add(child);
+ q.trace("something", 1);
+ String summary = render(execution, r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public final void testTracingOfNestedNodesWithDataAndSubnodes() throws JsonGenerationException, IOException, InterruptedException, ExecutionException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " },\n"
+ + " \"trace\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"No query profile is used\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"string payload\",\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"message\": \"in OO languages, nesting is for birds\"\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + "}\n";
+ Query q = new Query("/?query=a&tracelevel=1");
+ Execution execution = new Execution(
+ Execution.Context.createContextStub());
+ Result r = new Result(q);
+ execution.search(q);
+ final TraceNode child = new TraceNode("string payload", 0L);
+ final TraceNode childOfChild = new TraceNode(null, 0L);
+ child.add(childOfChild);
+ childOfChild.add(new TraceNode("in OO languages, nesting is for birds", 0L));
+ execution.trace().traceNode().add(child);
+ String summary = render(execution, r);
+ assertEqualJson(expected, summary);
+ }
+
+
+ @Test
+ public final void test() throws IOException, InterruptedException, ExecutionException, JSONException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"c\": \"d\",\n"
+ + " \"uri\": \"http://localhost/1\"\n"
+ + " },\n"
+ + " \"id\": \"http://localhost/1\",\n"
+ + " \"relevance\": 0.9,\n"
+ + " \"types\": [\n"
+ + " \"summary\"\n"
+ + " ]\n"
+ + " }\n"
+ + " ],\n"
+ + " \"id\": \"usual\",\n"
+ + " \"relevance\": 1.0\n"
+ + " },\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"e\": \"f\"\n"
+ + " },\n"
+ + " \"id\": \"type grouphit\",\n"
+ + " \"relevance\": 1.0,\n"
+ + " \"types\": [\n"
+ + " \"grouphit\"\n"
+ + " ]\n"
+ + " },\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"b\": \"foo\",\n"
+ + " \"uri\": \"http://localhost/\"\n"
+ + " },\n"
+ + " \"id\": \"http://localhost/\",\n"
+ + " \"relevance\": 0.95,\n"
+ + " \"types\": [\n"
+ + " \"summary\"\n"
+ + " ]\n"
+ + " }\n"
+ + " ],\n"
+ + " \"coverage\": {\n"
+ + " \"coverage\": 100,\n"
+ + " \"documents\": 500,\n"
+ + " \"full\": true,\n"
+ + " \"nodes\": 1,\n"
+ + " \"results\": 1,\n"
+ + " \"resultsFull\": 1\n"
+ + " },\n"
+ + " \"errors\": [\n"
+ + " {\n"
+ + " \"code\": 18,\n"
+ + " \"message\": \"boom\",\n"
+ + " \"summary\": \"Internal server error.\"\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}";
+ Query q = new Query("/?query=a&tracelevel=5&reportCoverage=true");
+ Execution execution = new Execution(
+ Execution.Context.createContextStub());
+ Result r = new Result(q);
+ r.setCoverage(new Coverage(500, 1, true));
+
+ FastHit h = new FastHit("http://localhost/", .95);
+ h.setField("$a", "Hello, world.");
+ h.setField("b", "foo");
+ r.hits().add(h);
+ HitGroup g = new HitGroup("usual");
+ h = new FastHit("http://localhost/1", .90);
+ h.setField("c", "d");
+ g.add(h);
+ r.hits().add(g);
+ HitGroup gg = new HitGroup("type grouphit");
+ gg.types().add("grouphit");
+ gg.setField("e", "f");
+ r.hits().add(gg);
+ r.hits().addError(ErrorMessage.createInternalServerError("boom"));
+ String summary = render(execution, r);
+ // System.out.println(summary);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public void testMoreTypes() throws InterruptedException, ExecutionException, JsonParseException, JsonMappingException, IOException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"bigDecimal\": 3.402823669209385e+38,\n"
+ + " \"bigInteger\": 340282366920938463463374607431768211455,\n"
+ + " \"byte\": 8,\n"
+ + " \"short\": 16\n"
+ + " },\n"
+ + " \"id\": \"moredatatypestuff\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ Hit h = new Hit("moredatatypestuff");
+ h.setField("byte", Byte.valueOf((byte) 8));
+ h.setField("short", Short.valueOf((short) 16));
+ h.setField("bigInteger", new BigInteger(
+ "340282366920938463463374607431768211455"));
+ h.setField("bigDecimal", new BigDecimal(
+ "340282366920938463463374607431768211456.5"));
+ h.setField("nanNumber", NanNumber.NaN);
+ r.hits().add(h);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public void testNullField() throws InterruptedException, ExecutionException, JsonParseException, JsonMappingException, IOException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"null\": null\n"
+ + " },\n"
+ + " \"id\": \"nullstuff\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ Hit h = new Hit("nullstuff");
+ h.setField("null", null);
+ r.hits().add(h);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public void testLazyDecoding() throws IOException {
+ FastHit f = new FastHit("http://a.b/c", 0.5);
+ String checkWeCanDecode = "bamse";
+ String dontCare = "don't care";
+ final String fieldName = "checkWeCanDecode";
+ f.setLazyStringField(fieldName, Utf8.toBytes(checkWeCanDecode));
+ final String fieldName2 = "dontCare";
+ f.setLazyStringField(fieldName2, Utf8.toBytes(dontCare));
+ assertEquals(checkWeCanDecode, f.getField(fieldName));
+
+ JsonGenerator mock = Mockito.mock(JsonGenerator.class);
+
+ renderer.setGenerator(mock);
+ assertTrue(renderer.tryDirectRendering(fieldName2, f));
+
+ byte[] expectedBytes = Utf8.toBytes(dontCare);
+ Mockito.verify(mock, times(1)).writeUTF8String(expectedBytes, 0, expectedBytes.length);
+ }
+
+ @Test
+ public void testHitWithSource() throws JsonParseException, JsonMappingException, IOException, InterruptedException, ExecutionException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"id\": \"datatypestuff\",\n"
+ + " \"relevance\": 1.0,\n"
+ + " \"source\": \"unit test\"\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ Hit h = new Hit("datatypestuff");
+ h.setSource("unit test");
+ r.hits().add(h);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public void testErrorWithStackTrace() throws InterruptedException,
+ ExecutionException, JsonParseException, JsonMappingException, IOException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"errors\": [\n"
+ + " {\n"
+ + " \"code\": 1234,\n"
+ + " \"message\": \"top of the day\",\n"
+ + " \"stackTrace\": \"java.lang.Throwable\\n\\tat com.yahoo.search.rendering.JsonRendererTestCase.testErrorWithStackTrace(JsonRendererTestCase.java:732)\\n\",\n"
+ + " \"summary\": \"hello\"\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Query q = new Query("/?query=a&tracelevel=5&reportCoverage=true");
+ Result r = new Result(q);
+ Throwable t = new Throwable();
+ StackTraceElement[] stack = new StackTraceElement[1];
+ stack[0] = new StackTraceElement(
+ "com.yahoo.search.rendering.JsonRendererTestCase",
+ "testErrorWithStackTrace", "JsonRendererTestCase.java", 732);
+ t.setStackTrace(stack);
+ ErrorMessage e = new ErrorMessage(1234, "hello", "top of the day", t);
+ r.hits().addError(e);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public void testContentHeader() {
+ assertEquals("utf-8", renderer.getEncoding());
+ assertEquals("application/json", renderer.getMimeType());
+ }
+
+ @Test
+ public void testGrouping() throws InterruptedException, ExecutionException, JsonParseException, JsonMappingException, IOException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"count()\": 7\n"
+ + " },\n"
+ + " \"value\": \"Jones\",\n"
+ + " \"id\": \"group:string:Jones\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"continuation\": {\n"
+ + " \"next\": \"CCCC\",\n"
+ + " \"prev\": \"BBBB\"\n"
+ + " },\n"
+ + " \"id\": \"grouplist:customer\",\n"
+ + " \"label\": \"customer\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"continuation\": {\n"
+ + " \"this\": \"AAAA\"\n"
+ + " },\n"
+ + " \"id\": \"group:root:0\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ RootGroup rg = new RootGroup(0, new Continuation() {
+ @Override
+ public String toString() {
+ return "AAAA";
+ }
+ });
+ GroupList gl = new GroupList("customer");
+ gl.continuations().put("prev", new Continuation() {
+ @Override
+ public String toString() {
+ return "BBBB";
+ }
+ });
+ gl.continuations().put("next", new Continuation() {
+ @Override
+ public String toString() {
+ return "CCCC";
+ }
+ });
+ Group g = new Group(new StringId("Jones"), new Relevance(1.0));
+ g.setField("count()", Integer.valueOf(7));
+ gl.add(g);
+ rg.add(gl);
+ r.hits().add(rg);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public void testGroupingWithBucket() throws InterruptedException, ExecutionException, JsonParseException, JsonMappingException, IOException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"something()\": 7\n"
+ + " },\n"
+ + " \"limits\": {\n"
+ + " \"from\": \"1.0\",\n"
+ + " \"to\": \"2.0\"\n"
+ + " },\n"
+ + " \"id\": \"group:double_bucket:1.0:2.0\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"id\": \"grouplist:customer\",\n"
+ + " \"label\": \"customer\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"continuation\": {\n"
+ + " \"this\": \"AAAA\"\n"
+ + " },\n"
+ + " \"id\": \"group:root:0\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ RootGroup rg = new RootGroup(0, new Continuation() {
+ @Override
+ public String toString() {
+ return "AAAA";
+ }
+ });
+ GroupList gl = new GroupList("customer");
+ Group g = new Group(new DoubleBucketId(1.0, 2.0), new Relevance(1.0));
+ g.setField("something()", Integer.valueOf(7));
+ gl.add(g);
+ rg.add(gl);
+ r.hits().add(rg);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public void testJsonObjects() throws JsonParseException, JsonMappingException, InterruptedException, ExecutionException, IOException, JSONException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"inspectable\": {\n"
+ + " \"a\": \"b\"\n"
+ + " },\n"
+ + " \"jackson\": {\n"
+ + " \"Nineteen-eighty-four\": 1984\n"
+ + " },\n"
+ + " \"json producer\": {\n"
+ + " \"long in structured\": 7809531904\n"
+ + " },\n"
+ + " \"org.json array\": [\n"
+ + " true,\n"
+ + " true,\n"
+ + " false\n"
+ + " ],\n"
+ + " \"org.json object\": {\n"
+ + " \"forty-two\": 42\n"
+ + " }\n"
+ + " },\n"
+ + " \"id\": \"json objects\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 0\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ Hit h = new Hit("json objects");
+ JSONObject o = new JSONObject();
+ JSONArray a = new JSONArray();
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode j = mapper.createObjectNode();
+ JSONString s = new JSONString("{\"a\": \"b\"}");
+ Slime slime = new Slime();
+ Cursor c = slime.setObject();
+ c.setLong("long in structured", 7809531904L);
+ SlimeAdapter slimeInit = new SlimeAdapter(slime.get());
+ StructuredData struct = new StructuredData(slimeInit);
+ ((ObjectNode) j).put("Nineteen-eighty-four", 1984);
+ o.put("forty-two", 42);
+ a.put(true);
+ a.put(true);
+ a.put(false);
+ h.setField("inspectable", s);
+ h.setField("jackson", j);
+ h.setField("json producer", struct);
+ h.setField("org.json array", a);
+ h.setField("org.json object", o);
+ r.hits().add(h);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public final void testFieldValueInHit() throws IOException, InterruptedException, ExecutionException, JSONException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"fromDocumentApi\":{\"integerField\":123, \"stringField\":\"abc\"}"
+ + " },\n"
+ + " \"id\": \"fieldValueTest\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ Hit h = new Hit("fieldValueTest");
+ StructDataType structType = new StructDataType("jsonRenderer");
+ structType.addField(new Field("stringField", DataType.STRING));
+ structType.addField(new Field("integerField", DataType.INT));
+ Struct struct = structType.createFieldValue();
+ struct.setFieldValue("stringField", "abc");
+ struct.setFieldValue("integerField", 123);
+ h.setField("fromDocumentApi", struct);
+ r.hits().add(h);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public final void testHiddenFields() throws IOException, InterruptedException, ExecutionException, JSONException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"id\": \"hiddenFields\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = newEmptyResult();
+ Hit h = createHitWithOnlyHiddenFields();
+ r.hits().add(h);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ private Hit createHitWithOnlyHiddenFields() {
+ Hit h = new Hit("hiddenFields");
+ h.setField("NaN", NanNumber.NaN);
+ h.setField("emptyString", "");
+ h.setField("emptyStringFieldValue", new StringFieldValue(""));
+ h.setField("$vespaImplementationDetail", "Hello, World!");
+ return h;
+ }
+
+ @Test
+ public final void testDebugRendering() throws IOException, InterruptedException, ExecutionException, JSONException {
+ String expected = "{\n"
+ + " \"root\": {\n"
+ + " \"children\": [\n"
+ + " {\n"
+ + " \"fields\": {\n"
+ + " \"NaN\": \"NaN\",\n"
+ + " \"emptyString\": \"\",\n"
+ + " \"emptyStringFieldValue\": \"\",\n"
+ + " \"$vespaImplementationDetail\": \"Hello, World!\"\n"
+ + " },\n"
+ + " \"id\": \"hiddenFields\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + " ],\n"
+ + " \"fields\": {\n"
+ + " \"totalCount\": 1\n"
+ + " },\n"
+ + " \"id\": \"toplevel\",\n"
+ + " \"relevance\": 1.0\n"
+ + " }\n"
+ + "}\n";
+ Result r = new Result(new Query("/?renderer.json.debug=true"));
+ Hit h = createHitWithOnlyHiddenFields();
+ r.hits().add(h);
+ r.setTotalHitCount(1L);
+ String summary = render(r);
+ assertEqualJson(expected, summary);
+ }
+
+ @Test
+ public final void testTimingRendering() throws InterruptedException, ExecutionException, JsonParseException, JsonMappingException, IOException {
+ String expected = "{"
+ + " \"root\": {"
+ + " \"fields\": {"
+ + " \"totalCount\": 0"
+ + " },"
+ + " \"id\": \"toplevel\","
+ + " \"relevance\": 1.0"
+ + " },"
+ + " \"timing\": {"
+ + " \"querytime\": 0.006,"
+ + " \"searchtime\": 0.007,"
+ + " \"summaryfetchtime\": 0.0"
+ + " }"
+ + "}";
+ Result r = new Result(new Query("/?renderer.json.debug=true&presentation.timing=true"));
+ TimeTracker t = new TimeTracker(new Chain<Searcher>(
+ new UselessSearcher("first"), new UselessSearcher("second"),
+ new UselessSearcher("third")));
+ ElapsedTimeTestCase.doInjectTimeSource(t, new CreativeTimeSource(
+ new long[] { 1L, 2L, 3L, 4L, 5L, 6L, 7L }));
+ t.sampleSearch(0, true);
+ t.sampleSearch(1, true);
+ t.sampleSearch(2, true);
+ t.sampleSearch(3, true);
+ t.sampleSearchReturn(2, true, null);
+ t.sampleSearchReturn(1, true, null);
+ t.sampleSearchReturn(0, true, null);
+ r.getElapsedTime().add(t);
+ renderer.setTimeSource(() -> 8L);
+ String summary = render(r);
+ System.out.println(summary);
+ assertEqualJson(expected, summary);
+ }
+
+ private String render(Result r) throws InterruptedException,
+ ExecutionException {
+ Execution execution = new Execution(
+ Execution.Context.createContextStub());
+ return render(execution, r);
+ }
+
+ private String render(Execution execution, Result r)
+ throws InterruptedException, ExecutionException {
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ ListenableFuture<Boolean> f = renderer.render(bs, r, execution, null);
+ assertTrue(f.get());
+ String summary = Utf8.toString(bs.toByteArray());
+ return summary;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void assertEqualJson(String expected, String generated) throws JsonParseException, JsonMappingException, IOException {
+ ObjectMapper m = new ObjectMapper();
+ Map<String, Object> exp = m.readValue(expected, Map.class);
+ Map<String, Object> gen = m.readValue(generated, Map.class);
+ assertEquals(exp, gen);
+ }
+
+}