diff options
Diffstat (limited to 'framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java')
-rw-r--r-- | framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java | 2283 |
1 files changed, 2283 insertions, 0 deletions
diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java new file mode 100644 index 00000000..459bd3d5 --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java @@ -0,0 +1,2283 @@ +/* + * 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> + * <target name="test-int-chars" depends="jar-test"> + * <echo message="testing international characters"/> + * <junit printsummary="no" haltonfailure="yes" fork="false"> + * <classpath refid="classpath"/> + * <formatter type="plain" usefile="false" /> + * <test name="org.apache.ecs.InternationalCharTest" /> + * </junit> + * </target> + * </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> + * <target name="run-tests" depends="dump-info,compile-tests" if="junit.present"> + * <junit printsummary="no" haltonfailure="yes" fork="${junit.fork}"> + * <jvmarg value="-classic"/> + * <classpath refid="tests-classpath"/> + * <sysproperty key="build.tests" value="${build.tests}"/> + * <formatter type="brief" usefile="false" /> + * <batchtest> + * <fileset dir="${tests.dir}"> + * <include name="**/*Test*" /> + * </fileset> + * </batchtest> + * </junit> + * </target> + * </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><junit></code> and + * <code><batch></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 + * <batchtest> 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'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 "null". + * @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); + } +} |