All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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 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)); } } }