blob: 459bd3d5db6544de0fb09027b699bd22632162a1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
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;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.ExecuteWatchdog;
import org.apache.tools.ant.taskdefs.LogOutputStream;
import org.apache.tools.ant.taskdefs.PumpStreamHandler;
import org.apache.tools.ant.types.Assertions;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.CommandlineJava;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.Environment;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Permissions;
import org.apache.tools.ant.types.PropertySet;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.LoaderUtils;
import org.apache.tools.ant.util.SplitClassLoader;
/**
* Runs JUnit tests.
*
* <p> JUnit is a framework to create unit tests. It has been initially
* created by Erich Gamma and Kent Beck. JUnit can be found at <a
* href="http://www.junit.org">http://www.junit.org</a>.
*
* <p> <code>JUnitTask</code> can run a single specific
* <code>JUnitTest</code> using the <code>test</code> element.</p>
* For example, the following target <code><pre>
* &lt;target name="test-int-chars" depends="jar-test"&gt;
* &lt;echo message="testing international characters"/&gt;
* &lt;junit printsummary="no" haltonfailure="yes" fork="false"&gt;
* &lt;classpath refid="classpath"/&gt;
* &lt;formatter type="plain" usefile="false" /&gt;
* &lt;test name="org.apache.ecs.InternationalCharTest" /&gt;
* &lt;/junit&gt;
* &lt;/target&gt;
* </pre></code>
* <p>runs a single junit test
* (<code>org.apache.ecs.InternationalCharTest</code>) in the current
* VM using the path with id <code>classpath</code> as classpath and
* presents the results formatted using the standard
* <code>plain</code> formatter on the command line.</p>
*
* <p> This task can also run batches of tests. The
* <code>batchtest</code> element creates a <code>BatchTest</code>
* based on a fileset. This allows, for example, all classes found in
* directory to be run as testcases.</p>
*
* <p>For example,</p><code><pre>
* &lt;target name="run-tests" depends="dump-info,compile-tests" if="junit.present"&gt;
* &lt;junit printsummary="no" haltonfailure="yes" fork="${junit.fork}"&gt;
* &lt;jvmarg value="-classic"/&gt;
* &lt;classpath refid="tests-classpath"/&gt;
* &lt;sysproperty key="build.tests" value="${build.tests}"/&gt;
* &lt;formatter type="brief" usefile="false" /&gt;
* &lt;batchtest&gt;
* &lt;fileset dir="${tests.dir}"&gt;
* &lt;include name="**&#047;*Test*" /&gt;
* &lt;/fileset&gt;
* &lt;/batchtest&gt;
* &lt;/junit&gt;
* &lt;/target&gt;
* </pre></code>
* <p>this target finds any classes with a <code>test</code> directory
* anywhere in their path (under the top <code>${tests.dir}</code>, of
* course) and creates <code>JUnitTest</code>'s for each one.</p>
*
* <p> Of course, <code>&lt;junit&gt;</code> and
* <code>&lt;batch&gt;</code> elements can be combined for more
* complex tests. For an example, see the ant <code>build.xml</code>
* target <code>run-tests</code> (the second example is an edited
* version).</p>
*
* <p> To spawn a new Java VM to prevent interferences between
* different testcases, you need to enable <code>fork</code>. A
* number of attributes and elements allow you to set up how this JVM
* runs.
*
*
* @since Ant 1.2
*
* @see JUnitTest
* @see BatchTest
*/
public class JUnitTask extends Task {
private static final String LINE_SEP
= System.getProperty("line.separator");
private static final String CLASSPATH = "CLASSPATH";
private CommandlineJava commandline;
private final Vector<JUnitTest> tests = new Vector<JUnitTest>();
private final Vector<BatchTest> batchTests = new Vector<BatchTest>();
private final Vector<FormatterElement> formatters = new Vector<FormatterElement>();
private File dir = null;
private Integer timeout = null;
private boolean summary = false;
private boolean reloading = true;
private String summaryValue = "";
private JUnitTaskMirror.JUnitTestRunnerMirror runner = null;
private boolean newEnvironment = false;
private final Environment env = new Environment();
private boolean includeAntRuntime = true;
private Path antRuntimeClasses = null;
// Do we send output to System.out/.err in addition to the formatters?
private boolean showOutput = false;
// Do we send output to the formatters ?
private boolean outputToFormatters = true;
private boolean logFailedTests = true;
private File tmpDir;
private AntClassLoader classLoader = null;
private Permissions perm = null;
private ForkMode forkMode = new ForkMode("perTest");
private boolean splitJUnit = false;
private boolean enableTestListenerEvents = false;
private JUnitTaskMirror delegate;
private ClassLoader mirrorLoader;
/** A boolean on whether to get the forked path for ant classes */
private boolean forkedPathChecked = false;
/* set when a test fails/errs with haltonfailure/haltonerror and >1 thread to stop other threads */
private volatile BuildException caughtBuildException = null;
// Attributes for basetest
private boolean haltOnError = false;
private boolean haltOnFail = false;
private boolean filterTrace = true;
private boolean fork = false;
private int threads = 1;
private String failureProperty;
private String errorProperty;
private static final int STRING_BUFFER_SIZE = 128;
/**
* @since Ant 1.7
*/
public static final String TESTLISTENER_PREFIX =
"junit.framework.TestListener: ";
/**
* Name of magic property that enables test listener events.
*/
public static final String ENABLE_TESTLISTENER_EVENTS =
"ant.junit.enabletestlistenerevents";
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
/**
* 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(final boolean value) {
reloading = value;
}
/**
* If true, smartly filter the stack frames of
* JUnit errors and failures before reporting them.
*
* <p>This property is applied on all BatchTest (batchtest) and
* JUnitTest (test) however it can possibly be overridden by their
* own properties.</p>
* @param value <tt>false</tt> if it should not filter, otherwise
* <tt>true<tt>
*
* @since Ant 1.5
*/
public void setFiltertrace(final boolean value) {
this.filterTrace = value;
}
/**
* If true, stop the build process when there is an error in a test.
* This property is applied on all BatchTest (batchtest) and JUnitTest
* (test) however it can possibly be overridden by their own
* properties.
* @param value <tt>true</tt> if it should halt, otherwise
* <tt>false</tt>
*
* @since Ant 1.2
*/
public void setHaltonerror(final boolean value) {
this.haltOnError = value;
}
/**
* Property to set to "true" if there is a error in a test.
*
* <p>This property is applied on all BatchTest (batchtest) and
* JUnitTest (test), however, it can possibly be overridden by
* their own properties.</p>
* @param propertyName the name of the property to set in the
* event of an error.
*
* @since Ant 1.4
*/
public void setErrorProperty(final String propertyName) {
this.errorProperty = propertyName;
}
/**
* If true, stop the build process if a test fails
* (errors are considered failures as well).
* This property is applied on all BatchTest (batchtest) and
* JUnitTest (test) however it can possibly be overridden by their
* own properties.
* @param value <tt>true</tt> if it should halt, otherwise
* <tt>false</tt>
*
* @since Ant 1.2
*/
public void setHaltonfailure(final boolean value) {
this.haltOnFail = value;
}
/**
* Property to set to "true" if there is a failure in a test.
*
* <p>This property is applied on all BatchTest (batchtest) and
* JUnitTest (test), however, it can possibly be overridden by
* their own properties.</p>
* @param propertyName the name of the property to set in the
* event of an failure.
*
* @since Ant 1.4
*/
public void setFailureProperty(final String propertyName) {
this.failureProperty = propertyName;
}
/**
* If true, JVM should be forked for each test.
*
* <p>It avoids interference between testcases and possibly avoids
* hanging the build. this property is applied on all BatchTest
* (batchtest) and JUnitTest (test) however it can possibly be
* overridden by their own properties.</p>
* @param value <tt>true</tt> if a JVM should be forked, otherwise
* <tt>false</tt>
* @see #setTimeout
*
* @since Ant 1.2
*/
public void setFork(final boolean value) {
this.fork = value;
}
/**
* Set the behavior when {@link #setFork fork} fork has been enabled.
*
* <p>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
* &lt;batchtest&gt; in the same Java VM.</p>
*
* <p>This attribute will be ignored if tests run in the same VM
* as Ant.</p>
*
* <p>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 multiple VMs.</p>
* @param mode the mode to use.
* @since Ant 1.6.2
*/
public void setForkMode(final ForkMode mode) {
this.forkMode = mode;
}
/**
* Set the number of test threads to be used for parallel test
* execution. The default is 1, which is the same behavior as
* before parallel test execution was possible.
*
* <p>This attribute will be ignored if tests run in the same VM
* as Ant.</p>
*
* @since Ant 1.9.4
*/
public void setThreads(final int threads) {
if (threads >= 0) {
this.threads = threads;
}
}
/**
* If true, print one-line statistics for each test, or "withOutAndErr"
* to also show standard output and error.
*
* Can take the values on, off, and withOutAndErr.
* @param value <tt>true</tt> to print a summary,
* <tt>withOutAndErr</tt> to include the test&apos;s output as
* well, <tt>false</tt> otherwise.
* @see SummaryJUnitResultFormatter
*
* @since Ant 1.2
*/
public void setPrintsummary(final SummaryAttribute value) {
summaryValue = value.getValue();
summary = value.asBoolean();
}
/**
* Print summary enumeration values.
*/
public static class SummaryAttribute extends EnumeratedAttribute {
/**
* list the possible values
* @return array of allowed values
*/
@Override
public String[] getValues() {
return new String[] {"true", "yes", "false", "no",
"on", "off", "withOutAndErr"};
}
/**
* gives the boolean equivalent of the authorized values
* @return boolean equivalent of the value
*/
public boolean asBoolean() {
final String v = getValue();
return "true".equals(v)
|| "on".equals(v)
|| "yes".equals(v)
|| "withOutAndErr".equals(v);
}
}
/**
* Set the timeout value (in milliseconds).
*
* <p>If the test is running for more than this value, the test
* will be canceled. (works only when in 'fork' mode).</p>
* @param value the maximum time (in milliseconds) allowed before
* declaring the test as 'timed-out'
* @see #setFork(boolean)
*
* @since Ant 1.2
*/
public void setTimeout(final Integer value) {
timeout = value;
}
/**
* Set the maximum memory to be used by all forked JVMs.
* @param max the value as defined by <tt>-mx</tt> or <tt>-Xmx</tt>
* in the java command line options.
*
* @since Ant 1.2
*/
public void setMaxmemory(final String max) {
getCommandline().setMaxmemory(max);
}
/**
* The command used to invoke the Java Virtual Machine,
* default is 'java'. The command is resolved by
* java.lang.Runtime.exec(). Ignored if fork is disabled.
*
* @param value the new VM to use instead of <tt>java</tt>
* @see #setFork(boolean)
*
* @since Ant 1.2
*/
public void setJvm(final String value) {
getCommandline().setVm(value);
}
/**
* Adds a JVM argument; ignored if not forking.
*
* @return create a new JVM argument so that any argument can be
* passed to the JVM.
* @see #setFork(boolean)
*
* @since Ant 1.2
*/
public Commandline.Argument createJvmarg() {
return getCommandline().createVmArgument();
}
/**
* The directory to invoke the VM in. Ignored if no JVM is forked.
* @param dir the directory to invoke the JVM from.
* @see #setFork(boolean)
*
* @since Ant 1.2
*/
public void setDir(final File dir) {
this.dir = dir;
}
/**
* Adds a system property that tests can access.
* This might be useful to transfer Ant properties to the
* testcases when JVM forking is not enabled.
*
* @since Ant 1.3
* @deprecated since ant 1.6
* @param sysp environment variable to add
*/
@Deprecated
public void addSysproperty(final Environment.Variable sysp) {
getCommandline().addSysproperty(sysp);
}
/**
* Adds a system property that tests can access.
* This might be useful to transfer Ant properties to the
* testcases when JVM forking is not enabled.
* @param sysp new environment variable to add
* @since Ant 1.6
*/
public void addConfiguredSysproperty(final Environment.Variable sysp) {
// get a build exception if there is a missing key or value
// see bugzilla report 21684
final String testString = sysp.getContent();
getProject().log("sysproperty added : " + testString, Project.MSG_DEBUG);
getCommandline().addSysproperty(sysp);
}
/**
* Adds a set of properties that will be used as system properties
* that tests can access.
*
* This might be useful to transfer Ant properties to the
* testcases when JVM forking is not enabled.
*
* @param sysp set of properties to be added
* @since Ant 1.6
*/
public void addSyspropertyset(final PropertySet sysp) {
getCommandline().addSyspropertyset(sysp);
}
/**
* Adds path to classpath used for tests.
*
* @return reference to the classpath in the embedded java command line
* @since Ant 1.2
*/
public Path createClasspath() {
return getCommandline().createClasspath(getProject()).createPath();
}
/**
* Adds a path to the bootclasspath.
* @return reference to the bootclasspath in the embedded java command line
* @since Ant 1.6
*/
public Path createBootclasspath() {
return getCommandline().createBootclasspath(getProject()).createPath();
}
/**
* Adds an environment variable; used when forking.
*
* <p>Will be ignored if we are not forking a new VM.</p>
* @param var environment variable to be added
* @since Ant 1.5
*/
public void addEnv(final Environment.Variable var) {
env.addVariable(var);
}
/**
* If true, use a new environment when forked.
*
* <p>Will be ignored if we are not forking a new VM.</p>
*
* @param newenv boolean indicating if setting a new environment is wished
* @since Ant 1.5
*/
public void setNewenvironment(final boolean newenv) {
newEnvironment = newenv;
}
/**
* Preset the attributes of the test
* before configuration in the build
* script.
* This allows attributes in the <junit> task
* be be defaults for the tests, but allows
* individual tests to override the defaults.
*/
private void preConfigure(final BaseTest test) {
test.setFiltertrace(filterTrace);
test.setHaltonerror(haltOnError);
if (errorProperty != null) {
test.setErrorProperty(errorProperty);
}
test.setHaltonfailure(haltOnFail);
if (failureProperty != null) {
test.setFailureProperty(failureProperty);
}
test.setFork(fork);
}
/**
* Add a new single testcase.
* @param test a new single testcase
* @see JUnitTest
*
* @since Ant 1.2
*/
public void addTest(final JUnitTest test) {
tests.addElement(test);
preConfigure(test);
}
/**
* Adds a set of tests based on pattern matching.
*
* @return a new instance of a batch test.
* @see BatchTest
*
* @since Ant 1.2
*/
public BatchTest createBatchTest() {
final BatchTest test = new BatchTest(getProject());
batchTests.addElement(test);
preConfigure(test);
return test;
}
/**
* Add a new formatter to all tests of this task.
*
* @param fe formatter element
* @since Ant 1.2
*/
public void addFormatter(final FormatterElement fe) {
formatters.addElement(fe);
}
/**
* If true, include ant.jar, optional.jar and junit.jar in the forked VM.
*
* @param b include ant run time yes or no
* @since Ant 1.5
*/
public void setIncludeantruntime(final boolean b) {
includeAntRuntime = b;
}
/**
* If true, send any output generated by tests to Ant's logging system
* as well as to the formatters.
* By default only the formatters receive the output.
*
* <p>Output will always be passed to the formatters and not by
* shown by default. This option should for example be set for
* tests that are interactive and prompt the user to do
* something.</p>
*
* @param showOutput if true, send output to Ant's logging system too
* @since Ant 1.5
*/
public void setShowOutput(final boolean showOutput) {
this.showOutput = showOutput;
}
/**
* If true, send any output generated by tests to the formatters.
*
* @param outputToFormatters if true, send output to formatters (Default
* is true).
* @since Ant 1.7.0
*/
public void setOutputToFormatters(final boolean outputToFormatters) {
this.outputToFormatters = outputToFormatters;
}
/**
* If true, write a single "FAILED" line for failed tests to Ant's
* log system.
*
* @since Ant 1.8.0
*/
public void setLogFailedTests(final boolean logFailedTests) {
this.logFailedTests = logFailedTests;
}
/**
* Assertions to enable in this program (if fork=true)
* @since Ant 1.6
* @param asserts assertion set
*/
public void addAssertions(final Assertions asserts) {
if (getCommandline().getAssertions() != null) {
throw new BuildException("Only one assertion declaration is allowed");
}
getCommandline().setAssertions(asserts);
}
/**
* Sets the permissions for the application run inside the same JVM.
* @since Ant 1.6
* @return .
*/
public Permissions createPermissions() {
if (perm == null) {
perm = new Permissions();
}
return perm;
}
/**
* If set, system properties will be copied to the cloned VM - as
* well as the bootclasspath unless you have explicitly specified
* a bootclasspath.
*
* <p>Doesn't have any effect unless fork is true.</p>
* @param cloneVm a <code>boolean</code> value.
* @since Ant 1.7
*/
public void setCloneVm(final boolean cloneVm) {
getCommandline().setCloneVm(cloneVm);
}
/**
* Creates a new JUnitRunner and enables fork of a new Java VM.
*
* @throws Exception under ??? circumstances
* @since Ant 1.2
*/
public JUnitTask() throws Exception {
}
/**
* Where Ant should place temporary files.
*
* @param tmpDir location where temporary files should go to
* @since Ant 1.6
*/
public void setTempdir(final File tmpDir) {
if (tmpDir != null) {
if (!tmpDir.exists() || !tmpDir.isDirectory()) {
throw new BuildException(tmpDir.toString()
+ " is not a valid temp directory");
}
}
this.tmpDir = tmpDir;
}
/**
* Whether test listener events shall be generated.
*
* <p>Defaults to false.</p>
*
* <p>This value will be overridden by the magic property
* ant.junit.enabletestlistenerevents if it has been set.</p>
*
* @since Ant 1.8.2
*/
public void setEnableTestListenerEvents(final boolean b) {
enableTestListenerEvents = b;
}
/**
* Whether test listener events shall be generated.
* @since Ant 1.8.2
*/
public boolean getEnableTestListenerEvents() {
final String e = getProject().getProperty(ENABLE_TESTLISTENER_EVENTS);
if (e != null) {
return Project.toBoolean(e);
}
return enableTestListenerEvents;
}
/**
* Adds the jars or directories containing Ant, this task and
* JUnit to the classpath - this should make the forked JVM work
* without having to specify them directly.
*
* @since Ant 1.4
*/
@Override
public void init() {
antRuntimeClasses = new Path(getProject());
splitJUnit = !addClasspathResource("/junit/framework/TestCase.class");
addClasspathEntry("/org/apache/tools/ant/launch/AntMain.class");
addClasspathEntry("/org/apache/tools/ant/Task.class");
addClasspathEntry("/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunner.class");
addClasspathEntry("/org/apache/tools/ant/taskdefs/optional/junit/JUnit4TestMethodAdapter.class");
}
private static JUnitTaskMirror createMirror(final JUnitTask task, final ClassLoader loader) {
try {
loader.loadClass("junit.framework.Test"); // sanity check
} catch (final ClassNotFoundException e) {
throw new BuildException(
"The <classpath> for <junit> must include junit.jar "
+ "if not in Ant's own classpath",
e, task.getLocation());
}
try {
final Class c = loader.loadClass(JUnitTaskMirror.class.getName() + "Impl");
if (c.getClassLoader() != loader) {
throw new BuildException("Overdelegating loader", task.getLocation());
}
final Constructor cons = c.getConstructor(new Class[] {JUnitTask.class});
return (JUnitTaskMirror) cons.newInstance(new Object[] {task});
} catch (final Exception e) {
throw new BuildException(e, task.getLocation());
}
}
/**
* Sets up the delegate that will actually run the tests.
*
* <p>Will be invoked implicitly once the delegate is needed.</p>
*
* @since Ant 1.7.1
*/
protected void setupJUnitDelegate() {
final ClassLoader myLoader = JUnitTask.class.getClassLoader();
if (splitJUnit) {
final Path path = new Path(getProject());
path.add(antRuntimeClasses);
final Path extra = getCommandline().getClasspath();
if (extra != null) {
path.add(extra);
}
mirrorLoader = (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new SplitClassLoader(myLoader, path, getProject(),
new String[] {
"BriefJUnitResultFormatter",
"JUnit4TestMethodAdapter",
"JUnitResultFormatter",
"JUnitTaskMirrorImpl",
"JUnitTestRunner",
"JUnitVersionHelper",
"OutErrSummaryJUnitResultFormatter",
"PlainJUnitResultFormatter",
"SummaryJUnitResultFormatter",
"TearDownOnVmCrash",
"XMLJUnitResultFormatter",
"IgnoredTestListener",
"IgnoredTestResult",
"CustomJUnit4TestAdapterCache",
"TestListenerWrapper"
});
}
});
} else {
mirrorLoader = myLoader;
}
delegate = createMirror(this, mirrorLoader);
}
/**
* Runs the testcase.
*
* @throws BuildException in case of test failures or errors
* @since Ant 1.2
*/
@Override
public void execute() throws BuildException {
checkMethodLists();
setupJUnitDelegate();
final List<List> testLists = new ArrayList<List>();
/* parallel test execution is only supported for multi-process execution */
final int threads = ((!fork) || (forkMode.getValue().equals(ForkMode.ONCE))
? 1
: this.threads);
final boolean forkPerTest = forkMode.getValue().equals(ForkMode.PER_TEST);
if (forkPerTest || forkMode.getValue().equals(ForkMode.ONCE)) {
testLists.addAll(executeOrQueue(getIndividualTests(),
forkPerTest));
} else { /* forkMode.getValue().equals(ForkMode.PER_BATCH) */
final int count = batchTests.size();
for (int i = 0; i < count; i++) {
final BatchTest batchtest = batchTests.elementAt(i);
testLists.addAll(executeOrQueue(batchtest.elements(), false));
}
testLists.addAll(executeOrQueue(tests.elements(), forkPerTest));
}
try {
/* prior to parallel the code in 'oneJunitThread' used to be here. */
runTestsInThreads(testLists, threads);
} finally {
cleanup();
}
}
/*
* When the list of tests is established, an array of threads is created to pick the
* tests off the list one at a time and execute them until the list is empty. Tests are
* not assigned to threads until the thread is available.
*
* This class is the runnable thread subroutine that takes care of passing the shared
* list iterator and the handle back to the main class to the test execution subroutine
* code 'runTestsInThreads'. One object is created for each thread and each one gets
* a unique thread id that can be useful for tracing test starts and stops.
*
* Because the threads are picking tests off the same list, it is the list *iterator*
* that must be shared, not the list itself - and the iterator must have a thread-safe
* ability to pop the list - hence the synchronized 'getNextTest'.
*/
private class JunitTestThread implements Runnable {
JunitTestThread(final JUnitTask master, final Iterator<List> iterator, final int id) {
this.masterTask = master;
this.iterator = iterator;
this.id = id;
}
public void run() {
try {
masterTask.oneJunitThread(iterator, id);
} catch (final BuildException b) {
/* saved to rethrow in main thread to be like single-threaded case */
caughtBuildException = b;
}
}
private final JUnitTask masterTask;
private final Iterator<List> iterator;
private final int id;
}
/*
* Because the threads are picking tests off the same list, it is the list *iterator*
* that must be shared, not the list itself - and the iterator must have a thread-safe
* ability to pop the list - hence the synchronized 'getNextTest'. We can't have two
* threads get the same test, or two threads simultaneously pop the list so that a test
* gets skipped!
*/
private List getNextTest(final Iterator<List> iter) {
synchronized(iter) {
if (iter.hasNext()) {
return iter.next();
}
return null;
}
}
/*
* This code loops keeps executing the next test or test bunch (depending on fork mode)
* on the list of test cases until none are left. Basically this body of code used to
* be in the execute routine above; now, several copies (one for each test thread) execute
* simultaneously. The while loop was modified to call the new thread-safe atomic list
* popping subroutine and the logging messages were added.
*
* If one thread aborts due to a BuildException (haltOnError, haltOnFailure, or any other
* fatal reason, no new tests/batches will be started but the running threads will be
* permitted to complete. Additional tests may start in already-running batch-test threads.
*/
private void oneJunitThread(final Iterator<List> iter, final int threadId) {
List l;
log("Starting test thread " + threadId, Project.MSG_VERBOSE);
while ((caughtBuildException == null) && ((l = getNextTest(iter)) != null)) {
log("Running test " + l.get(0).toString() + "(" + l.size() + ") in thread " + threadId, Project.MSG_VERBOSE);
if (l.size() == 1) {
execute((JUnitTest) l.get(0), threadId);
} else {
execute(l, threadId);
}
}
log("Ending test thread " + threadId, Project.MSG_VERBOSE);
}
private void runTestsInThreads(final List<List> testList, final int numThreads) {
Iterator<List> iter = testList.iterator();
if (numThreads == 1) {
/* with just one thread just run the test - don't create any threads */
oneJunitThread(iter, 0);
} else {
final Thread[] threads = new Thread[numThreads];
int i;
boolean exceptionOccurred;
/* Need to split apart tests, which are still grouped in batches */
/* is there a simpler Java mechanism to do this? */
/* I assume we don't want to do this with "per batch" forking. */
List<List> newlist = new ArrayList<List>();
if (forkMode.getValue().equals(ForkMode.PER_TEST)) {
final Iterator<List> i1 = testList.iterator();
while (i1.hasNext()) {
final List l = i1.next();
if (l.size() == 1) {
newlist.add(l);
} else {
final Iterator i2 = l.iterator();
while (i2.hasNext()) {
final List tmpSingleton = new ArrayList();
tmpSingleton.add(i2.next());
newlist.add(tmpSingleton);
}
}
}
} else {
newlist = testList;
}
iter = newlist.iterator();
/* create 1 thread using the passthrough class, and let each thread start */
for (i = 0; i < numThreads; i++) {
threads[i] = new Thread(new JunitTestThread(this, iter, i+1));
threads[i].start();
}
/* wait for all of the threads to complete. Not sure if the exception can actually occur in this use case. */
do {
exceptionOccurred = false;
try {
for (i = 0; i < numThreads; i++) {
threads[i].join();
}
} catch (final InterruptedException e) {
exceptionOccurred = true;
}
} while (exceptionOccurred);
/* an exception occurred in one of the threads - usually a haltOnError/Failure.
throw the exception again so it behaves like the single-thread case */
if (caughtBuildException != null) {
throw new BuildException(caughtBuildException);
}
/* all threads are completed - that's all there is to do. */
/* control will flow back to the test cleanup call and then execute is done. */
}
}
/**
* Run the tests.
* @param arg one JUnitTest
* @param thread Identifies which thread is test running in (0 for single-threaded runs)
* @throws BuildException in case of test failures or errors
*/
protected void execute(final JUnitTest arg, final int thread) throws BuildException {
validateTestName(arg.getName());
final JUnitTest test = (JUnitTest) arg.clone();
test.setThread(thread);
// set the default values if not specified
//@todo should be moved to the test class instead.
if (test.getTodir() == null) {
test.setTodir(getProject().resolveFile("."));
}
if (test.getOutfile() == null) {
test.setOutfile("TEST-" + test.getName());
}
// execute the test and get the return code
TestResultHolder result = null;
if (!test.getFork()) {
result = executeInVM(test);
} else {
final ExecuteWatchdog watchdog = createWatchdog();
result = executeAsForked(test, watchdog, null);
// null watchdog means no timeout, you'd better not check with null
}
actOnTestResult(result, test, "Test " + test.getName());
}
/**
* Run the tests.
* @param arg one JUnitTest
* @throws BuildException in case of test failures or errors
*/
protected void execute(final JUnitTest arg) throws BuildException {
execute(arg, 0);
}
/**
* Throws a <code>BuildException</code> if the given test name is invalid.
* Validity is defined as not <code>null</code>, not empty, and not the
* string &quot;null&quot;.
* @param testName the test name to be validated
* @throws BuildException if <code>testName</code> is not a valid test name
*/
private void validateTestName(final String testName) throws BuildException {
if (testName == null || testName.length() == 0
|| testName.equals("null")) {
throw new BuildException("test name must be specified");
}
}
/**
* Execute a list of tests in a single forked Java VM.
* @param testList the list of tests to execute.
* @param thread Identifies which thread is test running in (0 for single-threaded runs)
* @throws BuildException on error.
*/
protected void execute(final List testList, final int thread) throws BuildException {
JUnitTest test = null;
// Create a temporary file to pass the test cases to run to
// the runner (one test case per line)
final File casesFile = createTempPropertiesFile("junittestcases");
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(casesFile));
log("Creating casesfile '" + casesFile.getAbsolutePath()
+ "' with content: ", Project.MSG_VERBOSE);
final PrintStream logWriter =
new PrintStream(new LogOutputStream(this, Project.MSG_VERBOSE));
final Iterator iter = testList.iterator();
while (iter.hasNext()) {
test = (JUnitTest) iter.next();
test.setThread(thread);
printDual(writer, logWriter, test.getName());
if (test.getMethods() != null) {
printDual(writer, logWriter, ":" + test.getMethodsString().replace(',', '+'));
}
if (test.getTodir() == null) {
printDual(writer, logWriter,
"," + getProject().resolveFile("."));
} else {
printDual(writer, logWriter, "," + test.getTodir());
}
if (test.getOutfile() == null) {
printlnDual(writer, logWriter,
"," + "TEST-" + test.getName());
} else {
printlnDual(writer, logWriter, "," + test.getOutfile());
}
}
writer.flush();
writer.close();
writer = null;
// execute the test and get the return code
final ExecuteWatchdog watchdog = createWatchdog();
final TestResultHolder result =
executeAsForked(test, watchdog, casesFile);
actOnTestResult(result, test, "Tests");
} catch (final IOException e) {
log(e.toString(), Project.MSG_ERR);
throw new BuildException(e);
} finally {
FileUtils.close(writer);
try {
FILE_UTILS.tryHardToDelete(casesFile);
} catch (final Exception e) {
log(e.toString(), Project.MSG_ERR);
}
}
}
/**
* Execute a list of tests in a single forked Java VM.
* @param testList the list of tests to execute.
* @throws BuildException on error.
*/
protected void execute(final List testList) throws BuildException {
execute(testList, 0);
}
/**
* 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 <tt>null</tt>, in this case
* the test could probably hang forever.
* @param casesFile list of test cases to execute. Can be <tt>null</tt>,
* in this case only one test is executed.
* @return the test results from the JVM itself.
* @throws BuildException in case of error creating a temporary property file,
* or if the junit process can not be forked
*/
private TestResultHolder executeAsForked(JUnitTest test,
final ExecuteWatchdog watchdog,
final File casesFile)
throws BuildException {
if (perm != null) {
log("Permissions ignored when running in forked mode!",
Project.MSG_WARN);
}
CommandlineJava cmd;
try {
cmd = (CommandlineJava) (getCommandline().clone());
} catch (final CloneNotSupportedException e) {
throw new BuildException("This shouldn't happen", e, getLocation());
}
if (casesFile == null) {
cmd.createArgument().setValue(test.getName());
if (test.getMethods() != null) {
cmd.createArgument().setValue(Constants.METHOD_NAMES + test.getMethodsString());
}
} else {
log("Running multiple tests in the same VM", Project.MSG_VERBOSE);
cmd.createArgument().setValue(Constants.TESTSFILE + casesFile);
}
cmd.createArgument().setValue(Constants.SKIP_NON_TESTS + String.valueOf(test.isSkipNonTests()));
cmd.createArgument().setValue(Constants.FILTERTRACE + test.getFiltertrace());
cmd.createArgument().setValue(Constants.HALT_ON_ERROR + test.getHaltonerror());
cmd.createArgument().setValue(Constants.HALT_ON_FAILURE
+ test.getHaltonfailure());
checkIncludeAntRuntime(cmd);
checkIncludeSummary(cmd);
cmd.createArgument().setValue(Constants.SHOWOUTPUT
+ String.valueOf(showOutput));
cmd.createArgument().setValue(Constants.OUTPUT_TO_FORMATTERS
+ String.valueOf(outputToFormatters));
cmd.createArgument().setValue(Constants.LOG_FAILED_TESTS
+ String.valueOf(logFailedTests));
cmd.createArgument().setValue(Constants.THREADID
+ String.valueOf(test.getThread()));
// #31885
cmd.createArgument().setValue(Constants.LOGTESTLISTENEREVENTS
+ String.valueOf(getEnableTestListenerEvents()));
StringBuffer formatterArg = new StringBuffer(STRING_BUFFER_SIZE);
final FormatterElement[] feArray = mergeFormatters(test);
for (int i = 0; i < feArray.length; i++) {
final FormatterElement fe = feArray[i];
if (fe.shouldUse(this)) {
formatterArg.append(Constants.FORMATTER);
formatterArg.append(fe.getClassname());
final File outFile = getOutput(fe, test);
if (outFile != null) {
formatterArg.append(",");
formatterArg.append(outFile);
}
cmd.createArgument().setValue(formatterArg.toString());
formatterArg = new StringBuffer();
}
}
final File vmWatcher = createTempPropertiesFile("junitvmwatcher");
cmd.createArgument().setValue(Constants.CRASHFILE
+ vmWatcher.getAbsolutePath());
final File propsFile = createTempPropertiesFile("junit");
cmd.createArgument().setValue(Constants.PROPSFILE
+ propsFile.getAbsolutePath());
final Hashtable p = getProject().getProperties();
final Properties props = new Properties();
for (final Enumeration e = p.keys(); e.hasMoreElements();) {
final Object key = e.nextElement();
props.put(key, p.get(key));
}
try {
final FileOutputStream outstream = new FileOutputStream(propsFile);
props.store(outstream, "Ant JUnitTask generated properties file");
outstream.close();
} catch (final java.io.IOException e) {
FILE_UTILS.tryHardToDelete(propsFile);
throw new BuildException("Error creating temporary properties "
+ "file.", e, getLocation());
}
final Execute execute = new Execute(
new JUnitLogStreamHandler(
this,
Project.MSG_INFO,
Project.MSG_WARN),
watchdog);
execute.setCommandline(cmd.getCommandline());
execute.setAntRun(getProject());
if (dir != null) {
execute.setWorkingDirectory(dir);
}
final String[] environment = env.getVariables();
if (environment != null) {
for (int i = 0; i < environment.length; i++) {
log("Setting environment variable: " + environment[i],
Project.MSG_VERBOSE);
}
}
execute.setNewenvironment(newEnvironment);
execute.setEnvironment(environment);
log(cmd.describeCommand(), Project.MSG_VERBOSE);
checkForkedPath(cmd);
final TestResultHolder result = new TestResultHolder();
try {
result.exitCode = execute.execute();
} catch (final IOException e) {
throw new BuildException("Process fork failed.", e, getLocation());
} finally {
String vmCrashString = "unknown";
BufferedReader br = null;
try {
if (vmWatcher.exists()) {
br = new BufferedReader(new FileReader(vmWatcher));
vmCrashString = br.readLine();
} else {
vmCrashString = "Monitor file ("
+ vmWatcher.getAbsolutePath()
+ ") missing, location not writable,"
+ " testcase not started or mixing ant versions?";
}
} catch (final Exception e) {
e.printStackTrace();
// ignored.
} finally {
FileUtils.close(br);
if (vmWatcher.exists()) {
FILE_UTILS.tryHardToDelete(vmWatcher);
}
}
final boolean crash = (watchdog != null && watchdog.killedProcess())
|| !Constants.TERMINATED_SUCCESSFULLY.equals(vmCrashString);
if (casesFile != null && crash) {
test = createDummyTestForBatchTest(test);
}
if (watchdog != null && watchdog.killedProcess()) {
result.timedOut = true;
logTimeout(feArray, test, vmCrashString);
} else if (crash) {
result.crashed = true;
logVmCrash(feArray, test, vmCrashString);
}
if (!FILE_UTILS.tryHardToDelete(propsFile)) {
throw new BuildException("Could not delete temporary "
+ "properties file '"
+ propsFile.getAbsolutePath() + "'.");
}
}
return result;
}
/**
* Adding ant runtime.
* @param cmd command to run
*/
private void checkIncludeAntRuntime(final CommandlineJava cmd) {
if (includeAntRuntime) {
final Map/*<String, String>*/ env = Execute.getEnvironmentVariables();
final String cp = (String) env.get(CLASSPATH);
if (cp != null) {
cmd.createClasspath(getProject()).createPath()
.append(new Path(getProject(), cp));
}
log("Implicitly adding " + antRuntimeClasses + " to CLASSPATH",
Project.MSG_VERBOSE);
cmd.createClasspath(getProject()).createPath()
.append(antRuntimeClasses);
}
}
/**
* check for the parameter being "withoutanderr" in a locale-independent way.
* @param summaryOption the summary option -can be null
* @return true if the run should be withoutput and error
*/
private boolean equalsWithOutAndErr(final String summaryOption) {
return "withoutanderr".equalsIgnoreCase(summaryOption);
}
private void checkIncludeSummary(final CommandlineJava cmd) {
if (summary) {
String prefix = "";
if (equalsWithOutAndErr(summaryValue)) {
prefix = "OutErr";
}
cmd.createArgument()
.setValue(Constants.FORMATTER
+ "org.apache.tools.ant.taskdefs.optional.junit."
+ prefix + "SummaryJUnitResultFormatter");
}
}
/**
* Check the path for multiple different versions of
* ant.
* @param cmd command to execute
*/
private void checkForkedPath(final CommandlineJava cmd) {
if (forkedPathChecked) {
return;
}
forkedPathChecked = true;
if (!cmd.haveClasspath()) {
return;
}
AntClassLoader loader = null;
try {
loader =
AntClassLoader.newAntClassLoader(null, getProject(),
cmd.createClasspath(getProject()),
true);
final String projectResourceName =
LoaderUtils.classNameToResource(Project.class.getName());
URL previous = null;
try {
for (final Enumeration e = loader.getResources(projectResourceName);
e.hasMoreElements();) {
final URL current = (URL) e.nextElement();
if (previous != null && !urlEquals(current, previous)) {
log("WARNING: multiple versions of ant detected "
+ "in path for junit "
+ LINE_SEP + " " + previous
+ LINE_SEP + " and " + current,
Project.MSG_WARN);
return;
}
previous = current;
}
} catch (final Exception ex) {
// Ignore exception
}
} finally {
if (loader != null) {
loader.cleanup();
}
}
}
/**
* Compares URLs for equality but takes case-sensitivity into
* account when comparing file URLs and ignores the jar specific
* part of the URL if present.
*/
private static boolean urlEquals(final URL u1, final URL u2) {
final String url1 = maybeStripJarAndClass(u1);
final String url2 = maybeStripJarAndClass(u2);
if (url1.startsWith("file:") && url2.startsWith("file:")) {
return new File(FILE_UTILS.fromURI(url1))
.equals(new File(FILE_UTILS.fromURI(url2)));
}
return url1.equals(url2);
}
private static String maybeStripJarAndClass(final URL u) {
String s = u.toString();
if (s.startsWith("jar:")) {
final int pling = s.indexOf('!');
s = s.substring(4, pling == -1 ? s.length() : pling);
}
return s;
}
/**
* Create a temporary file to pass the properties to a new process.
* Will auto-delete on (graceful) exit.
* The file will be in the project basedir unless tmpDir declares
* something else.
* @param prefix
* @return created file
*/
private File createTempPropertiesFile(final String prefix) {
final File propsFile =
FILE_UTILS.createTempFile(prefix, ".properties",
tmpDir != null ? tmpDir : getProject().getBaseDir(), true, true);
return propsFile;
}
/**
* Pass output sent to System.out to the TestRunner so it can
* collect it for the formatters.
*
* @param output output coming from System.out
* @since Ant 1.5
*/
@Override
protected void handleOutput(final String output) {
if (output.startsWith(TESTLISTENER_PREFIX)) {
log(output, Project.MSG_VERBOSE);
} else if (runner != null) {
if (outputToFormatters) {
runner.handleOutput(output);
}
if (showOutput) {
super.handleOutput(output);
}
} else {
super.handleOutput(output);
}
}
/**
* Handle an input request by this task.
* @see Task#handleInput(byte[], int, int)
* This implementation delegates to a runner if it
* present.
* @param buffer the buffer into which data is to be read.
* @param offset the offset into the buffer at which data is stored.
* @param length the amount of data to read.
*
* @return the number of bytes read.
* @exception IOException if the data cannot be read.
*
* @since Ant 1.6
*/
@Override
protected int handleInput(final byte[] buffer, final int offset, final int length)
throws IOException {
if (runner != null) {
return runner.handleInput(buffer, offset, length);
} else {
return super.handleInput(buffer, offset, length);
}
}
/**
* Pass output sent to System.out to the TestRunner so it can
* collect ot for the formatters.
*
* @param output output coming from System.out
* @since Ant 1.5.2
*/
@Override
protected void handleFlush(final String output) {
if (runner != null) {
runner.handleFlush(output);
if (showOutput) {
super.handleFlush(output);
}
} else {
super.handleFlush(output);
}
}
/**
* Pass output sent to System.err to the TestRunner so it can
* collect it for the formatters.
*
* @param output output coming from System.err
* @since Ant 1.5
*/
@Override
public void handleErrorOutput(final String output) {
if (runner != null) {
runner.handleErrorOutput(output);
if (showOutput) {
super.handleErrorOutput(output);
}
} else {
super.handleErrorOutput(output);
}
}
/**
* Pass output sent to System.err to the TestRunner so it can
* collect it for the formatters.
*
* @param output coming from System.err
* @since Ant 1.5.2
*/
@Override
public void handleErrorFlush(final String output) {
if (runner != null) {
runner.handleErrorFlush(output);
if (showOutput) {
super.handleErrorFlush(output);
}
} else {
super.handleErrorFlush(output);
}
}
// in VM is not very nice since it could probably hang the
// whole build. IMHO this method should be avoided and it would be best
// to remove it in future versions. TBD. (SBa)
/**
* Execute inside VM.
* @param arg one JUnitTest
* @throws BuildException under unspecified circumstances
* @return the results
*/
private TestResultHolder executeInVM(final JUnitTest arg) throws BuildException {
if (delegate == null) {
setupJUnitDelegate();
}
final JUnitTest test = (JUnitTest) arg.clone();
test.setProperties(getProject().getProperties());
if (dir != null) {
log("dir attribute ignored if running in the same VM",
Project.MSG_WARN);
}
if (newEnvironment || null != env.getVariables()) {
log("Changes to environment variables are ignored if running in "
+ "the same VM.", Project.MSG_WARN);
}
if (getCommandline().getBootclasspath() != null) {
log("bootclasspath is ignored if running in the same VM.",
Project.MSG_WARN);
}
final CommandlineJava.SysProperties sysProperties =
getCommandline().getSystemProperties();
if (sysProperties != null) {
sysProperties.setSystem();
}
try {
log("Using System properties " + System.getProperties(),
Project.MSG_VERBOSE);
if (splitJUnit) {
classLoader = (AntClassLoader) delegate.getClass().getClassLoader();
} else {
createClassLoader();
}
if (classLoader != null) {
classLoader.setThreadContextLoader();
}
runner = delegate.newJUnitTestRunner(test, test.getMethods(), test.getHaltonerror(),
test.getFiltertrace(),
test.getHaltonfailure(), false,
getEnableTestListenerEvents(),
classLoader);
if (summary) {
final JUnitTaskMirror.SummaryJUnitResultFormatterMirror f =
delegate.newSummaryJUnitResultFormatter();
f.setWithOutAndErr(equalsWithOutAndErr(summaryValue));
f.setOutput(getDefaultOutput());
runner.addFormatter(f);
}
runner.setPermissions(perm);
final FormatterElement[] feArray = mergeFormatters(test);
for (int i = 0; i < feArray.length; i++) {
final FormatterElement fe = feArray[i];
if (fe.shouldUse(this)) {
final File outFile = getOutput(fe, test);
if (outFile != null) {
fe.setOutfile(outFile);
} else {
fe.setOutput(getDefaultOutput());
}
runner.addFormatter(fe.createFormatter(classLoader));
}
}
runner.run();
final TestResultHolder result = new TestResultHolder();
result.exitCode = runner.getRetCode();
return result;
} finally {
if (sysProperties != null) {
sysProperties.restoreSystem();
}
if (classLoader != null) {
classLoader.resetThreadContextLoader();
}
}
}
/**
* @return <tt>null</tt> if there is a timeout value, otherwise the
* watchdog instance.
*
* @throws BuildException under unspecified circumstances
* @since Ant 1.2
*/
protected ExecuteWatchdog createWatchdog() throws BuildException {
if (timeout == null) {
return null;
}
return new ExecuteWatchdog((long) timeout.intValue());
}
/**
* Get the default output for a formatter.
*
* @return default output stream for a formatter
* @since Ant 1.3
*/
protected OutputStream getDefaultOutput() {
return new LogOutputStream(this, Project.MSG_INFO);
}
/**
* Merge all individual tests from the batchtest with all individual tests
* and return an enumeration over all <tt>JUnitTest</tt>.
*
* @return enumeration over individual tests
* @since Ant 1.3
*/
protected Enumeration<JUnitTest> getIndividualTests() {
final int count = batchTests.size();
final Enumeration[] enums = new Enumeration[ count + 1];
for (int i = 0; i < count; i++) {
final BatchTest batchtest = batchTests.elementAt(i);
enums[i] = batchtest.elements();
}
enums[enums.length - 1] = tests.elements();
return Enumerations.fromCompound(enums);
}
/**
* Verifies all <code>test</code> elements having the <code>methods</code>
* attribute specified and having the <code>if</code>-condition resolved
* to true, that the value of the <code>methods</code> attribute is valid.
* @exception BuildException if some of the tests matching the described
* conditions has invalid value of the
* <code>methods</code> attribute
* @since 1.8.2
*/
private void checkMethodLists() throws BuildException {
if (tests.isEmpty()) {
return;
}
final Enumeration<JUnitTest> testsEnum = tests.elements();
while (testsEnum.hasMoreElements()) {
final JUnitTest test = testsEnum.nextElement();
if (test.hasMethodsSpecified() && test.shouldRun(getProject())) {
test.resolveMethods();
}
}
}
/**
* return an enumeration listing each test, then each batchtest
* @return enumeration
* @since Ant 1.3
*/
protected Enumeration<JUnitTest> allTests() {
final Enumeration[] enums = {tests.elements(), batchTests.elements()};
return Enumerations.fromCompound(enums);
}
/**
* @param test junit test
* @return array of FormatterElement
* @since Ant 1.3
*/
private FormatterElement[] mergeFormatters(final JUnitTest test) {
final Vector<FormatterElement> feVector = (Vector<FormatterElement>) formatters.clone();
test.addFormattersTo(feVector);
final FormatterElement[] feArray = new FormatterElement[feVector.size()];
feVector.copyInto(feArray);
return feArray;
}
/**
* If the formatter sends output to a file, return that file.
* null otherwise.
* @param fe formatter element
* @param test one JUnit test
* @return file reference
* @since Ant 1.3
*/
protected File getOutput(final FormatterElement fe, final JUnitTest test) {
if (fe.getUseFile()) {
String base = test.getOutfile();
if (base == null) {
base = JUnitTaskMirror.JUnitTestRunnerMirror.IGNORED_FILE_NAME;
}
final String filename = base + fe.getExtension();
final File destFile = new File(test.getTodir(), filename);
final String absFilename = destFile.getAbsolutePath();
return getProject().resolveFile(absFilename);
}
return null;
}
/**
* Search for the given resource and add the directory or archive
* that contains it to the classpath.
*
* <p>Doesn't work for archives in JDK 1.1 as the URL returned by
* getResource doesn't contain the name of the archive.</p>
*
* @param resource resource that one wants to lookup
* @since Ant 1.4
*/
protected void addClasspathEntry(final String resource) {
addClasspathResource(resource);
}
/**
* Implementation of addClasspathEntry.
*
* @param resource resource that one wants to lookup
* @return true if something was in fact added
* @since Ant 1.7.1
*/
private boolean addClasspathResource(String resource) {
/*
* pre Ant 1.6 this method used to call getClass().getResource
* while Ant 1.6 will call ClassLoader.getResource().
*
* The difference is that Class.getResource expects a leading
* slash for "absolute" resources and will strip it before
* delegating to ClassLoader.getResource - so we now have to
* emulate Class's behavior.
*/
if (resource.startsWith("/")) {
resource = resource.substring(1);
} else {
resource = "org/apache/tools/ant/taskdefs/optional/junit/"
+ resource;
}
final File f = LoaderUtils.getResourceSource(JUnitTask.class.getClassLoader(),
resource);
if (f != null) {
log("Found " + f.getAbsolutePath(), Project.MSG_DEBUG);
antRuntimeClasses.createPath().setLocation(f);
return true;
} else {
log("Couldn\'t find " + resource, Project.MSG_DEBUG);
return false;
}
}
static final String TIMEOUT_MESSAGE =
"Timeout occurred. Please note the time in the report does"
+ " not reflect the time until the timeout.";
/**
* Take care that some output is produced in report files if the
* watchdog kills the test.
*
* @since Ant 1.5.2
*/
private void logTimeout(final FormatterElement[] feArray, final JUnitTest test,
final String testCase) {
logVmExit(feArray, test, TIMEOUT_MESSAGE, testCase);
}
/**
* 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(final FormatterElement[] feArray, final JUnitTest test, final String testCase) {
logVmExit(
feArray, test,
"Forked Java VM exited abnormally. Please note the time in the report"
+ " does not reflect the time until the VM exit.",
testCase);
}
/**
* Take care that some output is produced in report files if the
* forked machine terminated before the test suite finished
*
* @since Ant 1.7
*/
private void logVmExit(final FormatterElement[] feArray, final JUnitTest test,
final String message, final String testCase) {
if (delegate == null) {
setupJUnitDelegate();
}
try {
log("Using System properties " + System.getProperties(),
Project.MSG_VERBOSE);
if (splitJUnit) {
classLoader = (AntClassLoader) delegate.getClass().getClassLoader();
} else {
createClassLoader();
}
if (classLoader != null) {
classLoader.setThreadContextLoader();
}
test.setCounts(1, 0, 1, 0);
test.setProperties(getProject().getProperties());
for (int i = 0; i < feArray.length; i++) {
final FormatterElement fe = feArray[i];
if (fe.shouldUse(this)) {
final JUnitTaskMirror.JUnitResultFormatterMirror formatter =
fe.createFormatter(classLoader);
if (formatter != null) {
OutputStream out = null;
final File outFile = getOutput(fe, test);
if (outFile != null) {
try {
out = new FileOutputStream(outFile);
} catch (final IOException e) {
// ignore
}
}
if (out == null) {
out = getDefaultOutput();
}
delegate.addVmExit(test, formatter, out, message,
testCase);
}
}
}
if (summary) {
final JUnitTaskMirror.SummaryJUnitResultFormatterMirror f =
delegate.newSummaryJUnitResultFormatter();
f.setWithOutAndErr(equalsWithOutAndErr(summaryValue));
delegate.addVmExit(test, f, getDefaultOutput(), message, testCase);
}
} finally {
if (classLoader != null) {
classLoader.resetThreadContextLoader();
}
}
}
/**
* Creates and configures an AntClassLoader instance from the
* nested classpath element.
*
* @since Ant 1.6
*/
private void createClassLoader() {
final Path userClasspath = getCommandline().getClasspath();
if (userClasspath != null) {
if (reloading || classLoader == null) {
deleteClassLoader();
final Path classpath = (Path) userClasspath.clone();
if (includeAntRuntime) {
log("Implicitly adding " + antRuntimeClasses
+ " to CLASSPATH", Project.MSG_VERBOSE);
classpath.append(antRuntimeClasses);
}
classLoader = getProject().createClassLoader(classpath);
if (getClass().getClassLoader() != null
&& getClass().getClassLoader() != Project.class.getClassLoader()) {
classLoader.setParent(getClass().getClassLoader());
}
classLoader.setParentFirst(false);
classLoader.addJavaLibraries();
log("Using CLASSPATH " + classLoader.getClasspath(),
Project.MSG_VERBOSE);
// make sure the test will be accepted as a TestCase
classLoader.addSystemPackageRoot("junit");
// make sure the test annotation are accepted
classLoader.addSystemPackageRoot("org.junit");
// will cause trouble in JDK 1.1 if omitted
classLoader.addSystemPackageRoot("org.apache.tools.ant");
}
}
}
/**
* Removes resources.
*
* <p>Is invoked in {@link #execute execute}. Subclasses that
* don't invoke execute should invoke this method in a finally
* block.</p>
*
* @since Ant 1.7.1
*/
protected void cleanup() {
deleteClassLoader();
delegate = null;
}
/**
* Removes a classloader if needed.
* @since Ant 1.7
*/
private void deleteClassLoader() {
if (classLoader != null) {
classLoader.cleanup();
classLoader = null;
}
if (mirrorLoader instanceof SplitClassLoader) {
((SplitClassLoader) mirrorLoader).cleanup();
}
mirrorLoader = null;
}
/**
* Get the command line used to run the tests.
* @return the command line.
* @since Ant 1.6.2
*/
protected CommandlineJava getCommandline() {
if (commandline == null) {
commandline = new CommandlineJava();
commandline.setClassname("org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner");
}
return commandline;
}
/**
* Forked test support
* @since Ant 1.6.2
*/
private static final class ForkedTestConfiguration {
private final boolean filterTrace;
private final boolean haltOnError;
private final boolean haltOnFailure;
private final String errorProperty;
private final String failureProperty;
/**
* constructor for forked test configuration
* @param filterTrace
* @param haltOnError
* @param haltOnFailure
* @param errorProperty
* @param failureProperty
*/
ForkedTestConfiguration(final boolean filterTrace, final boolean haltOnError,
final boolean haltOnFailure, final String errorProperty,
final String failureProperty) {
this.filterTrace = filterTrace;
this.haltOnError = haltOnError;
this.haltOnFailure = haltOnFailure;
this.errorProperty = errorProperty;
this.failureProperty = failureProperty;
}
/**
* configure from a test; sets member variables to attributes of the test
* @param test
*/
ForkedTestConfiguration(final JUnitTest test) {
this(test.getFiltertrace(),
test.getHaltonerror(),
test.getHaltonfailure(),
test.getErrorProperty(),
test.getFailureProperty());
}
/**
* equality test checks all the member variables
* @param other
* @return true if everything is equal
*/
@Override
public boolean equals(final Object other) {
if (other == null
|| other.getClass() != ForkedTestConfiguration.class) {
return false;
}
final 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)));
}
/**
* hashcode is based only on the boolean members, and returns a value
* in the range 0-7.
* @return hash code value
*/
@Override
public int hashCode() {
// CheckStyle:MagicNumber OFF
return (filterTrace ? 1 : 0)
+ (haltOnError ? 2 : 0)
+ (haltOnFailure ? 4 : 0);
// CheckStyle:MagicNumber ON
}
}
/**
* These are the different forking options
* @since 1.6.2
*/
public static final class ForkMode extends EnumeratedAttribute {
/**
* fork once only
*/
public static final String ONCE = "once";
/**
* fork once per test class
*/
public static final String PER_TEST = "perTest";
/**
* fork once per batch of tests
*/
public static final String PER_BATCH = "perBatch";
/** No arg constructor. */
public ForkMode() {
super();
}
/**
* Constructor using a value.
* @param value the value to use - once, perTest or perBatch.
*/
public ForkMode(final String value) {
super();
setValue(value);
}
/** {@inheritDoc}. */
@Override
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.
* @param testList the list of tests to be executed or queued.
* @param runIndividual if true execute each test individually.
* @return a list of tasks to be executed.
* @since 1.6.2
*/
protected Collection<List> executeOrQueue(final Enumeration<JUnitTest> testList,
final boolean runIndividual) {
final Map<ForkedTestConfiguration, List> testConfigurations = new HashMap<ForkedTestConfiguration, List>();
while (testList.hasMoreElements()) {
final JUnitTest test = testList.nextElement();
if (test.shouldRun(getProject())) {
/* with multi-threaded runs need to defer execution of even */
/* individual tests so the threads can pick tests off the queue. */
if ((runIndividual || !test.getFork()) && (threads == 1)) {
execute(test, 0);
} else {
final ForkedTestConfiguration c =
new ForkedTestConfiguration(test);
List<JUnitTest> l = testConfigurations.get(c);
if (l == null) {
l = new ArrayList<JUnitTest>();
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
* occurred or sets a property.
* @param exitValue the exitValue of the test.
* @param wasKilled if true, the test had been killed.
* @param test the test in question.
* @param name the name of the test.
* @since Ant 1.6.2
*/
protected void actOnTestResult(final int exitValue, final boolean wasKilled,
final JUnitTest test, final String name) {
final TestResultHolder t = new TestResultHolder();
t.exitCode = exitValue;
t.timedOut = wasKilled;
actOnTestResult(t, test, name);
}
/**
* Logs information about failed tests, potentially stops
* processing (by throwing a BuildException) if a failure/error
* occurred or sets a property.
* @param result the result of the test.
* @param test the test in question.
* @param name the name of the test.
* @since Ant 1.7
*/
protected void actOnTestResult(final TestResultHolder result, final JUnitTest test,
final String name) {
// if there is an error/failure and that it should halt, stop
// everything otherwise just log a statement
final boolean fatal = result.timedOut || result.crashed;
final boolean errorOccurredHere =
result.exitCode == JUnitTaskMirror.JUnitTestRunnerMirror.ERRORS || fatal;
final boolean failureOccurredHere =
result.exitCode != JUnitTaskMirror.JUnitTestRunnerMirror.SUCCESS || fatal;
if (errorOccurredHere || failureOccurredHere) {
if ((errorOccurredHere && test.getHaltonerror())
|| (failureOccurredHere && test.getHaltonfailure())) {
throw new BuildException(name + " failed"
+ (result.timedOut ? " (timeout)" : "")
+ (result.crashed ? " (crashed)" : ""), getLocation());
} else {
if (logFailedTests) {
log(name + " FAILED"
+ (result.timedOut ? " (timeout)" : "")
+ (result.crashed ? " (crashed)" : ""),
Project.MSG_ERR);
}
if (errorOccurredHere && test.getErrorProperty() != null) {
getProject().setNewProperty(test.getErrorProperty(), "true");
}
if (failureOccurredHere && test.getFailureProperty() != null) {
getProject().setNewProperty(test.getFailureProperty(), "true");
}
}
}
}
/**
* A value class that contains the result of a test.
*/
protected static class TestResultHolder {
// CheckStyle:VisibilityModifier OFF - bc
/** the exit code of the test. */
public int exitCode = JUnitTaskMirror.JUnitTestRunnerMirror.ERRORS;
/** true if the test timed out */
public boolean timedOut = false;
/** true if the test crashed */
public boolean crashed = false;
// CheckStyle:VisibilityModifier ON
}
/**
* A stream handler for handling the junit task.
* @since Ant 1.7
*/
protected static class JUnitLogOutputStream extends LogOutputStream {
private final Task task; // local copy since LogOutputStream.task is private
/**
* Constructor.
* @param task the task being logged.
* @param level the log level used to log data written to this stream.
*/
public JUnitLogOutputStream(final Task task, final int level) {
super(task, level);
this.task = task;
}
/**
* Logs a line.
* If the line starts with junit.framework.TestListener: set the level
* to MSG_VERBOSE.
* @param line the line to log.
* @param level the logging level to use.
*/
@Override
protected void processLine(final String line, final int level) {
if (line.startsWith(TESTLISTENER_PREFIX)) {
task.log(line, Project.MSG_VERBOSE);
} else {
super.processLine(line, level);
}
}
}
/**
* A log stream handler for junit.
* @since Ant 1.7
*/
protected static class JUnitLogStreamHandler extends PumpStreamHandler {
/**
* Constructor.
* @param task the task to log.
* @param outlevel the level to use for standard output.
* @param errlevel the level to use for error output.
*/
public JUnitLogStreamHandler(final Task task, final int outlevel, final int errlevel) {
super(new JUnitLogOutputStream(task, outlevel),
new LogOutputStream(task, errlevel));
}
}
static final String NAME_OF_DUMMY_TEST = "Batch-With-Multiple-Tests";
/**
* Creates a JUnitTest instance that shares all flags with the
* passed in instance but has a more meaningful name.
*
* <p>If a VM running multiple tests crashes, we don't know which
* test failed. Prior to Ant 1.8.0 Ant would log the error with
* the last test of the batch test, which caused some confusion
* since the log might look as if a test had been executed last
* that was never started. With Ant 1.8.0 the test's name will
* indicate that something went wrong with a test inside the batch
* without giving it a real name.</p>
*
* @see "https://issues.apache.org/bugzilla/show_bug.cgi?id=45227"
*/
private static JUnitTest createDummyTestForBatchTest(final JUnitTest test) {
final JUnitTest t = (JUnitTest) test.clone();
final int index = test.getName().lastIndexOf('.');
// make sure test looks as if it was in the same "package" as
// the last test of the batch
final String pack = index > 0 ? test.getName().substring(0, index + 1) : "";
t.setName(pack + NAME_OF_DUMMY_TEST);
return t;
}
private static void printDual(final BufferedWriter w, final PrintStream s, final String text)
throws IOException {
w.write(String.valueOf(text));
s.print(text);
}
private static void printlnDual(final BufferedWriter w, final PrintStream s, final String text)
throws IOException {
w.write(String.valueOf(text));
w.newLine();
s.println(text);
}
}