/*
* Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership. Crate 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial agreement.
*/
package io.crate.testing;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import io.crate.Version;
import io.crate.analyze.symbol.ValueSymbolVisitor;
import io.crate.analyze.where.DocKeys;
import io.crate.core.collections.Sorted;
import io.crate.data.Bucket;
import io.crate.data.Buckets;
import io.crate.data.Row;
import io.crate.metadata.*;
import io.crate.operation.aggregation.impl.AggregationImplModule;
import io.crate.operation.operator.OperatorModule;
import io.crate.operation.predicate.PredicateModule;
import io.crate.operation.scalar.ScalarFunctionModule;
import io.crate.operation.tablefunctions.TableFunctionModule;
import io.crate.types.DataType;
import io.crate.types.DataTypes;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.inject.ModulesBuilder;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.threadpool.ThreadPool;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.Is.is;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.*;
public class TestingHelpers {
/**
* prints the contents of a result array as a human readable table
*
* @param result the data to be printed
* @return a string representing a table
*/
public static String printedTable(Object[][] result) {
return printRows(Arrays.asList(result));
}
public static String printedTable(Bucket result) {
return printRows(Arrays.asList(Buckets.materialize(result)));
}
public static String printRows(Iterable<Object[]> rows) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintStream out = new PrintStream(os);
for (Object[] row : rows) {
boolean first = true;
for (Object o : row) {
first = printObject(out, first, o);
}
out.print("\n");
}
return os.toString();
}
private static boolean printObject(PrintStream out, boolean first, Object o) {
if (!first) {
out.print("| ");
} else {
first = false;
}
if (o == null) {
out.print("NULL");
} else if (o instanceof BytesRef) {
out.print(((BytesRef) o).utf8ToString());
} else if (o instanceof Object[]) {
out.print("[");
Object[] oArray = (Object[]) o;
for (int i = 0; i < oArray.length; i++) {
printObject(out, true, oArray[i]);
if (i < oArray.length - 1) {
out.print(", ");
}
}
out.print("]");
} else if (o.getClass().isArray()) {
out.print("[");
boolean arrayFirst = true;
for (int i = 0, length = Array.getLength(o); i < length; i++) {
if (!arrayFirst) {
out.print(",v");
} else {
arrayFirst = false;
}
printObject(out, first, Array.get(o, i));
}
out.print("]");
} else if (o instanceof Map) {
out.print("{");
out.print(MAP_JOINER.join(Sorted.sortRecursive((Map<String, Object>) o, true)));
out.print("}");
} else {
out.print(o.toString());
}
return first;
}
private final static Joiner.MapJoiner MAP_JOINER = Joiner.on(", ").useForNull("null").withKeyValueSeparator("=");
public static String mapToSortedString(Map<String, Object> map) {
return MAP_JOINER.join(Sorted.sortRecursive(map));
}
public static Functions getFunctions() {
return new ModulesBuilder()
.add(new AggregationImplModule())
.add(new PredicateModule())
.add(new TableFunctionModule())
.add(new ScalarFunctionModule())
.add(new OperatorModule()).createInjector().getInstance(Functions.class);
}
public static Reference createReference(String columnName, DataType dataType) {
return createReference("dummyTable", new ColumnIdent(columnName), dataType);
}
public static Reference createReference(ColumnIdent columnIdent, DataType dataType) {
return createReference("dummyTable", columnIdent, dataType);
}
public static Reference createReference(String tableName, ColumnIdent columnIdent, DataType dataType) {
return new Reference(
new ReferenceIdent(new TableIdent(null, tableName), columnIdent),
RowGranularity.DOC,
dataType);
}
public static String readFile(String path) throws IOException {
byte[] encoded = Files.readAllBytes(Paths.get(path));
return new BytesRef(encoded).utf8ToString();
}
private static final com.google.common.base.Function<Object, Object> bytesRefToString =
new com.google.common.base.Function<Object, Object>() {
@Nullable
@Override
public Object apply(@Nullable Object input) {
if (input instanceof BytesRef) {
return ((BytesRef) input).utf8ToString();
}
return input;
}
};
public static Matcher<Row> isNullRow() {
return isRow((Object) null);
}
public static Matcher<Row> isRow(Object... cells) {
if (cells == null) {
cells = new Object[]{null};
}
final List<Object> expected = Lists.transform(Arrays.asList(cells), bytesRefToString);
return new TypeSafeDiagnosingMatcher<Row>() {
@Override
protected boolean matchesSafely(Row item, Description mismatchDescription) {
if (item.numColumns() != expected.size()) {
mismatchDescription.appendText("row size does not match: ")
.appendValue(item.numColumns()).appendText(" != ").appendValue(expected.size());
return false;
}
for (int i = 0; i < item.numColumns(); i++) {
Object actual = bytesRefToString.apply(item.get(i));
if (!Objects.equals(expected.get(i), actual)) {
mismatchDescription.appendText("value at pos ")
.appendValue(i)
.appendText(" does not match: ")
.appendValue(expected.get(i))
.appendText(" != ")
.appendValue(actual);
return false;
}
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText("is Row with cells: ")
.appendValue(expected);
}
};
}
public static Matcher<DocKeys.DocKey> isNullDocKey() {
return isDocKey(new Object[]{null});
}
public static Matcher<DocKeys.DocKey> isDocKey(Object... keys) {
final List<Object> expected = Arrays.asList(keys);
return new TypeSafeDiagnosingMatcher<DocKeys.DocKey>() {
@Override
protected boolean matchesSafely(DocKeys.DocKey item, Description mismatchDescription) {
List objects = Lists.transform(
Lists.transform(item.values(), ValueSymbolVisitor.VALUE.function), bytesRefToString);
if (!expected.equals(objects)) {
mismatchDescription.appendText("is DocKey with values: ").appendValue(objects);
return false;
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText("is DocKey with values: ")
.appendValue(expected);
}
};
}
/**
* Get the values at column index <code>index</code> within all <code>rows</code>
*/
public static
@Nullable
Object[] getColumn(Object[][] rows, int index) throws Exception {
if (rows.length == 0 || rows[0].length <= index) {
throw new NoSuchElementException("no column with index " + index);
}
Object[] column = new Object[rows.length];
for (int i = 0; i < rows.length; i++) {
column[i] = rows[i][index];
}
return column;
}
public static ThreadPool newMockedThreadPool() {
ThreadPool threadPool = Mockito.mock(ThreadPool.class);
final ExecutorService executorService = Executors.newSingleThreadExecutor();
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
executorService.shutdown();
return null;
}
}).when(threadPool).shutdown();
when(threadPool.executor(anyString())).thenReturn(executorService);
try {
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
executorService.awaitTermination(1, TimeUnit.SECONDS);
return null;
}
}).when(threadPool).awaitTermination(anyLong(), any(TimeUnit.class));
} catch (InterruptedException e) {
throw Throwables.propagate(e);
}
return threadPool;
}
public static Reference refInfo(String fqColumnName, DataType dataType, RowGranularity rowGranularity, String... nested) {
String[] parts = fqColumnName.split("\\.");
ReferenceIdent refIdent;
List<String> nestedParts = null;
if (nested.length > 0) {
nestedParts = Arrays.asList(nested);
}
switch (parts.length) {
case 2:
refIdent = new ReferenceIdent(new TableIdent(null, parts[0]), parts[1], nestedParts);
break;
case 3:
refIdent = new ReferenceIdent(new TableIdent(parts[0], parts[1]), parts[2], nestedParts);
break;
default:
throw new IllegalArgumentException("fqColumnName must contain <table>.<column> or <schema>.<table>.<column>");
}
return new Reference(refIdent, rowGranularity, dataType);
}
public static <T> Matcher<T> isSQL(final String stmt) {
return new BaseMatcher<T>() {
@Override
public boolean matches(Object item) {
return SQLPrinter.print(item).equals(stmt);
}
@Override
public void describeTo(Description description) {
description.appendText(stmt);
}
@Override
public void describeMismatch(Object item, Description description) {
description.appendText(SQLPrinter.print(item));
}
};
}
public static <T, K extends Comparable> Matcher<Iterable<? extends T>> isSortedBy(final com.google.common.base.Function<T, K> extractSortingKeyFunction) {
return isSortedBy(extractSortingKeyFunction, false, null);
}
public static <T, K extends Comparable> Matcher<Iterable<? extends T>> isSortedBy(final com.google.common.base.Function<T, K> extractSortingKeyFunction,
final boolean descending,
@Nullable final Boolean nullsFirst) {
Ordering<K> ordering = Ordering.natural();
if (descending) {
ordering = ordering.reverse();
}
if (nullsFirst != null && nullsFirst) {
ordering = ordering.nullsFirst();
} else {
ordering = ordering.nullsLast();
}
final Ordering<K> ord = ordering;
return new TypeSafeDiagnosingMatcher<Iterable<? extends T>>() {
@Override
protected boolean matchesSafely(Iterable<? extends T> item, Description mismatchDescription) {
K previous = null;
int i = 0;
for (T elem : item) {
K current = extractSortingKeyFunction.apply(elem);
if (previous != null) {
if (ord.compare(previous, current) > 0) {
mismatchDescription
.appendText("element ").appendValue(current)
.appendText(" at position ").appendValue(i)
.appendText(" is ")
.appendText(descending ? "bigger" : "smaller")
.appendText(" than previous element ")
.appendValue(previous);
return false;
}
}
i++;
previous = current;
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText("expected iterable to be sorted ");
if (descending) {
description.appendText("in DESCENDING order");
} else {
description.appendText("in ASCENDING order");
}
}
};
}
public static Matcher<Iterable<? extends Row>> hasSortedRows(final int sortingPos, final boolean reverse, @Nullable final Boolean nullsFirst) {
return TestingHelpers.isSortedBy(new com.google.common.base.Function<Row, Comparable>() {
@Nullable
@Override
public Comparable apply(@Nullable Row input) {
assert input != null;
return (Comparable) input.get(sortingPos);
}
}, reverse, nullsFirst);
}
public static DataType randomPrimitiveType() {
return DataTypes.PRIMITIVE_TYPES.get(ThreadLocalRandom.current().nextInt(DataTypes.PRIMITIVE_TYPES.size()));
}
public static Map<String, Object> jsonMap(String json) {
try {
return JsonXContent.jsonXContent.createParser(json).map();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Convert {@param s} into UTF8 encoded BytesRef with random offset and extra length
* <p>
* This should be preferred over `new BytesRef` in tests to make sure that implementations using BytesRef
* handle offset and length correctly (use {@link BytesRef#length} instead of {@link BytesRef#bytes#length}
*/
public static BytesRef bytesRef(String s, Random random) {
byte[] strBytes = s.getBytes(StandardCharsets.UTF_8);
int extraLength = random.nextInt(100);
int offset = 0;
if (extraLength > 0) {
offset = random.nextInt(extraLength);
}
byte[] buffer = new byte[strBytes.length + extraLength];
random.nextBytes(buffer);
System.arraycopy(strBytes, 0, buffer, offset, strBytes.length);
return new BytesRef(buffer, offset, strBytes.length);
}
/**
* Converts file path separators of a string into canonical form
* e.g. Windows: "/test/" --> "\test\"
* UNIX: "/test/" --> "/test/"
* @param str The string that contains file path separator
* @return the resolved string
*/
public static String resolveCanonicalString(String str) {
return str.replaceAll("/", java.util.regex.Matcher.quoteReplacement(File.separator));
}
public static void assertCrateVersion(Object object, Version versionCreated, Version versionUpgraded) {
assertThat((Map<String, String>) object,
allOf(
hasEntry(is(Version.Property.CREATED.toString()),
versionCreated == null ? nullValue() : is(Version.toStringMap(versionCreated))),
hasEntry(is(Version.Property.UPGRADED.toString()),
versionUpgraded == null ? nullValue() : is(Version.toStringMap(versionUpgraded)))));
}
}