content: /*
   *  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.apache.tools.ant.util.LeadPipeInputStream;
  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.BufferedOutputStream;
  import java.io.File;
  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[0]));
                      } 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 = new BufferedOutputStream(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(".");
              sb.append(listener.getExtension());
              filename = sb.toString();
          }
          if (listener.getOutputDir() != null) {
              // use the output dir defined on the listener
              return new File(listener.getOutputDir(), filename).toPath();
          }
          // check on the enclosing test definition, in context of which this listener is being run
          if (test.getOutputDir() != null) {
              return new File(test.getOutputDir(), filename).toPath();
          }
          // 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 LeadPipeInputStream(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);
          }
      }
  
  }
  