blob: 59f739aa8f0c87f5a68db30227c2f4ef20260f37 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.apache.druid.segment;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.apache.druid.data.input.MapBasedRow;
import org.apache.druid.data.input.Row;
import org.apache.druid.guice.DruidSecondaryModule;
import org.apache.druid.guice.GuiceAnnotationIntrospector;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.guava.Sequence;
import org.apache.druid.math.expr.ExprEval;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.query.Result;
import org.apache.druid.query.expression.TestExprMacroTable;
import org.apache.druid.query.groupby.ResultRow;
import org.apache.druid.query.timeseries.TimeseriesResultValue;
import org.apache.druid.query.topn.TopNResultValue;
import org.apache.druid.segment.column.ColumnConfig;
import org.apache.druid.segment.writeout.SegmentWriteOutMediumFactory;
import org.apache.druid.timeline.DataSegment.PruneSpecsHolder;
import org.junit.Assert;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
*
*/
public class TestHelper
{
public static final ObjectMapper JSON_MAPPER = makeJsonMapper();
public static final ColumnConfig NO_CACHE_COLUMN_CONFIG = () -> 0;
public static IndexMergerV9 getTestIndexMergerV9(SegmentWriteOutMediumFactory segmentWriteOutMediumFactory)
{
return new IndexMergerV9(JSON_MAPPER, getTestIndexIO(), segmentWriteOutMediumFactory);
}
public static IndexIO getTestIndexIO()
{
return getTestIndexIO(NO_CACHE_COLUMN_CONFIG);
}
public static IndexIO getTestIndexIO(ColumnConfig columnConfig)
{
return new IndexIO(JSON_MAPPER, columnConfig);
}
public static AnnotationIntrospector makeAnnotationIntrospector()
{
// Prepare annotationIntrospector with similar logic, except skip Guice loading
// because most tests don't use Guice injection.
return new GuiceAnnotationIntrospector()
{
@Override
public Object findInjectableValueId(AnnotatedMember m)
{
return null;
}
};
}
public static ObjectMapper makeJsonMapper()
{
final ObjectMapper mapper = new DefaultObjectMapper();
AnnotationIntrospector introspector = makeAnnotationIntrospector();
DruidSecondaryModule.setupAnnotationIntrospector(mapper, introspector);
mapper.setInjectableValues(
new InjectableValues.Std()
.addValue(ExprMacroTable.class.getName(), TestExprMacroTable.INSTANCE)
.addValue(ObjectMapper.class.getName(), mapper)
.addValue(PruneSpecsHolder.class, PruneSpecsHolder.DEFAULT)
);
return mapper;
}
public static ObjectMapper makeSmileMapper()
{
final ObjectMapper mapper = new DefaultObjectMapper();
DruidSecondaryModule.setupAnnotationIntrospector(mapper, makeAnnotationIntrospector());
mapper.setInjectableValues(
new InjectableValues.Std()
.addValue(ExprMacroTable.class.getName(), TestExprMacroTable.INSTANCE)
.addValue(ObjectMapper.class.getName(), mapper)
);
return mapper;
}
public static <T> Iterable<T> revert(Iterable<T> input)
{
return Lists.reverse(Lists.newArrayList(input));
}
public static <T> void assertExpectedResults(Iterable<Result<T>> expectedResults, Sequence<Result<T>> results)
{
assertResults(expectedResults, results.toList(), "");
}
public static <T> void assertExpectedResults(Iterable<Result<T>> expectedResults, Iterable<Result<T>> results)
{
assertResults(expectedResults, results, "");
}
public static <T> void assertExpectedResults(
Iterable<Result<T>> expectedResults,
Iterable<Result<T>> results,
String failMsg
)
{
assertResults(expectedResults, results, failMsg);
}
public static <T> void assertExpectedObjects(Iterable<T> expectedResults, Iterable<T> results, String failMsg)
{
assertObjects(expectedResults, results, failMsg);
}
public static <T> void assertExpectedObjects(Iterable<T> expectedResults, Sequence<T> results, String failMsg)
{
assertObjects(expectedResults, results.toList(), failMsg);
}
private static <T> void assertResults(
Iterable<Result<T>> expectedResults,
Iterable<Result<T>> actualResults,
String failMsg
)
{
Iterator<? extends Result> resultsIter = actualResults.iterator();
Iterator<? extends Result> resultsIter2 = actualResults.iterator();
Iterator<? extends Result> expectedResultsIter = expectedResults.iterator();
while (resultsIter.hasNext() && resultsIter2.hasNext() && expectedResultsIter.hasNext()) {
Object expectedNext = expectedResultsIter.next();
final Object next = resultsIter.next();
final Object next2 = resultsIter2.next();
if (expectedNext instanceof ResultRow) {
// HACK! Special casing for groupBy
assertRow(failMsg, (ResultRow) expectedNext, (ResultRow) next);
assertRow(failMsg, (ResultRow) expectedNext, (ResultRow) next2);
} else if (expectedNext instanceof Result
&& (((Result) expectedNext).getValue()) instanceof TimeseriesResultValue) {
// Special case for GroupByTimeseriesQueryRunnerTest to allow a floating point delta to be used
// in result comparison
assertTimeseriesResultValue(failMsg, (Result) expectedNext, (Result) next);
assertTimeseriesResultValue(
StringUtils.format("%s: Second iterator bad, multiple calls to iterator() should be safe", failMsg),
(Result) expectedNext,
(Result) next2
);
} else if (expectedNext instanceof Result
&& (((Result) expectedNext).getValue()) instanceof TopNResultValue) {
// Special to allow a floating point delta to be used in result comparison due to legacy expected results
assertTopNResultValue(failMsg, (Result) expectedNext, (Result) next);
assertTopNResultValue(
StringUtils.format("%s: Second iterator bad, multiple calls to iterator() should be safe", failMsg),
(Result) expectedNext,
(Result) next2
);
} else {
assertResult(failMsg, (Result) expectedNext, (Result) next);
assertResult(
StringUtils.format("%s: Second iterator bad, multiple calls to iterator() should be safe", failMsg),
(Result) expectedNext,
(Result) next2
);
}
}
if (resultsIter.hasNext()) {
Assert.fail(
StringUtils.format(
"%s: Expected resultsIter to be exhausted, next element was %s",
failMsg,
resultsIter.next()
)
);
}
if (resultsIter2.hasNext()) {
Assert.fail(
StringUtils.format(
"%s: Expected resultsIter2 to be exhausted, next element was %s",
failMsg,
resultsIter.next()
)
);
}
if (expectedResultsIter.hasNext()) {
Assert.fail(
StringUtils.format(
"%s: Expected expectedResultsIter to be exhausted, next element was %s",
failMsg,
expectedResultsIter.next()
)
);
}
}
private static <T> void assertObjects(Iterable<T> expectedResults, Iterable<T> actualResults, String msg)
{
Iterator resultsIter = actualResults.iterator();
Iterator resultsIter2 = actualResults.iterator();
Iterator expectedResultsIter = expectedResults.iterator();
int index = 0;
while (resultsIter.hasNext() && resultsIter2.hasNext() && expectedResultsIter.hasNext()) {
Object expectedNext = expectedResultsIter.next();
final Object next = resultsIter.next();
final Object next2 = resultsIter2.next();
String failMsg = msg + "-" + index++;
String failMsg2 = StringUtils.format(
"%s: Second iterator bad, multiple calls to iterator() should be safe",
failMsg
);
if (expectedNext instanceof ResultRow) {
// HACK! Special casing for groupBy
assertRow(failMsg, (ResultRow) expectedNext, (ResultRow) next);
assertRow(failMsg2, (ResultRow) expectedNext, (ResultRow) next2);
} else {
Assert.assertEquals(failMsg, expectedNext, next);
Assert.assertEquals(failMsg2, expectedNext, next2);
}
}
if (resultsIter.hasNext()) {
Assert.fail(
StringUtils.format("%s: Expected resultsIter to be exhausted, next element was %s", msg, resultsIter.next())
);
}
if (resultsIter2.hasNext()) {
Assert.fail(
StringUtils.format("%s: Expected resultsIter2 to be exhausted, next element was %s", msg, resultsIter.next())
);
}
if (expectedResultsIter.hasNext()) {
Assert.fail(
StringUtils.format(
"%s: Expected expectedResultsIter to be exhausted, next element was %s",
msg,
expectedResultsIter.next()
)
);
}
}
private static void assertResult(String msg, Result<?> expected, Result actual)
{
Assert.assertEquals(msg, expected, actual);
}
private static void assertTimeseriesResultValue(String msg, Result expected, Result actual)
{
// Custom equals check to get fuzzy comparison of numerics, useful because different groupBy strategies don't
// always generate exactly the same results (different merge ordering / float vs double)
Assert.assertEquals(StringUtils.format("%s: timestamp", msg), expected.getTimestamp(), actual.getTimestamp());
TimeseriesResultValue expectedVal = (TimeseriesResultValue) expected.getValue();
TimeseriesResultValue actualVal = (TimeseriesResultValue) actual.getValue();
final Map<String, Object> expectedMap = expectedVal.getBaseObject();
final Map<String, Object> actualMap = actualVal.getBaseObject();
assertRow(
msg,
new MapBasedRow(expected.getTimestamp(), expectedMap),
new MapBasedRow(actual.getTimestamp(), actualMap)
);
}
private static void assertTopNResultValue(String msg, Result expected, Result actual)
{
TopNResultValue expectedVal = (TopNResultValue) expected.getValue();
TopNResultValue actualVal = (TopNResultValue) actual.getValue();
List<Row> listExpectedRows = expectedVal.getValue()
.stream()
.map(dimensionAndMetricValueExtractor -> new MapBasedRow(
expected.getTimestamp(),
dimensionAndMetricValueExtractor.getBaseObject()
))
.collect(Collectors.toList());
List<Row> listActualRows = actualVal.getValue()
.stream()
.map(dimensionAndMetricValueExtractor -> new MapBasedRow(
actual.getTimestamp(),
dimensionAndMetricValueExtractor.getBaseObject()
))
.collect(Collectors.toList());
Assert.assertEquals("Size of list must match", listExpectedRows.size(), listActualRows.size());
IntStream.range(0, listExpectedRows.size()).forEach(value -> assertRow(
StringUtils.format("%s, on value number [%s]", msg, value),
listExpectedRows.get(value),
listActualRows.get(value)
));
}
private static void assertRow(String msg, Row expected, Row actual)
{
// Custom equals check to get fuzzy comparison of numerics, useful because different groupBy strategies don't
// always generate exactly the same results (different merge ordering / float vs double)
Assert.assertEquals(
StringUtils.format("%s: timestamp", msg),
expected.getTimestamp(),
actual.getTimestamp()
);
final Map<String, Object> expectedMap = ((MapBasedRow) expected).getEvent();
final Map<String, Object> actualMap = ((MapBasedRow) actual).getEvent();
Assert.assertEquals(StringUtils.format("%s: map keys", msg), expectedMap.keySet(), actualMap.keySet());
for (final String key : expectedMap.keySet()) {
final Object expectedValue = expectedMap.get(key);
final Object actualValue = actualMap.get(key);
if (expectedValue != null && expectedValue.getClass().isArray()) {
Assert.assertArrayEquals((Object[]) expectedValue, (Object[]) actualValue);
} else if (expectedValue instanceof Float || expectedValue instanceof Double) {
Assert.assertEquals(
StringUtils.format("%s: key[%s]", msg, key),
((Number) expectedValue).doubleValue(),
((Number) actualValue).doubleValue(),
Math.abs(((Number) expectedValue).doubleValue() * 1e-6)
);
} else {
Assert.assertEquals(
StringUtils.format("%s: key[%s]", msg, key),
expectedValue,
actualValue
);
}
}
}
private static void assertRow(String msg, ResultRow expected, ResultRow actual)
{
Assert.assertEquals(
StringUtils.format("%s: row length", msg),
expected.length(),
actual.length()
);
for (int i = 0; i < expected.length(); i++) {
final String message = StringUtils.format("%s: idx[%d]", msg, i);
final Object expectedValue = expected.get(i);
final Object actualValue = actual.get(i);
if (expectedValue != null && expectedValue.getClass().isArray()) {
// spilled results will materialize into lists, coerce them back to arrays if we expected arrays
if (actualValue instanceof List) {
Assert.assertEquals(
message,
(Object[]) expectedValue,
(Object[]) ExprEval.coerceListToArray((List) actualValue, true)
);
} else {
Assert.assertArrayEquals(
message,
(Object[]) expectedValue,
(Object[]) actualValue
);
}
} else if (expectedValue instanceof Float || expectedValue instanceof Double) {
Assert.assertEquals(
message,
((Number) expectedValue).doubleValue(),
((Number) actualValue).doubleValue(),
Math.abs(((Number) expectedValue).doubleValue() * 1e-6)
);
} else {
Assert.assertEquals(
message,
expectedValue,
actualValue
);
}
}
}
public static Map<String, Object> createExpectedMap(Object... vals)
{
Preconditions.checkArgument(vals.length % 2 == 0);
Map<String, Object> theVals = new HashMap<>();
for (int i = 0; i < vals.length; i += 2) {
theVals.put(vals[i].toString(), vals[i + 1]);
}
return theVals;
}
public static void testSerializesDeserializes(Object object)
{
testSerializesDeserializes(JSON_MAPPER, object);
}
public static void testSerializesDeserializes(ObjectMapper objectMapper, Object object)
{
try {
String serialized = objectMapper.writeValueAsString(object);
Object deserialized = objectMapper.readValue(serialized, object.getClass());
Assert.assertEquals(serialized, objectMapper.writeValueAsString(deserialized));
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}