Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r169626791
  
    --- 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 {
    +                closeable.close();
    +            } catch (Exception e) {
    +                // ignore
    +            }
    +        }
    +    }
    +
    +    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 == null ? 
Collections.emptyList() : resultFormatters;
    +        }
    +
    +        @Override
    +        public void run() {
    +            if (this.resultFormatters.isEmpty()) {
    +                // no one to feed the stream content to
    --- End diff --
    
    This check is actually "dead code", in the sense that this will never be 
true. I wanted to avoid running these threads when there's no result formatter 
interested in the sysout/syserr content. But that check obviously needs to 
happen before the thread is even created and in fact, there's already such a 
check in the `trySwitchSysOut` and `trySwitchSysErr` methods (the place where 
this thread gets created).
    
    So I've now updated the PR to remove this check.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@ant.apache.org
For additional commands, e-mail: dev-h...@ant.apache.org

Reply via email to