diff --git a/CONTRIBUTORS b/CONTRIBUTORS index e73226a4c..1004a6842 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -63,6 +63,7 @@ Glenn McAllister Glenn Twiggs Greg Nelson Harish Prabandham +Haroon Rafique Hiroaki Nakamura Holger Engels Ingenonsya France @@ -165,6 +166,7 @@ Roger Vaughn Russell Gold Sam Ruby Scott Carlson +Scott M. Stirling Sean Egan Sean P. Kane Shiraz Kanga diff --git a/build.xml b/build.xml index ab5c2acfd..2bd393518 100644 --- a/build.xml +++ b/build.xml @@ -53,6 +53,7 @@ + diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/FormatterElement.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/FormatterElement.java index 3caffc4cb..549c74fb9 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/FormatterElement.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/FormatterElement.java @@ -56,6 +56,13 @@ public class FormatterElement { private String ifProperty; private String unlessProperty; + public static final String XML_FORMATTER_CLASS_NAME = + "org.apache.tools.ant.taskdefs.optional.junit.XMLJUnitResultFormatter"; + public static final String BRIEF_FORMATTER_CLASS_NAME = + "org.apache.tools.ant.taskdefs.optional.junit.BriefJUnitResultFormatter"; + public static final String PLAIN_FORMATTER_CLASS_NAME = + "org.apache.tools.ant.taskdefs.optional.junit.PlainJUnitResultFormatter"; + /** *

Quick way to use a standard formatter. * @@ -71,15 +78,13 @@ public class FormatterElement { */ public void setType(TypeAttribute type) { if ("xml".equals(type.getValue())) { - setClassname("org.apache.tools.ant.taskdefs.optional.junit.XMLJUnitResultFormatter"); - setExtension(".xml"); + setClassname(XML_FORMATTER_CLASS_NAME); } else { if ("brief".equals(type.getValue())) { - setClassname("org.apache.tools.ant.taskdefs.optional.junit.BriefJUnitResultFormatter"); + setClassname(BRIEF_FORMATTER_CLASS_NAME); } else { // must be plain, ensured by TypeAttribute - setClassname("org.apache.tools.ant.taskdefs.optional.junit.PlainJUnitResultFormatter"); + setClassname(PLAIN_FORMATTER_CLASS_NAME); } - setExtension(".txt"); } } @@ -90,6 +95,13 @@ public class FormatterElement { */ public void setClassname(String classname) { this.classname = classname; + if (XML_FORMATTER_CLASS_NAME.equals(classname)) { + setExtension(".xml"); + } else if (PLAIN_FORMATTER_CLASS_NAME.equals(classname)) { + setExtension(".txt"); + } else if (BRIEF_FORMATTER_CLASS_NAME.equals(classname)) { + setExtension(".txt"); + } } /** diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java index 7ca21afab..b68c906c2 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java @@ -17,12 +17,21 @@ package org.apache.tools.ant.taskdefs.optional.junit; +import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; import java.util.Enumeration; +import java.util.HashMap; import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Vector; import org.apache.tools.ant.AntClassLoader; @@ -138,13 +147,15 @@ public class JUnitTask extends Task { private File tmpDir; private AntClassLoader classLoader = null; private Permissions perm = null; + private ForkStyle forkStyle = new ForkStyle("perTest"); private static final int STRING_BUFFER_SIZE = 128; + /** - * If true, force ant to re-classload all classes for each JUnit TestCase - * - * @param value force class reloading for each test case - */ + * If true, force ant to re-classload all classes for each JUnit TestCase + * + * @param value force class reloading for each test case + */ public void setReloading(boolean value) { reloading = value; } @@ -265,6 +276,29 @@ public class JUnitTask extends Task { } } + /** + * Set the bahvior when {@link #setFork fork} fork has been enabled. + * + *

Possible values are "once", "perTest" and "perBatch". If + * set to "once", only a single Java VM will be forked for all + * tests, with "perTest" (the default) each test will run in a + * fresh Java VM and "perBatch" will run all tests from the same + * <batchtest> in the same Java VM.

+ * + *

This attribute will be ignored if tests run in the same VM + * as Ant.

+ * + *

Only tests with the same configuration of haltonerror, + * haltonfailure, errorproperty, failureproperty and filtertrace + * can share a forked Java VM, so even if you set the value to + * "once", Ant may need to fork mutliple VMs.

+ * + * @since Ant 1.6.2 + */ + public void setForkStyle(ForkStyle style) { + this.forkStyle = style; + } + /** * If true, print one-line statistics for each test, or "withOutAndErr" * to also show standard output and error. @@ -599,12 +633,29 @@ public class JUnitTask extends Task { * @since Ant 1.2 */ public void execute() throws BuildException { - Enumeration list = getIndividualTests(); - while (list.hasMoreElements()) { - JUnitTest test = (JUnitTest) list.nextElement(); - if (test.shouldRun(getProject())) { - execute(test); + List testLists = new ArrayList(); + + boolean forkPerTest = forkStyle.getValue().equals(ForkStyle.PER_TEST); + if (forkPerTest || forkStyle.getValue().equals(ForkStyle.ONCE)) { + testLists.addAll(executeOrQueue(getIndividualTests(), + forkPerTest)); + } else { /* forkStyle.getValue().equals(ForkStyle.PER_BATCH) */ + final int count = batchTests.size(); + for (int i = 0; i < count; i++) { + BatchTest batchtest = (BatchTest) batchTests.elementAt(i); + testLists.addAll(executeOrQueue(batchtest.elements(), false)); } + testLists.addAll(executeOrQueue(tests.elements(), forkPerTest)); + } + + Iterator iter = testLists.iterator(); + while (iter.hasNext()) { + List l = (List) iter.next(); + if (l.size() == 1) { + execute((JUnitTest) l.get(0)); + } else { + execute(l); + } } } @@ -632,34 +683,75 @@ public class JUnitTask extends Task { exitValue = executeInVM(test); } else { ExecuteWatchdog watchdog = createWatchdog(); - exitValue = executeAsForked(test, watchdog); + exitValue = executeAsForked(test, watchdog, null); // null watchdog means no timeout, you'd better not check with null if (watchdog != null) { wasKilled = watchdog.killedProcess(); } } + actOnTestResult(exitValue, wasKilled, test, "Test " + test.getName()); + } - // if there is an error/failure and that it should halt, stop - // everything otherwise just log a statement - boolean errorOccurredHere = - exitValue == JUnitTestRunner.ERRORS || wasKilled; - boolean failureOccurredHere = - exitValue != JUnitTestRunner.SUCCESS || wasKilled; - if (errorOccurredHere || failureOccurredHere) { - if ((errorOccurredHere && test.getHaltonerror()) - || (failureOccurredHere && test.getHaltonfailure())) { - throw new BuildException("Test " + test.getName() + " failed" - + (wasKilled ? " (timeout)" : ""), getLocation()); - } else { - log("TEST " + test.getName() + " FAILED" - + (wasKilled ? " (timeout)" : ""), Project.MSG_ERR); - if (errorOccurredHere && test.getErrorProperty() != null) { - getProject().setNewProperty(test.getErrorProperty(), "true"); + /** + * Execute a list of tests in a single forked Java VM. + */ + protected void execute(List tests) throws BuildException { + JUnitTest test = null; + // Create a temporary file to pass the test cases to run to + // the runner (one test case per line) + File casesFile = + FileUtils.newFileUtils().createTempFile("junittestcases", + ".properties", + getProject().getBaseDir()); + casesFile.deleteOnExit(); + PrintWriter writer = null; + try { + writer = + new PrintWriter(new BufferedWriter(new FileWriter(casesFile))); + Iterator iter = tests.iterator(); + while (iter.hasNext()) { + test = (JUnitTest) iter.next(); + writer.print(test.getName()); + if (test.getTodir() == null) { + writer.print("," + getProject().resolveFile(".")); + } else { + writer.print("," + test.getTodir()); } - if (failureOccurredHere && test.getFailureProperty() != null) { - getProject().setNewProperty(test.getFailureProperty(), "true"); + + if (test.getOutfile() == null) { + writer.println("," + "TEST-" + test.getName()); + } else { + writer.println("," + test.getOutfile()); } } + writer.flush(); + writer.close(); + writer = null; + + // execute the test and get the return code + int exitValue = JUnitTestRunner.ERRORS; + boolean wasKilled = false; + ExecuteWatchdog watchdog = createWatchdog(); + exitValue = executeAsForked(test, watchdog, casesFile); + // null watchdog means no timeout, you'd better not check + // with null + if (watchdog != null) { + wasKilled = watchdog.killedProcess(); + } + actOnTestResult(exitValue, wasKilled, test, "Tests"); + } catch(IOException e) { + log(e.toString(), Project.MSG_ERR); + throw new BuildException(e); + } finally { + if (writer != null) { + writer.close(); + } + + try { + casesFile.delete(); + } catch (Exception e) { + log(e.toString(), Project.MSG_ERR); + } } } @@ -674,17 +766,25 @@ public class JUnitTask extends Task { * @throws BuildException in case of error creating a temporary property file, * or if the junit process can not be forked */ - private int executeAsForked(JUnitTest test, ExecuteWatchdog watchdog) + private int executeAsForked(JUnitTest test, ExecuteWatchdog watchdog, + File casesFile) throws BuildException { - if(perm != null) { - log("Permissions ignored when running in forked mode!", Project.MSG_WARN); + if (perm != null) { + log("Permissions ignored when running in forked mode!", + Project.MSG_WARN); } CommandlineJava cmd = (CommandlineJava) getCommandline().clone(); cmd.setClassname("org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner"); - cmd.createArgument().setValue(test.getName()); + if (casesFile == null) { + cmd.createArgument().setValue(test.getName()); + } else { + log("Running multiple tests in the same VM", Project.MSG_VERBOSE); + cmd.createArgument().setValue("testsfile=" + casesFile); + } + cmd.createArgument().setValue("filtertrace=" + test.getFiltertrace()); cmd.createArgument().setValue("haltOnError=" + test.getHaltonerror()); cmd.createArgument().setValue("haltOnFailure=" @@ -1179,4 +1279,140 @@ public class JUnitTask extends Task { } return commandline; } + + /** + * @since Ant 1.6.2 + */ + private final class ForkedTestConfiguration { + private boolean filterTrace; + private boolean haltOnError; + private boolean haltOnFailure; + private String errorProperty; + private String failureProperty; + ForkedTestConfiguration(boolean filterTrace, boolean haltOnError, + boolean haltOnFailure, String errorProperty, + String failureProperty) { + this.filterTrace = filterTrace; + this.haltOnError = haltOnError; + this.haltOnFailure = haltOnFailure; + this.errorProperty = errorProperty; + this.failureProperty = failureProperty; + } + + public boolean equals(Object other) { + if (other == null + || other.getClass() != ForkedTestConfiguration.class) { + return false; + } + ForkedTestConfiguration o = (ForkedTestConfiguration) other; + return filterTrace == o.filterTrace + && haltOnError == o.haltOnError + && haltOnFailure == o.haltOnFailure + && ((errorProperty == null && o.errorProperty == null) + || + (errorProperty != null + && errorProperty.equals(o.errorProperty))) + && ((failureProperty == null && o.failureProperty == null) + || + (failureProperty != null + && failureProperty.equals(o.failureProperty))); + } + + public int hashCode() { + return (filterTrace ? 1 : 0) + + (haltOnError ? 2 : 0) + + (haltOnFailure ? 4 : 0); + } + } + + /** + * @since 1.6.2 + */ + public static final class ForkStyle extends EnumeratedAttribute { + public static final String ONCE = "once"; + public static final String PER_TEST = "perTest"; + public static final String PER_BATCH = "perBatch"; + + public ForkStyle() { + super(); + } + + public ForkStyle(String value) { + super(); + setValue(value); + } + + public String[] getValues() { + return new String[] {ONCE, PER_TEST, PER_BATCH}; + } + } + + /** + * Executes all tests that don't need to be forked (or all tests + * if the runIndividual argument is true. Returns a collection of + * lists of tests that share the same VM configuration and haven't + * been executed yet. + * + * @since 1.6.2 + */ + protected Collection executeOrQueue(Enumeration testList, + boolean runIndividual) { + Map testConfigurations = new HashMap(); + while (testList.hasMoreElements()) { + JUnitTest test = (JUnitTest) testList.nextElement(); + if (test.shouldRun(getProject())) { + if (runIndividual || !test.getFork()) { + execute(test); + } else { + ForkedTestConfiguration c = + new ForkedTestConfiguration(test.getFiltertrace(), + test.getHaltonerror(), + test.getHaltonfailure(), + test.getErrorProperty(), + test.getFailureProperty()); + List l = (List) testConfigurations.get(c); + if (l == null) { + l = new ArrayList(); + testConfigurations.put(c, l); + } + l.add(test); + } + } + } + return testConfigurations.values(); + } + + /** + * Logs information about failed tests, potentially stops + * processing (by throwing a BuildException) if a failure/error + * occured or sets a property. + * + * @since Ant 1.6.2 + */ + protected void actOnTestResult(int exitValue, boolean wasKilled, + JUnitTest test, String name) { + // if there is an error/failure and that it should halt, stop + // everything otherwise just log a statement + boolean errorOccurredHere = + exitValue == JUnitTestRunner.ERRORS || wasKilled; + boolean failureOccurredHere = + exitValue != JUnitTestRunner.SUCCESS || wasKilled; + if (errorOccurredHere || failureOccurredHere) { + if ((errorOccurredHere && test.getHaltonerror()) + || (failureOccurredHere && test.getHaltonfailure())) { + throw new BuildException(name + " failed" + + (wasKilled ? " (timeout)" : ""), getLocation()); + } else { + log(name + " FAILED" + + (wasKilled ? " (timeout)" : ""), Project.MSG_ERR); + if (errorOccurredHere && test.getErrorProperty() != null) { + getProject().setNewProperty(test.getErrorProperty(), "true"); + } + if (failureOccurredHere && test.getFailureProperty() != null) { + getProject().setNewProperty(test.getFailureProperty(), "true"); + } + } + } + } + } diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunner.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunner.java index da2121c87..57fd81e57 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunner.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunner.java @@ -30,6 +30,7 @@ import java.lang.reflect.Method; import java.util.Enumeration; import java.util.Hashtable; import java.util.Properties; +import java.util.StringTokenizer; import java.util.Vector; import junit.framework.AssertionFailedError; import junit.framework.Test; @@ -154,6 +155,9 @@ public class JUnitTestRunner implements TestListener { /** is this runner running in forked mode? */ private boolean forked = false; + /** Running more than one test suite? */ + private static boolean multipleTests = false; + /** * Constructor for fork=true or when the user hasn't specified a * classpath. @@ -478,6 +482,11 @@ public class JUnitTestRunner implements TestListener { System.exit(ERRORS); } + if (args[0].startsWith("testsfile=")) { + multipleTests = true; + args[0] = args[0].substring(10 /* "testsfile=".length() */); + } + for (int i = 1; i < args.length; i++) { if (args[i].startsWith("haltOnError=")) { haltError = Project.toBoolean(args[i].substring(12)); @@ -502,30 +511,70 @@ public class JUnitTestRunner implements TestListener { } } - JUnitTest t = new JUnitTest(args[0]); - // Add/overlay system properties on the properties from the Ant project Hashtable p = System.getProperties(); for (Enumeration e = p.keys(); e.hasMoreElements();) { Object key = e.nextElement(); props.put(key, p.get(key)); } - t.setProperties(props); - JUnitTestRunner runner = new JUnitTestRunner(t, haltError, stackfilter, - haltFail, showOut); - runner.forked = true; - transferFormatters(runner); - runner.run(); - System.exit(runner.getRetCode()); + int returnCode = SUCCESS; + if (multipleTests) { + try { + java.io.BufferedReader reader = + new java.io.BufferedReader(new java.io.FileReader(args[0])); + String testCaseName; + int code = 0; + boolean errorOccured = false; + boolean failureOccured = false; + String line = null; + while ((line = reader.readLine()) != null) { + StringTokenizer st = new StringTokenizer(line, ","); + testCaseName = st.nextToken(); + JUnitTest t = new JUnitTest(testCaseName); + t.setTodir(new File(st.nextToken())); + t.setOutfile(st.nextToken()); + code = launch(t, haltError, stackfilter, haltFail, + showOut, props); + errorOccured = (code == ERRORS); + failureOccured = (code != SUCCESS); + if (errorOccured || failureOccured ) { + if ((errorOccured && haltError) + || (failureOccured && haltFail)) { + System.exit(code); + } else { + if (code > returnCode) { + returnCode = code; + } + System.out.println("TEST " + t.getName() + + " FAILED"); + } + } + } + } catch(IOException e) { + e.printStackTrace(); + } + } else { + returnCode = launch(new JUnitTest(args[0]), haltError, + stackfilter, haltFail, showOut, props); + } + + System.exit(returnCode); } private static Vector fromCmdLine = new Vector(); - private static void transferFormatters(JUnitTestRunner runner) { + private static void transferFormatters(JUnitTestRunner runner, + JUnitTest test) { for (int i = 0; i < fromCmdLine.size(); i++) { - runner.addFormatter((JUnitResultFormatter) fromCmdLine - .elementAt(i)); + FormatterElement fe = (FormatterElement) fromCmdLine.elementAt(i); + if (multipleTests && fe.getUseFile()) { + File destFile = + new File(test.getTodir(), + test.getOutfile() + fe.getExtension()); + fe.setOutfile(destFile); + } + runner.addFormatter(fe.createFormatter()); } } @@ -538,17 +587,20 @@ public class JUnitTestRunner implements TestListener { int pos = line.indexOf(','); if (pos == -1) { fe.setClassname(line); + fe.setUseFile(false); } else { fe.setClassname(line.substring(0, pos)); - fe.setOutfile(new File(line.substring(pos + 1))); + fe.setUseFile(true); + if (!multipleTests) { + fe.setOutfile(new File(line.substring(pos + 1))); + } } - fromCmdLine.addElement(fe.createFormatter()); + fromCmdLine.addElement(fe); } /** * Returns a filtered stack trace. * This is ripped out of junit.runner.BaseTestRunner. - * Scott M. Stirling. */ public static String getFilteredTrace(Throwable t) { String trace = StringUtils.getStackTrace(t); @@ -589,4 +641,19 @@ public class JUnitTestRunner implements TestListener { return false; } + /** + * @since Ant 1.6.2 + */ + private static int launch(JUnitTest t, boolean haltError, + boolean stackfilter, boolean haltFail, + boolean showOut, Properties props) { + t.setProperties(props); + JUnitTestRunner runner = + new JUnitTestRunner(t, haltError, stackfilter, haltFail, showOut); + runner.forked = true; + transferFormatters(runner, t); + + runner.run(); + return runner.getRetCode(); + } } // JUnitTestRunner