package org.jmlspecs.openjmltest; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.PrintWriter; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import javax.tools.Diagnostic; import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; import org.jmlspecs.openjml.JmlSpecs; import org.jmlspecs.openjml.Main; import org.jmlspecs.openjml.Utils; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.rules.TestName; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.JCDiagnostic; import com.sun.tools.javac.util.Log; import com.sun.tools.javac.util.Options; /** This class provides basic functionality for the JUnit tests for OpenJML. * It is expected that actual JUnit test suites will derive from this class. * <P> * It creates a DiagnosticListener that collects all of the error and warning * messages from executing the test. This class does not capture other messages * (e.g. straight to std out), but some subclasses do. The captured error and * warning messages are compared against test supplied text - for both the text * of the message, which includes file name and line number - and the column * number. * * @author David Cok * */ public abstract class JmlTestCase { public final static String specsdir = System.getenv("SPECSDIR"); static protected boolean isWindows = System.getProperty("os.name").contains("Wind"); static protected String projLocation = System.getProperty("openjml.eclipseProjectLocation"); static protected String root = new File(".").getAbsoluteFile().getParentFile().getParentFile().getParent(); protected boolean ignoreNotes = false; /** This is here so we can get the name of a test, using name.getMethodName() */ @Rule public TestName name = new TestName(); /** The java executable */ // TODO: This is going to use the external setting for java, rather than // the current environment within Eclipse protected String jdk = System.getProperty("java.home") + "/bin/java"; /** A purposefully short abbreviation for the system path separator * ( ; or : ) */ static final public String z = java.io.File.pathSeparator; /** Cached value of the end of line character string */ static final public String eol = System.getProperty("line.separator"); /** A Diagnostic listener that can report all the collected diagnostics */ static public interface DiagnosticListenerX<S> extends DiagnosticListener<S> { public List<Diagnostic<? extends S>> getDiagnostics(); } final static public class FilteredDiagnosticCollector<S> implements DiagnosticListenerX<S> { /** Constructs a diagnostic listener that collects all of the diagnostics, * with the ability to filter out the notes. * @param filtered if true, no notes (only errors and warnings) are collected */ public FilteredDiagnosticCollector(boolean filtered) { this.filtered = filtered; } /** If true, no notes are collected. */ boolean filtered; /** The collection (in order) of diagnostics heard so far. */ private java.util.List<Diagnostic<? extends S>> diagnostics = Collections.synchronizedList(new ArrayList<Diagnostic<? extends S>>()); /** The method called by the system when there is a diagnostic to report. */ public void report(Diagnostic<? extends S> diagnostic) { diagnostic.getClass(); // null check if (!filtered || diagnostic.getKind() != Diagnostic.Kind.NOTE) diagnostics.add(diagnostic); } /** * Gets a list view of diagnostics collected by this object. * * @return a list view of diagnostics */ public java.util.List<Diagnostic<? extends S>> getDiagnostics() { return Collections.unmodifiableList(diagnostics); } } public static class StreamGobbler extends Thread { private InputStream is; private StringBuffer input = new StringBuffer(); public StreamGobbler(InputStream is) { this.is = is; } public String input() { return input.toString(); } public void run() { try { char[] cbuf = new char[10000]; InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); int n; while ((n = br.read(cbuf)) != -1) { input.append(cbuf,0,n); } } catch (IOException ioe) { ioe.printStackTrace(); } } } private static class InterruptScheduler extends TimerTask { Thread target = null; public InterruptScheduler(Thread target) { this.target = target; } @Override public void run() { target.interrupt(); } } public static boolean timeout(Process p, long milliseconds) { // Set a timer to interrupt the process if it does not return within the timeout period Timer timer = new Timer(); timer.schedule(new InterruptScheduler(Thread.currentThread()), new Date(System.currentTimeMillis()+milliseconds)); try { p.waitFor(); } catch (InterruptedException e) { // Stop the process from running p.destroy(); return true; } finally { // Stop the timer timer.cancel(); } return false; } // References to various tools needed in testing protected Context context; protected Main main; protected Options options; protected JmlSpecs specs; // initialized in derived classes protected LinkedList<JavaFileObject> mockFiles; /** Normally false, but set to true in tests of the test harness itself, to * avoid printing out diagnostic messages when a test fails. */ public boolean noExtraPrinting = false; /** Set this to true in a test to print out more detailed information about * what the test is doing (as a debugging aid). */ public boolean print = false; /** Set this to true (in the setUp for a test, before calling super.setUp) * if you want diagnostics to be printed as they occur, rather than being * collected. */ public boolean noCollectDiagnostics = false; /** A collector for all of the diagnostic messages */ protected DiagnosticListenerX<JavaFileObject> collector = new FilteredDiagnosticCollector<JavaFileObject>(false); /** Set this to true (for an individual test) if you want debugging information */ public boolean jmldebug = false; /** This does some setup, but most of it has to be left to the derived classes because we have to * set the options before we register most of the JML tools. */ @Before public void setUp() throws Exception { main = new Main("",new PrintWriter(System.out, true),!noCollectDiagnostics?collector:null,null); context = main.context(); options = Options.instance(context); if (jmldebug) { // FIXME - this is not the right way to set debugging Utils.instance(context).jmlverbose = Utils.JMLDEBUG; main.addOptions("-jmlverbose", "4"); } main.addOptions("-properties", "../OpenJML/openjml.properties"); print = false; mockFiles = new LinkedList<JavaFileObject>(); Log.instance(context).multipleErrors = true; //System.out.println("JUnit: Testing " + getName()); } /** Nulls out all the references visible in this class */ @After public void tearDown() throws Exception { context = null; main = null; collector = null; options = null; specs = null; } /** Prints a diagnostic as it is in an error or warning message, but without * any source line. This is how dd.toString() used to behave, but in OpenJDK * build 55, toString also included the source information. So we need to * wrap dd in this call (or change all of the tests). * @param dd the diagnostic * @return */ protected String noSource(Diagnostic<? extends JavaFileObject> dd) { return dd instanceof JCDiagnostic ? noSource((JCDiagnostic)dd) : dd.toString(); } /** Prints out the errors collected by the diagnostic listener */ public void printDiagnostics() { System.out.println("DIAGNOSTICS " + collector.getDiagnostics().size() + " " + name.getMethodName()); for (Diagnostic<? extends JavaFileObject> dd: collector.getDiagnostics()) { long line = dd.getLineNumber(); long start = dd.getStartPosition(); long pos = dd.getPosition(); long end = dd.getEndPosition(); long col = dd.getColumnNumber(); System.out.println(noSource(dd) + " line=" + line + " col=" + col + " pos=" + pos + " start=" + start + " end=" + end); } } /** Checks that all of the collected diagnostic messages match the data supplied * in the arguments. * @param messages an array of expected messages that are checked against the actual messages * @param cols an array of expected column numbers that are checked against the actual diagnostics */ public void checkMessages(/*@ non_null */String[] messages, /*@ non_null */int[] cols) { List<Diagnostic<? extends JavaFileObject>> diags = collector.getDiagnostics(); if (print || (!noExtraPrinting && messages.length != diags.size())) printDiagnostics(); assertEquals("Saw wrong number of errors ",messages.length,diags.size()); assertEquals("Saw wrong number of columns ",cols.length,diags.size()); for (int i = 0; i<diags.size(); ++i) { assertEquals("Message for item " + i,messages[i],noSource(diags.get(i))); assertEquals("Column number for item " + i,cols[i],diags.get(i).getColumnNumber()); // Column number is 1-based } } /** Checks that all of the collected messages match the data supplied * in the arguments. * @param a a sequence of expected values, alternating between error message and column numbers */ public void checkMessages(/*@ nonnullelements */Object ... a) { try { assertEquals("Wrong number of messages seen",a.length,2*collector.getDiagnostics().size()); List<Diagnostic<? extends JavaFileObject>> diags = collector.getDiagnostics(); if (print || (!noExtraPrinting && 2*diags.size() != a.length)) printDiagnostics(); assertEquals("Saw wrong number of errors ",a.length,2*diags.size()); for (int i = 0; i<diags.size(); ++i) { assertEquals("Message for item " + i,a[2*i].toString(),noSource(diags.get(i))); assertEquals("Column number for item " + i,((Integer)a[2*i+1]).intValue(),diags.get(i).getColumnNumber()); // Column number is 1-based } } catch (AssertionError ae) { if (!print && !noExtraPrinting) printDiagnostics(); throw ae; } } /** Checks that there are no diagnostic messages */ public void checkMessages() { if (print || (!noExtraPrinting && 0 != 2*collector.getDiagnostics().size())) printDiagnostics(); assertEquals("Saw wrong number of messages ",0,collector.getDiagnostics().size()); } /** Used to add a pseudo file to the file system. Note that for testing, a * typical filename given here might be #B/A.java, where #B denotes a * mock directory on the specification path * @param filename the name of the file, including leading directory components * @param content the String constituting the content of the pseudo-file */ protected void addMockFile(/*@ non_null */ String filename, /*@ non_null */String content) { try { addMockFile(filename,new TestJavaFileObject(new URI("file:///" + filename),content)); } catch (Exception e) { fail("Exception in creating a URI: " + e); } } protected ByteArrayOutputStream berr; protected ByteArrayOutputStream bout; protected PrintStream savederr; protected PrintStream savedout; protected String actualErr; protected String actualOut; public void collectOutput(boolean collect) { if (collect) { actualOut = null; actualErr = null; savederr = System.err; savedout = System.out; System.setErr(new PrintStream(berr=new ByteArrayOutputStream(10000))); System.setOut(new PrintStream(bout=new ByteArrayOutputStream(10000))); } else { System.err.flush(); System.out.flush(); actualErr = berr.toString(); actualOut = bout.toString(); berr = null; bout = null; System.setErr(savederr); System.setOut(savedout); } } public String output() { return actualOut; } public String errorOutput() { return actualErr; } /** Used to add a pseudo file to the file system. Note that for testing, a * typical filename given here might be #B/A.java, where #B denotes a * mock directory on the specification path * @param filename the name of the file, including leading directory components * @param file the JavaFileObject to be associated with this name */ protected void addMockFile(String filename, JavaFileObject file) { if (filename.endsWith(".java")) mockFiles.add(file); specs.addMockFile(filename,file); } /** Used to add a pseudo file to the command-line. * @param filename the name of the file, including leading directory components * @param file the JavaFileObject to be associated with this name */ protected void addMockJavaFile(String filename, /*@ non_null */String content) { try { addMockJavaFile(filename,new TestJavaFileObject(new URI("file:///" + filename),content)); } catch (Exception e) { fail("Exception in creating a URI: " + e); } } /** Used to add a pseudo file to the command-line. * @param filename the name of the file, including leading directory components * @param file the JavaFileObject to be associated with this name */ protected void addMockJavaFile(String filename, JavaFileObject file) { mockFiles.add(file); } /** Returns the diagnostic message without source location information */ String noSource(JCDiagnostic dd) { return dd.noSource(); } /** Compares two files, returning null if the same; returning a String of * explanation if they are different. */ public String compareFiles(String expected, String actual) { BufferedReader exp = null,act = null; String diff = ""; try { exp = new BufferedReader(new FileReader(expected)); act = new BufferedReader(new FileReader(actual)); boolean same = true; int line = 0; while (true) { line++; String sexp = exp.readLine(); if (sexp != null) sexp = sexp.replace("\r\n", "\n"); String sact = act.readLine(); if (sact != null) sact = sact.replace("\r\n", "\n"); while (ignoreNotes && sact != null && sact.startsWith("Note: ")) { sact = act.readLine(); } if (sexp == null && sact == null) return diff.isEmpty() ? null : diff; while (ignoreNotes && sexp != null && sexp.startsWith("Note: ")) { sexp = exp.readLine(); if (sexp != null) sexp = sexp.replace("\r\n", "\n"); } if (sexp == null && sact == null) return diff.isEmpty() ? null : diff; if (sexp != null && sact == null) { if (sexp == null) { return diff.isEmpty() ? null : diff; } else { diff += ("Less actual input than expected" + eol); return diff; } } if (sexp == null && sact != null) { diff += ("More actual input than expected" + eol); return diff; } sexp = sexp.replace("$ROOT",root); String env = System.getenv("SPECSDIR"); if (env == null) System.out.println("The SPECSDIR environment variable is required to be set for testing"); else sexp = sexp.replace("$SPECS", env); if (!sexp.equals(sact) && !sexp.replace('\\','/').equals(sact.replace('\\','/'))) { int k = sexp.indexOf('('); if (k != -1 && sexp.contains("at java.") && sexp.substring(0,k).equals(sact.substring(0,k))) { // OK } else { diff += ("Lines differ at " + line + eol) + ("EXP: " + sexp + eol) + ("ACT: " + sact + eol); } } } } catch (FileNotFoundException e) { diff += ("No expected file found: " + expected + eol); } catch (Exception e) { diff += ("Exception on file comparison" + eol); } finally { try { if (exp != null) exp.close(); if (act != null) act.close(); } catch (Exception e) {} } return diff.isEmpty() ? null : diff; } public void compareFileToMultipleFiles(String actualFile, String dir, String root) { String diffs = ""; for (String f: new File(dir).list()) { if (!f.contains(root)) continue; diffs = compareFiles(dir + "/" + f, actualFile); if (diffs == null) break; } if (diffs != null) { if (diffs.isEmpty()) { fail("No expected output file"); } else { System.out.println(diffs); fail("Unexpected output: " + diffs); } } else { new File(actualFile).delete(); } } public void compareTextToMultipleFiles(String output, String dir, String root, String actualLocation) { String diffs = ""; for (String f: new File(dir).list()) { if (!f.contains(root)) continue; diffs = compareText(dir + "/" + f,output); if (diffs == null) break; } if (diffs != null) { try (BufferedWriter b = new BufferedWriter(new FileWriter(actualLocation));) { b.write(output); } catch (IOException e) { fail("Failure writing output"); } if (diffs.isEmpty()) { fail("No expected output file"); } else { System.out.println(diffs); fail("Unexpected output: " + diffs); } } else { new File(actualLocation).delete(); } } /** Compares a file to an actual String (ignoring difference kinds of * line separators); returns null if they are the same, returns the * explanation string if they are different. */ public String compareText(String expectedFile, String actual) { String term = "\n|(\r(\n)?)"; // any of the kinds of line terminators BufferedReader exp = null; String[] lines = actual.split(term,-1); // -1 so we do not discard empty lines String diff = ""; try { exp = new BufferedReader(new FileReader(expectedFile)); boolean same = true; int line = 0; while (true) { line++; String sexp = exp.readLine(); if (sexp == null) { if (line == lines.length) return diff.isEmpty() ? null : diff; else { diff += ("More actual input than expected" + eol); return diff; } } if (line > lines.length) { diff += ("Less actual input than expected" + eol); return diff; } sexp = sexp.replace("$ROOT",root); String env = System.getenv("SPECSDIR"); if (env == null) System.out.println("The SPECSDIR environment variable is required to be set for testing"); else sexp = sexp.replace("$SPECS", env); String sact = lines[line-1]; if (sexp.equals(sact)) { // OK } else if (sexp.replace('\\','/').equals(sact.replace('\\','/'))) { // OK } else { int k = sexp.indexOf('('); if (k != -1 && sexp.contains("at java.") && sexp.substring(0,k).equals(sact.substring(0,k))) { // OK } else { diff += ("Lines differ at " + line + eol) + ("EXP: " + sexp + eol) + ("ACT: " + sact + eol); } } } } catch (FileNotFoundException e) { diff += ("No expected file found: " + expectedFile + eol); } catch (Exception e) { diff += ("Exception on file comparison" + eol); } finally { try { if (exp != null) exp.close(); } catch (Exception e) {} } return diff.isEmpty() ? null : diff; } }