content: /*
   * 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);
      }
    }
  }
  