From b9eb1bba65d9bfa1b4c34a2d8289cf99575c8558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Fri, 24 Feb 2017 10:29:31 +0100 Subject: [PATCH] Add unit tests for ParentToChildAggregator (#23305) Adds unit tests for the `children` aggregation. This change also add the ability to mock Mapperservice in subtests of AggregatorTestCase. --- .../aggregations/AggregatorTestCase.java | 17 +- .../ParentToChildrenAggregatorTests.java | 189 ++++++++++++++++++ 2 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 core/src/test/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregatorTests.java diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java b/core/src/test/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java index a59a62deb58..0b2488ae86d 100644 --- a/core/src/test/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java +++ b/core/src/test/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java @@ -43,6 +43,7 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; +import org.elasticsearch.indices.fielddata.cache.IndicesFieldDataCache; import org.elasticsearch.search.fetch.FetchPhase; import org.elasticsearch.search.fetch.subphase.DocValueFieldsFetchSubPhase; import org.elasticsearch.search.fetch.subphase.FetchSourceSubPhase; @@ -50,6 +51,7 @@ import org.elasticsearch.search.internal.ContextIndexSearcher; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.IndexSettingsModule; import java.io.IOException; import java.util.ArrayList; @@ -112,11 +114,15 @@ public abstract class AggregatorTestCase extends ESTestCase { }).when(searchContext).addReleasable(anyObject(), anyObject()); // TODO: now just needed for top_hits, this will need to be revised for other agg unit tests: - MapperService mapperService = mock(MapperService.class); + MapperService mapperService = mapperServiceMock(); when(mapperService.hasNested()).thenReturn(false); when(searchContext.mapperService()).thenReturn(mapperService); + IndexFieldDataService ifds = new IndexFieldDataService(IndexSettingsModule.newIndexSettings("test", Settings.EMPTY), + new IndicesFieldDataCache(Settings.EMPTY, new IndexFieldDataCache.Listener() { + }), circuitBreakerService, mapperService); + when(searchContext.fieldData()).thenReturn(ifds); - SearchLookup searchLookup = new SearchLookup(mapperService, mock(IndexFieldDataService.class), new String[]{"type"}); + SearchLookup searchLookup = new SearchLookup(mapperService, ifds, new String[]{"type"}); when(searchContext.lookup()).thenReturn(searchLookup); QueryShardContext queryShardContext = mock(QueryShardContext.class); @@ -132,6 +138,13 @@ public abstract class AggregatorTestCase extends ESTestCase { return aggregator; } + /** + * sub-tests that need a more complex mock can overwrite this + */ + protected MapperService mapperServiceMock() { + return mock(MapperService.class); + } + protected A search(IndexSearcher searcher, Query query, AggregationBuilder builder, diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregatorTests.java b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregatorTests.java new file mode 100644 index 00000000000..47aa35bf924 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/children/ParentToChildrenAggregatorTests.java @@ -0,0 +1,189 @@ +/* + * 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.search.aggregations.bucket.children; + +import org.apache.lucene.document.Field; +import org.apache.lucene.document.SortedDocValuesField; +import org.apache.lucene.document.SortedNumericDocValuesField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermInSetQuery; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.mapper.ContentPath; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.Mapper; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.NumberFieldMapper; +import org.elasticsearch.index.mapper.ParentFieldMapper; +import org.elasticsearch.index.mapper.TypeFieldMapper; +import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.mapper.UidFieldMapper; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.search.aggregations.AggregatorTestCase; +import org.elasticsearch.search.aggregations.metrics.min.InternalMin; +import org.elasticsearch.search.aggregations.metrics.min.MinAggregationBuilder; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ParentToChildrenAggregatorTests extends AggregatorTestCase { + + private static final String CHILD_TYPE = "child_type"; + private static final String PARENT_TYPE = "parent_type"; + + public void testNoDocs() throws IOException { + Directory directory = newDirectory(); + + RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); + // intentionally not writing any docs + indexWriter.close(); + IndexReader indexReader = DirectoryReader.open(directory); + + testCase(new MatchAllDocsQuery(), newSearcher(indexReader, false, true), parentToChild -> { + assertEquals(0, parentToChild.getDocCount()); + assertEquals(Double.POSITIVE_INFINITY, ((InternalMin) parentToChild.getAggregations().get("in_child")).getValue(), + Double.MIN_VALUE); + }); + indexReader.close(); + directory.close(); + } + + public void testParentChild() throws IOException { + Directory directory = newDirectory(); + RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory); + + final Map> expectedParentChildRelations = setupIndex(indexWriter); + indexWriter.close(); + + IndexReader indexReader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(directory), + new ShardId(new Index("foo", "_na_"), 1)); + // TODO set "maybeWrap" to true for IndexSearcher once #23338 is resolved + IndexSearcher indexSearcher = newSearcher(indexReader, false, true); + + testCase(new MatchAllDocsQuery(), indexSearcher, child -> { + int expectedTotalChildren = 0; + int expectedMinValue = Integer.MAX_VALUE; + for (Tuple expectedValues : expectedParentChildRelations.values()) { + expectedTotalChildren += expectedValues.v1(); + expectedMinValue = Math.min(expectedMinValue, expectedValues.v2()); + } + assertEquals(expectedTotalChildren, child.getDocCount()); + assertEquals(expectedMinValue, ((InternalMin) child.getAggregations().get("in_child")).getValue(), Double.MIN_VALUE); + }); + + for (String parent : expectedParentChildRelations.keySet()) { + testCase(new TermInSetQuery(UidFieldMapper.NAME, new BytesRef(Uid.createUid(PARENT_TYPE, parent))), indexSearcher, child -> { + assertEquals((long) expectedParentChildRelations.get(parent).v1(), child.getDocCount()); + assertEquals(expectedParentChildRelations.get(parent).v2(), + ((InternalMin) child.getAggregations().get("in_child")).getValue(), Double.MIN_VALUE); + }); + } + indexReader.close(); + directory.close(); + } + + private static Map> setupIndex(RandomIndexWriter iw) throws IOException { + Map> expectedValues = new HashMap<>(); + int numParents = randomIntBetween(1, 10); + for (int i = 0; i < numParents; i++) { + String parent = "parent" + i; + iw.addDocument(createParentDocument(parent)); + int numChildren = randomIntBetween(1, 10); + int minValue = Integer.MAX_VALUE; + for (int c = 0; c < numChildren; c++) { + int randomValue = randomIntBetween(0, 100); + minValue = Math.min(minValue, randomValue); + iw.addDocument(createChildDocument("child" + c + "_" + parent, parent, randomValue)); + } + expectedValues.put(parent, new Tuple<>(numChildren, minValue)); + } + return expectedValues; + } + + private static List createParentDocument(String id) { + return Arrays.asList(new StringField(TypeFieldMapper.NAME, PARENT_TYPE, Field.Store.NO), + new StringField(UidFieldMapper.NAME, Uid.createUid(PARENT_TYPE, id), Field.Store.NO), + createJoinField(PARENT_TYPE, id)); + } + + private static List createChildDocument(String childId, String parentId, int value) { + return Arrays.asList(new StringField(TypeFieldMapper.NAME, CHILD_TYPE, Field.Store.NO), + new StringField(UidFieldMapper.NAME, Uid.createUid(CHILD_TYPE, childId), Field.Store.NO), + new SortedNumericDocValuesField("number", value), + createJoinField(PARENT_TYPE, parentId)); + } + + private static SortedDocValuesField createJoinField(String parentType, String id) { + return new SortedDocValuesField(ParentFieldMapper.joinField(parentType), new BytesRef(id)); + } + + @Override + protected MapperService mapperServiceMock() { + MapperService mapperService = mock(MapperService.class); + DocumentMapper childDocMapper = mock(DocumentMapper.class); + DocumentMapper parentDocMapper = mock(DocumentMapper.class); + ParentFieldMapper parentFieldMapper = createParentFieldMapper(); + when(childDocMapper.parentFieldMapper()).thenReturn(parentFieldMapper); + when(parentDocMapper.parentFieldMapper()).thenReturn(parentFieldMapper); + when(mapperService.documentMapper(CHILD_TYPE)).thenReturn(childDocMapper); + when(mapperService.documentMapper(PARENT_TYPE)).thenReturn(parentDocMapper); + when(mapperService.docMappers(false)).thenReturn(Arrays.asList(new DocumentMapper[] { childDocMapper, parentDocMapper })); + when(parentDocMapper.typeFilter()).thenReturn(new TypeFieldMapper.TypesQuery(new BytesRef(PARENT_TYPE))); + when(childDocMapper.typeFilter()).thenReturn(new TypeFieldMapper.TypesQuery(new BytesRef(CHILD_TYPE))); + return mapperService; + } + + private static ParentFieldMapper createParentFieldMapper() { + Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(); + return new ParentFieldMapper.Builder("parent").type(PARENT_TYPE).build(new Mapper.BuilderContext(settings, new ContentPath(0))); + } + + private void testCase(Query query, IndexSearcher indexSearcher, Consumer verify) + throws IOException { + + ChildrenAggregationBuilder aggregationBuilder = new ChildrenAggregationBuilder("_name", CHILD_TYPE); + aggregationBuilder.subAggregation(new MinAggregationBuilder("in_child").field("number")); + + MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG); + fieldType.setName("number"); + InternalChildren result = search(indexSearcher, query, aggregationBuilder, fieldType); + verify.accept(result); + } +}