ant-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jaikiran <...@git.apache.org>
Subject [GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task
Date Wed, 21 Feb 2018 12:54:09 GMT
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-unsubscribe@ant.apache.org
For additional commands, e-mail: dev-help@ant.apache.org


Mime
View raw message