| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * https://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| |
| package org.apache.tools.ant.taskdefs.optional.junitlauncher; |
| |
| |
| import org.apache.tools.ant.BuildException; |
| import org.apache.tools.ant.MagicNames; |
| import org.apache.tools.ant.Project; |
| import org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.LaunchDefinition; |
| import org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.ListenerDefinition; |
| import org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.NamedTest; |
| import org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.SingleTestClass; |
| import org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.TestClasses; |
| import org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.TestDefinition; |
| import org.apache.tools.ant.util.FileUtils; |
| import org.apache.tools.ant.util.KeepAliveOutputStream; |
| import org.junit.platform.engine.Filter; |
| import org.junit.platform.engine.discovery.DiscoverySelectors; |
| import org.junit.platform.launcher.EngineFilter; |
| import org.junit.platform.launcher.Launcher; |
| import org.junit.platform.launcher.LauncherDiscoveryRequest; |
| import org.junit.platform.launcher.TagFilter; |
| import org.junit.platform.launcher.TestExecutionListener; |
| import org.junit.platform.launcher.TestIdentifier; |
| import org.junit.platform.launcher.TestPlan; |
| import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; |
| 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.io.UncheckedIOException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.text.NumberFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.concurrent.BlockingQueue; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Responsible for doing the real work involved in launching the JUnit platform |
| * and passing it the relevant tests that need to be executed by the JUnit platform. |
| * <p> |
| * This class relies on a {@link LaunchDefinition} for setting up the launch of the |
| * JUnit platform. |
| * <p> |
| * The {@code LauncherSupport} isn't concerned with whether or not |
| * it's being executed in the same JVM as the build in which the {@code junitlauncher} |
| * was triggered or if it's running as part of a forked JVM. Instead it just relies |
| * on the {@code LaunchDefinition} to do whatever decisions need to be done before and |
| * after launching the tests. |
| * <p> |
| * This class is not thread-safe and isn't expected to be used for launching from |
| * multiple different threads simultaneously. |
| * <p>This class is an internal implementation detail of the Ant project and although |
| * it's a public class, it isn't meant to be used outside of this project. This class |
| * can be changed, across releases, without any backward compatible guarantees and hence |
| * shouldn't be used or relied upon outside of this project. |
| */ |
| public class LauncherSupport { |
| |
| private final LaunchDefinition launchDefinition; |
| private final TestExecutionContext testExecutionContext; |
| |
| private boolean testsFailed; |
| |
| /** |
| * Create a {@link LauncherSupport} for the passed {@link LaunchDefinition} |
| * |
| * @param definition The launch definition which will be used for launching the tests |
| * @param testExecutionContext The {@link TestExecutionContext} to use for the tests |
| */ |
| public LauncherSupport(final LaunchDefinition definition, final TestExecutionContext testExecutionContext) { |
| if (definition == null) { |
| throw new IllegalArgumentException("Launch definition cannot be null"); |
| } |
| if (testExecutionContext == null) { |
| throw new IllegalArgumentException("Test execution context cannot be null"); |
| } |
| this.launchDefinition = definition; |
| this.testExecutionContext = testExecutionContext; |
| } |
| |
| /** |
| * Launches the tests defined in the {@link LaunchDefinition} |
| * |
| * @throws BuildException If any tests failed and the launch definition was configured to throw |
| * an exception, or if any other exception occurred before or after launching |
| * the tests |
| */ |
| public void launch() throws BuildException { |
| final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader(); |
| try { |
| Thread.currentThread().setContextClassLoader(this.launchDefinition.getClassLoader()); |
| final Launcher launcher = LauncherFactory.create(); |
| final List<TestRequest> requests = buildTestRequests(); |
| for (final TestRequest testRequest : requests) { |
| try { |
| final TestDefinition test = testRequest.getOwner(); |
| final LauncherDiscoveryRequest request = testRequest.getDiscoveryRequest().build(); |
| final List<TestExecutionListener> testExecutionListeners = new ArrayList<>(); |
| // a listener that we always put at the front of list of listeners |
| // for this request. |
| final Listener firstListener = new Listener(System.out); |
| // we always enroll the summary generating listener, to the request, so that we |
| // get to use some of the details of the summary for our further decision making |
| testExecutionListeners.add(firstListener); |
| testExecutionListeners.addAll(getListeners(testRequest, this.launchDefinition.getClassLoader())); |
| final PrintStream originalSysOut = System.out; |
| final PrintStream originalSysErr = System.err; |
| try { |
| firstListener.switchedSysOutHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_OUT, originalSysErr); |
| firstListener.switchedSysErrHandle = trySwitchSysOutErr(testRequest, StreamType.SYS_ERR, originalSysErr); |
| 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 |
| } |
| // close the streams that we had used to redirect System.out/System.err |
| try { |
| firstListener.switchedSysOutHandle.ifPresent((h) -> { |
| try { |
| h.close(); |
| } catch (Exception e) { |
| // ignore |
| } |
| }); |
| } catch (Exception e) { |
| // ignore |
| } |
| try { |
| firstListener.switchedSysErrHandle.ifPresent((h) -> { |
| try { |
| h.close(); |
| } catch (Exception e) { |
| // ignore |
| } |
| }); |
| } catch (Exception e) { |
| // ignore |
| } |
| } |
| handleTestExecutionCompletion(test, firstListener.getSummary()); |
| } finally { |
| try { |
| testRequest.close(); |
| } catch (Exception e) { |
| // log and move on |
| log("Failed to cleanly close test request", e, Project.MSG_DEBUG); |
| } |
| } |
| } |
| } finally { |
| Thread.currentThread().setContextClassLoader(previousClassLoader); |
| } |
| } |
| |
| /** |
| * Returns true if there were any test failures, when this {@link LauncherSupport} was used |
| * to {@link #launch()} tests. False otherwise. |
| * |
| * @return |
| */ |
| boolean hasTestFailures() { |
| return this.testsFailed; |
| } |
| |
| private List<TestRequest> buildTestRequests() { |
| final List<TestDefinition> tests = this.launchDefinition.getTests(); |
| if (tests.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| final List<TestRequest> requests = new ArrayList<>(); |
| for (final TestDefinition test : tests) { |
| final List<TestRequest> testRequests; |
| if (test instanceof SingleTestClass || test instanceof TestClasses) { |
| testRequests = createTestRequests(test); |
| } else { |
| throw new BuildException("Unexpected test definition type " + test.getClass().getName()); |
| } |
| if (testRequests == null || testRequests.isEmpty()) { |
| continue; |
| } |
| requests.addAll(testRequests); |
| } |
| return requests; |
| } |
| |
| private List<TestExecutionListener> getListeners(final TestRequest testRequest, final ClassLoader classLoader) { |
| final TestDefinition test = testRequest.getOwner(); |
| final List<ListenerDefinition> applicableListenerElements = test.getListeners().isEmpty() |
| ? this.launchDefinition.getListeners() : test.getListeners(); |
| final List<TestExecutionListener> listeners = new ArrayList<>(); |
| final Optional<Project> project = this.testExecutionContext.getProject(); |
| for (final ListenerDefinition applicableListener : applicableListenerElements) { |
| if (project.isPresent() && !applicableListener.shouldUse(project.get())) { |
| log("Excluding listener " + applicableListener.getClassName() + " since it's not applicable" + |
| " in the context of project", null, Project.MSG_DEBUG); |
| continue; |
| } |
| final TestExecutionListener listener = requireTestExecutionListener(applicableListener, classLoader); |
| if (listener instanceof TestResultFormatter) { |
| // setup/configure the result formatter |
| setupResultFormatter(testRequest, applicableListener, (TestResultFormatter) listener); |
| } |
| listeners.add(listener); |
| } |
| return listeners; |
| } |
| |
| private void setupResultFormatter(final TestRequest testRequest, final ListenerDefinition formatterDefinition, |
| final TestResultFormatter resultFormatter) { |
| |
| testRequest.closeUponCompletion(resultFormatter); |
| // set the execution context |
| resultFormatter.setContext(this.testExecutionContext); |
| resultFormatter.setUseLegacyReportingName(formatterDefinition.isUseLegacyReportingName()); |
| // set the destination output stream for writing out the formatted result |
| final java.nio.file.Path resultOutputFile = getListenerOutputFile(testRequest, formatterDefinition); |
| 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 Path getListenerOutputFile(final TestRequest testRequest, final ListenerDefinition listener) { |
| final TestDefinition test = testRequest.getOwner(); |
| final String filename; |
| if (listener.getResultFile() != null) { |
| filename = listener.getResultFile(); |
| } else { |
| // compute a file name |
| final StringBuilder sb = new StringBuilder("TEST-"); |
| sb.append(testRequest.getName() == null ? "unknown" : testRequest.getName()); |
| sb.append("."); |
| final String suffix; |
| if ("org.apache.tools.ant.taskdefs.optional.junitlauncher.LegacyXmlResultFormatter".equals(listener.getClassName())) { |
| suffix = "xml"; |
| } else { |
| suffix = "txt"; |
| } |
| sb.append(suffix); |
| filename = sb.toString(); |
| } |
| if (listener.getOutputDir() != null) { |
| // use the output dir defined on the listener |
| return Paths.get(listener.getOutputDir(), filename); |
| } |
| // check on the enclosing test definition, in context of which this listener is being run |
| if (test.getOutputDir() != null) { |
| return Paths.get(test.getOutputDir(), filename); |
| } |
| // neither listener nor the test define a output dir, so use basedir of the project |
| final TestExecutionContext testExecutionContext = this.testExecutionContext; |
| final String baseDir = testExecutionContext.getProperties().getProperty(MagicNames.PROJECT_BASEDIR); |
| return Paths.get(baseDir, filename); |
| } |
| |
| 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) klass.getDeclaredConstructor().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.getTotalFailureCount() != 0; |
| if (hasTestFailures) { |
| // keep track of the test failure(s) for the entire launched instance |
| this.testsFailed = true; |
| } |
| try { |
| if (hasTestFailures && test.getFailureProperty() != null) { |
| // if there are test failures and the test is configured to set a property in case |
| // of failure, then set the property to true |
| final TestExecutionContext testExecutionContext = this.testExecutionContext; |
| if (testExecutionContext.getProject().isPresent()) { |
| final Project project = testExecutionContext.getProject().get(); |
| project.setNewProperty(test.getFailureProperty(), "true"); |
| } |
| } |
| } finally { |
| if (hasTestFailures && test.isHaltOnFailure()) { |
| // if the test is configured to halt on test failures, throw a build error |
| final String errorMessage; |
| if (test instanceof NamedTest) { |
| errorMessage = "Test " + ((NamedTest) test).getName() + " has " + summary.getTestsFailedCount() + " failure(s)"; |
| } else { |
| errorMessage = "Some test(s) have failure(s)"; |
| } |
| throw new BuildException(errorMessage); |
| } |
| } |
| } |
| |
| private Optional<SwitchedStreamHandle> trySwitchSysOutErr(final TestRequest testRequest, final StreamType streamType, |
| final PrintStream originalSysErr) { |
| switch (streamType) { |
| case SYS_OUT: { |
| if (!testRequest.interestedInSysOut()) { |
| return Optional.empty(); |
| } |
| break; |
| } |
| case SYS_ERR: { |
| if (!testRequest.interestedInSysErr()) { |
| return Optional.empty(); |
| } |
| break; |
| } |
| default: { |
| // unknown, but no need to error out, just be lenient |
| // and return back |
| return Optional.empty(); |
| } |
| } |
| final PipedOutputStream pipedOutputStream = new PipedOutputStream(); |
| final PipedInputStream pipedInputStream; |
| try { |
| pipedInputStream = new PipedInputStream(pipedOutputStream); |
| } catch (IOException ioe) { |
| // log and return |
| return Optional.empty(); |
| } |
| final PrintStream printStream = new PrintStream(pipedOutputStream, true); |
| final SysOutErrStreamReader streamer; |
| switch (streamType) { |
| case SYS_OUT: { |
| System.setOut(new PrintStream(printStream)); |
| streamer = new SysOutErrStreamReader(this, pipedInputStream, |
| StreamType.SYS_OUT, testRequest.getSysOutInterests(), originalSysErr); |
| final Thread sysOutStreamer = new Thread(streamer); |
| sysOutStreamer.setDaemon(true); |
| sysOutStreamer.setName("junitlauncher-sysout-stream-reader"); |
| sysOutStreamer.setUncaughtExceptionHandler((t, e) -> { |
| // skip the logging redirection infrastructure of junitlauncher task (which is what has |
| // failed here) and instead directly write out the error to the original System.err |
| originalSysErr.println("Failed in sysout streaming"); |
| e.printStackTrace(originalSysErr); |
| }); |
| sysOutStreamer.start(); |
| break; |
| } |
| case SYS_ERR: { |
| System.setErr(new PrintStream(printStream)); |
| streamer = new SysOutErrStreamReader(this, pipedInputStream, |
| StreamType.SYS_ERR, testRequest.getSysErrInterests(), originalSysErr); |
| final Thread sysErrStreamer = new Thread(streamer); |
| sysErrStreamer.setDaemon(true); |
| sysErrStreamer.setName("junitlauncher-syserr-stream-reader"); |
| sysErrStreamer.setUncaughtExceptionHandler((t, e) -> { |
| // skip the logging redirection infrastructure of junitlauncher task (which is what has |
| // failed here) and instead directly write out the error to the original System.err |
| originalSysErr.println("Failed in syserr streaming"); |
| e.printStackTrace(originalSysErr); |
| }); |
| sysErrStreamer.start(); |
| break; |
| } |
| default: { |
| return Optional.empty(); |
| } |
| } |
| return Optional.of(new SwitchedStreamHandle(pipedOutputStream, streamer)); |
| } |
| |
| private void log(final String message, final Throwable t, final int level) { |
| final TestExecutionContext testExecutionContext = this.testExecutionContext; |
| if (testExecutionContext.getProject().isPresent()) { |
| testExecutionContext.getProject().get().log(message, t, level); |
| return; |
| } |
| if (t == null) { |
| System.out.println(message); |
| } else { |
| System.err.println(message); |
| t.printStackTrace(); |
| } |
| } |
| |
| |
| private List<TestRequest> createTestRequests(final TestDefinition test) { |
| // create TestRequest(s) and add necessary selectors, filters to it |
| |
| if (test instanceof SingleTestClass) { |
| final SingleTestClass singleTestClass = (SingleTestClass) test; |
| final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request(); |
| final TestRequest request = new TestRequest(test, requestBuilder); |
| request.setName(singleTestClass.getName()); |
| final String[] methods = singleTestClass.getMethods(); |
| if (methods == null) { |
| requestBuilder.selectors(DiscoverySelectors.selectClass(singleTestClass.getName())); |
| } else { |
| // add specific methods |
| for (final String method : methods) { |
| requestBuilder.selectors(DiscoverySelectors.selectMethod(singleTestClass.getName(), method)); |
| } |
| } |
| addFilters(request); |
| return Collections.singletonList(request); |
| } |
| |
| if (test instanceof TestClasses) { |
| final List<String> testClasses = ((TestClasses) test).getTestClassNames(); |
| if (testClasses.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| final List<TestRequest> requests = new ArrayList<>(); |
| for (final String testClass : testClasses) { |
| final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request(); |
| final TestRequest request = new TestRequest(test, requestBuilder); |
| request.setName(testClass); |
| requestBuilder.selectors(DiscoverySelectors.selectClass(testClass)); |
| addFilters(request); |
| |
| requests.add(request); |
| } |
| return requests; |
| } |
| return Collections.emptyList(); |
| } |
| |
| /** |
| * Add necessary {@link Filter JUnit filters} to the {@code testRequest} |
| * |
| * @param testRequest The test request |
| */ |
| private void addFilters(final TestRequest testRequest) { |
| final LauncherDiscoveryRequestBuilder requestBuilder = testRequest.getDiscoveryRequest(); |
| // add any engine filters |
| final String[] enginesToInclude = testRequest.getOwner().getIncludeEngines(); |
| if (enginesToInclude != null && enginesToInclude.length > 0) { |
| requestBuilder.filters(EngineFilter.includeEngines(enginesToInclude)); |
| } |
| final String[] enginesToExclude = testRequest.getOwner().getExcludeEngines(); |
| if (enginesToExclude != null && enginesToExclude.length > 0) { |
| requestBuilder.filters(EngineFilter.excludeEngines(enginesToExclude)); |
| } |
| // add any tag filters |
| if (this.launchDefinition.getIncludeTags().size() > 0) { |
| requestBuilder.filters(TagFilter.includeTags(this.launchDefinition.getIncludeTags())); |
| } |
| if (this.launchDefinition.getExcludeTags().size() > 0) { |
| requestBuilder.filters(TagFilter.excludeTags(this.launchDefinition.getExcludeTags())); |
| } |
| } |
| |
| private enum StreamType { |
| SYS_OUT, |
| SYS_ERR |
| } |
| |
| // Implementation note: Logging from this class is prohibited since it can lead |
| // to deadlocks (see bz-64733 for details) |
| private static final class SysOutErrStreamReader implements Runnable { |
| private static final byte[] EMPTY = new byte[0]; |
| |
| private final LauncherSupport launchManager; |
| private final PrintStream originalSysErr; |
| private final InputStream sourceStream; |
| private final StreamType streamType; |
| private final Collection<TestResultFormatter> resultFormatters; |
| private volatile SysOutErrContentDeliverer contentDeliverer; |
| |
| SysOutErrStreamReader(final LauncherSupport launchManager, final InputStream source, |
| final StreamType streamType, final Collection<TestResultFormatter> resultFormatters, |
| final PrintStream originalSysErr) { |
| this.launchManager = launchManager; |
| this.sourceStream = source; |
| this.streamType = streamType; |
| this.resultFormatters = resultFormatters; |
| this.originalSysErr = originalSysErr; |
| } |
| |
| @Override |
| public void run() { |
| final SysOutErrContentDeliverer streamContentDeliver = new SysOutErrContentDeliverer(this.streamType, this.resultFormatters); |
| final Thread deliveryThread = new Thread(streamContentDeliver); |
| deliveryThread.setName("junitlauncher-" + (this.streamType == StreamType.SYS_OUT ? "sysout" : "syserr") + "-stream-deliverer"); |
| deliveryThread.setDaemon(true); |
| deliveryThread.start(); |
| this.contentDeliverer = streamContentDeliver; |
| int numRead = -1; |
| final byte[] data = new byte[1024]; |
| try { |
| while ((numRead = this.sourceStream.read(data)) != -1) { |
| final byte[] copy = Arrays.copyOf(data, numRead); |
| streamContentDeliver.availableData.offer(copy); |
| } |
| } catch (IOException e) { |
| // let the UncaughtExceptionHandler of this thread deal with this exception |
| throw new UncheckedIOException(e); |
| } finally { |
| streamContentDeliver.stop = true; |
| // just "wakeup" the delivery thread, to take into account |
| // those race conditions, where that other thread didn't yet |
| // notice that it was asked to stop and has now gone into a |
| // X amount of wait, waiting for any new data |
| streamContentDeliver.availableData.offer(EMPTY); |
| } |
| } |
| } |
| |
| private static final class SysOutErrContentDeliverer implements Runnable { |
| private volatile boolean stop; |
| private final Collection<TestResultFormatter> resultFormatters; |
| private final StreamType streamType; |
| private final BlockingQueue<byte[]> availableData = new LinkedBlockingQueue<>(); |
| private final CountDownLatch completionLatch = new CountDownLatch(1); |
| |
| SysOutErrContentDeliverer(final StreamType streamType, final Collection<TestResultFormatter> resultFormatters) { |
| this.streamType = streamType; |
| this.resultFormatters = resultFormatters; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| while (!this.stop) { |
| final byte[] streamData; |
| try { |
| streamData = this.availableData.poll(2, TimeUnit.SECONDS); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| return; |
| } |
| if (streamData != null) { |
| deliver(streamData); |
| } |
| } |
| // drain it |
| final List<byte[]> remaining = new ArrayList<>(); |
| this.availableData.drainTo(remaining); |
| if (!remaining.isEmpty()) { |
| for (final byte[] data : remaining) { |
| deliver(data); |
| } |
| } |
| } finally { |
| this.completionLatch.countDown(); |
| } |
| } |
| |
| private void deliver(final byte[] data) { |
| if (data == null || data.length == 0) { |
| return; |
| } |
| for (final TestResultFormatter resultFormatter : this.resultFormatters) { |
| // send it to the formatter |
| switch (streamType) { |
| case SYS_OUT: { |
| resultFormatter.sysOutAvailable(data); |
| break; |
| } |
| case SYS_ERR: { |
| resultFormatter.sysErrAvailable(data); |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| private final class SwitchedStreamHandle implements AutoCloseable { |
| private final PipedOutputStream outputStream; |
| private final SysOutErrStreamReader streamReader; |
| |
| SwitchedStreamHandle(final PipedOutputStream outputStream, final SysOutErrStreamReader streamReader) { |
| this.streamReader = streamReader; |
| this.outputStream = outputStream; |
| } |
| |
| @Override |
| public void close() throws Exception { |
| outputStream.close(); |
| streamReader.sourceStream.close(); |
| } |
| } |
| |
| private final class Listener extends SummaryGeneratingListener { |
| private final PrintStream originalSysOut; |
| |
| private Optional<SwitchedStreamHandle> switchedSysOutHandle; |
| private Optional<SwitchedStreamHandle> switchedSysErrHandle; |
| |
| private Listener(final PrintStream originalSysOut) { |
| this.originalSysOut = originalSysOut; |
| } |
| |
| @Override |
| public void executionStarted(final TestIdentifier testIdentifier) { |
| super.executionStarted(testIdentifier); |
| AbstractJUnitResultFormatter.isTestClass(testIdentifier).ifPresent(testClass -> { |
| this.originalSysOut.println("Running " + testClass.getClassName()); |
| }); |
| } |
| |
| |
| private static final double ONE_SECOND = 1000.0; |
| // We use this only in the testPlanExecutionFinished method, which |
| // as per the JUnit5 platform semantics won't be called concurrently |
| // by multiple threads (https://github.com/junit-team/junit5/issues/2539#issuecomment-766325555). |
| // So it's safe to use this without any additional thread safety access controls. |
| private NumberFormat timeFormatter = NumberFormat.getInstance(); |
| |
| @Override |
| public void testPlanExecutionFinished(final TestPlan testPlan) { |
| super.testPlanExecutionFinished(testPlan); |
| if (!testPlan.containsTests()) { |
| // we print the summary only if any tests are present |
| return; |
| } |
| if (launchDefinition.isPrintSummary()) { |
| final TestExecutionSummary summary = this.getSummary(); |
| // Keep the summary as close to as the old junit task summary |
| // tests run, failed, skipped, duration |
| final StringBuilder sb = new StringBuilder("Tests run: "); |
| sb.append(summary.getTestsStartedCount()); |
| sb.append(", Failures: "); |
| sb.append(summary.getTestsFailedCount()); |
| sb.append(", Aborted: "); |
| sb.append(summary.getTestsAbortedCount()); |
| sb.append(", Skipped: "); |
| sb.append(summary.getTestsSkippedCount()); |
| sb.append(", Time elapsed: "); |
| final long elapsedMs = summary.getTimeFinished() - summary.getTimeStarted(); |
| sb.append(timeFormatter.format(elapsedMs / ONE_SECOND)); |
| sb.append(" sec"); |
| this.originalSysOut.println(sb.toString()); |
| } |
| // now that the test plan execution is finished, close the switched sysout/syserr output streams |
| // and wait for the sysout and syserr content delivery, to result formatters, to finish |
| if (this.switchedSysOutHandle.isPresent()) { |
| final SwitchedStreamHandle sysOut = this.switchedSysOutHandle.get(); |
| try { |
| closeAndWait(sysOut); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| return; |
| } |
| } |
| if (this.switchedSysErrHandle.isPresent()) { |
| final SwitchedStreamHandle sysErr = this.switchedSysErrHandle.get(); |
| try { |
| closeAndWait(sysErr); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| } |
| } |
| } |
| |
| private void closeAndWait(final SwitchedStreamHandle handle) throws InterruptedException { |
| FileUtils.close(handle.outputStream); |
| if (handle.streamReader.contentDeliverer == null) { |
| return; |
| } |
| // wait for a few seconds |
| handle.streamReader.contentDeliverer.completionLatch.await(2, TimeUnit.SECONDS); |
| } |
| } |
| |
| } |