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.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.Map.Entry; import java.util.Scanner; import java.util.Set; import javax.tools.Diagnostic; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.ToolProvider; import org.checkerframework.framework.util.PluginUtil; import org.junit.Assert; public class TestUtilities { public static final boolean isJSR308Compiler; public static final boolean isAtLeast8Jvm; static { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); OutputStream err = new ByteArrayOutputStream(); compiler.run(null, null, err, "-version"); isJSR308Compiler = err.toString().contains("jsr308"); isAtLeast8Jvm = org.checkerframework.framework.util.PluginUtil.getJreVersion() >= 1.8d; } 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) { List<List<File>> filesPerDirectory = new ArrayList<>(); for (String dirName : dirNames) { File dir = new File(parent, dirName); 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, for each * subdirectory of {@code dir} and also a list of Java files in 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) { assert dir.isDirectory(); List<List<File>> fileGroupedByDirectory = new ArrayList<>(); List<File> fileInDir = new ArrayList<>(); fileGroupedByDirectory.add(fileInDir); for (String fileName : dir.list()) { File file = new File(dir, fileName); if (file.isDirectory()) { fileGroupedByDirectory.addAll(findJavaTestFilesInDirectory(file)); } else if (isJavaTestFile(file)) { fileInDir.add(file); } } if (fileInDir.isEmpty()) { fileGroupedByDirectory.remove(fileInDir); } return fileGroupedByDirectory; } public static List<Object[]> findFilesInParent(File parent, String... fileNames) { List<Object[]> files = new ArrayList<Object[]>(); for (String fileName : fileNames) { files.add(new Object[] {new File(parent, fileName)}); } return files; } /** Traverses the directories listed looking for java test files */ public static List<File> getJavaFilesAsArgumentList(File... dirs) { List<File> arguments = new ArrayList<File>(); for (File dir : dirs) { List<File> javaFiles = deeplyEnclosedJavaTestFiles(dir); for (File javaFile : javaFiles) { arguments.add(javaFile); } } 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<File>(); File[] 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-java8-jdk-skip-test // if (!isAtLeast8Jvm && file.getAbsolutePath().contains("java8")) { // 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") || (!isJSR308Compiler && nextLine.contains("@non-308-skip-test")) || (!isAtLeast8Jvm && nextLine.contains("@below-java8-jdk-skip-test"))) { in.close(); return false; } } in.close(); return true; } public static 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; if (result.contains("\n")) { firstLine = result.substring(0, result.indexOf('\n')); } else { firstLine = result; } if (firstLine.contains(".java:")) { firstLine = firstLine.substring(firstLine.indexOf(".java:") + 5).trim(); } result = firstLine; } } return result; } public static Set<String> diagnosticsToStrings( final Iterable<Diagnostic<? extends JavaFileObject>> actualDiagnostics, boolean usingAnomsgtxt) { Set<String> actualDiagnosticsStr = new LinkedHashSet<String>(); for (Diagnostic<? extends JavaFileObject> diagnostic : actualDiagnostics) { String diagnosticStr = TestUtilities.diagnosticToString(diagnostic, usingAnomsgtxt); if (diagnosticStr != null) { actualDiagnosticsStr.add(diagnosticStr); } } return actualDiagnosticsStr; } public static String summarizeSourceFiles(List<File> javaFiles) { StringBuilder listStrBuilder = new StringBuilder(); boolean first = true; for (File file : javaFiles) { if (first) { first = false; } else { listStrBuilder.append(", "); } listStrBuilder.append(file.getAbsolutePath()); } return listStrBuilder.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, String> options) { List<String> optionList = new ArrayList<>(options.size() * 2); for (Entry<String, 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 { final BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); bw.write("File: " + testFile.getAbsolutePath() + "\n"); bw.write("TestFailed: " + testFailed + "\n"); bw.write("Using nomsgtxt: " + usingNoMsgText + "\n"); bw.write( "#Missing: " + missing.size() + " #Unexpected: " + unexpected.size() + "\n"); bw.write("Expected:\n"); bw.write(PluginUtil.join("\n", expected)); bw.newLine(); bw.write("Actual:\n"); bw.write(PluginUtil.join("\n", actual)); bw.newLine(); bw.write("Missing:\n"); bw.write(PluginUtil.join("\n", missing)); bw.newLine(); bw.write("Unexpected:\n"); bw.write(PluginUtil.join("\n", unexpected)); bw.newLine(); bw.newLine(); bw.newLine(); bw.flush(); bw.close(); } 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 { final BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); bw.write("Files:\n"); for (JavaFileObject f : files) { bw.write(" " + f.getName()); bw.newLine(); } bw.newLine(); bw.write("Options:\n"); for (String o : options) { bw.write(" " + o); bw.newLine(); } bw.newLine(); bw.write("Processors:\n"); for (String p : processors) { bw.write(" " + p); bw.newLine(); } bw.newLine(); bw.newLine(); bw.flush(); bw.close(); } catch (IOException e) { throw new RuntimeException(e); } } /** * TODO: REDO COMMENT Compares the result of the compiler against an array of Strings. * * <p>In a checker, we treat a more specific error message as subsumed by a general one. For * example, "new.array.type.invalid" is subsumed by "type.invalid". This is not the case in the * test framework; the exact error key is expected. */ public static void assertResultsAreValid(TypecheckResult testResult) { if (testResult.didTestFail()) { Assert.fail(testResult.summarize()); } } public static void ensureDirectoryExists(File path) { if (!path.exists()) { if (!path.mkdirs()) { throw new RuntimeException("Could not make directory: " + path.getAbsolutePath()); } } } public static boolean testBooleanProperty(String propName) { return testBooleanProperty(propName, false); } public static boolean testBooleanProperty(String propName, boolean defaultValue) { return System.getProperty(propName, String.valueOf(defaultValue)).equalsIgnoreCase("true"); } public static boolean getShouldEmitDebugInfo() { String emitDebug = System.getProperty("emit.test.debug"); return emitDebug != null && emitDebug.equalsIgnoreCase("true"); } }