Browse Source

Support for fork mode in junitlauncher

master
Jaikiran Pai 6 years ago
parent
commit
c9ca84fd53
13 changed files with 1457 additions and 381 deletions
  1. +12
    -0
      src/etc/testcases/taskdefs/optional/junitlauncher.xml
  2. +54
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java
  3. +156
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java
  4. +183
    -369
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
  5. +75
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java
  6. +513
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java
  7. +53
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java
  8. +0
    -1
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java
  9. +92
    -8
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java
  10. +259
    -0
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/StandaloneLauncher.java
  11. +30
    -2
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java
  12. +21
    -1
      src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java
  13. +9
    -0
      src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java

+ 12
- 0
src/etc/testcases/taskdefs/optional/junitlauncher.xml View File

@@ -30,6 +30,8 @@

<path id="junit.engine.vintage.classpath">
<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 id="junit.engine.jupiter.classpath">
@@ -109,5 +111,15 @@
</testclasses>
</junitlauncher>
</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>


+ 54
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/Constants.java View File

@@ -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() {

}
}

+ 156
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ForkDefinition.java View File

@@ -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";
}

}

+ 183
- 369
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java View File

@@ -21,37 +21,32 @@ 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.CommandlineJava;
import org.apache.tools.ant.types.Environment;
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.InputStream;
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.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Optional;
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.
@@ -84,55 +79,22 @@ public class JUnitLauncherTask extends Task {

@Override
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 {
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;
}
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;
}
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
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
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
public Properties getProperties() {
return this.props;
public ClassLoader getClassLoader() {
return this.executionCL;
}

@Override
public Optional<Project> getProject() {
return Optional.of(JUnitLauncherTask.this.getProject());
public TestExecutionContext getTestExecutionContext() {
return this.testExecutionContext;
}
}
}

+ 75
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LaunchDefinition.java View File

@@ -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();
}

+ 513
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LauncherSupport.java View File

@@ -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);
}
}

}

+ 53
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/ListenerDefinition.java View File

@@ -21,12 +21,24 @@ import org.apache.tools.ant.Project;
import org.apache.tools.ant.PropertyHelper;
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 &lt;listener&gt;} element within the {@code &lt;junitlauncher&gt;}
* task
*/
public class ListenerDefinition {


private static final String LEGACY_PLAIN = "legacy-plain";
private static final String LEGACY_BRIEF = "legacy-brief";
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());
}

}

+ 0
- 1
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/NamedTest.java View File

@@ -23,7 +23,6 @@ package org.apache.tools.ant.taskdefs.optional.junitlauncher;
public interface NamedTest {

/**
*
* @return Returns the name of the test
*/
String getName();


+ 92
- 8
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/SingleTestClass.java View File

@@ -17,17 +17,28 @@
*/
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.launcher.EngineFilter;
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.LinkedHashSet;
import java.util.List;
import java.util.Set;
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}
*/
@@ -85,13 +96,7 @@ public class SingleTestClass extends TestDefinition implements NamedTest {
}

@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();
if (!this.hasMethodsSpecified()) {
requestBuilder.selectors(DiscoverySelectors.selectClass(this.testClass));
@@ -112,4 +117,83 @@ public class SingleTestClass extends TestDefinition implements NamedTest {
}
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());
}
}

+ 259
- 0
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/StandaloneLauncher.java View File

@@ -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;
}
}
}

+ 30
- 2
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestClasses.java View File

@@ -21,11 +21,17 @@ import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
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.util.ArrayList;
import java.util.Collections;
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}
*/
@@ -42,14 +48,14 @@ public class TestClasses extends TestDefinition {
}

@Override
List<TestRequest> createTestRequests(final JUnitLauncherTask launcherTask) {
List<TestRequest> createTestRequests() {
final List<SingleTestClass> tests = this.getTests();
if (tests.isEmpty()) {
return Collections.emptyList();
}
final List<TestRequest> requests = new ArrayList<>();
for (final SingleTestClass test : tests) {
requests.addAll(test.createTestRequests(launcherTask));
requests.addAll(test.createTestRequests());
}
return requests;
}
@@ -126,4 +132,26 @@ public class TestClasses extends TestDefinition {
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;
}
}

+ 21
- 1
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/TestDefinition.java View File

@@ -17,9 +17,12 @@
*/
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.PropertyHelper;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
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}
*/
abstract class TestDefinition {

protected String ifProperty;
protected String unlessProperty;
protected Boolean haltOnFailure;
@@ -35,6 +39,7 @@ abstract class TestDefinition {
protected String outputDir;
protected String includeEngines;
protected String excludeEngines;
protected ForkDefinition forkDefinition;

protected List<ListenerDefinition> listeners = new ArrayList<>();

@@ -90,7 +95,19 @@ abstract class TestDefinition {
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) {
final PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper(project);
@@ -127,4 +144,7 @@ abstract class TestDefinition {
}
return parts.toArray(new String[parts.size()]);
}

protected abstract void toForkedRepresentation(JUnitLauncherTask task, XMLStreamWriter writer) throws XMLStreamException;

}

+ 9
- 0
src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTaskTest.java View File

@@ -125,4 +125,13 @@ public class JUnitLauncherTaskTest {
public void testTestClasses() {
buildRule.executeTarget("test-batch");
}

/**
* Tests the execution of a forked test
*/
@Test
public void testBasicFork() {
buildRule.executeTarget("test-basic-fork");

}
}

Loading…
Cancel
Save