Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.apache.calcite.test.Matchers Maven / Gradle / Ivy
/*
* 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.calcite.test;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelValidityChecker;
import org.apache.calcite.rel.hint.Hintable;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.util.TestUtil;
import org.apache.calcite.util.Util;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.RangeSet;
import org.apiguardian.api.API;
import org.hamcrest.BaseMatcher;
import org.hamcrest.CoreMatchers;
import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.core.Is;
import org.hamcrest.core.StringContains;
import java.nio.charset.Charset;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.StreamSupport;
import static org.hamcrest.CoreMatchers.equalTo;
/**
* Matchers for testing SQL queries.
*/
public class Matchers {
private static final Pattern PATTERN = Pattern.compile(", id = [0-9]+");
/** A small positive value. */
public static final double EPSILON = 1.0e-5;
private Matchers() {}
/** Allows passing the actual result from the {@code matchesSafely} method to
* the {@code describeMismatchSafely} method that will show the difference. */
private static final ThreadLocal THREAD_ACTUAL = new ThreadLocal<>();
/**
* Creates a matcher that matches if the examined result set returns the
* given collection of rows in some order.
*
* Closes the result set after reading.
*
*
For example:
*
assertThat(statement.executeQuery("select empno from emp"),
* returnsUnordered("empno=1234", "empno=100"));
*/
public static Matcher super ResultSet> returnsUnordered(String... lines) {
final List expectedList = Lists.newArrayList(lines);
Collections.sort(expectedList);
return new CustomTypeSafeMatcher(Arrays.toString(lines)) {
@Override protected void describeMismatchSafely(ResultSet item,
Description description) {
final Object value = THREAD_ACTUAL.get();
THREAD_ACTUAL.remove();
description.appendText("was ").appendValue(value);
}
@Override protected boolean matchesSafely(ResultSet resultSet) {
final List actualList = new ArrayList<>();
try {
CalciteAssert.toStringList(resultSet, actualList);
resultSet.close();
} catch (SQLException e) {
throw TestUtil.rethrow(e);
}
Collections.sort(actualList);
THREAD_ACTUAL.set(actualList);
final boolean equals = actualList.equals(expectedList);
if (!equals) {
THREAD_ACTUAL.set(actualList);
}
return equals;
}
};
}
public static Matcher> equalsUnordered(
E... lines) {
final List expectedList =
Lists.newArrayList(toStringList(Arrays.asList(lines)));
Collections.sort(expectedList);
final String description = Util.lines(expectedList);
return new CustomTypeSafeMatcher>(description) {
@Override protected void describeMismatchSafely(Iterable actuals,
Description description) {
final List actualList =
Lists.newArrayList(toStringList(actuals));
Collections.sort(actualList);
description.appendText("was ")
.appendValue(Util.lines(actualList));
}
@Override protected boolean matchesSafely(Iterable actuals) {
final List actualList =
Lists.newArrayList(toStringList(actuals));
Collections.sort(actualList);
return actualList.equals(expectedList);
}
};
}
private static Iterable toStringList(Iterable items) {
return StreamSupport.stream(items.spliterator(), false)
.map(Object::toString)
.collect(Util.toImmutableList());
}
/**
* Creates a matcher that matches when the examined object is within
* {@code epsilon} of the specified {@code value}.
*/
public static Matcher within(T value, double epsilon) {
return new IsWithin<>(value, epsilon);
}
/**
* Creates a matcher that matches when the examined object is within
* {@link #EPSILON} of the specified operand
.
*/
public static Matcher isAlmost(double value) {
return within(value, EPSILON);
}
/**
* Creates a matcher that matches if the examined value is between bounds:
* min ≤ value ≤ max
.
*
* @param value type
* @param min Lower bound
* @param max Upper bound
*/
public static > Matcher between(T min, T max) {
return new CustomTypeSafeMatcher("between " + min + " and " + max) {
@Override protected boolean matchesSafely(T item) {
return min.compareTo(item) <= 0
&& item.compareTo(max) <= 0;
}
};
}
/** Creates a matcher by applying a function to a value before calling
* another matcher. */
public static Matcher compose(Matcher matcher,
Function f) {
return new ComposingMatcher<>(matcher, f);
}
/**
* Creates a Matcher that matches when the examined string is equal to the
* specified {@code value} when all Windows-style line endings ("\r\n")
* have been converted to Unix-style line endings ("\n").
*
* Thus, if {@code foo()} is a function that returns "hello{newline}world"
* in the current operating system's line endings, then
*
*
* assertThat(foo(), isLinux("hello\nworld"));
*
*
* will succeed on all platforms.
*
* @see Util#toLinux(String)
*/
public static Matcher isLinux(final String value) {
return compose(Is.is(value), input -> input == null ? null : Util.toLinux(input));
}
/** Matcher that matches a {@link RelNode} if the {@code RelNode} is valid
* per {@link RelValidityChecker}. */
public static Matcher relIsValid() {
return new TypeSafeMatcher() {
@Override public void describeTo(Description description) {
description.appendText("rel is valid");
}
@Override protected boolean matchesSafely(RelNode rel) {
RelValidityChecker checker = new RelValidityChecker();
checker.go(rel);
return checker.invalidCount() == 0;
}
};
}
/**
* Creates a Matcher that matches a {@link RelNode} if its string
* representation, after converting Windows-style line endings ("\r\n")
* to Unix-style line endings ("\n"), is equal to the given {@code value}.
*/
public static Matcher hasTree(final String value) {
return compose(Is.is(value), input -> {
// Convert RelNode to a string with Linux line-endings
return Util.toLinux(RelOptUtil.toString(input));
});
}
/**
* Creates a Matcher that matches a {@link RelNode} if its field
* names, converting to a list, are equal to the given {@code value}.
*/
public static Matcher hasFieldNames(String fieldNames) {
return new TypeSafeMatcher() {
@Override public void describeTo(Description description) {
description.appendText("has fields ").appendText(fieldNames);
}
@Override protected boolean matchesSafely(RelNode r) {
return r.getRowType().getFieldNames().toString().equals(fieldNames);
}
};
}
/**
* Creates a Matcher that matches a {@link RelNode} if its string
* representation, after converting Windows-style line endings ("\r\n")
* to Unix-style line endings ("\n"), contains the given {@code value}
* as a substring.
*/
public static Matcher inTree(final String value) {
return compose(StringContains.containsString(value), input -> {
// Convert RelNode to a string with Linux line-endings
return Util.toLinux(RelOptUtil.toString(input));
});
}
/**
* Creates a Matcher that matches a {@link RexNode} if its string
* representation, after converting Windows-style line endings ("\r\n")
* to Unix-style line endings ("\n"), is equal to the given {@code value}.
*/
public static Matcher hasRex(final String value) {
return compose(Is.is(value), input -> {
// Convert RexNode to a string with Linux line-endings
return Util.toLinux(input.toString());
});
}
/**
* Creates a Matcher that matches a {@link RelNode} if its hints string
* representation is equal to the given {@code value}.
*/
public static Matcher hasHints(final String value) {
return compose(Is.is(value),
input -> input instanceof Hintable
? ((Hintable) input).getHints().toString()
: "[]");
}
/**
* Creates a Matcher that matches a {@link RangeSet} if its string
* representation, after changing "ߩ" to "..",
* is equal to the given {@code value}.
*
* This method is necessary because {@link RangeSet#toString()} changed
* behavior. Guava 19 - 28 used a unicode symbol; Guava 29 onwards uses "..".
*/
@SuppressWarnings("BetaApi")
public static Matcher isRangeSet(final String value) {
return compose(Is.is(value), input -> {
// Change all '\u2025' (a unicode symbol denoting a range) to '..',
// consistent with Guava 29+.
return input.toString().replace("\u2025", "..");
});
}
/**
* Creates a {@link Matcher} that matches execution plan and trims {@code , id=123} node ids.
* {@link RelNode#getId()} is not stable across runs, so this matcher enables to trim those.
* @param value execpted execution plan
* @return matcher
*/
@API(since = "1.22", status = API.Status.EXPERIMENTAL)
public static Matcher containsWithoutNodeIds(String value) {
return compose(CoreMatchers.containsString(value), Matchers::trimNodeIds);
}
/**
* Creates a matcher that matches when the examined string is equal to the
* specified operand
when all Windows-style line endings ("\r\n")
* have been converted to Unix-style line endings ("\n").
*
* Thus, if {@code foo()} is a function that returns "hello{newline}world"
* in the current operating system's line endings, then
*
*
* assertThat(foo(), isLinux("hello\nworld"));
*
*
* will succeed on all platforms.
*
* @see Util#toLinux(String)
*/
public static Matcher containsStringLinux(String value) {
return compose(CoreMatchers.containsString(value), Util::toLinux);
}
public static String trimNodeIds(String s) {
return PATTERN.matcher(s).replaceAll("");
}
/**
* Creates a matcher that matches if the examined value is expected throwable.
*
* @param expected Throwable to match.
*/
public static Matcher super Throwable> expectThrowable(Throwable expected) {
return new BaseMatcher() {
@Override public boolean matches(Object item) {
if (!(item instanceof Throwable)) {
return false;
}
Throwable error = (Throwable) item;
return expected != null
&& Objects.equals(error.getClass(), expected.getClass())
&& Objects.equals(error.getMessage(), expected.getMessage());
}
@Override public void describeTo(Description description) {
description.appendText("is ").appendText(expected.toString());
}
};
}
/**
* Creates a matcher that matches if the examined value has a given name.
*
* @param charsetName Name of character set
*
* @see Charset#forName
*/
public static Matcher isCharset(String charsetName) {
return new TypeSafeMatcher() {
@Override public void describeTo(Description description) {
description.appendText("is charset ").appendText(charsetName);
}
@Override protected boolean matchesSafely(Charset item) {
return item.name().equals(charsetName);
}
};
}
/**
* Matcher that succeeds for any collection that, when converted to strings
* and sorted on those strings, matches the given reference string.
*
* Use it as an alternative to {@link CoreMatchers#is} if items in your
* list might occur in any order.
*
*
For example:
*
*
{@code
* List ints = Arrays.asList(2, 500, 12);
* assertThat(ints, sortsAs("[12, 2, 500]");
* }
*/
public static Matcher> sortsAs(final String value) {
return compose(equalTo(value), item -> {
final List strings = new ArrayList<>();
for (T t : item) {
strings.add(t.toString());
}
Collections.sort(strings);
return strings.toString();
});
}
/** Matcher that tests whether the numeric value is within a given difference
* another value.
*
* @param Value type
*/
public static class IsWithin extends BaseMatcher {
private final T expectedValue;
private final double epsilon;
public IsWithin(T expectedValue, double epsilon) {
Preconditions.checkArgument(epsilon >= 0D);
this.expectedValue = expectedValue;
this.epsilon = epsilon;
}
@Override public boolean matches(Object actualValue) {
return isWithin(actualValue, expectedValue, epsilon);
}
@Override public void describeTo(Description description) {
description.appendValue(expectedValue + " +/-" + epsilon);
}
private static boolean isWithin(Object actual, Number expected,
double epsilon) {
if (actual == null) {
return expected == null;
}
if (actual.equals(expected)) {
return true;
}
final double a = ((Number) actual).doubleValue();
final double min = expected.doubleValue() - epsilon;
final double max = expected.doubleValue() + epsilon;
return min <= a && a <= max;
}
}
/** Matcher that transforms the input value using a function before
* passing to another matcher.
*
* @param From type: the type of value to be matched
* @param To type: type returned by function, and the resulting matcher
*/
private static class ComposingMatcher extends TypeSafeMatcher {
private final Matcher matcher;
private final Function f;
ComposingMatcher(Matcher matcher, Function f) {
this.matcher = matcher;
this.f = f;
}
@Override protected boolean matchesSafely(F item) {
return Unsafe.matches(matcher, f.apply(item));
}
@Override public void describeTo(Description description) {
matcher.describeTo(description);
}
@Override protected void describeMismatchSafely(F item,
Description mismatchDescription) {
mismatchDescription.appendText("was ").appendValue(f.apply(item));
}
}
}