diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/ForkedVMWatcher.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/ForkedVMWatcher.java new file mode 100644 index 000000000..a5e516416 --- /dev/null +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/ForkedVMWatcher.java @@ -0,0 +1,108 @@ +/* + * Copyright 2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.tools.ant.taskdefs.optional.junit; + +import java.io.IOException; +import java.io.OutputStream; +import junit.framework.AssertionFailedError; +import junit.framework.Test; +import org.apache.tools.ant.BuildException; + +/** + * writes a single 0 byte to the given output stream in endTestSuite. + */ +public class ForkedVMWatcher implements JUnitResultFormatter { + + /** + * OutputStream to write to. + */ + private OutputStream out; + + /** + * Empty + */ + public ForkedVMWatcher() { + } + /** + * Empty + */ + public void startTestSuite(JUnitTest suite) { + } + /** + * Empty + */ + public void startTest(Test t) { + } + /** + * Empty + */ + public void endTest(Test test) { + } + /** + * Empty + */ + public void addFailure(Test test, Throwable t) { + } + /** + * Interface TestListener for JUnit > 3.4. + * + *
A Test failed. + */ + public void addFailure(Test test, AssertionFailedError t) { + addFailure(test, (Throwable) t); + } + /** + * Empty + */ + public void addError(Test test, Throwable t) { + } + /** + * Empty + */ + public void setSystemOutput(String out) { + } + /** + * Empty + */ + public void setSystemError(String err) { + } + + public void setOutput(OutputStream out) { + this.out = out; + } + + /** + * The whole testsuite ended. + */ + public void endTestSuite(JUnitTest suite) throws BuildException { + try { + out.write(0); + out.flush(); + } catch (IOException ioex) { + throw new BuildException("Unable to write output", ioex); + } finally { + if (out != System.out && out != System.err) { + try { + out.close(); + } catch (IOException e) { + // ignore + } + } + } + } +} 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 655cdb074..41fac1b75 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 @@ -683,19 +683,15 @@ public class JUnitTask extends Task { } // execute the test and get the return code - int exitValue = JUnitTestRunner.ERRORS; - boolean wasKilled = false; + TestResultHolder result = null; if (!test.getFork()) { - exitValue = executeInVM(test); + result = executeInVM(test); } else { ExecuteWatchdog watchdog = createWatchdog(); - exitValue = executeAsForked(test, watchdog, null); + result = 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()); + actOnTestResult(result, test, "Test " + test.getName()); } /** @@ -731,16 +727,10 @@ public class JUnitTask extends Task { 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"); + TestResultHolder result = + executeAsForked(test, watchdog, casesFile); + actOnTestResult(result, test, "Tests"); } catch(IOException e) { log(e.toString(), Project.MSG_ERR); throw new BuildException(e); @@ -758,18 +748,22 @@ public class JUnitTask extends Task { } /** - * Execute a testcase by forking a new JVM. The command will block until - * it finishes. To know if the process was destroyed or not, use the - * killedProcess() method of the watchdog class. + * Execute a testcase by forking a new JVM. The command will block + * until it finishes. To know if the process was destroyed or not + * or whether the forked Java VM exited abnormally, use the + * attributes of the returned holder object. * @param test the testcase to execute. * @param watchdog the watchdog in charge of cancelling the test if it * exceeds a certain amount of time. Can be null, in this case * the test could probably hang forever. + * @param ForkedVMState will hold information about the forked + * VM's sanity. * @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, - File casesFile) + private TestResultHolder executeAsForked(JUnitTest test, + ExecuteWatchdog watchdog, + File casesFile) throws BuildException { if (perm != null) { @@ -836,6 +830,12 @@ public class JUnitTask extends Task { } } + File vmWatcher = createTempPropertiesFile("junitvmwatcher"); + formatterArg.append("formatter="); + formatterArg.append(ForkedVMWatcher.class.getName()); + formatterArg.append(","); + formatterArg.append(vmWatcher); + cmd.createArgument().setValue(formatterArg.toString()); File propsFile = createTempPropertiesFile("junit"); cmd.createArgument().setValue("propsfile=" @@ -877,15 +877,20 @@ public class JUnitTask extends Task { execute.setEnvironment(environment); log(cmd.describeCommand(), Project.MSG_VERBOSE); - int retVal; + TestResultHolder result = new TestResultHolder(); try { - retVal = execute.execute(); + result.exitCode = execute.execute(); } catch (IOException e) { throw new BuildException("Process fork failed.", e, getLocation()); } finally { if (watchdog != null && watchdog.killedProcess()) { + result.timedOut = true; logTimeout(feArray, test); + } else if (vmWatcher.length() == 0) { + result.crashed = true; + logVmCrash(feArray, test); } + vmWatcher.delete(); if (!propsFile.delete()) { throw new BuildException("Could not delete temporary " @@ -893,7 +898,7 @@ public class JUnitTask extends Task { } } - return retVal; + return result; } /** @@ -1010,7 +1015,7 @@ public class JUnitTask extends Task { * @param arg one JUnitTest * @throws BuildException under unspecified circumstances */ - private int executeInVM(JUnitTest arg) throws BuildException { + private TestResultHolder executeInVM(JUnitTest arg) throws BuildException { JUnitTest test = (JUnitTest) arg.clone(); test.setProperties(getProject().getProperties()); if (dir != null) { @@ -1072,7 +1077,9 @@ public class JUnitTask extends Task { } runner.run(); - return runner.getRetCode(); + TestResultHolder result = new TestResultHolder(); + result.exitCode = runner.getRetCode(); + return result; } finally { if (sysProperties != null) { sysProperties.restoreSystem(); @@ -1211,6 +1218,28 @@ public class JUnitTask extends Task { */ private void logTimeout(FormatterElement[] feArray, JUnitTest test) { + logVmExit(feArray, test, "Timeout occurred"); + } + + /** + * Take care that some output is produced in report files if the + * forked machine exited before the test suite finished but the + * reason is not a timeout. + * + * @since Ant 1.7 + */ + private void logVmCrash(FormatterElement[] feArray, JUnitTest test) { + logVmExit(feArray, test, "forked Java VM exited abnormally"); + } + + /** + * Take care that some output is produced in report files if the + * forked machine existed before the test suite finished + * + * @since Ant 1.7 + */ + private void logVmExit(FormatterElement[] feArray, JUnitTest test, + String message) { createClassLoader(); test.setCounts(1, 0, 1); test.setProperties(getProject().getProperties()); @@ -1221,7 +1250,7 @@ public class JUnitTask extends Task { if (outFile != null && formatter != null) { try { OutputStream out = new FileOutputStream(outFile); - addTimeout(test, formatter, out); + addVmExit(test, formatter, out, message); } catch (IOException e) { // ignore } @@ -1230,31 +1259,31 @@ public class JUnitTask extends Task { if (summary) { SummaryJUnitResultFormatter f = new SummaryJUnitResultFormatter(); f.setWithOutAndErr("withoutanderr".equalsIgnoreCase(summaryValue)); - addTimeout(test, f, getDefaultOutput()); + addVmExit(test, f, getDefaultOutput(), message); } } /** - * Adds the actual timeout to the formatter. - * Only used from the logTimeout method. - * @since Ant 1.6 + * Adds the actual error message to the formatter. + * Only used from the logVmExit method. + * @since Ant 1.7 */ - private void addTimeout(JUnitTest test, JUnitResultFormatter formatter, - OutputStream out) { + private void addVmExit(JUnitTest test, JUnitResultFormatter formatter, + OutputStream out, final String message) { formatter.setOutput(out); formatter.startTestSuite(test); //the trick to integrating test output to the formatter, is to - //create a special test class that asserts a timout occurred, + //create a special test class that asserts an error //and tell the formatter that it raised. Test t = new Test() { public int countTestCases() { return 1; } public void run(TestResult r) { - throw new AssertionFailedError("Timeout occurred"); + throw new AssertionFailedError(message); } }; formatter.startTest(t); - formatter.addError(t, new AssertionFailedError("Timeout occurred")); + formatter.addError(t, new AssertionFailedError(message)); formatter.endTestSuite(test); } @@ -1445,22 +1474,25 @@ public class JUnitTask extends Task { * * @since Ant 1.6.2 */ - protected void actOnTestResult(int exitValue, boolean wasKilled, - JUnitTest test, String name) { + protected void actOnTestResult(TestResultHolder result,JUnitTest test, + String name) { // if there is an error/failure and that it should halt, stop // everything otherwise just log a statement + boolean fatal = result.timedOut || result.crashed; boolean errorOccurredHere = - exitValue == JUnitTestRunner.ERRORS || wasKilled; + result.exitCode == JUnitTestRunner.ERRORS || fatal; boolean failureOccurredHere = - exitValue != JUnitTestRunner.SUCCESS || wasKilled; + result.exitCode != JUnitTestRunner.SUCCESS || fatal; if (errorOccurredHere || failureOccurredHere) { if ((errorOccurredHere && test.getHaltonerror()) || (failureOccurredHere && test.getHaltonfailure())) { throw new BuildException(name + " failed" - + (wasKilled ? " (timeout)" : ""), getLocation()); + + (result.timedOut ? " (timeout)" : "") + + (result.crashed ? " (crashed)" : ""), getLocation()); } else { log(name + " FAILED" - + (wasKilled ? " (timeout)" : ""), Project.MSG_ERR); + + (result.timedOut ? " (timeout)" : "") + + (result.crashed ? " (crashed)" : ""), Project.MSG_ERR); if (errorOccurredHere && test.getErrorProperty() != null) { getProject().setNewProperty(test.getErrorProperty(), "true"); } @@ -1471,4 +1503,9 @@ public class JUnitTask extends Task { } } + private class TestResultHolder { + public int exitCode = JUnitTestRunner.ERRORS; + public boolean timedOut = false; + public boolean crashed = false; + } } 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 b674e8cbb..e999a605f 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 @@ -244,7 +244,7 @@ public class JUnitTestRunner implements TestListener { if (exception != null) { // had an exception in the constructor for (int i = 0; i < formatters.size(); i++) { ((TestListener) formatters.elementAt(i)).addError(null, - exception); + exception); } junitTest.setCounts(1, 0, 1); junitTest.setRunTime(0);