You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ant.apache.org by jaikiran <gi...@git.apache.org> on 2018/02/15 13:49:12 UTC

[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

GitHub user jaikiran opened a pull request:

    https://github.com/apache/ant/pull/60

    JUnit 5 support - A new junitlauncher task

    This is the initial working version of a new `junitlauncher` task that support using JUnit 5 framework for testing, within Ant build files. The commit in this PR is the initial set of goals that I had in mind for the first release of this task.
    
    The manual for this task can be (temporarily) found at https://builds.apache.org/job/Ant-Build-Jaikiran/ws/manual/Tasks/junitlauncher.html

You can merge this pull request into a Git repository by running:

    $ git pull https://github.com/jaikiran/ant junit5

Alternatively you can review and apply these changes as the patch at:

    https://github.com/apache/ant/pull/60.patch

To close this pull request, make a commit to your master/trunk branch
with (at least) the following in the commit message:

    This closes #60
    
----
commit 991bddff07b4fed1db2dc7f6b83f47557e62213b
Author: Jaikiran Pai <ja...@...>
Date:   2017-12-13T13:37:41Z

    JUnit 5 support - A new junitlauncher task

----


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on the issue:

    https://github.com/apache/ant/pull/60
  
    Please ignore the Jenkins failures on this PR. I'll sort out that part in a separate discussion.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r169245116
  
    --- 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);
    --- End diff --
    
    `create*` is called before the attribute setters, If you call `preconfigure` here the test won't see the actual values set at the task level.
    
    `addConfigured` would get called after the attribute setters, alternatively you need to defer pre-configuration until `execute` is called. With both approaches you need to ensure you don't overwrite the test's explicit configuration with values from the task, of course.
    
    Same for `createTestClasses`.


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on the issue:

    https://github.com/apache/ant/pull/60
  
    Thank you Stefan for the thorough review. I'll clean up the commits to squash them into one and do these minor final changes before merging.



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/46/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/47/



---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r172043778
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,295 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.apache.tools.ant.util.FileUtils;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.BufferedReader;
    +import java.io.ByteArrayInputStream;
    +import java.io.Closeable;
    +import java.io.FileOutputStream;
    +import java.io.FileReader;
    +import java.io.IOException;
    +import java.io.InputStreamReader;
    +import java.io.Reader;
    +import java.io.Writer;
    +import java.nio.BufferOverflowException;
    +import java.nio.ByteBuffer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Objects;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Task task;
    +
    +    private SysOutErrContentStore sysOutStore;
    +    private SysOutErrContentStore sysErrStore;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStore == null) {
    +            this.sysOutStore = new SysOutErrContentStore(true);
    +        }
    +        try {
    +            this.sysOutStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStore == null) {
    +            this.sysErrStore = new SysOutErrContentStore(false);
    +        }
    +        try {
    +            this.sysErrStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void setExecutingTask(final Task task) {
    +        this.task = task;
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stdout data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysOut() {
    +        return this.sysOutStore != null && this.sysOutStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stderr data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysErr() {
    +        return this.sysErrStore != null && this.sysErrStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stdout data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysOut()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysOutReader() throws IOException {
    +        return this.sysOutStore.getReader();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stderr data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysErr()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysErrReader() throws IOException {
    +        return this.sysErrStore.getReader();
    +    }
    +
    +    /**
    +     * Writes out any stdout data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysOut(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysOutStore, writer);
    +    }
    +
    +    /**
    +     * Writes out any stderr data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysErr(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysErrStore, writer);
    +    }
    +
    +    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
    +        if (isTestClass(testIdentifier).isPresent()) {
    +            return Optional.of(testIdentifier);
    +        }
    +        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
    +        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
    +    }
    +
    +    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
    +        if (testIdentifier == null) {
    +            return Optional.empty();
    +        }
    +        final Optional<TestSource> source = testIdentifier.getSource();
    +        if (!source.isPresent()) {
    +            return Optional.empty();
    +        }
    +        final TestSource testSource = source.get();
    +        if (testSource instanceof ClassSource) {
    +            return Optional.of((ClassSource) testSource);
    +        }
    +        return Optional.empty();
    +    }
    +
    +    private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
    +        final char[] chars = new char[1024];
    +        int numRead = -1;
    +        try (final Reader reader = store.getReader()) {
    +            while ((numRead = reader.read(chars)) != -1) {
    +                writer.write(chars, 0, numRead);
    +            }
    +        }
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        FileUtils.close(this.sysOutStore);
    +        FileUtils.close(this.sysErrStore);
    +    }
    +
    +    protected void handleException(final Throwable t) {
    +        // we currently just log it and move on.
    +        task.getProject().log("Exception in listener " + this.getClass().getName(), t, Project.MSG_DEBUG);
    +    }
    +
    +
    +    /*
    +    A "store" for sysout/syserr content that gets sent to the AbstractJUnitResultFormatter.
    +    This store first uses a relatively decent sized in-memory buffer for storing the sysout/syserr
    +    content. This in-memory buffer will be used as long as it can fit in the new content that
    +    keeps coming in. When the size limit is reached, this store switches to a file based store
    +    by creating a temporarily file and writing out the already in-memory held buffer content
    +    and any new content that keeps arriving to this store. Once the file has been created,
    +    the in-memory buffer will never be used any more and in fact is destroyed as soon as the
    +    file is created.
    +    Instances of this class are not thread-safe and users of this class are expected to use necessary thread
    +    safety guarantees, if they want to use an instance of this class by multiple threads.
    +    */
    +    private static final class SysOutErrContentStore implements Closeable {
    +        private static final int DEFAULT_CAPACITY_IN_BYTES = 50 * 1024; // 50 KB
    +        private static final Reader EMPTY_READER = new Reader() {
    +            @Override
    +            public int read(final char[] cbuf, final int off, final int len) throws IOException {
    +                return -1;
    +            }
    +
    +            @Override
    +            public void close() throws IOException {
    +            }
    +        };
    +
    +        private final String tmpFileSuffix;
    +        private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
    +        private boolean usingFileStore = false;
    +        private Path filePath;
    +        private FileOutputStream fileOutputStream;
    +
    +        private SysOutErrContentStore(final boolean isSysOut) {
    +            this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
    +        }
    +
    +        private void store(final byte[] data) throws IOException {
    +            if (this.usingFileStore) {
    +                this.storeToFile(data, 0, data.length);
    +                return;
    +            }
    +            // we haven't yet created a file store and the data can fit in memory,
    +            // so we write it in our buffer
    +            try {
    +                this.inMemoryStore.put(data);
    +                return;
    +            } catch (BufferOverflowException boe) {
    +                // the buffer capacity can't hold this incoming data, so this
    +                // incoming data hasn't been transferred to the buffer. let's
    +                // now fall back to a file store
    +                this.usingFileStore = true;
    --- End diff --
    
    maybe defer setting the flag until `storeToFile` below succeeds?
    
    Also, is there any chance this method could be invoked concurrently? I haven't fully checked every execution path so maybe you are synchronizing somewhere. Tests could spawn new threads you are not aware of and these threads could be writing to syserr/out concurrently.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r172044717
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,295 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.apache.tools.ant.util.FileUtils;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.BufferedReader;
    +import java.io.ByteArrayInputStream;
    +import java.io.Closeable;
    +import java.io.FileOutputStream;
    +import java.io.FileReader;
    +import java.io.IOException;
    +import java.io.InputStreamReader;
    +import java.io.Reader;
    +import java.io.Writer;
    +import java.nio.BufferOverflowException;
    +import java.nio.ByteBuffer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Objects;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Task task;
    +
    +    private SysOutErrContentStore sysOutStore;
    +    private SysOutErrContentStore sysErrStore;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStore == null) {
    +            this.sysOutStore = new SysOutErrContentStore(true);
    +        }
    +        try {
    +            this.sysOutStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStore == null) {
    +            this.sysErrStore = new SysOutErrContentStore(false);
    +        }
    +        try {
    +            this.sysErrStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void setExecutingTask(final Task task) {
    +        this.task = task;
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stdout data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysOut() {
    +        return this.sysOutStore != null && this.sysOutStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stderr data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysErr() {
    +        return this.sysErrStore != null && this.sysErrStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stdout data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysOut()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysOutReader() throws IOException {
    +        return this.sysOutStore.getReader();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stderr data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysErr()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysErrReader() throws IOException {
    +        return this.sysErrStore.getReader();
    +    }
    +
    +    /**
    +     * Writes out any stdout data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysOut(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysOutStore, writer);
    +    }
    +
    +    /**
    +     * Writes out any stderr data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysErr(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysErrStore, writer);
    +    }
    +
    +    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
    +        if (isTestClass(testIdentifier).isPresent()) {
    +            return Optional.of(testIdentifier);
    +        }
    +        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
    +        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
    +    }
    +
    +    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
    +        if (testIdentifier == null) {
    +            return Optional.empty();
    +        }
    +        final Optional<TestSource> source = testIdentifier.getSource();
    +        if (!source.isPresent()) {
    +            return Optional.empty();
    +        }
    +        final TestSource testSource = source.get();
    +        if (testSource instanceof ClassSource) {
    +            return Optional.of((ClassSource) testSource);
    +        }
    +        return Optional.empty();
    +    }
    +
    +    private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
    +        final char[] chars = new char[1024];
    +        int numRead = -1;
    +        try (final Reader reader = store.getReader()) {
    +            while ((numRead = reader.read(chars)) != -1) {
    +                writer.write(chars, 0, numRead);
    +            }
    +        }
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        FileUtils.close(this.sysOutStore);
    +        FileUtils.close(this.sysErrStore);
    +    }
    +
    +    protected void handleException(final Throwable t) {
    +        // we currently just log it and move on.
    +        task.getProject().log("Exception in listener " + this.getClass().getName(), t, Project.MSG_DEBUG);
    +    }
    +
    +
    +    /*
    +    A "store" for sysout/syserr content that gets sent to the AbstractJUnitResultFormatter.
    +    This store first uses a relatively decent sized in-memory buffer for storing the sysout/syserr
    +    content. This in-memory buffer will be used as long as it can fit in the new content that
    +    keeps coming in. When the size limit is reached, this store switches to a file based store
    +    by creating a temporarily file and writing out the already in-memory held buffer content
    +    and any new content that keeps arriving to this store. Once the file has been created,
    +    the in-memory buffer will never be used any more and in fact is destroyed as soon as the
    +    file is created.
    +    Instances of this class are not thread-safe and users of this class are expected to use necessary thread
    +    safety guarantees, if they want to use an instance of this class by multiple threads.
    +    */
    +    private static final class SysOutErrContentStore implements Closeable {
    +        private static final int DEFAULT_CAPACITY_IN_BYTES = 50 * 1024; // 50 KB
    +        private static final Reader EMPTY_READER = new Reader() {
    +            @Override
    +            public int read(final char[] cbuf, final int off, final int len) throws IOException {
    +                return -1;
    +            }
    +
    +            @Override
    +            public void close() throws IOException {
    +            }
    +        };
    +
    +        private final String tmpFileSuffix;
    +        private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
    +        private boolean usingFileStore = false;
    +        private Path filePath;
    +        private FileOutputStream fileOutputStream;
    +
    +        private SysOutErrContentStore(final boolean isSysOut) {
    +            this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
    +        }
    +
    +        private void store(final byte[] data) throws IOException {
    +            if (this.usingFileStore) {
    +                this.storeToFile(data, 0, data.length);
    +                return;
    +            }
    +            // we haven't yet created a file store and the data can fit in memory,
    +            // so we write it in our buffer
    +            try {
    +                this.inMemoryStore.put(data);
    +                return;
    +            } catch (BufferOverflowException boe) {
    +                // the buffer capacity can't hold this incoming data, so this
    +                // incoming data hasn't been transferred to the buffer. let's
    +                // now fall back to a file store
    +                this.usingFileStore = true;
    +            }
    +            // since the content couldn't be transferred into in-memory buffer,
    +            // we now create a file and transfer already (previously) stored in-memory
    +            // content into that file, before finally transferring this new content
    +            // into the file too. We then finally discard this in-memory buffer and
    +            // just keep using the file store instead
    +            this.fileOutputStream = createFileStore();
    +            // first the existing in-memory content
    +            storeToFile(this.inMemoryStore.array(), 0, this.inMemoryStore.position());
    +            storeToFile(data, 0, data.length);
    +            // discard the in-memory store
    +            this.inMemoryStore = null;
    +        }
    +
    +        private void storeToFile(final byte[] data, final int offset, final int length) throws IOException {
    +            if (this.fileOutputStream == null) {
    +                // no backing file was created so we can't do anything
    +                return;
    +            }
    +            this.fileOutputStream.write(data, offset, length);
    +        }
    +
    +        private FileOutputStream createFileStore() throws IOException {
    +            this.filePath = Files.createTempFile(null, this.tmpFileSuffix);
    +            this.filePath.toFile().deleteOnExit();
    --- End diff --
    
    I hadn't noticed that API. I think it makes sense to use it for consistency.



---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r168983237
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,139 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.OutputStream;
    +import java.io.Writer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Path sysOutFilePath;
    +    protected Path sysErrFilePath;
    +    protected Task task;
    +
    +    private OutputStream sysOutStream;
    +    private OutputStream sysErrStream;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStream == null) {
    +            try {
    +                this.sysOutFilePath = Files.createTempFile(null, "sysout");
    +                this.sysOutFilePath.toFile().deleteOnExit();
    +                this.sysOutStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysOutStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStream == null) {
    +            try {
    +                this.sysErrFilePath = Files.createTempFile(null, "syserr");
    +                this.sysErrFilePath.toFile().deleteOnExit();
    +                this.sysErrStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysErrStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void setExecutingTask(final Task task) {
    +        this.task = task;
    +    }
    +
    +    protected void writeSysOut(final Writer writer) throws IOException {
    +        this.writeFrom(this.sysOutFilePath, writer);
    +    }
    +
    +    protected void writeSysErr(final Writer writer) throws IOException {
    +        this.writeFrom(this.sysErrFilePath, writer);
    +    }
    +
    +    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
    +        if (isTestClass(testIdentifier).isPresent()) {
    +            return Optional.of(testIdentifier);
    +        }
    +        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
    +        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
    +    }
    +
    +    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
    +        if (testIdentifier == null) {
    +            return Optional.empty();
    +        }
    +        final Optional<TestSource> source = testIdentifier.getSource();
    +        if (!source.isPresent()) {
    +            return Optional.empty();
    +        }
    +        final TestSource testSource = source.get();
    +        if (testSource instanceof ClassSource) {
    +            return Optional.of((ClassSource) testSource);
    +        }
    +        return Optional.empty();
    +    }
    +
    +    private void writeFrom(final Path path, final Writer writer) throws IOException {
    +        final byte[] content = new byte[1024];
    +        int numBytes;
    +        try (final InputStream is = Files.newInputStream(path)) {
    +            while ((numBytes = is.read(content)) != -1) {
    +                writer.write(new String(content, 0, numBytes));
    +            }
    +        }
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        if (this.sysOutStream != null) {
    +            try {
    +                this.sysOutStream.close();
    +            } catch (Exception e) {
    +                // ignore
    +            }
    +        }
    --- End diff --
    
    Yes, make sense. Fixed.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r168963955
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,139 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.OutputStream;
    +import java.io.Writer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Path sysOutFilePath;
    +    protected Path sysErrFilePath;
    +    protected Task task;
    +
    +    private OutputStream sysOutStream;
    +    private OutputStream sysErrStream;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStream == null) {
    +            try {
    +                this.sysOutFilePath = Files.createTempFile(null, "sysout");
    +                this.sysOutFilePath.toFile().deleteOnExit();
    +                this.sysOutStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysOutStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStream == null) {
    +            try {
    +                this.sysErrFilePath = Files.createTempFile(null, "syserr");
    +                this.sysErrFilePath.toFile().deleteOnExit();
    +                this.sysErrStream = Files.newOutputStream(this.sysOutFilePath);
    --- End diff --
    
    copy-paste error, you want that to be `sysErrFilePath` :-)


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/41/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/38/



---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r168964295
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,139 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.OutputStream;
    +import java.io.Writer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Path sysOutFilePath;
    +    protected Path sysErrFilePath;
    +    protected Task task;
    +
    +    private OutputStream sysOutStream;
    +    private OutputStream sysErrStream;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStream == null) {
    +            try {
    +                this.sysOutFilePath = Files.createTempFile(null, "sysout");
    +                this.sysOutFilePath.toFile().deleteOnExit();
    +                this.sysOutStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysOutStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStream == null) {
    +            try {
    +                this.sysErrFilePath = Files.createTempFile(null, "syserr");
    +                this.sysErrFilePath.toFile().deleteOnExit();
    +                this.sysErrStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysErrStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void setExecutingTask(final Task task) {
    +        this.task = task;
    +    }
    +
    +    protected void writeSysOut(final Writer writer) throws IOException {
    +        this.writeFrom(this.sysOutFilePath, writer);
    +    }
    +
    +    protected void writeSysErr(final Writer writer) throws IOException {
    +        this.writeFrom(this.sysErrFilePath, writer);
    +    }
    +
    +    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
    +        if (isTestClass(testIdentifier).isPresent()) {
    +            return Optional.of(testIdentifier);
    +        }
    +        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
    +        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
    +    }
    +
    +    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
    +        if (testIdentifier == null) {
    +            return Optional.empty();
    +        }
    +        final Optional<TestSource> source = testIdentifier.getSource();
    +        if (!source.isPresent()) {
    +            return Optional.empty();
    +        }
    +        final TestSource testSource = source.get();
    +        if (testSource instanceof ClassSource) {
    +            return Optional.of((ClassSource) testSource);
    +        }
    +        return Optional.empty();
    +    }
    +
    +    private void writeFrom(final Path path, final Writer writer) throws IOException {
    +        final byte[] content = new byte[1024];
    +        int numBytes;
    +        try (final InputStream is = Files.newInputStream(path)) {
    +            while ((numBytes = is.read(content)) != -1) {
    +                writer.write(new String(content, 0, numBytes));
    +            }
    --- End diff --
    
    I don't think this is going to work reliably. The `read` may have split a multi-byte sequence at the end of `content` and then creating a string from it is going to break. Is there any reason you want to use a stream when reading the temporary file rather than a reader?


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r168983221
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,139 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.OutputStream;
    +import java.io.Writer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Path sysOutFilePath;
    +    protected Path sysErrFilePath;
    +    protected Task task;
    +
    +    private OutputStream sysOutStream;
    +    private OutputStream sysErrStream;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStream == null) {
    +            try {
    +                this.sysOutFilePath = Files.createTempFile(null, "sysout");
    +                this.sysOutFilePath.toFile().deleteOnExit();
    +                this.sysOutStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysOutStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStream == null) {
    +            try {
    +                this.sysErrFilePath = Files.createTempFile(null, "syserr");
    +                this.sysErrFilePath.toFile().deleteOnExit();
    +                this.sysErrStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysErrStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void setExecutingTask(final Task task) {
    +        this.task = task;
    +    }
    +
    +    protected void writeSysOut(final Writer writer) throws IOException {
    +        this.writeFrom(this.sysOutFilePath, writer);
    +    }
    +
    +    protected void writeSysErr(final Writer writer) throws IOException {
    +        this.writeFrom(this.sysErrFilePath, writer);
    +    }
    +
    +    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
    +        if (isTestClass(testIdentifier).isPresent()) {
    +            return Optional.of(testIdentifier);
    +        }
    +        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
    +        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
    +    }
    +
    +    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
    +        if (testIdentifier == null) {
    +            return Optional.empty();
    +        }
    +        final Optional<TestSource> source = testIdentifier.getSource();
    +        if (!source.isPresent()) {
    +            return Optional.empty();
    +        }
    +        final TestSource testSource = source.get();
    +        if (testSource instanceof ClassSource) {
    +            return Optional.of((ClassSource) testSource);
    +        }
    +        return Optional.empty();
    +    }
    +
    +    private void writeFrom(final Path path, final Writer writer) throws IOException {
    +        final byte[] content = new byte[1024];
    +        int numBytes;
    +        try (final InputStream is = Files.newInputStream(path)) {
    +            while ((numBytes = is.read(content)) != -1) {
    +                writer.write(new String(content, 0, numBytes));
    +            }
    --- End diff --
    
    You are right, good catch! I hadn't considered that scenario. I have now switched to using a reader instead.


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/44/



---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r172044509
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,295 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.apache.tools.ant.util.FileUtils;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.BufferedReader;
    +import java.io.ByteArrayInputStream;
    +import java.io.Closeable;
    +import java.io.FileOutputStream;
    +import java.io.FileReader;
    +import java.io.IOException;
    +import java.io.InputStreamReader;
    +import java.io.Reader;
    +import java.io.Writer;
    +import java.nio.BufferOverflowException;
    +import java.nio.ByteBuffer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Objects;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Task task;
    +
    +    private SysOutErrContentStore sysOutStore;
    +    private SysOutErrContentStore sysErrStore;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStore == null) {
    +            this.sysOutStore = new SysOutErrContentStore(true);
    +        }
    +        try {
    +            this.sysOutStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStore == null) {
    +            this.sysErrStore = new SysOutErrContentStore(false);
    +        }
    +        try {
    +            this.sysErrStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void setExecutingTask(final Task task) {
    +        this.task = task;
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stdout data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysOut() {
    +        return this.sysOutStore != null && this.sysOutStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stderr data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysErr() {
    +        return this.sysErrStore != null && this.sysErrStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stdout data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysOut()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysOutReader() throws IOException {
    +        return this.sysOutStore.getReader();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stderr data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysErr()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysErrReader() throws IOException {
    +        return this.sysErrStore.getReader();
    +    }
    +
    +    /**
    +     * Writes out any stdout data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysOut(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysOutStore, writer);
    +    }
    +
    +    /**
    +     * Writes out any stderr data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysErr(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysErrStore, writer);
    +    }
    +
    +    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
    +        if (isTestClass(testIdentifier).isPresent()) {
    +            return Optional.of(testIdentifier);
    +        }
    +        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
    +        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
    +    }
    +
    +    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
    +        if (testIdentifier == null) {
    +            return Optional.empty();
    +        }
    +        final Optional<TestSource> source = testIdentifier.getSource();
    +        if (!source.isPresent()) {
    +            return Optional.empty();
    +        }
    +        final TestSource testSource = source.get();
    +        if (testSource instanceof ClassSource) {
    +            return Optional.of((ClassSource) testSource);
    +        }
    +        return Optional.empty();
    +    }
    +
    +    private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
    +        final char[] chars = new char[1024];
    +        int numRead = -1;
    +        try (final Reader reader = store.getReader()) {
    +            while ((numRead = reader.read(chars)) != -1) {
    +                writer.write(chars, 0, numRead);
    +            }
    +        }
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        FileUtils.close(this.sysOutStore);
    +        FileUtils.close(this.sysErrStore);
    +    }
    +
    +    protected void handleException(final Throwable t) {
    +        // we currently just log it and move on.
    +        task.getProject().log("Exception in listener " + this.getClass().getName(), t, Project.MSG_DEBUG);
    +    }
    +
    +
    +    /*
    +    A "store" for sysout/syserr content that gets sent to the AbstractJUnitResultFormatter.
    +    This store first uses a relatively decent sized in-memory buffer for storing the sysout/syserr
    +    content. This in-memory buffer will be used as long as it can fit in the new content that
    +    keeps coming in. When the size limit is reached, this store switches to a file based store
    +    by creating a temporarily file and writing out the already in-memory held buffer content
    +    and any new content that keeps arriving to this store. Once the file has been created,
    +    the in-memory buffer will never be used any more and in fact is destroyed as soon as the
    +    file is created.
    +    Instances of this class are not thread-safe and users of this class are expected to use necessary thread
    +    safety guarantees, if they want to use an instance of this class by multiple threads.
    +    */
    +    private static final class SysOutErrContentStore implements Closeable {
    +        private static final int DEFAULT_CAPACITY_IN_BYTES = 50 * 1024; // 50 KB
    +        private static final Reader EMPTY_READER = new Reader() {
    +            @Override
    +            public int read(final char[] cbuf, final int off, final int len) throws IOException {
    +                return -1;
    +            }
    +
    +            @Override
    +            public void close() throws IOException {
    +            }
    +        };
    +
    +        private final String tmpFileSuffix;
    +        private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
    +        private boolean usingFileStore = false;
    +        private Path filePath;
    +        private FileOutputStream fileOutputStream;
    +
    +        private SysOutErrContentStore(final boolean isSysOut) {
    +            this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
    +        }
    +
    +        private void store(final byte[] data) throws IOException {
    +            if (this.usingFileStore) {
    +                this.storeToFile(data, 0, data.length);
    +                return;
    +            }
    +            // we haven't yet created a file store and the data can fit in memory,
    +            // so we write it in our buffer
    +            try {
    +                this.inMemoryStore.put(data);
    +                return;
    +            } catch (BufferOverflowException boe) {
    +                // the buffer capacity can't hold this incoming data, so this
    +                // incoming data hasn't been transferred to the buffer. let's
    +                // now fall back to a file store
    +                this.usingFileStore = true;
    --- End diff --
    
    >> maybe defer setting the flag until storeToFile below succeeds?
    I did actually think of it while coding this part. The reason I decided to go this way, was to make sure that as soon as the memory size limit is reached, I prefered it to be better to no more use the in memory store even if the file store cannot be created/used.
    
    That would imply that the subsequent syserr/sysout message will be lost but at least that won't lead to the in memory store going past the limit that's set.
    
    The alternate approach would be to let the in-memory store be used even past its limit (and not lose any sysout/syserr content) if the file store cannot be created for whatever reason. If that feels more better approach to take, I can switch this logic in here.



---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r169625878
  
    --- 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
    +                return;
    +            }
    +            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 = new byte[numRead];
    --- End diff --
    
    Done


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/50/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/42/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/45/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/45/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/34/



---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r172044665
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,295 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.apache.tools.ant.util.FileUtils;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.BufferedReader;
    +import java.io.ByteArrayInputStream;
    +import java.io.Closeable;
    +import java.io.FileOutputStream;
    +import java.io.FileReader;
    +import java.io.IOException;
    +import java.io.InputStreamReader;
    +import java.io.Reader;
    +import java.io.Writer;
    +import java.nio.BufferOverflowException;
    +import java.nio.ByteBuffer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Objects;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Task task;
    +
    +    private SysOutErrContentStore sysOutStore;
    +    private SysOutErrContentStore sysErrStore;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStore == null) {
    +            this.sysOutStore = new SysOutErrContentStore(true);
    +        }
    +        try {
    +            this.sysOutStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStore == null) {
    +            this.sysErrStore = new SysOutErrContentStore(false);
    +        }
    +        try {
    +            this.sysErrStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void setExecutingTask(final Task task) {
    +        this.task = task;
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stdout data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysOut() {
    +        return this.sysOutStore != null && this.sysOutStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stderr data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysErr() {
    +        return this.sysErrStore != null && this.sysErrStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stdout data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysOut()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysOutReader() throws IOException {
    +        return this.sysOutStore.getReader();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stderr data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysErr()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysErrReader() throws IOException {
    +        return this.sysErrStore.getReader();
    +    }
    +
    +    /**
    +     * Writes out any stdout data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysOut(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysOutStore, writer);
    +    }
    +
    +    /**
    +     * Writes out any stderr data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysErr(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysErrStore, writer);
    +    }
    +
    +    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
    +        if (isTestClass(testIdentifier).isPresent()) {
    +            return Optional.of(testIdentifier);
    +        }
    +        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
    +        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
    +    }
    +
    +    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
    +        if (testIdentifier == null) {
    +            return Optional.empty();
    +        }
    +        final Optional<TestSource> source = testIdentifier.getSource();
    +        if (!source.isPresent()) {
    +            return Optional.empty();
    +        }
    +        final TestSource testSource = source.get();
    +        if (testSource instanceof ClassSource) {
    +            return Optional.of((ClassSource) testSource);
    +        }
    +        return Optional.empty();
    +    }
    +
    +    private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
    +        final char[] chars = new char[1024];
    +        int numRead = -1;
    +        try (final Reader reader = store.getReader()) {
    +            while ((numRead = reader.read(chars)) != -1) {
    +                writer.write(chars, 0, numRead);
    +            }
    +        }
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        FileUtils.close(this.sysOutStore);
    +        FileUtils.close(this.sysErrStore);
    +    }
    +
    +    protected void handleException(final Throwable t) {
    +        // we currently just log it and move on.
    +        task.getProject().log("Exception in listener " + this.getClass().getName(), t, Project.MSG_DEBUG);
    +    }
    +
    +
    +    /*
    +    A "store" for sysout/syserr content that gets sent to the AbstractJUnitResultFormatter.
    +    This store first uses a relatively decent sized in-memory buffer for storing the sysout/syserr
    +    content. This in-memory buffer will be used as long as it can fit in the new content that
    +    keeps coming in. When the size limit is reached, this store switches to a file based store
    +    by creating a temporarily file and writing out the already in-memory held buffer content
    +    and any new content that keeps arriving to this store. Once the file has been created,
    +    the in-memory buffer will never be used any more and in fact is destroyed as soon as the
    +    file is created.
    +    Instances of this class are not thread-safe and users of this class are expected to use necessary thread
    +    safety guarantees, if they want to use an instance of this class by multiple threads.
    +    */
    +    private static final class SysOutErrContentStore implements Closeable {
    +        private static final int DEFAULT_CAPACITY_IN_BYTES = 50 * 1024; // 50 KB
    +        private static final Reader EMPTY_READER = new Reader() {
    +            @Override
    +            public int read(final char[] cbuf, final int off, final int len) throws IOException {
    +                return -1;
    +            }
    +
    +            @Override
    +            public void close() throws IOException {
    +            }
    +        };
    +
    +        private final String tmpFileSuffix;
    +        private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
    +        private boolean usingFileStore = false;
    +        private Path filePath;
    +        private FileOutputStream fileOutputStream;
    +
    +        private SysOutErrContentStore(final boolean isSysOut) {
    +            this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
    +        }
    +
    +        private void store(final byte[] data) throws IOException {
    +            if (this.usingFileStore) {
    +                this.storeToFile(data, 0, data.length);
    +                return;
    +            }
    +            // we haven't yet created a file store and the data can fit in memory,
    +            // so we write it in our buffer
    +            try {
    +                this.inMemoryStore.put(data);
    +                return;
    +            } catch (BufferOverflowException boe) {
    +                // the buffer capacity can't hold this incoming data, so this
    +                // incoming data hasn't been transferred to the buffer. let's
    +                // now fall back to a file store
    +                this.usingFileStore = true;
    --- End diff --
    
    >> Also, is there any chance this method could be invoked concurrently? I haven't fully checked every execution path so maybe you are synchronizing somewhere. Tests could spawn new threads you are not aware of and these threads could be writing to syserr/out concurrently.
    
    One thing I didn't note in the implementation comments is that the `sysOutAvailable` and `sysErrAvailable` on these result formatter implementations will always be invoked by a single thread - the thread that we create and control `SysOutErrContentDeliverer`. Multiple threads write out to our redirected sysout/syserr stream which then gets read by the `SysOutErrStreamReader` thread which then hands off that read data to the `SysOutErrContentDeliverer` thread to have it delivered to these result formatters.



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on the issue:

    https://github.com/apache/ant/pull/60
  
    retest this please


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/43/



---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r172043964
  
    --- 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.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.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.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);
    +        }
    +    }
    +
    +    /**
    +     * Adds the {@link Path} to the classpath which will be used for execution of the tests
    +     *
    +     * @param path The classpath
    +     */
    +    public void addConfiguredClassPath(final Path path) {
    +        if (this.classPath == null) {
    +            // create a "wrapper" path which can hold on to multiple
    +            // paths that get passed to this method (if at all the task in the build is
    +            // configured with multiple classpaht elements)
    +            this.classPath = new Path(getProject());
    +        }
    +        this.classPath.add(path);
    +    }
    +
    +    /**
    +     * Adds a {@link SingleTestClass} that will be passed on to the underlying JUnit platform
    +     * for possible execution of the test
    +     *
    +     * @param test The test
    +     */
    +    public void addConfiguredTest(final SingleTestClass test) {
    +        this.preConfigure(test);
    +        this.tests.add(test);
    +    }
    +
    +    /**
    +     * Adds {@link TestClasses} that will be passed on to the underlying JUnit platform for
    +     * possible execution of the tests
    +     *
    +     * @param testClasses The test classes
    +     */
    +    public void addConfiguredTestClasses(final TestClasses testClasses) {
    +        this.preConfigure(testClasses);
    +        this.tests.add(testClasses);
    +    }
    +
    +    /**
    +     * Adds a {@link ListenerDefinition listener} which will be enrolled for listening to test
    +     * execution events
    +     *
    +     * @param listener The listener
    +     */
    +    public void addConfiguredListener(final ListenerDefinition listener) {
    +        this.listeners.add(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) {
    +        if (test.getHaltOnFailure() == null) {
    +            test.setHaltOnFailure(this.haltOnFailure);
    +        }
    +        if (test.getFailureProperty() == null) {
    +            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));
    +    }
    +
    --- End diff --
    
    you could probably DRY up the two methods a little by extracting the common logic.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r169247383
  
    --- 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 --
    
    `FileUtils.close`?


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r172043835
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,295 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.apache.tools.ant.util.FileUtils;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.BufferedReader;
    +import java.io.ByteArrayInputStream;
    +import java.io.Closeable;
    +import java.io.FileOutputStream;
    +import java.io.FileReader;
    +import java.io.IOException;
    +import java.io.InputStreamReader;
    +import java.io.Reader;
    +import java.io.Writer;
    +import java.nio.BufferOverflowException;
    +import java.nio.ByteBuffer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Objects;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Task task;
    +
    +    private SysOutErrContentStore sysOutStore;
    +    private SysOutErrContentStore sysErrStore;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStore == null) {
    +            this.sysOutStore = new SysOutErrContentStore(true);
    +        }
    +        try {
    +            this.sysOutStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStore == null) {
    +            this.sysErrStore = new SysOutErrContentStore(false);
    +        }
    +        try {
    +            this.sysErrStore.store(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void setExecutingTask(final Task task) {
    +        this.task = task;
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stdout data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysOut() {
    +        return this.sysOutStore != null && this.sysOutStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns true if there's any stderr data, that was generated during the
    +     * tests, is available for use. Else returns false.
    +     */
    +    boolean hasSysErr() {
    +        return this.sysErrStore != null && this.sysErrStore.hasData();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stdout data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysOut()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysOutReader() throws IOException {
    +        return this.sysOutStore.getReader();
    +    }
    +
    +    /**
    +     * @return Returns a {@link Reader} for reading any stderr data that was generated
    +     * during the test execution. It is expected that the {@link #hasSysErr()} be first
    +     * called to see if any such data is available and only if there is, then this method
    +     * be called
    +     * @throws IOException If there's any I/O problem while creating the {@link Reader}
    +     */
    +    Reader getSysErrReader() throws IOException {
    +        return this.sysErrStore.getReader();
    +    }
    +
    +    /**
    +     * Writes out any stdout data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysOut(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysOutStore, writer);
    +    }
    +
    +    /**
    +     * Writes out any stderr data that was generated during the
    +     * test execution. If there was no such data then this method just returns.
    +     *
    +     * @param writer The {@link Writer} to use. Cannot be null.
    +     * @throws IOException If any I/O problem occurs during writing the data
    +     */
    +    void writeSysErr(final Writer writer) throws IOException {
    +        Objects.requireNonNull(writer, "Writer cannot be null");
    +        this.writeFrom(this.sysErrStore, writer);
    +    }
    +
    +    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
    +        if (isTestClass(testIdentifier).isPresent()) {
    +            return Optional.of(testIdentifier);
    +        }
    +        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
    +        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
    +    }
    +
    +    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
    +        if (testIdentifier == null) {
    +            return Optional.empty();
    +        }
    +        final Optional<TestSource> source = testIdentifier.getSource();
    +        if (!source.isPresent()) {
    +            return Optional.empty();
    +        }
    +        final TestSource testSource = source.get();
    +        if (testSource instanceof ClassSource) {
    +            return Optional.of((ClassSource) testSource);
    +        }
    +        return Optional.empty();
    +    }
    +
    +    private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
    +        final char[] chars = new char[1024];
    +        int numRead = -1;
    +        try (final Reader reader = store.getReader()) {
    +            while ((numRead = reader.read(chars)) != -1) {
    +                writer.write(chars, 0, numRead);
    +            }
    +        }
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        FileUtils.close(this.sysOutStore);
    +        FileUtils.close(this.sysErrStore);
    +    }
    +
    +    protected void handleException(final Throwable t) {
    +        // we currently just log it and move on.
    +        task.getProject().log("Exception in listener " + this.getClass().getName(), t, Project.MSG_DEBUG);
    +    }
    +
    +
    +    /*
    +    A "store" for sysout/syserr content that gets sent to the AbstractJUnitResultFormatter.
    +    This store first uses a relatively decent sized in-memory buffer for storing the sysout/syserr
    +    content. This in-memory buffer will be used as long as it can fit in the new content that
    +    keeps coming in. When the size limit is reached, this store switches to a file based store
    +    by creating a temporarily file and writing out the already in-memory held buffer content
    +    and any new content that keeps arriving to this store. Once the file has been created,
    +    the in-memory buffer will never be used any more and in fact is destroyed as soon as the
    +    file is created.
    +    Instances of this class are not thread-safe and users of this class are expected to use necessary thread
    +    safety guarantees, if they want to use an instance of this class by multiple threads.
    +    */
    +    private static final class SysOutErrContentStore implements Closeable {
    +        private static final int DEFAULT_CAPACITY_IN_BYTES = 50 * 1024; // 50 KB
    +        private static final Reader EMPTY_READER = new Reader() {
    +            @Override
    +            public int read(final char[] cbuf, final int off, final int len) throws IOException {
    +                return -1;
    +            }
    +
    +            @Override
    +            public void close() throws IOException {
    +            }
    +        };
    +
    +        private final String tmpFileSuffix;
    +        private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
    +        private boolean usingFileStore = false;
    +        private Path filePath;
    +        private FileOutputStream fileOutputStream;
    +
    +        private SysOutErrContentStore(final boolean isSysOut) {
    +            this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
    +        }
    +
    +        private void store(final byte[] data) throws IOException {
    +            if (this.usingFileStore) {
    +                this.storeToFile(data, 0, data.length);
    +                return;
    +            }
    +            // we haven't yet created a file store and the data can fit in memory,
    +            // so we write it in our buffer
    +            try {
    +                this.inMemoryStore.put(data);
    +                return;
    +            } catch (BufferOverflowException boe) {
    +                // the buffer capacity can't hold this incoming data, so this
    +                // incoming data hasn't been transferred to the buffer. let's
    +                // now fall back to a file store
    +                this.usingFileStore = true;
    +            }
    +            // since the content couldn't be transferred into in-memory buffer,
    +            // we now create a file and transfer already (previously) stored in-memory
    +            // content into that file, before finally transferring this new content
    +            // into the file too. We then finally discard this in-memory buffer and
    +            // just keep using the file store instead
    +            this.fileOutputStream = createFileStore();
    +            // first the existing in-memory content
    +            storeToFile(this.inMemoryStore.array(), 0, this.inMemoryStore.position());
    +            storeToFile(data, 0, data.length);
    +            // discard the in-memory store
    +            this.inMemoryStore = null;
    +        }
    +
    +        private void storeToFile(final byte[] data, final int offset, final int length) throws IOException {
    +            if (this.fileOutputStream == null) {
    +                // no backing file was created so we can't do anything
    +                return;
    +            }
    +            this.fileOutputStream.write(data, offset, length);
    +        }
    +
    +        private FileOutputStream createFileStore() throws IOException {
    +            this.filePath = Files.createTempFile(null, this.tmpFileSuffix);
    +            this.filePath.toFile().deleteOnExit();
    --- End diff --
    
    `FileUtils.createTempFile`? Probably not worth it.


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/44/



---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r172044906
  
    --- 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.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.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.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);
    +        }
    +    }
    +
    +    /**
    +     * Adds the {@link Path} to the classpath which will be used for execution of the tests
    +     *
    +     * @param path The classpath
    +     */
    +    public void addConfiguredClassPath(final Path path) {
    +        if (this.classPath == null) {
    +            // create a "wrapper" path which can hold on to multiple
    +            // paths that get passed to this method (if at all the task in the build is
    +            // configured with multiple classpaht elements)
    +            this.classPath = new Path(getProject());
    +        }
    +        this.classPath.add(path);
    +    }
    +
    +    /**
    +     * Adds a {@link SingleTestClass} that will be passed on to the underlying JUnit platform
    +     * for possible execution of the test
    +     *
    +     * @param test The test
    +     */
    +    public void addConfiguredTest(final SingleTestClass test) {
    +        this.preConfigure(test);
    +        this.tests.add(test);
    +    }
    +
    +    /**
    +     * Adds {@link TestClasses} that will be passed on to the underlying JUnit platform for
    +     * possible execution of the tests
    +     *
    +     * @param testClasses The test classes
    +     */
    +    public void addConfiguredTestClasses(final TestClasses testClasses) {
    +        this.preConfigure(testClasses);
    +        this.tests.add(testClasses);
    +    }
    +
    +    /**
    +     * Adds a {@link ListenerDefinition listener} which will be enrolled for listening to test
    +     * execution events
    +     *
    +     * @param listener The listener
    +     */
    +    public void addConfiguredListener(final ListenerDefinition listener) {
    +        this.listeners.add(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) {
    +        if (test.getHaltOnFailure() == null) {
    +            test.setHaltOnFailure(this.haltOnFailure);
    +        }
    +        if (test.getFailureProperty() == null) {
    +            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));
    +    }
    +
    --- End diff --
    
    Yes makes sense, I'll clean up this part a bit.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran closed the pull request at:

    https://github.com/apache/ant/pull/60


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/39/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/40/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/35/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on the issue:

    https://github.com/apache/ant/pull/60
  
    Nice work!
    
    I don't see any reason not to merge this PR.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r169624475
  
    --- 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());
    --- End diff --
    
    You are right. I hadn't taken that into account. I have updated the PR to change this part of the code.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
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: dev-unsubscribe@ant.apache.org
For additional commands, e-mail: dev-help@ant.apache.org


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r169624837
  
    --- 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);
    --- End diff --
    
    Thank you, for catching this. I went back and read the Ant manual about writing custom tasks (which I had skipped the first time around) and now understand this logic of execution better. I have updated the PR to fix this and use the `addConfiguredXXX` construct and also made sure my `preConfigure` implementation doesn't overwrite values that might have been specified on the nested elements.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r169248048
  
    --- 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
    +                return;
    +            }
    +            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 = new byte[numRead];
    --- End diff --
    
    `Arrays.copyOf`?


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/40/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on the issue:

    https://github.com/apache/ant/pull/60
  
    @bodewig, Thank you very much for the review. Based on the review comments, I have updated the PR with additional separate commits (for easier reference). I'll be squashing all these commits together during merge.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r168983118
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,139 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.OutputStream;
    +import java.io.Writer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Path sysOutFilePath;
    +    protected Path sysErrFilePath;
    +    protected Task task;
    +
    +    private OutputStream sysOutStream;
    +    private OutputStream sysErrStream;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStream == null) {
    +            try {
    +                this.sysOutFilePath = Files.createTempFile(null, "sysout");
    +                this.sysOutFilePath.toFile().deleteOnExit();
    +                this.sysOutStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysOutStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStream == null) {
    +            try {
    +                this.sysErrFilePath = Files.createTempFile(null, "syserr");
    +                this.sysErrFilePath.toFile().deleteOnExit();
    +                this.sysErrStream = Files.newOutputStream(this.sysOutFilePath);
    --- End diff --
    
    Indeed :) Fixed.


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r168964381
  
    --- Diff: src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/AbstractJUnitResultFormatter.java ---
    @@ -0,0 +1,139 @@
    +package org.apache.tools.ant.taskdefs.optional.junitlauncher;
    +
    +import org.apache.tools.ant.Project;
    +import org.apache.tools.ant.Task;
    +import org.junit.platform.engine.TestSource;
    +import org.junit.platform.engine.support.descriptor.ClassSource;
    +import org.junit.platform.launcher.TestIdentifier;
    +import org.junit.platform.launcher.TestPlan;
    +
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.OutputStream;
    +import java.io.Writer;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Optional;
    +
    +/**
    + * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
    + */
    +abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
    +
    +    protected static String NEW_LINE = System.getProperty("line.separator");
    +    protected Path sysOutFilePath;
    +    protected Path sysErrFilePath;
    +    protected Task task;
    +
    +    private OutputStream sysOutStream;
    +    private OutputStream sysErrStream;
    +
    +    @Override
    +    public void sysOutAvailable(final byte[] data) {
    +        if (this.sysOutStream == null) {
    +            try {
    +                this.sysOutFilePath = Files.createTempFile(null, "sysout");
    +                this.sysOutFilePath.toFile().deleteOnExit();
    +                this.sysOutStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysOutStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void sysErrAvailable(final byte[] data) {
    +        if (this.sysErrStream == null) {
    +            try {
    +                this.sysErrFilePath = Files.createTempFile(null, "syserr");
    +                this.sysErrFilePath.toFile().deleteOnExit();
    +                this.sysErrStream = Files.newOutputStream(this.sysOutFilePath);
    +            } catch (IOException e) {
    +                handleException(e);
    +                return;
    +            }
    +        }
    +        try {
    +            this.sysErrStream.write(data);
    +        } catch (IOException e) {
    +            handleException(e);
    +            return;
    +        }
    +    }
    +
    +    @Override
    +    public void setExecutingTask(final Task task) {
    +        this.task = task;
    +    }
    +
    +    protected void writeSysOut(final Writer writer) throws IOException {
    +        this.writeFrom(this.sysOutFilePath, writer);
    +    }
    +
    +    protected void writeSysErr(final Writer writer) throws IOException {
    +        this.writeFrom(this.sysErrFilePath, writer);
    +    }
    +
    +    static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
    +        if (isTestClass(testIdentifier).isPresent()) {
    +            return Optional.of(testIdentifier);
    +        }
    +        final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
    +        return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
    +    }
    +
    +    static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
    +        if (testIdentifier == null) {
    +            return Optional.empty();
    +        }
    +        final Optional<TestSource> source = testIdentifier.getSource();
    +        if (!source.isPresent()) {
    +            return Optional.empty();
    +        }
    +        final TestSource testSource = source.get();
    +        if (testSource instanceof ClassSource) {
    +            return Optional.of((ClassSource) testSource);
    +        }
    +        return Optional.empty();
    +    }
    +
    +    private void writeFrom(final Path path, final Writer writer) throws IOException {
    +        final byte[] content = new byte[1024];
    +        int numBytes;
    +        try (final InputStream is = Files.newInputStream(path)) {
    +            while ((numBytes = is.read(content)) != -1) {
    +                writer.write(new String(content, 0, numBytes));
    +            }
    +        }
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        if (this.sysOutStream != null) {
    +            try {
    +                this.sysOutStream.close();
    +            } catch (Exception e) {
    +                // ignore
    +            }
    +        }
    --- End diff --
    
    `FileUtils.close`?


---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r169248698
  
    --- 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 --
    
    I believe this may cause the `PipedOutputStream` to consider the `PipedInputStream` "broken" soon and throw on any `write` operation.


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/48/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by jaikiran <gi...@git.apache.org>.
Github user jaikiran commented on the issue:

    https://github.com/apache/ant/pull/60
  
    This is now merged upstream. Closing this PR. Thanks Stefan for the extensive review.



---

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


[GitHub] ant pull request #60: JUnit 5 support - A new junitlauncher task

Posted by bodewig <gi...@git.apache.org>.
Github user bodewig commented on a diff in the pull request:

    https://github.com/apache/ant/pull/60#discussion_r169243988
  
    --- 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());
    --- End diff --
    
    if users specify multiple nested classpath elements, only the last one will be used. You may want to return an already created `this.classpath` or move on to an `add` method and collect the paths inside of a wrapper `Path`.


---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Linux/41/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/49/



---

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


[GitHub] ant issue #60: JUnit 5 support - A new junitlauncher task

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit commented on the issue:

    https://github.com/apache/ant/pull/60
  
    
    Refer to this link for build results (access rights to CI server needed): 
    https://builds.apache.org/job/Ant%20Github-PR-Windows/51/



---

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