/* * 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.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.nio.file.Files; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Locale; 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; import org.apache.tools.ant.util.StringUtils; /** * 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 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(); private CommandlineJava commandline; private final List<JUnitTest> tests = new Vector<>(); private final List<BatchTest> batchTests = new Vector<>(); private final Vector<FormatterElement> formatters = new Vector<>(); 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; /** * 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(); } /** * Add a path to the modulepath. * * @return created modulepath. * @since 1.9.8 */ public Path createModulepath() { return getCommandline().createModulepath(getProject()).createPath(); } /** * Add a path to the upgrademodulepath. * * @return created upgrademodulepath. * @since 1.9.8 */ public Path createUpgrademodulepath() { return getCommandline().createUpgrademodulepath(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.add(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.add(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.add(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 never * @since Ant 1.2 */ public JUnitTask() throws Exception { //NOSONAR } /** * 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 && (!tmpDir.exists() || !tmpDir.isDirectory())) { throw new BuildException("%s is not a valid temp directory", tmpDir); } 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> or <modulepath> for <junit> must include junit.jar " + "if not in Ant's own classpath", e, task.getLocation()); } try { final Class<? extends JUnitTaskMirror> c = loader.loadClass(JUnitTaskMirror.class.getName() + "Impl").asSubclass(JUnitTaskMirror.class); if (c.getClassLoader() != loader) { throw new BuildException("Overdelegating loader", task.getLocation()); } final Constructor<? extends JUnitTaskMirror> cons = c.getConstructor(JUnitTask.class); return cons.newInstance(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); Path extra = getCommandline().getClasspath(); if (extra != null) { path.add(extra); } extra = getCommandline().getModulepath(); if (extra != null && !hasJunit(path)) { path.add(expandModulePath(extra)); } mirrorLoader = AccessController.doPrivileged( (PrivilegedAction<ClassLoader>) () -> 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(); checkModules(); setupJUnitDelegate(); final List<List<JUnitTest>> testLists = new ArrayList<>(); /* 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.PER_TEST.equals(forkMode.getValue()); if (forkPerTest || ForkMode.ONCE.equals(forkMode.getValue())) { testLists.addAll(executeOrQueue(getIndividualTests(), forkPerTest)); } else { /* forkMode.getValue().equals(ForkMode.PER_BATCH) */ batchTests.stream().map(b -> executeOrQueue(b.elements(), false)) .forEach(testLists::addAll); testLists.addAll( executeOrQueue(Collections.enumeration(tests), 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<JUnitTest>> iterator, final int id) { this.masterTask = master; this.iterator = iterator; this.id = id; } @Override 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<JUnitTest>> 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<JUnitTest> getNextTest(final Iterator<List<JUnitTest>> 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<JUnitTest>> iter, final int threadId) { List<JUnitTest> 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(l.get(0), threadId); } else { execute(l, threadId); } } log("Ending test thread " + threadId, Project.MSG_VERBOSE); } private void runTestsInThreads(final List<List<JUnitTest>> testList, final int numThreads) { Iterator<List<JUnitTest>> 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<JUnitTest>> newlist = new ArrayList<>(); if (forkMode.getValue().equals(ForkMode.PER_TEST)) { final Iterator<List<JUnitTest>> i1 = testList.iterator(); while (i1.hasNext()) { final List<JUnitTest> l = i1.next(); if (l.size() == 1) { newlist.add(l); } else { final Iterator<JUnitTest> i2 = l.iterator(); while (i2.hasNext()) { final List<JUnitTest> 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 = 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; if (test.getFork()) { final ExecuteWatchdog watchdog = createWatchdog(); result = executeAsForked(test, watchdog, null); // null watchdog means no timeout, you'd better not check with null } else { result = executeInVM(test); } 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.isEmpty() || "null".equals(testName)) { 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<JUnitTest> testList, final int thread) throws BuildException { // Create a temporary file to pass the test cases to run to // the runner (one test case per line) final File casesFile = createTempPropertiesFile("junittestcases"); try (BufferedWriter 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)); JUnitTest test = null; for (JUnitTest t : testList) { test = t; 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(); // 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 { 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<JUnitTest> 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 = 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())); StringBuilder formatterArg = new StringBuilder(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 StringBuilder(); } } 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<String, Object> p = getProject().getProperties(); final Properties props = new Properties(); p.forEach(props::put); try { final OutputStream outstream = Files.newOutputStream(propsFile.toPath()); props.store(outstream, "Ant JUnitTask generated properties file"); outstream.close(); } catch (final 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(); boolean success = false; try { result.exitCode = execute.execute(); success = true; } 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) { log(StringUtils.getStackTrace(e), Project.MSG_INFO); // 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)) { String msg = "Could not delete temporary properties file '" + propsFile.getAbsolutePath() + "'."; if (success) { throw new BuildException(msg); //NOSONAR } // don't hide inner exception log(msg, Project.MSG_ERR); } } 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 = 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; } try (AntClassLoader loader = AntClassLoader.newAntClassLoader(null, getProject(), cmd.createClasspath(getProject()), true)) { final String projectResourceName = LoaderUtils.classNameToResource(Project.class.getName()); URL previous = null; try { for (final Enumeration<URL> e = loader.getResources(projectResourceName); e.hasMoreElements();) { final URL current = 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 } } } /** * 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); } 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 = 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(); @SuppressWarnings("unchecked") final Enumeration<JUnitTest>[] enums = new Enumeration[ count + 1]; for (int i = 0; i < count; i++) { final BatchTest batchtest = batchTests.get(i); enums[i] = batchtest.elements(); } enums[enums.length - 1] = Collections.enumeration(tests); 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; } for (JUnitTest test : tests) { if (test.hasMethodsSpecified() && test.shouldRun(getProject())) { test.resolveMethods(); } } } /** * Checks a validity of module specific options. * @since 1.9.8 */ private void checkModules() { if (hasPath(getCommandline().getModulepath()) || hasPath(getCommandline().getUpgrademodulepath())) { if (!(batchTests.stream().allMatch(BaseTest::getFork) && tests.stream().allMatch(BaseTest::getFork))) { throw new BuildException( "The module path requires fork attribute to be set to true."); } } } /** * Checks is a junit is on given path. * @param path the {@link Path} to check * @return true when given {@link Path} contains junit * @since 1.9.8 */ private boolean hasJunit(final Path path) { try (AntClassLoader loader = AntClassLoader.newAntClassLoader( null, getProject(), path, true)) { try { loader.loadClass("junit.framework.Test"); return true; } catch (final Exception ex) { return false; } } } /** * Expands a module path to flat path of jars and root folders usable by classloader. * @param modulePath to be expanded * @return the expanded path * @since 1.9.8 */ private Path expandModulePath(Path modulePath) { final Path expanded = new Path(getProject()); for (String path : modulePath.list()) { final File modulePathEntry = getProject().resolveFile(path); if (modulePathEntry.isDirectory() && !hasModuleInfo(modulePathEntry)) { final File[] modules = modulePathEntry.listFiles((dir, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".jar")); if (modules != null) { for (File module : modules) { expanded.add(new Path(getProject(), String.format( "%s%s%s", //NOI18N path, File.separator, module.getName()))); } } } else { expanded.add(new Path(getProject(), path)); } } return expanded; } /** * return an enumeration listing each test, then each batchtest * @return enumeration * @since Ant 1.3 */ protected Enumeration<BaseTest> allTests() { return Enumerations.fromCompound(Collections.enumeration(tests), Collections.enumeration(batchTests)); } /** * @param test junit test * @return array of FormatterElement * @since Ant 1.3 */ private FormatterElement[] mergeFormatters(final JUnitTest test) { @SuppressWarnings("unchecked") 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 = Files.newOutputStream(outFile.toPath()); } 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(); final Path userModulepath = getCommandline().getModulepath(); if (userClasspath != null || userModulepath != null) { if (reloading || classLoader == null) { deleteClassLoader(); final Path path = new Path(getProject()); if (userClasspath != null) { path.add((Path) userClasspath.clone()); } if (userModulepath != null && !hasJunit(path)) { path.add(expandModulePath(userModulepath)); } if (includeAntRuntime) { log("Implicitly adding " + antRuntimeClasses + " to CLASSPATH", Project.MSG_VERBOSE); path.append(antRuntimeClasses); } classLoader = getProject().createClassLoader(path); 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<JUnitTest>> executeOrQueue( final Enumeration<JUnitTest> testList, final boolean runIndividual) { final Map<ForkedTestConfiguration, List<JUnitTest>> testConfigurations = new HashMap<>(); 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 { testConfigurations .computeIfAbsent(new ForkedTestConfiguration(test), k -> new ArrayList<>()) .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 = 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); } /** * Checks if a path exists and is non empty. * @param path to be checked * @return true if the path is non <code>null</code> and non empty. * @since 1.9.8 */ private static boolean hasPath(final Path path) { return path != null && path.size() > 0; } /** * Checks if a given folder is an unpacked module. * @param root the fodler to be checked * @return true if the root is an unpacked module * @since 1.9.8 */ private static boolean hasModuleInfo(final File root) { return new File(root, "module-info.class").exists(); //NOI18N } }