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