@@ -30,6 +30,8 @@ | |||||
<path id="junit.engine.vintage.classpath"> | <path id="junit.engine.vintage.classpath"> | ||||
<fileset dir="../../../../../lib/optional" includes="junit-vintage-engine*.jar"/> | <fileset dir="../../../../../lib/optional" includes="junit-vintage-engine*.jar"/> | ||||
<fileset dir="../../../../../lib/optional" includes="junit-*.jar"/> | |||||
<fileset dir="../../../../../lib/optional" includes="hamcrest*.jar"/> | |||||
</path> | </path> | ||||
<path id="junit.engine.jupiter.classpath"> | <path id="junit.engine.jupiter.classpath"> | ||||
@@ -109,5 +111,15 @@ | |||||
</testclasses> | </testclasses> | ||||
</junitlauncher> | </junitlauncher> | ||||
</target> | </target> | ||||
<target name="test-basic-fork" depends="init"> | |||||
<junitlauncher> | |||||
<classpath refid="test.classpath"/> | |||||
<test name="org.example.junitlauncher.vintage.JUnit4SampleTest" outputdir="${output.dir}"> | |||||
<fork dir="${basedir}"/> | |||||
<listener type="legacy-xml" sendSysErr="true" sendSysOut="true"/> | |||||
</test> | |||||
</junitlauncher> | |||||
</target> | |||||
</project> | </project> | ||||
@@ -0,0 +1,54 @@ | |||||
/* | |||||
* 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.junitlauncher; | |||||
/** | |||||
* Constants used within the junitlauncher task | |||||
*/ | |||||
final class Constants { | |||||
static final int FORK_EXIT_CODE_SUCCESS = 0; | |||||
static final int FORK_EXIT_CODE_EXCEPTION = 1; | |||||
static final int FORK_EXIT_CODE_TESTS_FAILED = 2; | |||||
static final int FORK_EXIT_CODE_TIMED_OUT = 3; | |||||
static final String ARG_PROPERTIES = "--properties"; | |||||
static final String ARG_LAUNCH_DEFINITION = "--launch-definition"; | |||||
static final String LD_XML_ELM_LAUNCH_DEF = "launch-definition"; | |||||
static final String LD_XML_ELM_TEST = "test"; | |||||
static final String LD_XML_ELM_TEST_CLASSES = "test-classes"; | |||||
static final String LD_XML_ATTR_HALT_ON_FAILURE = "haltOnFailure"; | |||||
static final String LD_XML_ATTR_OUTPUT_DIRECTORY = "outDir"; | |||||
static final String LD_XML_ATTR_INCLUDE_ENGINES = "includeEngines"; | |||||
static final String LD_XML_ATTR_EXCLUDE_ENGINES = "excludeEngines"; | |||||
static final String LD_XML_ATTR_CLASS_NAME = "classname"; | |||||
static final String LD_XML_ATTR_METHODS = "methods"; | |||||
static final String LD_XML_ATTR_PRINT_SUMMARY = "printSummary"; | |||||
static final String LD_XML_ELM_LISTENER = "listener"; | |||||
static final String LD_XML_ATTR_SEND_SYS_ERR = "sendSysErr"; | |||||
static final String LD_XML_ATTR_SEND_SYS_OUT = "sendSysOut"; | |||||
static final String LD_XML_ATTR_LISTENER_RESULT_FILE = "resultFile"; | |||||
private Constants() { | |||||
} | |||||
} |
@@ -0,0 +1,156 @@ | |||||
/* | |||||
* 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.junitlauncher; | |||||
import org.apache.tools.ant.BuildException; | |||||
import org.apache.tools.ant.Project; | |||||
import org.apache.tools.ant.Task; | |||||
import org.apache.tools.ant.launch.AntMain; | |||||
import org.apache.tools.ant.types.Commandline; | |||||
import org.apache.tools.ant.types.CommandlineJava; | |||||
import org.apache.tools.ant.types.Environment; | |||||
import org.apache.tools.ant.types.Path; | |||||
import org.apache.tools.ant.types.PropertySet; | |||||
import org.apache.tools.ant.util.LoaderUtils; | |||||
import org.junit.platform.commons.annotation.Testable; | |||||
import org.junit.platform.engine.TestEngine; | |||||
import org.junit.platform.launcher.core.LauncherFactory; | |||||
import java.io.File; | |||||
/** | |||||
* Represents the {@code fork} element within test definitions of the | |||||
* {@code junitlauncher} task | |||||
*/ | |||||
public class ForkDefinition { | |||||
private boolean includeAntRuntimeLibraries = true; | |||||
private boolean includeJunitPlatformLibraries = true; | |||||
private final CommandlineJava commandLineJava; | |||||
private final Environment env = new Environment(); | |||||
private String dir; | |||||
private long timeout = -1; | |||||
ForkDefinition() { | |||||
this.commandLineJava = new CommandlineJava(); | |||||
} | |||||
public void setDir(final String dir) { | |||||
this.dir = dir; | |||||
} | |||||
String getDir() { | |||||
return this.dir; | |||||
} | |||||
public void setTimeout(final long timeout) { | |||||
this.timeout = timeout; | |||||
} | |||||
long getTimeout() { | |||||
return this.timeout; | |||||
} | |||||
public Commandline.Argument createJvmArg() { | |||||
return this.commandLineJava.createVmArgument(); | |||||
} | |||||
public void addConfiguredSysProperty(final Environment.Variable sysProp) { | |||||
// validate that key/value are present | |||||
sysProp.validate(); | |||||
this.commandLineJava.addSysproperty(sysProp); | |||||
} | |||||
public void addConfiguredSysPropertySet(final PropertySet propertySet) { | |||||
this.commandLineJava.addSyspropertyset(propertySet); | |||||
} | |||||
public void addConfiguredEnv(final Environment.Variable var) { | |||||
this.env.addVariable(var); | |||||
} | |||||
public void addConfiguredModulePath(final Path modulePath) { | |||||
this.commandLineJava.createModulepath(modulePath.getProject()).add(modulePath); | |||||
} | |||||
public void addConfiguredUpgradeModulePath(final Path upgradeModulePath) { | |||||
this.commandLineJava.createUpgrademodulepath(upgradeModulePath.getProject()).add(upgradeModulePath); | |||||
} | |||||
Environment getEnv() { | |||||
return this.env; | |||||
} | |||||
/** | |||||
* Generates a new {@link CommandlineJava} constructed out of the configurations set on this | |||||
* {@link ForkDefinition} | |||||
* | |||||
* @param task The junitlaunchertask for which this is a fork definition | |||||
* @return | |||||
*/ | |||||
CommandlineJava generateCommandLine(final JUnitLauncherTask task) { | |||||
final CommandlineJava cmdLine; | |||||
try { | |||||
cmdLine = (CommandlineJava) this.commandLineJava.clone(); | |||||
} catch (CloneNotSupportedException e) { | |||||
throw new BuildException(e); | |||||
} | |||||
cmdLine.setClassname(StandaloneLauncher.class.getName()); | |||||
// VM arguments | |||||
final Project project = task.getProject(); | |||||
final Path antRuntimeResourceSources = new Path(project); | |||||
if (this.includeAntRuntimeLibraries) { | |||||
addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(AntMain.class)); | |||||
addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(Task.class)); | |||||
addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(JUnitLauncherTask.class)); | |||||
} | |||||
if (this.includeJunitPlatformLibraries) { | |||||
// platform-engine | |||||
addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(TestEngine.class)); | |||||
// platform-launcher | |||||
addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(LauncherFactory.class)); | |||||
// platform-commons | |||||
addAntRuntimeResourceSource(antRuntimeResourceSources, task, toResourceName(Testable.class)); | |||||
} | |||||
final Path classPath = cmdLine.createClasspath(project); | |||||
classPath.createPath().append(antRuntimeResourceSources); | |||||
return cmdLine; | |||||
} | |||||
private static boolean addAntRuntimeResourceSource(final Path path, final JUnitLauncherTask task, final String resource) { | |||||
final File f = LoaderUtils.getResourceSource(task.getClass().getClassLoader(), resource); | |||||
if (f == null) { | |||||
task.log("Could not locate source of resource " + resource); | |||||
return false; | |||||
} | |||||
task.log("Found source " + f + " of resource " + resource); | |||||
path.createPath().setLocation(f); | |||||
return true; | |||||
} | |||||
private static String toResourceName(final Class klass) { | |||||
final String name = klass.getName(); | |||||
return name.replaceAll("\\.", "/") + ".class"; | |||||
} | |||||
} |
@@ -21,37 +21,32 @@ import org.apache.tools.ant.AntClassLoader; | |||||
import org.apache.tools.ant.BuildException; | import org.apache.tools.ant.BuildException; | ||||
import org.apache.tools.ant.Project; | import org.apache.tools.ant.Project; | ||||
import org.apache.tools.ant.Task; | 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.CommandlineJava; | |||||
import org.apache.tools.ant.types.Environment; | |||||
import org.apache.tools.ant.types.Path; | import org.apache.tools.ant.types.Path; | ||||
import org.apache.tools.ant.util.FileUtils; | |||||
import org.apache.tools.ant.util.KeepAliveOutputStream; | |||||
import org.junit.platform.launcher.Launcher; | |||||
import org.junit.platform.launcher.LauncherDiscoveryRequest; | |||||
import org.junit.platform.launcher.TestExecutionListener; | |||||
import org.junit.platform.launcher.TestPlan; | |||||
import org.junit.platform.launcher.core.LauncherFactory; | |||||
import org.junit.platform.launcher.listeners.SummaryGeneratingListener; | |||||
import org.junit.platform.launcher.listeners.TestExecutionSummary; | |||||
import javax.xml.stream.XMLOutputFactory; | |||||
import javax.xml.stream.XMLStreamWriter; | |||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStream; | |||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import java.io.PipedInputStream; | |||||
import java.io.PipedOutputStream; | |||||
import java.io.PrintStream; | |||||
import java.io.PrintWriter; | |||||
import java.nio.charset.StandardCharsets; | |||||
import java.nio.file.Files; | import java.nio.file.Files; | ||||
import java.nio.file.Paths; | import java.nio.file.Paths; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Arrays; | |||||
import java.util.Collection; | |||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.Hashtable; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.Optional; | import java.util.Optional; | ||||
import java.util.Properties; | import java.util.Properties; | ||||
import java.util.concurrent.BlockingQueue; | |||||
import java.util.concurrent.CountDownLatch; | |||||
import java.util.concurrent.LinkedBlockingQueue; | |||||
import java.util.concurrent.TimeUnit; | |||||
import java.util.concurrent.TimeoutException; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_HALT_ON_FAILURE; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_PRINT_SUMMARY; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LAUNCH_DEF; | |||||
/** | /** | ||||
* An Ant {@link Task} responsible for launching the JUnit platform for running tests. | * An Ant {@link Task} responsible for launching the JUnit platform for running tests. | ||||
@@ -84,55 +79,22 @@ public class JUnitLauncherTask extends Task { | |||||
@Override | @Override | ||||
public void execute() throws BuildException { | public void execute() throws BuildException { | ||||
final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader(); | |||||
try { | |||||
final ClassLoader executionCL = createClassLoaderForTestExecution(); | |||||
Thread.currentThread().setContextClassLoader(executionCL); | |||||
final Launcher launcher = LauncherFactory.create(); | |||||
final List<TestRequest> requests = buildTestRequests(); | |||||
for (final TestRequest testRequest : requests) { | |||||
try { | |||||
final TestDefinition test = testRequest.getOwner(); | |||||
final LauncherDiscoveryRequest request = testRequest.getDiscoveryRequest().build(); | |||||
final List<TestExecutionListener> testExecutionListeners = new ArrayList<>(); | |||||
// a listener that we always put at the front of list of listeners | |||||
// for this request. | |||||
final Listener firstListener = new Listener(); | |||||
// we always enroll the summary generating listener, to the request, so that we | |||||
// get to use some of the details of the summary for our further decision making | |||||
testExecutionListeners.add(firstListener); | |||||
testExecutionListeners.addAll(getListeners(testRequest, executionCL)); | |||||
final PrintStream originalSysOut = System.out; | |||||
final PrintStream originalSysErr = System.err; | |||||
try { | |||||
firstListener.switchedSysOutHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_OUT); | |||||
firstListener.switchedSysErrHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_ERR); | |||||
launcher.execute(request, testExecutionListeners.toArray(new TestExecutionListener[testExecutionListeners.size()])); | |||||
} finally { | |||||
// switch back sysout/syserr to the original | |||||
try { | |||||
System.setOut(originalSysOut); | |||||
} catch (Exception e) { | |||||
// ignore | |||||
} | |||||
try { | |||||
System.setErr(originalSysErr); | |||||
} catch (Exception e) { | |||||
// ignore | |||||
} | |||||
} | |||||
handleTestExecutionCompletion(test, firstListener.getSummary()); | |||||
} finally { | |||||
try { | |||||
testRequest.close(); | |||||
} catch (Exception e) { | |||||
// log and move on | |||||
log("Failed to cleanly close test request", e, Project.MSG_DEBUG); | |||||
} | |||||
} | |||||
if (this.tests.isEmpty()) { | |||||
return; | |||||
} | |||||
final Project project = getProject(); | |||||
for (final TestDefinition test : this.tests) { | |||||
if (!test.shouldRun(project)) { | |||||
log("Excluding test " + test + " since it's considered not to run " + | |||||
"in context of project " + project, Project.MSG_DEBUG); | |||||
continue; | |||||
} | |||||
if (test.getForkDefinition() != null) { | |||||
forkTest(test); | |||||
} else { | |||||
final LauncherSupport launcherSupport = new LauncherSupport(new InVMLaunch(Collections.singletonList(test))); | |||||
launcherSupport.launch(); | |||||
} | } | ||||
} finally { | |||||
Thread.currentThread().setContextClassLoader(previousClassLoader); | |||||
} | } | ||||
} | } | ||||
@@ -204,360 +166,212 @@ public class JUnitLauncherTask extends Task { | |||||
} | } | ||||
} | } | ||||
private List<TestRequest> buildTestRequests() { | |||||
if (this.tests.isEmpty()) { | |||||
return Collections.emptyList(); | |||||
} | |||||
final List<TestRequest> requests = new ArrayList<>(); | |||||
for (final TestDefinition test : this.tests) { | |||||
final List<TestRequest> testRequests = test.createTestRequests(this); | |||||
if (testRequests == null || testRequests.isEmpty()) { | |||||
continue; | |||||
} | |||||
requests.addAll(testRequests); | |||||
private ClassLoader createClassLoaderForTestExecution() { | |||||
if (this.classPath == null) { | |||||
return this.getClass().getClassLoader(); | |||||
} | } | ||||
return requests; | |||||
return new AntClassLoader(this.getClass().getClassLoader(), getProject(), this.classPath, true); | |||||
} | } | ||||
private List<TestExecutionListener> getListeners(final TestRequest testRequest, final ClassLoader classLoader) { | |||||
final TestDefinition test = testRequest.getOwner(); | |||||
final List<ListenerDefinition> applicableListenerElements = test.getListeners().isEmpty() ? this.listeners : test.getListeners(); | |||||
final List<TestExecutionListener> listeners = new ArrayList<>(); | |||||
final Project project = getProject(); | |||||
for (final ListenerDefinition applicableListener : applicableListenerElements) { | |||||
if (!applicableListener.shouldUse(project)) { | |||||
log("Excluding listener " + applicableListener.getClassName() + " since it's not applicable" + | |||||
" in the context of project " + project, Project.MSG_DEBUG); | |||||
continue; | |||||
} | |||||
final TestExecutionListener listener = requireTestExecutionListener(applicableListener, classLoader); | |||||
if (listener instanceof TestResultFormatter) { | |||||
// setup/configure the result formatter | |||||
setupResultFormatter(testRequest, applicableListener, (TestResultFormatter) listener); | |||||
} | |||||
listeners.add(listener); | |||||
} | |||||
return listeners; | |||||
} | |||||
private void setupResultFormatter(final TestRequest testRequest, final ListenerDefinition formatterDefinition, | |||||
final TestResultFormatter resultFormatter) { | |||||
testRequest.closeUponCompletion(resultFormatter); | |||||
// set the execution context | |||||
resultFormatter.setContext(new InVMExecution()); | |||||
// set the destination output stream for writing out the formatted result | |||||
final TestDefinition test = testRequest.getOwner(); | |||||
final java.nio.file.Path outputDir = test.getOutputDir() != null ? Paths.get(test.getOutputDir()) : getProject().getBaseDir().toPath(); | |||||
final String filename = formatterDefinition.requireResultFile(test); | |||||
final java.nio.file.Path resultOutputFile = Paths.get(outputDir.toString(), filename); | |||||
try { | |||||
final OutputStream resultOutputStream = Files.newOutputStream(resultOutputFile); | |||||
// enroll the output stream to be closed when the execution of the TestRequest completes | |||||
testRequest.closeUponCompletion(resultOutputStream); | |||||
resultFormatter.setDestination(new KeepAliveOutputStream(resultOutputStream)); | |||||
} catch (IOException e) { | |||||
throw new BuildException(e); | |||||
} | |||||
// check if system.out/system.err content needs to be passed on to the listener | |||||
if (formatterDefinition.shouldSendSysOut()) { | |||||
testRequest.addSysOutInterest(resultFormatter); | |||||
} | |||||
if (formatterDefinition.shouldSendSysErr()) { | |||||
testRequest.addSysErrInterest(resultFormatter); | |||||
private java.nio.file.Path dumpProjectProperties() throws IOException { | |||||
final java.nio.file.Path propsPath = Files.createTempFile(null, "properties"); | |||||
propsPath.toFile().deleteOnExit(); | |||||
final Hashtable<String, Object> props = this.getProject().getProperties(); | |||||
final Properties projProperties = new Properties(); | |||||
projProperties.putAll(props); | |||||
try (final OutputStream os = Files.newOutputStream(propsPath)) { | |||||
// TODO: Is it always UTF-8? | |||||
projProperties.store(os, StandardCharsets.UTF_8.name()); | |||||
} | } | ||||
return propsPath; | |||||
} | } | ||||
private TestExecutionListener requireTestExecutionListener(final ListenerDefinition listener, final ClassLoader classLoader) { | |||||
final String className = listener.getClassName(); | |||||
if (className == null || className.trim().isEmpty()) { | |||||
throw new BuildException("classname attribute value is missing on listener element"); | |||||
private void forkTest(final TestDefinition test) { | |||||
// create launch command | |||||
final ForkDefinition forkDefinition = test.getForkDefinition(); | |||||
final CommandlineJava commandlineJava = forkDefinition.generateCommandLine(this); | |||||
if (this.classPath != null) { | |||||
commandlineJava.createClasspath(getProject()).createPath().append(this.classPath); | |||||
} | } | ||||
final Class<?> klass; | |||||
final java.nio.file.Path projectPropsPath; | |||||
try { | try { | ||||
klass = Class.forName(className, false, classLoader); | |||||
} catch (ClassNotFoundException e) { | |||||
throw new BuildException("Failed to load listener class " + className, e); | |||||
} | |||||
if (!TestExecutionListener.class.isAssignableFrom(klass)) { | |||||
throw new BuildException("Listener class " + className + " is not of type " + TestExecutionListener.class.getName()); | |||||
} | |||||
try { | |||||
return TestExecutionListener.class.cast(klass.newInstance()); | |||||
} catch (Exception e) { | |||||
throw new BuildException("Failed to create an instance of listener " + className, e); | |||||
projectPropsPath = dumpProjectProperties(); | |||||
} catch (IOException e) { | |||||
throw new BuildException("Could not create the necessary properties file while forking a process" + | |||||
" for a test", e); | |||||
} | } | ||||
} | |||||
// --properties <path-to-properties-file> | |||||
commandlineJava.createArgument().setValue(Constants.ARG_PROPERTIES); | |||||
commandlineJava.createArgument().setValue(projectPropsPath.toAbsolutePath().toString()); | |||||
private void handleTestExecutionCompletion(final TestDefinition test, final TestExecutionSummary summary) { | |||||
if (printSummary) { | |||||
// print the summary to System.out | |||||
summary.printTo(new PrintWriter(System.out, true)); | |||||
} | |||||
final boolean hasTestFailures = summary.getTestsFailedCount() != 0; | |||||
try { | |||||
if (hasTestFailures && test.getFailureProperty() != null) { | |||||
// if there are test failures and the test is configured to set a property in case | |||||
// of failure, then set the property to true | |||||
getProject().setNewProperty(test.getFailureProperty(), "true"); | |||||
} | |||||
} finally { | |||||
if (hasTestFailures && test.isHaltOnFailure()) { | |||||
// if the test is configured to halt on test failures, throw a build error | |||||
final String errorMessage; | |||||
if (test instanceof NamedTest) { | |||||
errorMessage = "Test " + ((NamedTest) test).getName() + " has " + summary.getTestsFailedCount() + " failure(s)"; | |||||
} else { | |||||
errorMessage = "Some test(s) have failure(s)"; | |||||
final java.nio.file.Path launchDefXmlPath = newLaunchDefinitionXml(); | |||||
try (final OutputStream os = Files.newOutputStream(launchDefXmlPath)) { | |||||
final XMLStreamWriter writer = XMLOutputFactory.newFactory().createXMLStreamWriter(os, "UTF-8"); | |||||
try { | |||||
writer.writeStartDocument(); | |||||
writer.writeStartElement(LD_XML_ELM_LAUNCH_DEF); | |||||
if (this.printSummary) { | |||||
writer.writeAttribute(LD_XML_ATTR_PRINT_SUMMARY, "true"); | |||||
} | } | ||||
throw new BuildException(errorMessage); | |||||
} | |||||
} | |||||
} | |||||
private ClassLoader createClassLoaderForTestExecution() { | |||||
if (this.classPath == null) { | |||||
return this.getClass().getClassLoader(); | |||||
} | |||||
return new AntClassLoader(this.getClass().getClassLoader(), getProject(), this.classPath, true); | |||||
} | |||||
@SuppressWarnings("resource") | |||||
private Optional<SwitchedStreamHandle> trySwitchSysOutErr(final TestRequest testRequest, final StreamType streamType) { | |||||
switch (streamType) { | |||||
case SYS_OUT: { | |||||
if (!testRequest.interestedInSysOut()) { | |||||
return Optional.empty(); | |||||
if (this.haltOnFailure) { | |||||
writer.writeAttribute(LD_XML_ATTR_HALT_ON_FAILURE, "true"); | |||||
} | } | ||||
break; | |||||
} | |||||
case SYS_ERR: { | |||||
if (!testRequest.interestedInSysErr()) { | |||||
return Optional.empty(); | |||||
// task level listeners | |||||
for (final ListenerDefinition listenerDef : this.listeners) { | |||||
if (!listenerDef.shouldUse(getProject())) { | |||||
continue; | |||||
} | |||||
// construct the listener definition argument | |||||
listenerDef.toForkedRepresentation(writer); | |||||
} | } | ||||
break; | |||||
} | |||||
default: { | |||||
// unknown, but no need to error out, just be lenient | |||||
// and return back | |||||
return Optional.empty(); | |||||
// test definition as XML | |||||
test.toForkedRepresentation(this, writer); | |||||
writer.writeEndElement(); | |||||
writer.writeEndDocument(); | |||||
} finally { | |||||
writer.close(); | |||||
} | } | ||||
} catch (Exception e) { | |||||
throw new BuildException("Failed to construct command line for test", e); | |||||
} | } | ||||
final PipedOutputStream pipedOutputStream = new PipedOutputStream(); | |||||
final PipedInputStream pipedInputStream; | |||||
try { | |||||
pipedInputStream = new PipedInputStream(pipedOutputStream); | |||||
} catch (IOException ioe) { | |||||
// log and return | |||||
return Optional.empty(); | |||||
} | |||||
final PrintStream printStream = new PrintStream(pipedOutputStream, true); | |||||
final SysOutErrStreamReader streamer; | |||||
switch (streamType) { | |||||
case SYS_OUT: { | |||||
System.setOut(new PrintStream(printStream)); | |||||
streamer = new SysOutErrStreamReader(this, pipedInputStream, | |||||
StreamType.SYS_OUT, testRequest.getSysOutInterests()); | |||||
final Thread sysOutStreamer = new Thread(streamer); | |||||
sysOutStreamer.setDaemon(true); | |||||
sysOutStreamer.setName("junitlauncher-sysout-stream-reader"); | |||||
sysOutStreamer.setUncaughtExceptionHandler((t, e) -> this.log("Failed in sysout streaming", e, Project.MSG_INFO)); | |||||
sysOutStreamer.start(); | |||||
// --launch-definition <xml-file-path> | |||||
commandlineJava.createArgument().setValue(Constants.ARG_LAUNCH_DEFINITION); | |||||
commandlineJava.createArgument().setValue(launchDefXmlPath.toAbsolutePath().toString()); | |||||
// launch the process and wait for process to complete | |||||
final int exitCode = executeForkedTest(forkDefinition, commandlineJava); | |||||
switch (exitCode) { | |||||
case Constants.FORK_EXIT_CODE_SUCCESS: { | |||||
// success | |||||
break; | break; | ||||
} | } | ||||
case SYS_ERR: { | |||||
System.setErr(new PrintStream(printStream)); | |||||
streamer = new SysOutErrStreamReader(this, pipedInputStream, | |||||
StreamType.SYS_ERR, testRequest.getSysErrInterests()); | |||||
final Thread sysErrStreamer = new Thread(streamer); | |||||
sysErrStreamer.setDaemon(true); | |||||
sysErrStreamer.setName("junitlauncher-syserr-stream-reader"); | |||||
sysErrStreamer.setUncaughtExceptionHandler((t, e) -> this.log("Failed in syserr streaming", e, Project.MSG_INFO)); | |||||
sysErrStreamer.start(); | |||||
case Constants.FORK_EXIT_CODE_EXCEPTION: { | |||||
// process failed with some exception | |||||
throw new BuildException("Forked test(s) failed with an exception"); | |||||
} | |||||
case Constants.FORK_EXIT_CODE_TESTS_FAILED: { | |||||
// test has failure(s) | |||||
try { | |||||
if (test.getFailureProperty() != null) { | |||||
// if there are test failures and the test is configured to set a property in case | |||||
// of failure, then set the property to true | |||||
this.getProject().setNewProperty(test.getFailureProperty(), "true"); | |||||
} | |||||
} finally { | |||||
if (test.isHaltOnFailure()) { | |||||
// if the test is configured to halt on test failures, throw a build error | |||||
final String errorMessage; | |||||
if (test instanceof NamedTest) { | |||||
errorMessage = "Test " + ((NamedTest) test).getName() + " has failure(s)"; | |||||
} else { | |||||
errorMessage = "Some test(s) have failure(s)"; | |||||
} | |||||
throw new BuildException(errorMessage); | |||||
} | |||||
} | |||||
break; | break; | ||||
} | } | ||||
default: { | |||||
return Optional.empty(); | |||||
case Constants.FORK_EXIT_CODE_TIMED_OUT: { | |||||
throw new BuildException(new TimeoutException("Forked test(s) timed out")); | |||||
} | } | ||||
} | } | ||||
return Optional.of(new SwitchedStreamHandle(pipedOutputStream, streamer)); | |||||
} | |||||
private enum StreamType { | |||||
SYS_OUT, | |||||
SYS_ERR | |||||
} | } | ||||
private static final class SysOutErrStreamReader implements Runnable { | |||||
private static final byte[] EMPTY = new byte[0]; | |||||
private final JUnitLauncherTask task; | |||||
private final InputStream sourceStream; | |||||
private final StreamType streamType; | |||||
private final Collection<TestResultFormatter> resultFormatters; | |||||
private volatile SysOutErrContentDeliverer contentDeliverer; | |||||
SysOutErrStreamReader(final JUnitLauncherTask task, final InputStream source, final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) { | |||||
this.task = task; | |||||
this.sourceStream = source; | |||||
this.streamType = streamType; | |||||
this.resultFormatters = resultFormatters; | |||||
private int executeForkedTest(final ForkDefinition forkDefinition, final CommandlineJava commandlineJava) { | |||||
final LogOutputStream outStream = new LogOutputStream(this, Project.MSG_INFO); | |||||
final LogOutputStream errStream = new LogOutputStream(this, Project.MSG_WARN); | |||||
final ExecuteWatchdog watchdog = forkDefinition.getTimeout() > 0 ? new ExecuteWatchdog(forkDefinition.getTimeout()) : null; | |||||
final Execute execute = new Execute(new PumpStreamHandler(outStream, errStream), watchdog); | |||||
execute.setCommandline(commandlineJava.getCommandline()); | |||||
execute.setAntRun(getProject()); | |||||
if (forkDefinition.getDir() != null) { | |||||
execute.setWorkingDirectory(Paths.get(forkDefinition.getDir()).toFile()); | |||||
} | } | ||||
final Environment env = forkDefinition.getEnv(); | |||||
if (env != null && env.getVariables() != null) { | |||||
execute.setEnvironment(env.getVariables()); | |||||
} | |||||
log(commandlineJava.describeCommand(), Project.MSG_VERBOSE); | |||||
int exitCode; | |||||
try { | |||||
exitCode = execute.execute(); | |||||
} catch (IOException e) { | |||||
throw new BuildException("Process fork failed", e, getLocation()); | |||||
} | |||||
return (watchdog != null && watchdog.killedProcess()) ? Constants.FORK_EXIT_CODE_TIMED_OUT : exitCode; | |||||
} | |||||
@Override | |||||
public void run() { | |||||
final SysOutErrContentDeliverer streamContentDeliver = new SysOutErrContentDeliverer(this.streamType, this.resultFormatters); | |||||
final Thread deliveryThread = new Thread(streamContentDeliver); | |||||
deliveryThread.setName("junitlauncher-" + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + "-stream-deliverer"); | |||||
deliveryThread.setDaemon(true); | |||||
deliveryThread.start(); | |||||
this.contentDeliverer = streamContentDeliver; | |||||
int numRead = -1; | |||||
final byte[] data = new byte[1024]; | |||||
try { | |||||
while ((numRead = this.sourceStream.read(data)) != -1) { | |||||
final byte[] copy = Arrays.copyOf(data, numRead); | |||||
streamContentDeliver.availableData.offer(copy); | |||||
} | |||||
} catch (IOException e) { | |||||
task.log("Failed while streaming " + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + " data", | |||||
e, Project.MSG_INFO); | |||||
} finally { | |||||
streamContentDeliver.stop = true; | |||||
// just "wakeup" the delivery thread, to take into account | |||||
// those race conditions, where that other thread didn't yet | |||||
// notice that it was asked to stop and has now gone into a | |||||
// X amount of wait, waiting for any new data | |||||
streamContentDeliver.availableData.offer(EMPTY); | |||||
} | |||||
private java.nio.file.Path newLaunchDefinitionXml() { | |||||
final java.nio.file.Path xmlFilePath; | |||||
try { | |||||
xmlFilePath = Files.createTempFile(null, ".xml"); | |||||
} catch (IOException e) { | |||||
throw new BuildException("Failed to construct command line for test", e); | |||||
} | } | ||||
xmlFilePath.toFile().deleteOnExit(); | |||||
return xmlFilePath; | |||||
} | } | ||||
private static final class SysOutErrContentDeliverer implements Runnable { | |||||
private volatile boolean stop; | |||||
private final Collection<TestResultFormatter> resultFormatters; | |||||
private final StreamType streamType; | |||||
private final BlockingQueue<byte[]> availableData = new LinkedBlockingQueue<>(); | |||||
private final CountDownLatch completionLatch = new CountDownLatch(1); | |||||
private final class InVMExecution implements TestExecutionContext { | |||||
private final Properties props; | |||||
SysOutErrContentDeliverer(final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) { | |||||
this.streamType = streamType; | |||||
this.resultFormatters = resultFormatters; | |||||
InVMExecution() { | |||||
this.props = new Properties(); | |||||
this.props.putAll(JUnitLauncherTask.this.getProject().getProperties()); | |||||
} | } | ||||
@Override | @Override | ||||
public void run() { | |||||
try { | |||||
while (!this.stop) { | |||||
final byte[] streamData; | |||||
try { | |||||
streamData = this.availableData.poll(2, TimeUnit.SECONDS); | |||||
} catch (InterruptedException e) { | |||||
Thread.currentThread().interrupt(); | |||||
return; | |||||
} | |||||
if (streamData != null) { | |||||
deliver(streamData); | |||||
} | |||||
} | |||||
// drain it | |||||
final List<byte[]> remaining = new ArrayList<>(); | |||||
this.availableData.drainTo(remaining); | |||||
if (!remaining.isEmpty()) { | |||||
for (final byte[] data : remaining) { | |||||
deliver(data); | |||||
} | |||||
} | |||||
} finally { | |||||
this.completionLatch.countDown(); | |||||
} | |||||
public Properties getProperties() { | |||||
return this.props; | |||||
} | } | ||||
private void deliver(final byte[] data) { | |||||
if (data == null || data.length == 0) { | |||||
return; | |||||
} | |||||
for (final TestResultFormatter resultFormatter : this.resultFormatters) { | |||||
// send it to the formatter | |||||
switch (streamType) { | |||||
case SYS_OUT: { | |||||
resultFormatter.sysOutAvailable(data); | |||||
break; | |||||
} | |||||
case SYS_ERR: { | |||||
resultFormatter.sysErrAvailable(data); | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
@Override | |||||
public Optional<Project> getProject() { | |||||
return Optional.of(JUnitLauncherTask.this.getProject()); | |||||
} | } | ||||
} | } | ||||
private final class SwitchedStreamHandle { | |||||
private final PipedOutputStream outputStream; | |||||
private final SysOutErrStreamReader streamReader; | |||||
private final class InVMLaunch implements LaunchDefinition { | |||||
SwitchedStreamHandle(final PipedOutputStream outputStream, final SysOutErrStreamReader streamReader) { | |||||
this.streamReader = streamReader; | |||||
this.outputStream = outputStream; | |||||
} | |||||
} | |||||
private final TestExecutionContext testExecutionContext = new InVMExecution(); | |||||
private final List<TestDefinition> inVMTests; | |||||
private final ClassLoader executionCL; | |||||
private final class Listener extends SummaryGeneratingListener { | |||||
private Optional<SwitchedStreamHandle> switchedSysOutHandle; | |||||
private Optional<SwitchedStreamHandle> switchedSysErrHandle; | |||||
private InVMLaunch(final List<TestDefinition> inVMTests) { | |||||
this.inVMTests = inVMTests; | |||||
this.executionCL = createClassLoaderForTestExecution(); | |||||
} | |||||
@Override | @Override | ||||
public void testPlanExecutionFinished(final TestPlan testPlan) { | |||||
super.testPlanExecutionFinished(testPlan); | |||||
// now that the test plan execution is finished, close the switched sysout/syserr output streams | |||||
// and wait for the sysout and syserr content delivery, to result formatters, to finish | |||||
if (this.switchedSysOutHandle.isPresent()) { | |||||
final SwitchedStreamHandle sysOut = this.switchedSysOutHandle.get(); | |||||
try { | |||||
closeAndWait(sysOut); | |||||
} catch (InterruptedException e) { | |||||
Thread.currentThread().interrupt(); | |||||
return; | |||||
} | |||||
} | |||||
if (this.switchedSysErrHandle.isPresent()) { | |||||
final SwitchedStreamHandle sysErr = this.switchedSysErrHandle.get(); | |||||
try { | |||||
closeAndWait(sysErr); | |||||
} catch (InterruptedException e) { | |||||
Thread.currentThread().interrupt(); | |||||
} | |||||
} | |||||
public List<TestDefinition> getTests() { | |||||
return this.inVMTests; | |||||
} | } | ||||
private void closeAndWait(final SwitchedStreamHandle handle) throws InterruptedException { | |||||
FileUtils.close(handle.outputStream); | |||||
if (handle.streamReader.contentDeliverer == null) { | |||||
return; | |||||
} | |||||
// wait for a few seconds | |||||
handle.streamReader.contentDeliverer.completionLatch.await(2, TimeUnit.SECONDS); | |||||
@Override | |||||
public List<ListenerDefinition> getListeners() { | |||||
return listeners; | |||||
} | } | ||||
} | |||||
private final class InVMExecution implements TestExecutionContext { | |||||
private final Properties props; | |||||
@Override | |||||
public boolean isPrintSummary() { | |||||
return printSummary; | |||||
} | |||||
InVMExecution() { | |||||
this.props = new Properties(); | |||||
this.props.putAll(JUnitLauncherTask.this.getProject().getProperties()); | |||||
@Override | |||||
public boolean isHaltOnFailure() { | |||||
return haltOnFailure; | |||||
} | } | ||||
@Override | @Override | ||||
public Properties getProperties() { | |||||
return this.props; | |||||
public ClassLoader getClassLoader() { | |||||
return this.executionCL; | |||||
} | } | ||||
@Override | @Override | ||||
public Optional<Project> getProject() { | |||||
return Optional.of(JUnitLauncherTask.this.getProject()); | |||||
public TestExecutionContext getTestExecutionContext() { | |||||
return this.testExecutionContext; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -0,0 +1,75 @@ | |||||
/* | |||||
* 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.junitlauncher; | |||||
import java.util.List; | |||||
/** | |||||
* Defines the necessary context for launching the JUnit platform for running | |||||
* tests. | |||||
*/ | |||||
public interface LaunchDefinition { | |||||
/** | |||||
* Returns the {@link TestDefinition tests} that have to be launched | |||||
* | |||||
* @return | |||||
*/ | |||||
List<TestDefinition> getTests(); | |||||
/** | |||||
* Returns the default {@link ListenerDefinition listeners} that will be used | |||||
* for the tests, if the {@link #getTests() tests} themselves don't specify any | |||||
* | |||||
* @return | |||||
*/ | |||||
List<ListenerDefinition> getListeners(); | |||||
/** | |||||
* Returns true if a summary needs to be printed out after the execution of the | |||||
* tests. False otherwise. | |||||
* | |||||
* @return | |||||
*/ | |||||
boolean isPrintSummary(); | |||||
/** | |||||
* Returns true if any remaining tests launch need to be stopped if any test execution | |||||
* failed. False otherwise. | |||||
* | |||||
* @return | |||||
*/ | |||||
boolean isHaltOnFailure(); | |||||
/** | |||||
* Returns the {@link ClassLoader} that has to be used for launching and execution of the | |||||
* tests | |||||
* | |||||
* @return | |||||
*/ | |||||
ClassLoader getClassLoader(); | |||||
/** | |||||
* Returns the {@link TestExecutionContext} that will be passed to {@link TestResultFormatter#setContext(TestExecutionContext) | |||||
* result formatters} which are applicable during the execution of the tests. | |||||
* | |||||
* @return | |||||
*/ | |||||
TestExecutionContext getTestExecutionContext(); | |||||
} |
@@ -0,0 +1,513 @@ | |||||
/* | |||||
* 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.junitlauncher; | |||||
import org.apache.tools.ant.BuildException; | |||||
import org.apache.tools.ant.Project; | |||||
import org.apache.tools.ant.util.FileUtils; | |||||
import org.apache.tools.ant.util.KeepAliveOutputStream; | |||||
import org.junit.platform.launcher.Launcher; | |||||
import org.junit.platform.launcher.LauncherDiscoveryRequest; | |||||
import org.junit.platform.launcher.TestExecutionListener; | |||||
import org.junit.platform.launcher.TestPlan; | |||||
import org.junit.platform.launcher.core.LauncherFactory; | |||||
import org.junit.platform.launcher.listeners.SummaryGeneratingListener; | |||||
import org.junit.platform.launcher.listeners.TestExecutionSummary; | |||||
import java.io.IOException; | |||||
import java.io.InputStream; | |||||
import java.io.OutputStream; | |||||
import java.io.PipedInputStream; | |||||
import java.io.PipedOutputStream; | |||||
import java.io.PrintStream; | |||||
import java.io.PrintWriter; | |||||
import java.nio.file.Files; | |||||
import java.nio.file.Path; | |||||
import java.nio.file.Paths; | |||||
import java.util.ArrayList; | |||||
import java.util.Arrays; | |||||
import java.util.Collection; | |||||
import java.util.Collections; | |||||
import java.util.List; | |||||
import java.util.Optional; | |||||
import java.util.concurrent.BlockingQueue; | |||||
import java.util.concurrent.CountDownLatch; | |||||
import java.util.concurrent.LinkedBlockingQueue; | |||||
import java.util.concurrent.TimeUnit; | |||||
/** | |||||
* Responsible for doing the real work involved in launching the JUnit platform | |||||
* and passing it the relevant tests that need to be executed by the JUnit platform. | |||||
* <p> | |||||
* This class relies on a {@link LaunchDefinition} for setting up the launch of the | |||||
* JUnit platform. | |||||
* <p> | |||||
* The {@code LauncherSupport} isn't concerned with whether or not | |||||
* it's being executed in the same JVM as the build in which the {@code junitlauncher} | |||||
* was triggered or if it's running as part of a forked JVM. Instead it just relies | |||||
* on the {@code LaunchDefinition} to do whatever decisions need to be done before and | |||||
* after launching the tests. | |||||
* <p> | |||||
* This class is not thread-safe and isn't expected to be used for launching from | |||||
* multiple different threads simultaneously. | |||||
*/ | |||||
class LauncherSupport { | |||||
private final LaunchDefinition launchDefinition; | |||||
private boolean testsFailed; | |||||
/** | |||||
* Create a {@link LauncherSupport} for the passed {@link LaunchDefinition} | |||||
* | |||||
* @param definition The launch definition which will be used for launching the tests | |||||
*/ | |||||
LauncherSupport(final LaunchDefinition definition) { | |||||
if (definition == null) { | |||||
throw new IllegalArgumentException("Launch definition cannot be null"); | |||||
} | |||||
this.launchDefinition = definition; | |||||
} | |||||
/** | |||||
* Launches the tests defined in the {@link LaunchDefinition} | |||||
* | |||||
* @throws BuildException If any tests failed and the launch definition was configured to throw | |||||
* an exception, or if any other exception occurred before or after launching | |||||
* the tests | |||||
*/ | |||||
void launch() throws BuildException { | |||||
final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader(); | |||||
try { | |||||
Thread.currentThread().setContextClassLoader(this.launchDefinition.getClassLoader()); | |||||
final Launcher launcher = LauncherFactory.create(); | |||||
final List<TestRequest> requests = buildTestRequests(); | |||||
for (final TestRequest testRequest : requests) { | |||||
try { | |||||
final TestDefinition test = testRequest.getOwner(); | |||||
final LauncherDiscoveryRequest request = testRequest.getDiscoveryRequest().build(); | |||||
final List<TestExecutionListener> testExecutionListeners = new ArrayList<>(); | |||||
// a listener that we always put at the front of list of listeners | |||||
// for this request. | |||||
final Listener firstListener = new Listener(); | |||||
// we always enroll the summary generating listener, to the request, so that we | |||||
// get to use some of the details of the summary for our further decision making | |||||
testExecutionListeners.add(firstListener); | |||||
testExecutionListeners.addAll(getListeners(testRequest, this.launchDefinition.getClassLoader())); | |||||
final PrintStream originalSysOut = System.out; | |||||
final PrintStream originalSysErr = System.err; | |||||
try { | |||||
firstListener.switchedSysOutHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_OUT); | |||||
firstListener.switchedSysErrHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_ERR); | |||||
launcher.execute(request, testExecutionListeners.toArray(new TestExecutionListener[testExecutionListeners.size()])); | |||||
} finally { | |||||
// switch back sysout/syserr to the original | |||||
try { | |||||
System.setOut(originalSysOut); | |||||
} catch (Exception e) { | |||||
// ignore | |||||
} | |||||
try { | |||||
System.setErr(originalSysErr); | |||||
} catch (Exception e) { | |||||
// ignore | |||||
} | |||||
} | |||||
handleTestExecutionCompletion(test, firstListener.getSummary()); | |||||
} finally { | |||||
try { | |||||
testRequest.close(); | |||||
} catch (Exception e) { | |||||
// log and move on | |||||
log("Failed to cleanly close test request", e, Project.MSG_DEBUG); | |||||
} | |||||
} | |||||
} | |||||
} finally { | |||||
Thread.currentThread().setContextClassLoader(previousClassLoader); | |||||
} | |||||
} | |||||
/** | |||||
* Returns true if there were any test failures, when this {@link LauncherSupport} was used | |||||
* to {@link #launch()} tests. False otherwise. | |||||
* | |||||
* @return | |||||
*/ | |||||
boolean hasTestFailures() { | |||||
return this.testsFailed; | |||||
} | |||||
private List<TestRequest> buildTestRequests() { | |||||
final List<TestDefinition> tests = this.launchDefinition.getTests(); | |||||
if (tests.isEmpty()) { | |||||
return Collections.emptyList(); | |||||
} | |||||
final List<TestRequest> requests = new ArrayList<>(); | |||||
for (final TestDefinition test : tests) { | |||||
final List<TestRequest> testRequests = test.createTestRequests(); | |||||
if (testRequests == null || testRequests.isEmpty()) { | |||||
continue; | |||||
} | |||||
requests.addAll(testRequests); | |||||
} | |||||
return requests; | |||||
} | |||||
private List<TestExecutionListener> getListeners(final TestRequest testRequest, final ClassLoader classLoader) { | |||||
final TestDefinition test = testRequest.getOwner(); | |||||
final List<ListenerDefinition> applicableListenerElements = test.getListeners().isEmpty() | |||||
? this.launchDefinition.getListeners() : test.getListeners(); | |||||
final List<TestExecutionListener> listeners = new ArrayList<>(); | |||||
final Optional<Project> project = this.launchDefinition.getTestExecutionContext().getProject(); | |||||
for (final ListenerDefinition applicableListener : applicableListenerElements) { | |||||
if (project.isPresent() && !applicableListener.shouldUse(project.get())) { | |||||
log("Excluding listener " + applicableListener.getClassName() + " since it's not applicable" + | |||||
" in the context of project", null, Project.MSG_DEBUG); | |||||
continue; | |||||
} | |||||
final TestExecutionListener listener = requireTestExecutionListener(applicableListener, classLoader); | |||||
if (listener instanceof TestResultFormatter) { | |||||
// setup/configure the result formatter | |||||
setupResultFormatter(testRequest, applicableListener, (TestResultFormatter) listener); | |||||
} | |||||
listeners.add(listener); | |||||
} | |||||
return listeners; | |||||
} | |||||
private void setupResultFormatter(final TestRequest testRequest, final ListenerDefinition formatterDefinition, | |||||
final TestResultFormatter resultFormatter) { | |||||
testRequest.closeUponCompletion(resultFormatter); | |||||
// set the execution context | |||||
resultFormatter.setContext(this.launchDefinition.getTestExecutionContext()); | |||||
// set the destination output stream for writing out the formatted result | |||||
final TestDefinition test = testRequest.getOwner(); | |||||
final TestExecutionContext testExecutionContext = this.launchDefinition.getTestExecutionContext(); | |||||
final Path baseDir = testExecutionContext.getProject().isPresent() | |||||
? testExecutionContext.getProject().get().getBaseDir().toPath() : Paths.get(System.getProperty("user.dir")); | |||||
final java.nio.file.Path outputDir = test.getOutputDir() != null ? Paths.get(test.getOutputDir()) : baseDir; | |||||
final String filename = formatterDefinition.requireResultFile(test); | |||||
final java.nio.file.Path resultOutputFile = Paths.get(outputDir.toString(), filename); | |||||
try { | |||||
final OutputStream resultOutputStream = Files.newOutputStream(resultOutputFile); | |||||
// enroll the output stream to be closed when the execution of the TestRequest completes | |||||
testRequest.closeUponCompletion(resultOutputStream); | |||||
resultFormatter.setDestination(new KeepAliveOutputStream(resultOutputStream)); | |||||
} catch (IOException e) { | |||||
throw new BuildException(e); | |||||
} | |||||
// check if system.out/system.err content needs to be passed on to the listener | |||||
if (formatterDefinition.shouldSendSysOut()) { | |||||
testRequest.addSysOutInterest(resultFormatter); | |||||
} | |||||
if (formatterDefinition.shouldSendSysErr()) { | |||||
testRequest.addSysErrInterest(resultFormatter); | |||||
} | |||||
} | |||||
private TestExecutionListener requireTestExecutionListener(final ListenerDefinition listener, final ClassLoader classLoader) { | |||||
final String className = listener.getClassName(); | |||||
if (className == null || className.trim().isEmpty()) { | |||||
throw new BuildException("classname attribute value is missing on listener element"); | |||||
} | |||||
final Class<?> klass; | |||||
try { | |||||
klass = Class.forName(className, false, classLoader); | |||||
} catch (ClassNotFoundException e) { | |||||
throw new BuildException("Failed to load listener class " + className, e); | |||||
} | |||||
if (!TestExecutionListener.class.isAssignableFrom(klass)) { | |||||
throw new BuildException("Listener class " + className + " is not of type " + TestExecutionListener.class.getName()); | |||||
} | |||||
try { | |||||
return TestExecutionListener.class.cast(klass.newInstance()); | |||||
} catch (Exception e) { | |||||
throw new BuildException("Failed to create an instance of listener " + className, e); | |||||
} | |||||
} | |||||
private void handleTestExecutionCompletion(final TestDefinition test, final TestExecutionSummary summary) { | |||||
if (this.launchDefinition.isPrintSummary()) { | |||||
// print the summary to System.out | |||||
summary.printTo(new PrintWriter(System.out, true)); | |||||
} | |||||
final boolean hasTestFailures = summary.getTestsFailedCount() != 0; | |||||
if (hasTestFailures) { | |||||
// keep track of the test failure(s) for the entire launched instance | |||||
this.testsFailed = true; | |||||
} | |||||
try { | |||||
if (hasTestFailures && test.getFailureProperty() != null) { | |||||
// if there are test failures and the test is configured to set a property in case | |||||
// of failure, then set the property to true | |||||
final TestExecutionContext testExecutionContext = this.launchDefinition.getTestExecutionContext(); | |||||
if (testExecutionContext.getProject().isPresent()) { | |||||
final Project project = testExecutionContext.getProject().get(); | |||||
project.setNewProperty(test.getFailureProperty(), "true"); | |||||
} | |||||
} | |||||
} finally { | |||||
if (hasTestFailures && test.isHaltOnFailure()) { | |||||
// if the test is configured to halt on test failures, throw a build error | |||||
final String errorMessage; | |||||
if (test instanceof NamedTest) { | |||||
errorMessage = "Test " + ((NamedTest) test).getName() + " has " + summary.getTestsFailedCount() + " failure(s)"; | |||||
} else { | |||||
errorMessage = "Some test(s) have failure(s)"; | |||||
} | |||||
throw new BuildException(errorMessage); | |||||
} | |||||
} | |||||
} | |||||
private Optional<SwitchedStreamHandle> trySwitchSysOutErr(final TestRequest testRequest, final StreamType streamType) { | |||||
switch (streamType) { | |||||
case SYS_OUT: { | |||||
if (!testRequest.interestedInSysOut()) { | |||||
return Optional.empty(); | |||||
} | |||||
break; | |||||
} | |||||
case SYS_ERR: { | |||||
if (!testRequest.interestedInSysErr()) { | |||||
return Optional.empty(); | |||||
} | |||||
break; | |||||
} | |||||
default: { | |||||
// unknown, but no need to error out, just be lenient | |||||
// and return back | |||||
return Optional.empty(); | |||||
} | |||||
} | |||||
final PipedOutputStream pipedOutputStream = new PipedOutputStream(); | |||||
final PipedInputStream pipedInputStream; | |||||
try { | |||||
pipedInputStream = new PipedInputStream(pipedOutputStream); | |||||
} catch (IOException ioe) { | |||||
// log and return | |||||
return Optional.empty(); | |||||
} | |||||
final PrintStream printStream = new PrintStream(pipedOutputStream, true); | |||||
final SysOutErrStreamReader streamer; | |||||
switch (streamType) { | |||||
case SYS_OUT: { | |||||
System.setOut(new PrintStream(printStream)); | |||||
streamer = new SysOutErrStreamReader(this, pipedInputStream, | |||||
StreamType.SYS_OUT, testRequest.getSysOutInterests()); | |||||
final Thread sysOutStreamer = new Thread(streamer); | |||||
sysOutStreamer.setDaemon(true); | |||||
sysOutStreamer.setName("junitlauncher-sysout-stream-reader"); | |||||
sysOutStreamer.setUncaughtExceptionHandler((t, e) -> this.log("Failed in sysout streaming", e, Project.MSG_INFO)); | |||||
sysOutStreamer.start(); | |||||
break; | |||||
} | |||||
case SYS_ERR: { | |||||
System.setErr(new PrintStream(printStream)); | |||||
streamer = new SysOutErrStreamReader(this, pipedInputStream, | |||||
StreamType.SYS_ERR, testRequest.getSysErrInterests()); | |||||
final Thread sysErrStreamer = new Thread(streamer); | |||||
sysErrStreamer.setDaemon(true); | |||||
sysErrStreamer.setName("junitlauncher-syserr-stream-reader"); | |||||
sysErrStreamer.setUncaughtExceptionHandler((t, e) -> this.log("Failed in syserr streaming", e, Project.MSG_INFO)); | |||||
sysErrStreamer.start(); | |||||
break; | |||||
} | |||||
default: { | |||||
return Optional.empty(); | |||||
} | |||||
} | |||||
return Optional.of(new SwitchedStreamHandle(pipedOutputStream, streamer)); | |||||
} | |||||
private void log(final String message, final Throwable t, final int level) { | |||||
final TestExecutionContext testExecutionContext = this.launchDefinition.getTestExecutionContext(); | |||||
if (testExecutionContext.getProject().isPresent()) { | |||||
testExecutionContext.getProject().get().log(message, t, level); | |||||
return; | |||||
} | |||||
if (t == null) { | |||||
System.out.println(message); | |||||
} else { | |||||
System.err.println(message); | |||||
t.printStackTrace(); | |||||
} | |||||
} | |||||
private enum StreamType { | |||||
SYS_OUT, | |||||
SYS_ERR | |||||
} | |||||
private static final class SysOutErrStreamReader implements Runnable { | |||||
private static final byte[] EMPTY = new byte[0]; | |||||
private final LauncherSupport launchManager; | |||||
private final InputStream sourceStream; | |||||
private final StreamType streamType; | |||||
private final Collection<TestResultFormatter> resultFormatters; | |||||
private volatile SysOutErrContentDeliverer contentDeliverer; | |||||
SysOutErrStreamReader(final LauncherSupport launchManager, final InputStream source, final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) { | |||||
this.launchManager = launchManager; | |||||
this.sourceStream = source; | |||||
this.streamType = streamType; | |||||
this.resultFormatters = resultFormatters; | |||||
} | |||||
@Override | |||||
public void run() { | |||||
final SysOutErrContentDeliverer streamContentDeliver = new SysOutErrContentDeliverer(this.streamType, this.resultFormatters); | |||||
final Thread deliveryThread = new Thread(streamContentDeliver); | |||||
deliveryThread.setName("junitlauncher-" + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + "-stream-deliverer"); | |||||
deliveryThread.setDaemon(true); | |||||
deliveryThread.start(); | |||||
this.contentDeliverer = streamContentDeliver; | |||||
int numRead = -1; | |||||
final byte[] data = new byte[1024]; | |||||
try { | |||||
while ((numRead = this.sourceStream.read(data)) != -1) { | |||||
final byte[] copy = Arrays.copyOf(data, numRead); | |||||
streamContentDeliver.availableData.offer(copy); | |||||
} | |||||
} catch (IOException e) { | |||||
this.launchManager.log("Failed while streaming " + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + " data", | |||||
e, Project.MSG_INFO); | |||||
} finally { | |||||
streamContentDeliver.stop = true; | |||||
// just "wakeup" the delivery thread, to take into account | |||||
// those race conditions, where that other thread didn't yet | |||||
// notice that it was asked to stop and has now gone into a | |||||
// X amount of wait, waiting for any new data | |||||
streamContentDeliver.availableData.offer(EMPTY); | |||||
} | |||||
} | |||||
} | |||||
private static final class SysOutErrContentDeliverer implements Runnable { | |||||
private volatile boolean stop; | |||||
private final Collection<TestResultFormatter> resultFormatters; | |||||
private final StreamType streamType; | |||||
private final BlockingQueue<byte[]> availableData = new LinkedBlockingQueue<>(); | |||||
private final CountDownLatch completionLatch = new CountDownLatch(1); | |||||
SysOutErrContentDeliverer(final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) { | |||||
this.streamType = streamType; | |||||
this.resultFormatters = resultFormatters; | |||||
} | |||||
@Override | |||||
public void run() { | |||||
try { | |||||
while (!this.stop) { | |||||
final byte[] streamData; | |||||
try { | |||||
streamData = this.availableData.poll(2, TimeUnit.SECONDS); | |||||
} catch (InterruptedException e) { | |||||
Thread.currentThread().interrupt(); | |||||
return; | |||||
} | |||||
if (streamData != null) { | |||||
deliver(streamData); | |||||
} | |||||
} | |||||
// drain it | |||||
final List<byte[]> remaining = new ArrayList<>(); | |||||
this.availableData.drainTo(remaining); | |||||
if (!remaining.isEmpty()) { | |||||
for (final byte[] data : remaining) { | |||||
deliver(data); | |||||
} | |||||
} | |||||
} finally { | |||||
this.completionLatch.countDown(); | |||||
} | |||||
} | |||||
private void deliver(final byte[] data) { | |||||
if (data == null || data.length == 0) { | |||||
return; | |||||
} | |||||
for (final TestResultFormatter resultFormatter : this.resultFormatters) { | |||||
// send it to the formatter | |||||
switch (streamType) { | |||||
case SYS_OUT: { | |||||
resultFormatter.sysOutAvailable(data); | |||||
break; | |||||
} | |||||
case SYS_ERR: { | |||||
resultFormatter.sysErrAvailable(data); | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
private final class SwitchedStreamHandle { | |||||
private final PipedOutputStream outputStream; | |||||
private final SysOutErrStreamReader streamReader; | |||||
SwitchedStreamHandle(final PipedOutputStream outputStream, final SysOutErrStreamReader streamReader) { | |||||
this.streamReader = streamReader; | |||||
this.outputStream = outputStream; | |||||
} | |||||
} | |||||
private final class Listener extends SummaryGeneratingListener { | |||||
private Optional<SwitchedStreamHandle> switchedSysOutHandle; | |||||
private Optional<SwitchedStreamHandle> switchedSysErrHandle; | |||||
@Override | |||||
public void testPlanExecutionFinished(final TestPlan testPlan) { | |||||
super.testPlanExecutionFinished(testPlan); | |||||
// now that the test plan execution is finished, close the switched sysout/syserr output streams | |||||
// and wait for the sysout and syserr content delivery, to result formatters, to finish | |||||
if (this.switchedSysOutHandle.isPresent()) { | |||||
final SwitchedStreamHandle sysOut = this.switchedSysOutHandle.get(); | |||||
try { | |||||
closeAndWait(sysOut); | |||||
} catch (InterruptedException e) { | |||||
Thread.currentThread().interrupt(); | |||||
return; | |||||
} | |||||
} | |||||
if (this.switchedSysErrHandle.isPresent()) { | |||||
final SwitchedStreamHandle sysErr = this.switchedSysErrHandle.get(); | |||||
try { | |||||
closeAndWait(sysErr); | |||||
} catch (InterruptedException e) { | |||||
Thread.currentThread().interrupt(); | |||||
} | |||||
} | |||||
} | |||||
private void closeAndWait(final SwitchedStreamHandle handle) throws InterruptedException { | |||||
FileUtils.close(handle.outputStream); | |||||
if (handle.streamReader.contentDeliverer == null) { | |||||
return; | |||||
} | |||||
// wait for a few seconds | |||||
handle.streamReader.contentDeliverer.completionLatch.await(2, TimeUnit.SECONDS); | |||||
} | |||||
} | |||||
} |
@@ -21,12 +21,24 @@ import org.apache.tools.ant.Project; | |||||
import org.apache.tools.ant.PropertyHelper; | import org.apache.tools.ant.PropertyHelper; | ||||
import org.apache.tools.ant.types.EnumeratedAttribute; | import org.apache.tools.ant.types.EnumeratedAttribute; | ||||
import javax.xml.stream.XMLStreamConstants; | |||||
import javax.xml.stream.XMLStreamException; | |||||
import javax.xml.stream.XMLStreamReader; | |||||
import javax.xml.stream.XMLStreamWriter; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_CLASS_NAME; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_LISTENER_RESULT_FILE; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_SEND_SYS_ERR; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_SEND_SYS_OUT; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LISTENER; | |||||
/** | /** | ||||
* Represents the {@code <listener>} element within the {@code <junitlauncher>} | * Represents the {@code <listener>} element within the {@code <junitlauncher>} | ||||
* task | * task | ||||
*/ | */ | ||||
public class ListenerDefinition { | public class ListenerDefinition { | ||||
private static final String LEGACY_PLAIN = "legacy-plain"; | private static final String LEGACY_PLAIN = "legacy-plain"; | ||||
private static final String LEGACY_BRIEF = "legacy-brief"; | private static final String LEGACY_BRIEF = "legacy-brief"; | ||||
private static final String LEGACY_XML = "legacy-xml"; | private static final String LEGACY_XML = "legacy-xml"; | ||||
@@ -135,4 +147,45 @@ public class ListenerDefinition { | |||||
} | } | ||||
} | } | ||||
void toForkedRepresentation(final XMLStreamWriter writer) throws XMLStreamException { | |||||
writer.writeStartElement(LD_XML_ELM_LISTENER); | |||||
writer.writeAttribute(LD_XML_ATTR_CLASS_NAME, this.className); | |||||
writer.writeAttribute(LD_XML_ATTR_SEND_SYS_ERR, Boolean.toString(this.sendSysErr)); | |||||
writer.writeAttribute(LD_XML_ATTR_SEND_SYS_OUT, Boolean.toString(this.sendSysOut)); | |||||
if (this.resultFile != null) { | |||||
writer.writeAttribute(LD_XML_ATTR_LISTENER_RESULT_FILE, this.resultFile); | |||||
} | |||||
writer.writeEndElement(); | |||||
} | |||||
static ListenerDefinition fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException { | |||||
reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_LISTENER); | |||||
final ListenerDefinition listenerDef = new ListenerDefinition(); | |||||
final String className = requireAttributeValue(reader, LD_XML_ATTR_CLASS_NAME); | |||||
listenerDef.setClassName(className); | |||||
final String sendSysErr = reader.getAttributeValue(null, LD_XML_ATTR_SEND_SYS_ERR); | |||||
if (sendSysErr != null) { | |||||
listenerDef.setSendSysErr(Boolean.parseBoolean(sendSysErr)); | |||||
} | |||||
final String sendSysOut = reader.getAttributeValue(null, LD_XML_ATTR_SEND_SYS_OUT); | |||||
if (sendSysOut != null) { | |||||
listenerDef.setSendSysOut(Boolean.parseBoolean(sendSysOut)); | |||||
} | |||||
final String resultFile = reader.getAttributeValue(null, LD_XML_ATTR_LISTENER_RESULT_FILE); | |||||
if (resultFile != null) { | |||||
listenerDef.setResultFile(resultFile); | |||||
} | |||||
reader.nextTag(); | |||||
reader.require(XMLStreamConstants.END_ELEMENT, null, LD_XML_ELM_LISTENER); | |||||
return listenerDef; | |||||
} | |||||
private static String requireAttributeValue(final XMLStreamReader reader, final String attrName) throws XMLStreamException { | |||||
final String val = reader.getAttributeValue(null, attrName); | |||||
if (val != null) { | |||||
return val; | |||||
} | |||||
throw new XMLStreamException("Attribute " + attrName + " is missing at " + reader.getLocation()); | |||||
} | |||||
} | } |
@@ -23,7 +23,6 @@ package org.apache.tools.ant.taskdefs.optional.junitlauncher; | |||||
public interface NamedTest { | public interface NamedTest { | ||||
/** | /** | ||||
* | |||||
* @return Returns the name of the test | * @return Returns the name of the test | ||||
*/ | */ | ||||
String getName(); | String getName(); | ||||
@@ -17,17 +17,28 @@ | |||||
*/ | */ | ||||
package org.apache.tools.ant.taskdefs.optional.junitlauncher; | package org.apache.tools.ant.taskdefs.optional.junitlauncher; | ||||
import org.apache.tools.ant.Project; | |||||
import org.junit.platform.engine.discovery.DiscoverySelectors; | import org.junit.platform.engine.discovery.DiscoverySelectors; | ||||
import org.junit.platform.launcher.EngineFilter; | import org.junit.platform.launcher.EngineFilter; | ||||
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; | import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; | ||||
import javax.xml.stream.XMLStreamConstants; | |||||
import javax.xml.stream.XMLStreamException; | |||||
import javax.xml.stream.XMLStreamReader; | |||||
import javax.xml.stream.XMLStreamWriter; | |||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.LinkedHashSet; | import java.util.LinkedHashSet; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.StringTokenizer; | import java.util.StringTokenizer; | ||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_CLASS_NAME; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_EXCLUDE_ENGINES; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_HALT_ON_FAILURE; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_INCLUDE_ENGINES; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_METHODS; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_OUTPUT_DIRECTORY; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST; | |||||
/** | /** | ||||
* Represents the single {@code test} (class) that's configured to be launched by the {@link JUnitLauncherTask} | * Represents the single {@code test} (class) that's configured to be launched by the {@link JUnitLauncherTask} | ||||
*/ | */ | ||||
@@ -85,13 +96,7 @@ public class SingleTestClass extends TestDefinition implements NamedTest { | |||||
} | } | ||||
@Override | @Override | ||||
List<TestRequest> createTestRequests(final JUnitLauncherTask launcherTask) { | |||||
final Project project = launcherTask.getProject(); | |||||
if (!shouldRun(project)) { | |||||
launcherTask.log("Excluding test " + this.testClass + " since it's considered not to run " + | |||||
"in context of project " + project, Project.MSG_DEBUG); | |||||
return Collections.emptyList(); | |||||
} | |||||
List<TestRequest> createTestRequests() { | |||||
final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request(); | final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request(); | ||||
if (!this.hasMethodsSpecified()) { | if (!this.hasMethodsSpecified()) { | ||||
requestBuilder.selectors(DiscoverySelectors.selectClass(this.testClass)); | requestBuilder.selectors(DiscoverySelectors.selectClass(this.testClass)); | ||||
@@ -112,4 +117,83 @@ public class SingleTestClass extends TestDefinition implements NamedTest { | |||||
} | } | ||||
return Collections.singletonList(new TestRequest(this, requestBuilder)); | return Collections.singletonList(new TestRequest(this, requestBuilder)); | ||||
} | } | ||||
@Override | |||||
protected void toForkedRepresentation(final JUnitLauncherTask task, final XMLStreamWriter writer) throws XMLStreamException { | |||||
writer.writeStartElement(LD_XML_ELM_TEST); | |||||
writer.writeAttribute(LD_XML_ATTR_CLASS_NAME, testClass); | |||||
if (testMethods != null) { | |||||
final StringBuilder sb = new StringBuilder(); | |||||
for (final String method : testMethods) { | |||||
if (sb.length() != 0) { | |||||
sb.append(","); | |||||
} | |||||
sb.append(method); | |||||
} | |||||
writer.writeAttribute(LD_XML_ATTR_METHODS, sb.toString()); | |||||
} | |||||
if (haltOnFailure != null) { | |||||
writer.writeAttribute(LD_XML_ATTR_HALT_ON_FAILURE, haltOnFailure.toString()); | |||||
} | |||||
if (outputDir != null) { | |||||
writer.writeAttribute(LD_XML_ATTR_OUTPUT_DIRECTORY, outputDir); | |||||
} | |||||
if (includeEngines != null) { | |||||
writer.writeAttribute(LD_XML_ATTR_INCLUDE_ENGINES, includeEngines); | |||||
} | |||||
if (excludeEngines != null) { | |||||
writer.writeAttribute(LD_XML_ATTR_EXCLUDE_ENGINES, excludeEngines); | |||||
} | |||||
// listeners for this test | |||||
if (listeners != null) { | |||||
for (final ListenerDefinition listenerDef : getListeners()) { | |||||
if (!listenerDef.shouldUse(task.getProject())) { | |||||
// not applicable | |||||
continue; | |||||
} | |||||
listenerDef.toForkedRepresentation(writer); | |||||
} | |||||
} | |||||
writer.writeEndElement(); | |||||
} | |||||
static TestDefinition fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException { | |||||
reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_TEST); | |||||
final SingleTestClass testDefinition = new SingleTestClass(); | |||||
final String testClassName = requireAttributeValue(reader, LD_XML_ATTR_CLASS_NAME); | |||||
testDefinition.setName(testClassName); | |||||
final String methodNames = reader.getAttributeValue(null, LD_XML_ATTR_METHODS); | |||||
if (methodNames != null) { | |||||
testDefinition.setMethods(methodNames); | |||||
} | |||||
final String halt = reader.getAttributeValue(null, LD_XML_ATTR_HALT_ON_FAILURE); | |||||
if (halt != null) { | |||||
testDefinition.setHaltOnFailure(Boolean.parseBoolean(halt)); | |||||
} | |||||
final String outDir = reader.getAttributeValue(null, LD_XML_ATTR_OUTPUT_DIRECTORY); | |||||
if (outDir != null) { | |||||
testDefinition.setOutputDir(outDir); | |||||
} | |||||
final String includeEngs = reader.getAttributeValue(null, LD_XML_ATTR_INCLUDE_ENGINES); | |||||
if (includeEngs != null) { | |||||
testDefinition.setIncludeEngines(includeEngs); | |||||
} | |||||
final String excludeEngs = reader.getAttributeValue(null, LD_XML_ATTR_EXCLUDE_ENGINES); | |||||
if (excludeEngs != null) { | |||||
testDefinition.setExcludeEngines(excludeEngs); | |||||
} | |||||
while (reader.nextTag() != XMLStreamConstants.END_ELEMENT) { | |||||
reader.require(XMLStreamConstants.START_ELEMENT, null, Constants.LD_XML_ELM_LISTENER); | |||||
testDefinition.addConfiguredListener(ListenerDefinition.fromForkedRepresentation(reader)); | |||||
} | |||||
return testDefinition; | |||||
} | |||||
private static String requireAttributeValue(final XMLStreamReader reader, final String attrName) throws XMLStreamException { | |||||
final String val = reader.getAttributeValue(null, attrName); | |||||
if (val != null) { | |||||
return val; | |||||
} | |||||
throw new XMLStreamException("Attribute " + attrName + " is missing at " + reader.getLocation()); | |||||
} | |||||
} | } |
@@ -0,0 +1,259 @@ | |||||
/* | |||||
* 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.junitlauncher; | |||||
import org.apache.tools.ant.BuildException; | |||||
import org.apache.tools.ant.Project; | |||||
import javax.xml.stream.XMLInputFactory; | |||||
import javax.xml.stream.XMLStreamReader; | |||||
import java.io.InputStream; | |||||
import java.nio.file.Files; | |||||
import java.nio.file.Path; | |||||
import java.nio.file.Paths; | |||||
import java.util.ArrayList; | |||||
import java.util.Collections; | |||||
import java.util.List; | |||||
import java.util.Optional; | |||||
import java.util.Properties; | |||||
import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT; | |||||
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; | |||||
import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT; | |||||
import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_HALT_ON_FAILURE; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ATTR_PRINT_SUMMARY; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LAUNCH_DEF; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_LISTENER; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST; | |||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST_CLASSES; | |||||
/** | |||||
* Used for launching forked tests from the {@link JUnitLauncherTask}. | |||||
* <p> | |||||
* Although this class is public, this isn't meant for external use. The contract of what | |||||
* program arguments {@link #main(String[]) the main method} accepts and how it interprets it, | |||||
* is also an internal detail and can change across Ant releases. | |||||
* | |||||
* @since Ant 1.10.6 | |||||
*/ | |||||
public class StandaloneLauncher { | |||||
/** | |||||
* Entry point to launching the forked test. | |||||
* | |||||
* @param args The arguments passed to this program for launching the tests | |||||
* @throws Exception | |||||
*/ | |||||
public static void main(final String[] args) throws Exception { | |||||
// The main responsibility of this entry point is to create a LaunchDefinition, | |||||
// by parsing the passed arguments and then use the LauncherSupport to | |||||
// LauncherSupport#launch the tests | |||||
try { | |||||
ForkedLaunch launchDefinition = null; | |||||
final ForkedExecution forkedExecution = new ForkedExecution(); | |||||
for (int i = 0; i < args.length; ) { | |||||
final String arg = args[i]; | |||||
int numArgsConsumed = 1; | |||||
switch (arg) { | |||||
case Constants.ARG_PROPERTIES: { | |||||
final Path propsPath = Paths.get(args[i + 1]); | |||||
if (!Files.isRegularFile(propsPath)) { | |||||
throw new IllegalArgumentException(propsPath + " does not point to a properties file"); | |||||
} | |||||
final Properties properties = new Properties(); | |||||
try (final InputStream is = Files.newInputStream(propsPath)) { | |||||
properties.load(is); | |||||
} | |||||
forkedExecution.setProperties(properties); | |||||
numArgsConsumed = 2; | |||||
break; | |||||
} | |||||
case Constants.ARG_LAUNCH_DEFINITION: { | |||||
final Path launchDefXmlPath = Paths.get(args[i + 1]); | |||||
if (!Files.isRegularFile(launchDefXmlPath)) { | |||||
throw new IllegalArgumentException(launchDefXmlPath + " does not point to a launch definition file"); | |||||
} | |||||
launchDefinition = parseLaunchDefinition(launchDefXmlPath); | |||||
numArgsConsumed = 2; | |||||
break; | |||||
} | |||||
} | |||||
i = i + numArgsConsumed; | |||||
} | |||||
launchDefinition.setTestExecutionContext(forkedExecution); | |||||
final LauncherSupport launcherSupport = new LauncherSupport(launchDefinition); | |||||
try { | |||||
launcherSupport.launch(); | |||||
} catch (Throwable t) { | |||||
if (launcherSupport.hasTestFailures()) { | |||||
System.exit(Constants.FORK_EXIT_CODE_TESTS_FAILED); | |||||
throw t; | |||||
} | |||||
} | |||||
if (launcherSupport.hasTestFailures()) { | |||||
System.exit(Constants.FORK_EXIT_CODE_TESTS_FAILED); | |||||
return; | |||||
} | |||||
System.exit(Constants.FORK_EXIT_CODE_SUCCESS); | |||||
return; | |||||
} catch (Throwable t) { | |||||
t.printStackTrace(); | |||||
throw t; | |||||
} | |||||
} | |||||
private static ForkedLaunch parseLaunchDefinition(final Path pathToLaunchDefXml) { | |||||
if (pathToLaunchDefXml == null || !Files.isRegularFile(pathToLaunchDefXml)) { | |||||
throw new IllegalArgumentException(pathToLaunchDefXml + " is not a file"); | |||||
} | |||||
final ForkedLaunch forkedLaunch = new ForkedLaunch(); | |||||
try (final InputStream is = Files.newInputStream(pathToLaunchDefXml)) { | |||||
final XMLStreamReader reader = XMLInputFactory.newFactory().createXMLStreamReader(is); | |||||
reader.require(START_DOCUMENT, null, null); | |||||
reader.nextTag(); | |||||
reader.require(START_ELEMENT, null, LD_XML_ELM_LAUNCH_DEF); | |||||
final String haltOnfFailure = reader.getAttributeValue(null, LD_XML_ATTR_HALT_ON_FAILURE); | |||||
if (haltOnfFailure != null) { | |||||
forkedLaunch.setHaltOnFailure(Boolean.parseBoolean(haltOnfFailure)); | |||||
} | |||||
final String printSummary = reader.getAttributeValue(null, LD_XML_ATTR_PRINT_SUMMARY); | |||||
if (printSummary != null) { | |||||
forkedLaunch.setPrintSummary(Boolean.parseBoolean(printSummary)); | |||||
} | |||||
if (haltOnfFailure != null) { | |||||
forkedLaunch.setHaltOnFailure(Boolean.parseBoolean(haltOnfFailure)); | |||||
} | |||||
reader.nextTag(); | |||||
reader.require(START_ELEMENT, null, null); | |||||
final String elementName = reader.getLocalName(); | |||||
switch (elementName) { | |||||
case LD_XML_ELM_TEST: { | |||||
forkedLaunch.addTests(Collections.singletonList(SingleTestClass.fromForkedRepresentation(reader))); | |||||
break; | |||||
} | |||||
case LD_XML_ELM_TEST_CLASSES: { | |||||
forkedLaunch.addTests(TestClasses.fromForkedRepresentation(reader)); | |||||
break; | |||||
} | |||||
case LD_XML_ELM_LISTENER: { | |||||
forkedLaunch.addListener(ListenerDefinition.fromForkedRepresentation(reader)); | |||||
break; | |||||
} | |||||
} | |||||
reader.nextTag(); | |||||
reader.require(END_ELEMENT, null, LD_XML_ELM_LAUNCH_DEF); | |||||
reader.next(); | |||||
reader.require(END_DOCUMENT, null, null); | |||||
return forkedLaunch; | |||||
} catch (Exception e) { | |||||
throw new BuildException("Failed to construct definition from forked representation", e); | |||||
} | |||||
} | |||||
private static final class ForkedExecution implements TestExecutionContext { | |||||
private Properties properties = new Properties(); | |||||
private ForkedExecution() { | |||||
} | |||||
private ForkedExecution setProperties(final Properties properties) { | |||||
this.properties = properties; | |||||
return this; | |||||
} | |||||
@Override | |||||
public Properties getProperties() { | |||||
return this.properties; | |||||
} | |||||
@Override | |||||
public Optional<Project> getProject() { | |||||
// forked execution won't have access to the Ant Project | |||||
return Optional.empty(); | |||||
} | |||||
} | |||||
private static final class ForkedLaunch implements LaunchDefinition { | |||||
private boolean printSummary; | |||||
private boolean haltOnFailure; | |||||
private TestExecutionContext testExecutionContext; | |||||
private List<TestDefinition> tests = new ArrayList<>(); | |||||
private List<ListenerDefinition> listeners = new ArrayList<>(); | |||||
@Override | |||||
public List<TestDefinition> getTests() { | |||||
return this.tests; | |||||
} | |||||
ForkedLaunch addTests(final List<TestDefinition> tests) { | |||||
this.tests.addAll(tests); | |||||
return this; | |||||
} | |||||
@Override | |||||
public List<ListenerDefinition> getListeners() { | |||||
return this.listeners; | |||||
} | |||||
ForkedLaunch addListener(final ListenerDefinition listener) { | |||||
this.listeners.add(listener); | |||||
return this; | |||||
} | |||||
@Override | |||||
public boolean isPrintSummary() { | |||||
return this.printSummary; | |||||
} | |||||
private ForkedLaunch setPrintSummary(final boolean printSummary) { | |||||
this.printSummary = printSummary; | |||||
return this; | |||||
} | |||||
@Override | |||||
public boolean isHaltOnFailure() { | |||||
return this.haltOnFailure; | |||||
} | |||||
public ForkedLaunch setHaltOnFailure(final boolean haltOnFailure) { | |||||
this.haltOnFailure = haltOnFailure; | |||||
return this; | |||||
} | |||||
public ForkedLaunch setTestExecutionContext(final TestExecutionContext testExecutionContext) { | |||||
this.testExecutionContext = testExecutionContext; | |||||
return this; | |||||
} | |||||
@Override | |||||
public ClassLoader getClassLoader() { | |||||
return this.getClass().getClassLoader(); | |||||
} | |||||
@Override | |||||
public TestExecutionContext getTestExecutionContext() { | |||||
return this.testExecutionContext; | |||||
} | |||||
} | |||||
} |
@@ -21,11 +21,17 @@ import org.apache.tools.ant.types.Resource; | |||||
import org.apache.tools.ant.types.ResourceCollection; | import org.apache.tools.ant.types.ResourceCollection; | ||||
import org.apache.tools.ant.types.resources.Resources; | import org.apache.tools.ant.types.resources.Resources; | ||||
import javax.xml.stream.XMLStreamConstants; | |||||
import javax.xml.stream.XMLStreamException; | |||||
import javax.xml.stream.XMLStreamReader; | |||||
import javax.xml.stream.XMLStreamWriter; | |||||
import java.io.File; | import java.io.File; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.List; | import java.util.List; | ||||
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.Constants.LD_XML_ELM_TEST_CLASSES; | |||||
/** | /** | ||||
* Represents a {@code testclasses} that's configured to be launched by the {@link JUnitLauncherTask} | * Represents a {@code testclasses} that's configured to be launched by the {@link JUnitLauncherTask} | ||||
*/ | */ | ||||
@@ -42,14 +48,14 @@ public class TestClasses extends TestDefinition { | |||||
} | } | ||||
@Override | @Override | ||||
List<TestRequest> createTestRequests(final JUnitLauncherTask launcherTask) { | |||||
List<TestRequest> createTestRequests() { | |||||
final List<SingleTestClass> tests = this.getTests(); | final List<SingleTestClass> tests = this.getTests(); | ||||
if (tests.isEmpty()) { | if (tests.isEmpty()) { | ||||
return Collections.emptyList(); | return Collections.emptyList(); | ||||
} | } | ||||
final List<TestRequest> requests = new ArrayList<>(); | final List<TestRequest> requests = new ArrayList<>(); | ||||
for (final SingleTestClass test : tests) { | for (final SingleTestClass test : tests) { | ||||
requests.addAll(test.createTestRequests(launcherTask)); | |||||
requests.addAll(test.createTestRequests()); | |||||
} | } | ||||
return requests; | return requests; | ||||
} | } | ||||
@@ -126,4 +132,26 @@ public class TestClasses extends TestDefinition { | |||||
return TestClasses.this.getExcludeEngines(); | return TestClasses.this.getExcludeEngines(); | ||||
} | } | ||||
} | } | ||||
@Override | |||||
protected void toForkedRepresentation(final JUnitLauncherTask task, final XMLStreamWriter writer) throws XMLStreamException { | |||||
writer.writeStartElement(LD_XML_ELM_TEST_CLASSES); | |||||
// write out as multiple SingleTestClass representations | |||||
for (final SingleTestClass singleTestClass : getTests()) { | |||||
singleTestClass.toForkedRepresentation(task, writer); | |||||
} | |||||
writer.writeEndElement(); | |||||
} | |||||
static List<TestDefinition> fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException { | |||||
reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_TEST_CLASSES); | |||||
final List<TestDefinition> testDefinitions = new ArrayList<>(); | |||||
// read out as multiple SingleTestClass representations | |||||
while (reader.nextTag() != XMLStreamConstants.END_ELEMENT) { | |||||
reader.require(XMLStreamConstants.START_ELEMENT, null, Constants.LD_XML_ELM_TEST); | |||||
testDefinitions.add(SingleTestClass.fromForkedRepresentation(reader)); | |||||
} | |||||
reader.require(XMLStreamConstants.END_ELEMENT, null, LD_XML_ELM_TEST_CLASSES); | |||||
return testDefinitions; | |||||
} | |||||
} | } |
@@ -17,9 +17,12 @@ | |||||
*/ | */ | ||||
package org.apache.tools.ant.taskdefs.optional.junitlauncher; | package org.apache.tools.ant.taskdefs.optional.junitlauncher; | ||||
import org.apache.tools.ant.BuildException; | |||||
import org.apache.tools.ant.Project; | import org.apache.tools.ant.Project; | ||||
import org.apache.tools.ant.PropertyHelper; | import org.apache.tools.ant.PropertyHelper; | ||||
import javax.xml.stream.XMLStreamException; | |||||
import javax.xml.stream.XMLStreamWriter; | |||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.List; | import java.util.List; | ||||
@@ -28,6 +31,7 @@ import java.util.List; | |||||
* Represents the configuration details of a test that needs to be launched by the {@link JUnitLauncherTask} | * Represents the configuration details of a test that needs to be launched by the {@link JUnitLauncherTask} | ||||
*/ | */ | ||||
abstract class TestDefinition { | abstract class TestDefinition { | ||||
protected String ifProperty; | protected String ifProperty; | ||||
protected String unlessProperty; | protected String unlessProperty; | ||||
protected Boolean haltOnFailure; | protected Boolean haltOnFailure; | ||||
@@ -35,6 +39,7 @@ abstract class TestDefinition { | |||||
protected String outputDir; | protected String outputDir; | ||||
protected String includeEngines; | protected String includeEngines; | ||||
protected String excludeEngines; | protected String excludeEngines; | ||||
protected ForkDefinition forkDefinition; | |||||
protected List<ListenerDefinition> listeners = new ArrayList<>(); | protected List<ListenerDefinition> listeners = new ArrayList<>(); | ||||
@@ -90,7 +95,19 @@ abstract class TestDefinition { | |||||
return this.outputDir; | return this.outputDir; | ||||
} | } | ||||
abstract List<TestRequest> createTestRequests(final JUnitLauncherTask launcherTask); | |||||
public ForkDefinition createFork() { | |||||
if (this.forkDefinition != null) { | |||||
throw new BuildException("Test definition cannot have more than one fork elements"); | |||||
} | |||||
this.forkDefinition = new ForkDefinition(); | |||||
return this.forkDefinition; | |||||
} | |||||
ForkDefinition getForkDefinition() { | |||||
return this.forkDefinition; | |||||
} | |||||
abstract List<TestRequest> createTestRequests(); | |||||
protected boolean shouldRun(final Project project) { | protected boolean shouldRun(final Project project) { | ||||
final PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper(project); | final PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper(project); | ||||
@@ -127,4 +144,7 @@ abstract class TestDefinition { | |||||
} | } | ||||
return parts.toArray(new String[parts.size()]); | return parts.toArray(new String[parts.size()]); | ||||
} | } | ||||
protected abstract void toForkedRepresentation(JUnitLauncherTask task, XMLStreamWriter writer) throws XMLStreamException; | |||||
} | } |
@@ -125,4 +125,13 @@ public class JUnitLauncherTaskTest { | |||||
public void testTestClasses() { | public void testTestClasses() { | ||||
buildRule.executeTarget("test-batch"); | buildRule.executeTarget("test-batch"); | ||||
} | } | ||||
/** | |||||
* Tests the execution of a forked test | |||||
*/ | |||||
@Test | |||||
public void testBasicFork() { | |||||
buildRule.executeTarget("test-basic-fork"); | |||||
} | |||||
} | } |