blob: 410619b1138585b3eb4678a0b1d480f1e6a30129 [file] [log] [blame]
/*
* 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.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Objects;
import java.util.SortedSet;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 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");
private static final Supplier<Integer> GUAVA_MAJOR_VERSION =
Suppliers.memoize(TestUtil::computeGuavaMajorVersion)::get;
/** 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');
}
/**
* Converts a string (which may contain quotes and newlines) into a java
* literal.
*
* <p>For example,
* <pre><code>string with "quotes" split
* across lines</code></pre>
*
* <p>becomes
*
* <blockquote><pre><code>"string with \"quotes\" split" + NL +
* "across lines"</code></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,</p>
*
* <blockquote><pre><code>string with "quotes" split
* across lines</code></pre></blockquote>
*
* <p>becomes</p>
*
* <blockquote><pre><code>TestUtil.fold(
* "string with \"quotes\" split\n",
* + "across lines")</code></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("]", "\\]");
}
/** 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) {
Objects.requireNonNull(version, "version");
if (version.startsWith("1.")) {
// running on version <= 8 (expecting string of type: x.y.z*)
final String[] versions = version.split("\\.");
return Integer.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 Integer.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;
}
/** 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;
}
}
}