blob: 01e738a11f9b20272b540dd7494da9e5b9bdaec4 [file] [log] [blame]
package org.checkerframework.framework.test;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.StringJoiner;
import javax.tools.Diagnostic;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.ToolProvider;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.SystemUtil;
import org.junit.Assert;
import org.plumelib.util.CollectionsPlume;
import org.plumelib.util.StringsPlume;
import org.plumelib.util.SystemPlume;
/** Utilities for testing. */
public class TestUtilities {
public static final boolean IS_AT_LEAST_9_JVM = SystemUtil.getJreVersion() >= 9;
public static final boolean IS_AT_LEAST_11_JVM = SystemUtil.getJreVersion() >= 11;
static {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
OutputStream err = new ByteArrayOutputStream();
compiler.run(null, null, err, "-version");
}
public static List<File> findNestedJavaTestFiles(String... dirNames) {
return findRelativeNestedJavaFiles(new File("tests"), dirNames);
}
public static List<File> findRelativeNestedJavaFiles(String parent, String... dirNames) {
return findRelativeNestedJavaFiles(new File(parent), dirNames);
}
public static List<File> findRelativeNestedJavaFiles(File parent, String... dirNames) {
File[] dirs = new File[dirNames.length];
int i = 0;
for (String dirName : dirNames) {
dirs[i] = new File(parent, dirName);
i += 1;
}
return getJavaFilesAsArgumentList(dirs);
}
/**
* Returns a list where each item is a list of Java files, excluding any skip tests, for each
* directory given by dirName and also a list for any subdirectory.
*
* @param parent parent directory of the dirNames directories
* @param dirNames names of directories to search
* @return list where each item is a list of Java test files grouped by directory
*/
public static List<List<File>> findJavaFilesPerDirectory(File parent, String... dirNames) {
if (!parent.exists()) {
throw new BugInCF(
"test parent directory does not exist: %s %s", parent, parent.getAbsoluteFile());
}
if (!parent.isDirectory()) {
throw new BugInCF(
"test parent directory is not a directory: %s %s", parent, parent.getAbsoluteFile());
}
List<List<File>> filesPerDirectory = new ArrayList<>();
for (String dirName : dirNames) {
File dir = new File(parent, dirName).toPath().toAbsolutePath().normalize().toFile();
// This fails for the whole-program-inference tests: their sources do not necessarily
// exist yet but will be created by a test that runs earlier than they do.
// if (!dir.isDirectory()) {
// throw new BugInCF("test directory does not exist: %s", dir);
// }
if (dir.isDirectory()) {
filesPerDirectory.addAll(findJavaTestFilesInDirectory(dir));
}
}
return filesPerDirectory;
}
/**
* Returns a list where each item is a list of Java files, excluding any skip tests. There is one
* list for {@code dir}, and one list for each subdirectory of {@code dir}.
*
* @param dir directory in which to search for Java files
* @return a list of list of Java test files
*/
private static List<List<File>> findJavaTestFilesInDirectory(File dir) {
List<List<File>> fileGroupedByDirectory = new ArrayList<>();
List<File> filesInDir = new ArrayList<>();
fileGroupedByDirectory.add(filesInDir);
String[] dirContents = dir.list();
if (dirContents == null) {
throw new Error("Not a directory: " + dir);
}
Arrays.sort(dirContents);
for (String fileName : dirContents) {
File file = new File(dir, fileName);
if (file.isDirectory()) {
fileGroupedByDirectory.addAll(findJavaTestFilesInDirectory(file));
} else if (isJavaTestFile(file)) {
filesInDir.add(file);
}
}
if (filesInDir.isEmpty()) {
fileGroupedByDirectory.remove(filesInDir);
}
return fileGroupedByDirectory;
}
/**
* Prepends a file to the beginning of each filename.
*
* @param parent a file to prepend to each filename
* @param fileNames file names
* @return the file names, each with {@code parent} prepended
*/
public static List<Object[]> findFilesInParent(File parent, String... fileNames) {
return CollectionsPlume.mapList(
(String fileName) -> new Object[] {new File(parent, fileName)}, fileNames);
}
/**
* Traverses the directories listed looking for Java test files.
*
* @param dirs directories in which to search for Java test files
* @return a list of Java test files found in the directories
*/
public static List<File> getJavaFilesAsArgumentList(File... dirs) {
List<File> arguments = new ArrayList<>();
for (File dir : dirs) {
arguments.addAll(deeplyEnclosedJavaTestFiles(dir));
}
return arguments;
}
/** Returns all the java files that are descendants of the given directory. */
public static List<File> deeplyEnclosedJavaTestFiles(File directory) {
if (!directory.exists()) {
throw new IllegalArgumentException(
"directory does not exist: " + directory + " " + directory.getAbsolutePath());
}
if (!directory.isDirectory()) {
throw new IllegalArgumentException("found file instead of directory: " + directory);
}
List<File> javaFiles = new ArrayList<>();
@SuppressWarnings("nullness") // checked above that it's a directory
File @NonNull [] in = directory.listFiles();
Arrays.sort(
in,
new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return o1.getName().compareTo(o2.getName());
}
});
for (File file : in) {
if (file.isDirectory()) {
javaFiles.addAll(deeplyEnclosedJavaTestFiles(file));
} else if (isJavaTestFile(file)) {
javaFiles.add(file);
}
}
return javaFiles;
}
public static boolean isJavaFile(File file) {
return file.isFile() && file.getName().endsWith(".java");
}
public static boolean isJavaTestFile(File file) {
if (!isJavaFile(file)) {
return false;
}
// We could implement special filtering based on directory names,
// but I prefer using @below-java9-jdk-skip-test
// if (!IS_AT_LEAST_9_JVM && file.getAbsolutePath().contains("java9")) {
// return false;
// }
Scanner in = null;
try {
in = new Scanner(file);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
while (in.hasNext()) {
String nextLine = in.nextLine();
if (nextLine.contains("@skip-test")
|| (!IS_AT_LEAST_9_JVM && nextLine.contains("@below-java9-jdk-skip-test"))
|| (!IS_AT_LEAST_11_JVM && nextLine.contains("@below-java11-jdk-skip-test"))) {
in.close();
return false;
}
}
in.close();
return true;
}
public static @Nullable String diagnosticToString(
final Diagnostic<? extends JavaFileObject> diagnostic, boolean usingAnomsgtxt) {
String result = diagnostic.toString().trim();
// suppress Xlint warnings
if (result.contains("uses unchecked or unsafe operations.")
|| result.contains("Recompile with -Xlint:unchecked for details.")
|| result.endsWith(" declares unsafe vararg methods.")
|| result.contains("Recompile with -Xlint:varargs for details.")) {
return null;
}
if (usingAnomsgtxt) {
// Lines with "unexpected Throwable" are stack traces
// and should be printed in full.
if (!result.contains("unexpected Throwable")) {
String firstLine;
int lineSepPos = result.indexOf(System.lineSeparator());
if (lineSepPos != -1) {
firstLine = result.substring(0, lineSepPos);
} else {
firstLine = result;
}
int javaPos = firstLine.indexOf(".java:");
if (javaPos != -1) {
firstLine = firstLine.substring(javaPos + 5).trim();
}
result = firstLine;
}
}
return result;
}
public static Set<String> diagnosticsToStrings(
final Iterable<Diagnostic<? extends JavaFileObject>> actualDiagnostics,
boolean usingAnomsgtxt) {
Set<String> actualDiagnosticsStr = new LinkedHashSet<>();
for (Diagnostic<? extends JavaFileObject> diagnostic : actualDiagnostics) {
String diagnosticStr = TestUtilities.diagnosticToString(diagnostic, usingAnomsgtxt);
if (diagnosticStr != null) {
actualDiagnosticsStr.add(diagnosticStr);
}
}
return actualDiagnosticsStr;
}
/**
* Return the file absolute pathnames, separated by commas.
*
* @param javaFiles a list of Java files
* @return the file absolute pathnames, separated by commas
*/
public static String summarizeSourceFiles(List<File> javaFiles) {
StringJoiner sj = new StringJoiner(", ");
for (File file : javaFiles) {
sj.add(file.getAbsolutePath());
}
return sj.toString();
}
public static File getTestFile(String fileRelativeToTestsDir) {
return new File("tests", fileRelativeToTestsDir);
}
public static File findComparisonFile(File testFile) {
final File comparisonFile =
new File(testFile.getParent(), testFile.getName().replace(".java", ".out"));
return comparisonFile;
}
public static List<String> optionMapToList(Map<String, @Nullable String> options) {
List<String> optionList = new ArrayList<>(options.size() * 2);
for (Map.Entry<String, @Nullable String> opt : options.entrySet()) {
optionList.add(opt.getKey());
if (opt.getValue() != null) {
optionList.add(opt.getValue());
}
}
return optionList;
}
public static void writeLines(File file, Iterable<?> lines) {
try {
final BufferedWriter bw = new BufferedWriter(new FileWriter(file, true));
Iterator<?> iter = lines.iterator();
while (iter.hasNext()) {
Object next = iter.next();
if (next == null) {
bw.write("<null>");
} else {
bw.write(next.toString());
}
bw.newLine();
}
bw.flush();
bw.close();
} catch (IOException io) {
throw new RuntimeException(io);
}
}
public static void writeDiagnostics(
File file,
File testFile,
List<String> expected,
List<String> actual,
List<String> unexpected,
List<String> missing,
boolean usingNoMsgText,
boolean testFailed) {
try (PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file, true)))) {
pw.println("File: " + testFile.getAbsolutePath());
pw.println("TestFailed: " + testFailed);
pw.println("Using nomsgtxt: " + usingNoMsgText);
pw.println("#Missing: " + missing.size() + " #Unexpected: " + unexpected.size());
pw.println("Expected:");
pw.println(StringsPlume.joinLines(expected));
pw.println();
pw.println("Actual:");
pw.println(StringsPlume.joinLines(actual));
pw.println();
pw.println("Missing:");
pw.println(StringsPlume.joinLines(missing));
pw.println();
pw.println("Unexpected:");
pw.println(StringsPlume.joinLines(unexpected));
pw.println();
pw.println();
pw.println();
pw.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void writeTestConfiguration(File file, TestConfiguration config) {
try {
final BufferedWriter bw = new BufferedWriter(new FileWriter(file, true));
bw.write(config.toString());
bw.newLine();
bw.newLine();
bw.flush();
bw.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void writeJavacArguments(
File file,
Iterable<? extends JavaFileObject> files,
Iterable<String> options,
Iterable<String> processors) {
try (PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file, true)))) {
pw.println("Files:");
for (JavaFileObject f : files) {
pw.println(" " + f.getName());
}
pw.println();
pw.println("Options:");
for (String o : options) {
pw.println(" " + o);
}
pw.println();
pw.println("Processors:");
for (String p : processors) {
pw.println(" " + p);
}
pw.println();
pw.println();
pw.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* If the given TypecheckResult has unexpected or missing diagnostics, fail the running JUnit
* test.
*
* @param testResult the result of type-checking
*/
public static void assertTestDidNotFail(TypecheckResult testResult) {
if (testResult.didTestFail()) {
if (getShouldEmitDebugInfo()) {
System.out.println("---------------- start of javac ouput ----------------");
System.out.println(testResult.getCompilationResult().getJavacOutput());
System.out.println("---------------- end of javac ouput ----------------");
} else {
System.out.println("To see the javac command line and output, run with: -Pemit.test.debug");
}
Assert.fail(testResult.summarize());
}
}
/**
* Create the directory (and its parents) if it does not exist.
*
* @param dir the directory to create
*/
public static void ensureDirectoryExists(String dir) {
try {
Files.createDirectories(Paths.get(dir));
} catch (FileAlreadyExistsException e) {
// directory already exists
} catch (IOException e) {
throw new RuntimeException("Could not make directory: " + dir + ": " + e.getMessage());
}
}
/**
* Return true if the system property is set to "true". Return false if the system property is not
* set or is set to "false". Otherwise, errs.
*
* @param key system property to check
* @return true if the system property is set to "true". Return false if the system property is
* not set or is set to "false". Otherwise, errs.
* @deprecated Use {@link SystemUtil#getBooleanSystemProperty(String)} instead.
*/
@Deprecated // 2020-04-30
public static boolean testBooleanProperty(String key) {
return testBooleanProperty(key, false);
}
/**
* If the system property is set, return its boolean value; otherwise return {@code defaultValue}.
* Errs if the system property is set to a non-boolean value.
*
* @param key system property to check
* @param defaultValue value to use if the property is not set
* @return the boolean value of {@code key} or {@code defaultValue} if {@code key} is not set
* @deprecated Use {@link SystemUtil#getBooleanSystemProperty(String, boolean)} instead.
*/
@Deprecated
public static boolean testBooleanProperty(String key, boolean defaultValue) {
return SystemUtil.getBooleanSystemProperty(key, defaultValue);
}
/**
* Returns the value of system property "emit.test.debug".
*
* @return the value of system property "emit.test.debug"
*/
public static boolean getShouldEmitDebugInfo() {
return SystemPlume.getBooleanSystemProperty("emit.test.debug");
}
}