Github user jaikiran commented on a diff in the pull request:
https://github.com/apache/ant/pull/60#discussion_r169624865
--- Diff:
src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/JUnitLauncherTask.java
---
@@ -0,0 +1,508 @@
+package org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+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.types.Path;
+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.Closeable;
+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.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+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;
+
+/**
+ * An Ant {@link Task} responsible for launching the JUnit platform for
running tests.
+ * This requires a minimum of JUnit 5, since that's the version in which
the JUnit platform launcher
+ * APIs were introduced.
+ * <p>
+ * This task in itself doesn't run the JUnit tests, instead the sole
responsibility of
+ * this task is to setup the JUnit platform launcher, build requests,
launch those requests and then parse the
+ * result of the execution to present in a way that's been configured on
this Ant task.
+ * </p>
+ * <p>
+ * Furthermore, this task allows users control over which classes to
select for passing on to the JUnit 5
+ * platform for test execution. It however, is solely the JUnit 5
platform, backed by test engines that
+ * decide and execute the tests.
+ *
+ * @see <a href="https://junit.org/junit5/">JUnit 5 documentation</a> for
more details
+ * on how JUnit manages the platform and the test engines.
+ */
+public class JUnitLauncherTask extends Task {
+
+ private Path classPath;
+ private boolean haltOnFailure;
+ private String failureProperty;
+ private final List<TestDefinition> tests = new ArrayList<>();
+ private final List<ListenerDefinition> listeners = new ArrayList<>();
+
+ public JUnitLauncherTask() {
+ }
+
+ @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 =
trySwitchSysOut(testRequest);
+ firstListener.switchedSysErrHandle =
trySwitchSysErr(testRequest);
+ 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);
+ }
+ }
+
+ /**
+ * @return Creates and returns the a {@link Path} which will be used
as the classpath of this
+ * task. This classpath will then be used for execution of the tests
+ */
+ public Path createClassPath() {
+ this.classPath = new Path(getProject());
+ return this.classPath;
+ }
+
+ /**
+ * @return Creates and returns a {@link SingleTestClass}. This test
will be considered part of the
+ * tests that will be passed on to the underlying JUnit platform for
possible execution of the test
+ */
+ public SingleTestClass createTest() {
+ final SingleTestClass test = new SingleTestClass();
+ this.preConfigure(test);
+ this.tests.add(test);
+ return test;
+ }
+
+ /**
+ * @return Creates and returns a {@link TestClasses}. The {@link
TestClasses#getTests() tests} that belong to it,
+ * will be passed on to the underlying JUnit platform for possible
execution of the tests
+ */
+ public TestClasses createTestClasses() {
+ final TestClasses batch = new TestClasses();
+ this.preConfigure(batch);
+ this.tests.add(batch);
+ return batch;
+ }
+
+ public ListenerDefinition createListener() {
+ final ListenerDefinition listener = new ListenerDefinition();
+ this.listeners.add(listener);
+ return listener;
+ }
+
+ public void setHaltonfailure(final boolean haltonfailure) {
+ this.haltOnFailure = haltonfailure;
+ }
+
+ public void setFailureProperty(final String failureProperty) {
+ this.failureProperty = failureProperty;
+ }
+
+ private void preConfigure(final TestDefinition test) {
+ test.setHaltOnFailure(this.haltOnFailure);
+ test.setFailureProperty(this.failureProperty);
+ }
+
+ 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);
+ }
+ 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.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 executing task
+ resultFormatter.setExecutingTask(this);
+ // 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 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) {
+ 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)";
+ }
+ 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);
+ }
+
+ private Optional<SwitchedStreamHandle> trySwitchSysOut(final
TestRequest testRequest) {
+ if (!testRequest.interestedInSysOut()) {
+ 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);
+ System.setOut(new PrintStream(printStream));
+
+ final SysOutErrStreamReader 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();
+ return Optional.of(new SwitchedStreamHandle(pipedOutputStream,
streamer));
+ }
+
+ private Optional<SwitchedStreamHandle> trySwitchSysErr(final
TestRequest testRequest) {
+ if (!testRequest.interestedInSysErr()) {
+ 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);
+ System.setErr(new PrintStream(printStream));
+
+ final SysOutErrStreamReader 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();
+ return Optional.of(new SwitchedStreamHandle(pipedOutputStream,
streamer));
+ }
+
+ private static void safeClose(final Closeable... closeables) {
+ for (final Closeable closeable : closeables) {
+ try {
--- End diff --
Fixed.
---
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]