From 9e741cd13d27db608b1499b7b620ae74f10f960e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Wed, 7 Jun 2017 21:01:20 +0200 Subject: [PATCH] Tests: Add ability to generate random new fields for xContent parsing test (#23437) For the response parsing we want to be lenient when it comes to parsing new xContent fields. In order to ensure this in our testing, this change adds a utility method to XContentTestUtils that takes xContent bytes representation as input and recursively a random field on each object level. Sometimes we also want to exclude a whole subtree from this treatment (e.g. skipping "_source"), other times an element (e.g. "fields", "highlight" in SearchHit) can have arbitraryly named objects. Those cases can be specified as exceptions. --- .../org/elasticsearch/search/SearchHit.java | 15 +- .../org/elasticsearch/search/SearchHits.java | 15 +- .../action/main/MainResponseTests.java | 5 +- .../elasticsearch/search/SearchHitTests.java | 30 ++- .../elasticsearch/search/SearchHitsTests.java | 35 ++++ .../org/elasticsearch/test/ESTestCase.java | 4 - .../elasticsearch/test/XContentTestUtils.java | 171 ++++++++++++++++ .../test/rest/yaml/ObjectPath.java | 17 ++ .../test/XContentTestUtilsTests.java | 188 ++++++++++++++++++ .../test/test/ESTestCaseTests.java | 17 +- 10 files changed, 472 insertions(+), 25 deletions(-) create mode 100644 test/framework/src/test/java/org/elasticsearch/test/XContentTestUtilsTests.java diff --git a/core/src/main/java/org/elasticsearch/search/SearchHit.java b/core/src/main/java/org/elasticsearch/search/SearchHit.java index 6172f974b14..81cba7d8db7 100644 --- a/core/src/main/java/org/elasticsearch/search/SearchHit.java +++ b/core/src/main/java/org/elasticsearch/search/SearchHit.java @@ -41,6 +41,7 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParser.Token; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.shard.ShardId; @@ -65,8 +66,8 @@ import static org.elasticsearch.common.lucene.Lucene.writeExplanation; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureFieldName; import static org.elasticsearch.common.xcontent.XContentParserUtils.parseStoredFieldsValue; -import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField; import static org.elasticsearch.search.fetch.subphase.highlight.HighlightField.readHighlightField; /** @@ -482,7 +483,7 @@ public final class SearchHit implements Streamable, ToXContentObject, Iterable, Void> MAP_PARSER = new ObjectParser<>("innerHitsParser", HashMap::new); + private static ObjectParser, Void> MAP_PARSER = new ObjectParser<>("innerHitParser", true, HashMap::new); static { declareInnerHitsParseFields(MAP_PARSER); @@ -614,7 +615,10 @@ public final class SearchHit implements Streamable, ToXContentObject, Iterable innerHits = new HashMap<>(); while ((parser.nextToken()) != XContentParser.Token.END_OBJECT) { ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation); - innerHits.put(parser.currentName(), SearchHits.fromXContent(parser)); + String name = parser.currentName(); + ensureExpectedToken(Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + ensureFieldName(parser, parser.nextToken(), SearchHits.Fields.HITS); + innerHits.put(name, SearchHits.fromXContent(parser)); ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser::getTokenLocation); } return innerHits; @@ -649,7 +653,7 @@ public final class SearchHit implements Streamable, ToXContentObject, Iterable PARSER = new ConstructingObjectParser<>( - "nested_identity", + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("nested_identity", true, ctorArgs -> new NestedIdentity((String) ctorArgs[0], (int) ctorArgs[1], (NestedIdentity) ctorArgs[2])); static { PARSER.declareString(constructorArg(), new ParseField(FIELD)); diff --git a/core/src/main/java/org/elasticsearch/search/SearchHits.java b/core/src/main/java/org/elasticsearch/search/SearchHits.java index 0b49ba8ec12..f7250a7f07a 100644 --- a/core/src/main/java/org/elasticsearch/search/SearchHits.java +++ b/core/src/main/java/org/elasticsearch/search/SearchHits.java @@ -34,7 +34,6 @@ import java.util.List; import java.util.Objects; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; -import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField; public final class SearchHits implements Streamable, ToXContent, Iterable { @@ -148,19 +147,21 @@ public final class SearchHits implements Streamable, ToXContent, Iterable pathsToExclude = path -> (path.endsWith("highlight") || path.endsWith("fields") || path.contains("_source") + || path.contains("inner_hits")); + BytesReference withRandomFields = insertRandomFields(xContentType, originalBytes, pathsToExclude, random()); + + SearchHit parsed; + try (XContentParser parser = createParser(xContentType.xContent(), withRandomFields)) { + parser.nextToken(); // jump to first START_OBJECT + parsed = SearchHit.fromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + assertNull(parser.nextToken()); + } + assertToXContentEquivalent(originalBytes, toXContent(parsed, xContentType, true), xContentType); + } + /** * When e.g. with "stored_fields": "_none_", only "_index" and "_score" are returned. */ diff --git a/core/src/test/java/org/elasticsearch/search/SearchHitsTests.java b/core/src/test/java/org/elasticsearch/search/SearchHitsTests.java index 4c41e6fbcda..c25eb7da814 100644 --- a/core/src/test/java/org/elasticsearch/search/SearchHitsTests.java +++ b/core/src/test/java/org/elasticsearch/search/SearchHitsTests.java @@ -30,8 +30,10 @@ import org.elasticsearch.test.ESTestCase; import java.io.IOException; import java.util.Collections; +import java.util.function.Predicate; import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; +import static org.elasticsearch.test.XContentTestUtils.insertRandomFields; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; public class SearchHitsTests extends ESTestCase { @@ -54,6 +56,10 @@ public class SearchHitsTests extends ESTestCase { BytesReference originalBytes = toShuffledXContent(searchHits, xcontentType, ToXContent.EMPTY_PARAMS, humanReadable); SearchHits parsed; try (XContentParser parser = createParser(xcontentType.xContent(), originalBytes)) { + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals(SearchHits.Fields.HITS, parser.currentName()); + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); parsed = SearchHits.fromXContent(parser); assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); @@ -62,6 +68,35 @@ public class SearchHitsTests extends ESTestCase { assertToXContentEquivalent(originalBytes, toXContent(parsed, xcontentType, humanReadable), xcontentType); } + /** + * This test adds randomized fields on all json objects and checks that we + * can parse it to ensure the parsing is lenient for forward compatibility. + * We need to exclude json objects with the "highlight" and "fields" field + * name since these objects allow arbitrary keys (the field names that are + * queries). Also we want to exclude to add anything under "_source" since + * it is not parsed. + */ + public void testFromXContentLenientParsing() throws IOException { + SearchHits searchHits = createTestItem(); + XContentType xcontentType = randomFrom(XContentType.values()); + BytesReference originalBytes = toXContent(searchHits, xcontentType, ToXContent.EMPTY_PARAMS, true); + Predicate pathsToExclude = path -> (path.isEmpty() || path.endsWith("highlight") || path.endsWith("fields") + || path.contains("_source")); + BytesReference withRandomFields = insertRandomFields(xcontentType, originalBytes, pathsToExclude, random()); + SearchHits parsed = null; + try (XContentParser parser = createParser(xcontentType.xContent(), withRandomFields)) { + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals(SearchHits.Fields.HITS, parser.currentName()); + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + parsed = SearchHits.fromXContent(parser); + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + assertNull(parser.nextToken()); + } + assertToXContentEquivalent(originalBytes, toXContent(parsed, xcontentType, true), xcontentType); + } + public void testToXContent() throws IOException { SearchHit[] hits = new SearchHit[] { new SearchHit(1, "id1", new Text("type"), Collections.emptyMap()), diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 256227779aa..0d3e8131ab2 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -97,7 +97,6 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptEngine; import org.elasticsearch.script.ScriptModule; -import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.MockSearchService; import org.elasticsearch.test.junit.listeners.LoggingListener; @@ -139,12 +138,9 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; -import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.elasticsearch.common.util.CollectionUtils.arrayAsArrayList; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; diff --git a/test/framework/src/main/java/org/elasticsearch/test/XContentTestUtils.java b/test/framework/src/main/java/org/elasticsearch/test/XContentTestUtils.java index dcad7187fbe..16953b45d19 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/XContentTestUtils.java +++ b/test/framework/src/main/java/org/elasticsearch/test/XContentTestUtils.java @@ -19,17 +19,30 @@ package org.elasticsearch.test; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.rest.yaml.ObjectPath; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Random; +import java.util.Stack; +import java.util.function.Predicate; +import java.util.function.Supplier; +import static com.carrotsearch.randomizedtesting.generators.RandomStrings.randomAsciiOfLength; import static org.elasticsearch.common.xcontent.ToXContent.EMPTY_PARAMS; +import static org.elasticsearch.common.xcontent.XContentHelper.createParser; public final class XContentTestUtils { private XContentTestUtils() { @@ -123,4 +136,162 @@ public final class XContentTestUtils { } } + /** + * This method takes the input xContent data and adds a random field value, inner object or array into each + * json object. This can e.g. be used to test if parsers that handle the resulting xContent can handle the + * augmented xContent correctly, for example when testing lenient parsing. + * + * If the xContent output contains objects that should be skipped of such treatment, an optional filtering + * {@link Predicate} can be supplied that checks xContent paths that should be excluded from this treatment. + * + * This predicate should check the xContent path that we want to insert to and return true if the + * path should be excluded. Paths are string concatenating field names and array indices, so e.g. in: + * + *
+     * {
+     *      "foo1 : {
+     *          "bar" : [
+     *              { ... },
+     *              { ... },
+     *              {
+     *                  "baz" : {
+     *                      // insert here
+     *                  }
+     *              }
+     *          ]
+     *      }
+     * }
+     * 
+ * + * "foo1.bar.2.baz" would point to the desired insert location. + * + * To exclude inserting into the "foo1" object we would user a {@link Predicate} like + *
+     * {@code
+     *      (path) -> path.endsWith("foo1")
+     * }
+     * 
+ * + * or if we don't want any random insertions in the "foo1" tree we could use + *
+     * {@code
+     *      (path) -> path.contains("foo1")
+     * }
+     * 
+ */ + public static BytesReference insertRandomFields(XContentType contentType, BytesReference xContent, Predicate excludeFilter, + Random random) throws IOException { + List insertPaths; + + // we can use NamedXContentRegistry.EMPTY here because we only traverse the xContent once and don't use it + try (XContentParser parser = createParser(NamedXContentRegistry.EMPTY, xContent, contentType)) { + parser.nextToken(); + List possiblePaths = XContentTestUtils.getInsertPaths(parser, new Stack<>()); + if (excludeFilter == null) { + insertPaths = possiblePaths; + } else { + insertPaths = new ArrayList<>(); + possiblePaths.stream().filter(excludeFilter.negate()).forEach(insertPaths::add); + } + } + + try (XContentParser parser = createParser(NamedXContentRegistry.EMPTY, xContent, contentType)) { + Supplier value = () -> { + if (random.nextBoolean()) { + return RandomObjects.randomStoredFieldValues(random, contentType); + } else { + if (random.nextBoolean()) { + return Collections.singletonMap(randomAsciiOfLength(random, 10), randomAsciiOfLength(random, 10)); + } else { + return Collections.singletonList(randomAsciiOfLength(random, 10)); + } + } + }; + return XContentTestUtils + .insertIntoXContent(contentType.xContent(), xContent, insertPaths, () -> randomAsciiOfLength(random, 10), value) + .bytes(); + } + } + + /** + * This utility method takes an XContentParser and walks the xContent structure to find all + * possible paths to where a new object or array starts. This can be used in tests that add random + * xContent values to test parsing code for errors or to check their robustness against new fields. + * + * The path uses dot separated fieldnames and numbers for array indices, similar to what we do in + * {@link ObjectPath}. + * + * The {@link Stack} passed in should initially be empty, it gets pushed to by recursive calls + * + * As an example, the following json xContent: + *
+     *     {
+     *         "foo" : "bar",
+     *         "foo1" : [ 1, { "foo2" : "baz" }, 3, 4]
+     *         "foo3" : {
+     *             "foo4" : {
+     *                  "foo5": "buzz"
+     *             }
+     *         }
+     *     }
+     * 
+ * + * Would return the following list: + * + *
    + *
  • "" (the empty string is the path to the root object)
  • + *
  • "foo1.1"
  • + *
  • "foo3
  • + *
  • "foo3.foo4
  • + *
+ */ + static List getInsertPaths(XContentParser parser, Stack currentPath) throws IOException { + assert parser.currentToken() == XContentParser.Token.START_OBJECT || parser.currentToken() == XContentParser.Token.START_ARRAY : + "should only be called when new objects or arrays start"; + List validPaths = new ArrayList<>(); + // parser.currentName() can be null for root object and unnamed objects in arrays + if (parser.currentName() != null) { + currentPath.push(parser.currentName()); + } + if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + validPaths.add(String.join(".", currentPath.toArray(new String[currentPath.size()]))); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + if (parser.currentToken() == XContentParser.Token.START_OBJECT + || parser.currentToken() == XContentParser.Token.START_ARRAY) { + validPaths.addAll(getInsertPaths(parser, currentPath)); + } + } + } else if (parser.currentToken() == XContentParser.Token.START_ARRAY) { + int itemCount = 0; + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + if (parser.currentToken() == XContentParser.Token.START_OBJECT + || parser.currentToken() == XContentParser.Token.START_ARRAY) { + currentPath.push(Integer.toString(itemCount)); + validPaths.addAll(getInsertPaths(parser, currentPath)); + currentPath.pop(); + } + itemCount++; + } + } + if (parser.currentName() != null) { + currentPath.pop(); + } + return validPaths; + } + + /** + * Inserts key/value pairs into xContent passed in as {@link BytesReference} and returns a new {@link XContentBuilder} + * The paths argument uses dot separated fieldnames and numbers for array indices, similar to what we do in + * {@link ObjectPath}. + * The key/value arguments can suppliers that either return fixed or random values. + */ + static XContentBuilder insertIntoXContent(XContent xContent, BytesReference original, List paths, Supplier key, + Supplier value) throws IOException { + ObjectPath object = ObjectPath.createFromXContent(xContent, original); + for (String path : paths) { + Map insertMap = object.evaluate(path); + insertMap.put(key.get(), value.get()); + } + return object.toXContentBuilder(xContent); + } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/ObjectPath.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/ObjectPath.java index 3deb2efd92e..7b5952c7a5e 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/ObjectPath.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/ObjectPath.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; @@ -148,4 +149,20 @@ public class ObjectPath { return list.toArray(new String[list.size()]); } + + /** + * Create a new {@link XContentBuilder} from the xContent object underlying this {@link ObjectPath}. + * This only works for {@link ObjectPath} instances created from an xContent object, not from nested + * substructures. We throw an {@link UnsupportedOperationException} in those cases. + */ + @SuppressWarnings("unchecked") + public XContentBuilder toXContentBuilder(XContent xContent) throws IOException { + XContentBuilder builder = XContentBuilder.builder(xContent); + if (this.object instanceof Map) { + builder.map((Map) this.object); + } else { + throw new UnsupportedOperationException("Only ObjectPath created from a map supported."); + } + return builder; + } } diff --git a/test/framework/src/test/java/org/elasticsearch/test/XContentTestUtilsTests.java b/test/framework/src/test/java/org/elasticsearch/test/XContentTestUtilsTests.java new file mode 100644 index 00000000000..38970645505 --- /dev/null +++ b/test/framework/src/test/java/org/elasticsearch/test/XContentTestUtilsTests.java @@ -0,0 +1,188 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.test; + +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.function.Predicate; + +import static org.elasticsearch.test.XContentTestUtils.insertRandomFields; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf;; + +public class XContentTestUtilsTests extends ESTestCase { + + public void testGetInsertPaths() throws IOException { + XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + { + builder.field("field1", "value"); + builder.startArray("list1"); + { + builder.value(0); + builder.value(1); + builder.startObject(); + builder.endObject(); + builder.value(3); + builder.startObject(); + builder.endObject(); + } + builder.endArray(); + builder.startObject("inner1"); + { + builder.field("inner1field1", "value"); + builder.startObject("inner2"); + { + builder.field("inner2field1", "value"); + } + builder.endObject(); + } + builder.endObject(); + } + builder.endObject(); + + try (XContentParser parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, builder.bytes(), builder.contentType())) { + parser.nextToken(); + List insertPaths = XContentTestUtils.getInsertPaths(parser, new Stack<>()); + assertEquals(5, insertPaths.size()); + assertThat(insertPaths, hasItem(equalTo(""))); + assertThat(insertPaths, hasItem(equalTo("list1.2"))); + assertThat(insertPaths, hasItem(equalTo("list1.4"))); + assertThat(insertPaths, hasItem(equalTo("inner1"))); + assertThat(insertPaths, hasItem(equalTo("inner1.inner2"))); + } + } + + @SuppressWarnings("unchecked") + public void testInsertIntoXContent() throws IOException { + XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + builder.endObject(); + builder = XContentTestUtils.insertIntoXContent(XContentType.JSON.xContent(), builder.bytes(), Collections.singletonList(""), + () -> "inner1", () -> new HashMap<>()); + builder = XContentTestUtils.insertIntoXContent(XContentType.JSON.xContent(), builder.bytes(), Collections.singletonList(""), + () -> "field1", () -> "value1"); + builder = XContentTestUtils.insertIntoXContent(XContentType.JSON.xContent(), builder.bytes(), Collections.singletonList("inner1"), + () -> "inner2", () -> new HashMap<>()); + builder = XContentTestUtils.insertIntoXContent(XContentType.JSON.xContent(), builder.bytes(), Collections.singletonList("inner1"), + () -> "field2", () -> "value2"); + try (XContentParser parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, builder.bytes(), builder.contentType())) { + Map map = parser.map(); + assertEquals(2, map.size()); + assertEquals("value1", map.get("field1")); + assertThat(map.get("inner1"), instanceOf(Map.class)); + Map innerMap = (Map) map.get("inner1"); + assertEquals(2, innerMap.size()); + assertEquals("value2", innerMap.get("field2")); + assertThat(innerMap.get("inner2"), instanceOf(Map.class)); + assertEquals(0, ((Map) innerMap.get("inner2")).size()); + } + } + + + @SuppressWarnings("unchecked") + public void testInsertRandomXContent() throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + { + builder.startObject("foo"); + { + builder.field("bar", 1); + } + builder.endObject(); + builder.startObject("foo1"); + { + builder.startObject("foo2"); + { + builder.field("buzz", 1); + } + builder.endObject(); + } + builder.endObject(); + builder.field("foo3", 2); + builder.startArray("foo4"); + { + builder.startObject(); + { + builder.field("foo5", 1); + } + builder.endObject(); + } + builder.endArray(); + } + builder.endObject(); + + Map resultMap; + + try (XContentParser parser = createParser(XContentType.JSON.xContent(), + insertRandomFields(builder.contentType(), builder.bytes(), null, random()))) { + resultMap = parser.map(); + } + assertEquals(5, resultMap.keySet().size()); + assertEquals(2, ((Map) resultMap.get("foo")).keySet().size()); + Map foo1 = (Map) resultMap.get("foo1"); + assertEquals(2, foo1.keySet().size()); + assertEquals(2, ((Map) foo1.get("foo2")).keySet().size()); + List foo4List = (List) resultMap.get("foo4"); + assertEquals(1, foo4List.size()); + assertEquals(2, ((Map) foo4List.get(0)).keySet().size()); + + Predicate pathsToExclude = path -> path.endsWith("foo1"); + try (XContentParser parser = createParser(XContentType.JSON.xContent(), + insertRandomFields(builder.contentType(), builder.bytes(), pathsToExclude, random()))) { + resultMap = parser.map(); + } + assertEquals(5, resultMap.keySet().size()); + assertEquals(2, ((Map) resultMap.get("foo")).keySet().size()); + foo1 = (Map) resultMap.get("foo1"); + assertEquals(1, foo1.keySet().size()); + assertEquals(2, ((Map) foo1.get("foo2")).keySet().size()); + foo4List = (List) resultMap.get("foo4"); + assertEquals(1, foo4List.size()); + assertEquals(2, ((Map) foo4List.get(0)).keySet().size()); + + pathsToExclude = path -> path.contains("foo1"); + try (XContentParser parser = createParser(XContentType.JSON.xContent(), + insertRandomFields(builder.contentType(), builder.bytes(), pathsToExclude, random()))) { + resultMap = parser.map(); + } + assertEquals(5, resultMap.keySet().size()); + assertEquals(2, ((Map) resultMap.get("foo")).keySet().size()); + foo1 = (Map) resultMap.get("foo1"); + assertEquals(1, foo1.keySet().size()); + assertEquals(1, ((Map) foo1.get("foo2")).keySet().size()); + foo4List = (List) resultMap.get("foo4"); + assertEquals(1, foo4List.size()); + assertEquals(2, ((Map) foo4List.get(0)).keySet().size()); + } +} diff --git a/test/framework/src/test/java/org/elasticsearch/test/test/ESTestCaseTests.java b/test/framework/src/test/java/org/elasticsearch/test/test/ESTestCaseTests.java index d30a6a57db5..bf4c786c110 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/test/ESTestCaseTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/test/ESTestCaseTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.test.test; import junit.framework.AssertionFailedError; + import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -97,16 +98,20 @@ public class ESTestCaseTests extends ESTestCase { builder.field("field2", "value2"); { builder.startObject("object1"); - builder.field("inner1", "value1"); - builder.field("inner2", "value2"); - builder.field("inner3", "value3"); + { + builder.field("inner1", "value1"); + builder.field("inner2", "value2"); + builder.field("inner3", "value3"); + } builder.endObject(); } { builder.startObject("object2"); - builder.field("inner4", "value4"); - builder.field("inner5", "value5"); - builder.field("inner6", "value6"); + { + builder.field("inner4", "value4"); + builder.field("inner5", "value5"); + builder.field("inner6", "value6"); + } builder.endObject(); } }