TestUtil.java

/*
 * 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.util;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableSortedSet;

import org.junit.jupiter.api.Assertions;

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.List;
import java.util.SortedSet;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.base.Preconditions.checkArgument;

import static org.apache.calcite.util.Util.first;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

import static java.lang.Double.parseDouble;
import static java.lang.Integer.parseInt;
import static java.util.Objects.requireNonNull;

/**
 * Static utilities for JUnit tests.
 */
public abstract class TestUtil {
  //~ Static fields/initializers ---------------------------------------------

  private static final Pattern LINE_BREAK_PATTERN =
      Pattern.compile("\r\n|\r|\n");

  private static final Pattern TAB_PATTERN = Pattern.compile("\t");

  private static final String LINE_BREAK =
      "\\\\n\"" + Util.LINE_SEPARATOR + " + \"";

  private static final String JAVA_VERSION =
      System.getProperties().getProperty("java.version");

  public static final Version AVATICA_VERSION =
      Version.of(first(System.getProperty("calcite.avatica.version"), "0"));

  private static final Supplier<Integer> GUAVA_MAJOR_VERSION =
      Suppliers.memoize(TestUtil::computeGuavaMajorVersion);

  /** Matches a number with at least four zeros after the point. */
  private static final Pattern TRAILING_ZERO_PATTERN =
      Pattern.compile("-?[0-9]+\\.([0-9]*[1-9])?(00000*[0-9][0-9]?)");

  /** Matches a number with at least four nines after the point. */
  private static final Pattern TRAILING_NINE_PATTERN =
      Pattern.compile("-?[0-9]+\\.([0-9]*[0-8])?(99999*[0-9][0-9]?)");

  /** This is to be used by {@link #rethrow(Throwable, String)} to add extra information via
   * {@link Throwable#addSuppressed(Throwable)}. */
  private static class ExtraInformation extends Throwable {
    ExtraInformation(String message) {
      super(message);
    }
  }

  //~ Methods ----------------------------------------------------------------

  public static void assertEqualsVerbose(
      String expected,
      String actual) {
    Assertions.assertEquals(expected, actual,
        () -> "Expected:\n"
            + expected
            + "\nActual:\n"
            + actual
            + "\nActual java:\n"
            + toJavaString(actual) + '\n');
  }

  public static void assertThatScientific(String value, org.hamcrest.Matcher<String> matcher) {
    double d = parseDouble(value);
    assertThat(Util.toScientificNotation(d), matcher);
  }

  /**
   * Converts a string (which may contain quotes and newlines) into a java
   * literal.
   *
   * <p>For example,
   *
   * <blockquote><pre>{@code
   * string with "quotes" split
   * across lines}</pre></blockquote>
   *
   * <p>becomes
   *
   * <blockquote><pre>{@code
   * "string with \"quotes\" split" + "\n"
   *   + "across lines"}</pre></blockquote>
   */
  public static String quoteForJava(String s) {
    s = Util.replace(s, "\\", "\\\\");
    s = Util.replace(s, "\"", "\\\"");
    s = LINE_BREAK_PATTERN.matcher(s).replaceAll(LINE_BREAK);
    s = TAB_PATTERN.matcher(s).replaceAll("\\\\t");
    s = "\"" + s + "\"";
    final String spurious = " + \n\"\"";
    if (s.endsWith(spurious)) {
      s = s.substring(0, s.length() - spurious.length());
    }
    return s;
  }

  /**
   * Converts a string (which may contain quotes and newlines) into a java
   * literal.
   *
   * <p>For example,
   *
   * <blockquote><pre>{code
   * string with "quotes" split
   * across lines}</pre></blockquote>
   *
   * <p>becomes
   *
   * <blockquote><pre>{@code
   * TestUtil.fold("string with \"quotes\" split\n",
   *     + "across lines")}</pre></blockquote>
   */
  public static String toJavaString(String s) {
    // Convert [string with "quotes" split
    // across lines]
    // into [fold(
    // "string with \"quotes\" split\n"
    // + "across lines")]
    //
    s = Util.replace(s, "\"", "\\\"");
    s = LINE_BREAK_PATTERN.matcher(s).replaceAll(LINE_BREAK);
    s = TAB_PATTERN.matcher(s).replaceAll("\\\\t");
    s = "\"" + s + "\"";
    String spurious = "\n \\+ \"\"";
    if (s.endsWith(spurious)) {
      s = s.substring(0, s.length() - spurious.length());
    }
    return s;
  }

  /**
   * Combines an array of strings, each representing a line, into a single
   * string containing line separators.
   */
  public static String fold(String... strings) {
    StringBuilder buf = new StringBuilder();
    for (String string : strings) {
      buf.append(string);
      buf.append('\n');
    }
    return buf.toString();
  }

  /** Quotes a string for Java or JSON. */
  public static String escapeString(String s) {
    return escapeString(new StringBuilder(), s).toString();
  }

  /** Quotes a string for Java or JSON, into a builder. */
  public static StringBuilder escapeString(StringBuilder buf, String s) {
    buf.append('"');
    int n = s.length();
    char lastChar = 0;
    for (int i = 0; i < n; ++i) {
      char c = s.charAt(i);
      switch (c) {
      case '\\':
        buf.append("\\\\");
        break;
      case '"':
        buf.append("\\\"");
        break;
      case '\n':
        buf.append("\\n");
        break;
      case '\r':
        if (lastChar != '\n') {
          buf.append("\\r");
        }
        break;
      default:
        buf.append(c);
        break;
      }
      lastChar = c;
    }
    return buf.append('"');
  }

  /**
   * Quotes a pattern.
   */
  public static String quotePattern(String s) {
    return s.replace("\\", "\\\\")
        .replace(".", "\\.")
        .replace("+", "\\+")
        .replace("{", "\\{")
        .replace("}", "\\}")
        .replace("|", "\\||")
        .replace("$", "\\$")
        .replace("?", "\\?")
        .replace("*", "\\*")
        .replace("(", "\\(")
        .replace(")", "\\)")
        .replace("[", "\\[")
        .replace("]", "\\]")
        .replace("\n", "\\n")
        .replace("^", "\\^");
  }

  /** Removes floating-point rounding errors from the end of a string.
   *
   * <p>{@code 12.300000006} becomes {@code 12.3};
   * {@code -12.37999999991} becomes {@code -12.38}. */
  public static String correctRoundedFloat(String s) {
    if (s == null) {
      return s;
    }
    final Matcher m = TRAILING_ZERO_PATTERN.matcher(s);
    if (m.matches()) {
      s = s.substring(0, s.length() - m.group(2).length());
    }
    final Matcher m2 = TRAILING_NINE_PATTERN.matcher(s);
    if (m2.matches()) {
      s = s.substring(0, s.length() - m2.group(2).length());
      if (s.length() > 0) {
        final char c = s.charAt(s.length() - 1);
        switch (c) {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case  '8':
          // '12.3499999996' became '12.34', now we make it '12.35'
          s = s.substring(0, s.length() - 1) + (char) (c + 1);
          break;
        case '.':
          // '12.9999991' became '12.', which we leave as is.
          break;
        }
      }
    }
    return s;
  }

  /**
   * Returns the Java major version: 7 for JDK 1.7, 8 for JDK 8, 10 for
   * JDK 10, etc. depending on current system property {@code java.version}.
   */
  public static int getJavaMajorVersion() {
    return majorVersionFromString(JAVA_VERSION);
  }

  /**
   * Detects java major version given long format of full JDK version.
   * See <a href="http://openjdk.java.net/jeps/223">JEP 223: New Version-String Scheme</a>.
   *
   * @param version current version as string usually from {@code java.version} property.
   * @return major java version ({@code 8, 9, 10, 11} etc.)
   */
  @VisibleForTesting
  static int majorVersionFromString(String version) {
    requireNonNull(version, "version");

    if (version.startsWith("1.")) {
      // running on version <= 8 (expecting string of type: x.y.z*)
      final String[] versions = version.split("\\.");
      return parseInt(versions[1]);
    }
    // probably running on > 8 (just get first integer which is major version)
    Matcher matcher = Pattern.compile("^\\d+").matcher(version);
    if (!matcher.lookingAt()) {
      throw new IllegalArgumentException("Can't parse (detect) JDK version from " + version);
    }

    return parseInt(matcher.group());
  }

  /** Returns the Guava major version. */
  public static int getGuavaMajorVersion() {
    return GUAVA_MAJOR_VERSION.get();
  }

  /** Computes the Guava major version. */
  private static int computeGuavaMajorVersion() {
    // A list of classes and the Guava version that they were introduced.
    // The list should not contain any classes that are removed in future
    // versions of Guava.
    return new VersionChecker()
        .tryClass(2, "com.google.common.collect.ImmutableList")
        .tryClass(14, "com.google.common.reflect.Parameter")
        .tryClass(17, "com.google.common.base.VerifyException")
        .tryClass(21, "com.google.common.io.RecursiveDeleteOption")
        .tryClass(23, "com.google.common.util.concurrent.FluentFuture")
        .tryClass(26, "com.google.common.util.concurrent.ExecutionSequencer")
        .bestVersion;
  }

  /** Returns the JVM vendor. */
  public static String getJavaVirtualMachineVendor() {
    return System.getProperty("java.vm.vendor");
  }

  /** Returns the root directory of the source tree. */
  public static File getBaseDir(Class<?> klass) {
    // Algorithm:
    // 1) Find location of TestUtil.class
    // 2) Climb via getParentFile() until we detect pom.xml
    // 3) It means we've got BASE/testkit/pom.xml, and we need to get BASE
    final URL resource = klass.getResource(klass.getSimpleName() + ".class");
    final File classFile =
        Sources.of(requireNonNull(resource, "resource")).file();

    File file = classFile.getAbsoluteFile();
    for (int i = 0; i < 42; i++) {
      if (isProjectDir(file)) {
        // Ok, file == BASE/testkit/
        break;
      }
      file = file.getParentFile();
    }
    if (!isProjectDir(file)) {
      fail("Could not find pom.xml, build.gradle.kts or gradle.properties. "
          + "Started with " + classFile.getAbsolutePath()
          + ", the current path is " + file.getAbsolutePath());
    }
    return file.getParentFile();
  }

  private static boolean isProjectDir(File dir) {
    return new File(dir, "pom.xml").isFile()
        || new File(dir, "build.gradle.kts").isFile()
        || new File(dir, "gradle.properties").isFile();
  }

  /** Given a list, returns the number of elements that are not between an
   * element that is less and an element that is greater. */
  public static <E extends Comparable<E>> SortedSet<E> outOfOrderItems(List<E> list) {
    E previous = null;
    final ImmutableSortedSet.Builder<E> b = ImmutableSortedSet.naturalOrder();
    for (E e : list) {
      if (previous != null && previous.compareTo(e) > 0) {
        b.add(e);
      }
      previous = e;
    }
    return b.build();
  }

  /** Checks if exceptions have give substring. That is handy to prevent logging SQL text twice */
  public static boolean hasMessage(Throwable t, String substring) {
    while (t != null) {
      String message = t.getMessage();
      if (message != null && message.contains(substring)) {
        return true;
      }
      t = t.getCause();
    }
    return false;
  }

  /** Rethrows given exception keeping stacktraces clean and compact. */
  public static <E extends Throwable> RuntimeException rethrow(Throwable e) throws E {
    if (e instanceof InvocationTargetException) {
      e = e.getCause();
    }
    throw (E) e;
  }

  /** Rethrows given exception keeping stacktraces clean and compact. */
  public static <E extends Throwable> RuntimeException rethrow(Throwable e,
      String message) throws E {
    e.addSuppressed(new ExtraInformation(message));
    throw (E) e;
  }

  /** Returns string representation of the given {@link Throwable}. */
  public static String printStackTrace(Throwable t) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    t.printStackTrace(pw);
    pw.flush();
    return sw.toString();
  }

  /** Checks whether a given class exists, and updates a version if it does. */
  private static class VersionChecker {
    int bestVersion = -1;

    VersionChecker tryClass(int version, String className) {
      try {
        Class.forName(className);
        bestVersion = Math.max(version, bestVersion);
      } catch (ClassNotFoundException e) {
        // ignore
      }
      return this;
    }
  }

  /** Returns a {@code CharSequence} that contains a given string repeated
   * {@code count} times. Unlike a String with the same contents, it
   * is virtual, and only becomes real when, say, someone calls
   * {@link StringBuilder#append(CharSequence)} with it. */
  public static CharSequence repeat(String s, int count) {
    final int length = s.length() * count;
    return new RepeatCharSequence(s, length);
  }

  /** CharSequence that repeats a given string up to a given length. */
  private static class RepeatCharSequence implements CharSequence {
    private final int length;
    private final String s;

    RepeatCharSequence(String s, int length) {
      this.s = requireNonNull(s, "s");
      this.length = length;
      checkArgument(!s.isEmpty());
      checkArgument(length >= 0);
    }

    @Override public String toString() {
      //noinspection StringBufferReplaceableByString
      return new StringBuilder().append(this).toString();
    }

    @Override public int length() {
      return length;
    }

    @Override public char charAt(int index) {
      return s.charAt(index % s.length());
    }

    @Override public CharSequence subSequence(int start, int end) {
      final int offset = start % s.length();
      if (offset == 0) {
        return new RepeatCharSequence(s, end - start);
      }
      final String rotated = s.substring(offset) + s.substring(0, offset);
      return new RepeatCharSequence(rotated, end - start);
    }
  }
}