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.util.DOMElementWriter;
  import org.apache.tools.ant.util.DateUtils;
  import org.apache.tools.ant.util.StringUtils;
  import org.junit.platform.engine.TestExecutionResult;
  import org.junit.platform.engine.TestSource;
  import org.junit.platform.engine.reporting.ReportEntry;
  import org.junit.platform.engine.support.descriptor.ClassSource;
  import org.junit.platform.engine.support.descriptor.MethodSource;
  import org.junit.platform.launcher.TestIdentifier;
  import org.junit.platform.launcher.TestPlan;
  
  import javax.xml.stream.XMLOutputFactory;
  import javax.xml.stream.XMLStreamException;
  import javax.xml.stream.XMLStreamWriter;
  import java.io.IOException;
  import java.io.OutputStream;
  import java.io.Reader;
  import java.util.Date;
  import java.util.Map;
  import java.util.Optional;
  import java.util.Properties;
  import java.util.Set;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.atomic.AtomicLong;
  
  /**
   * A {@link TestResultFormatter} which generates an XML report of the tests. The generated XML reports
   * conforms to the schema of the XML that was generated by the {@code junit} task's XML
   * report formatter and can be used by the {@code junitreport} task
   */
  class LegacyXmlResultFormatter extends AbstractJUnitResultFormatter implements TestResultFormatter {
  
      private static final double ONE_SECOND = 1000.0;
  
      private OutputStream outputStream;
      private final Map<TestIdentifier, Stats> testIds = new ConcurrentHashMap<>();
      private final Map<TestIdentifier, Optional<String>> skipped = new ConcurrentHashMap<>();
      private final Map<TestIdentifier, Optional<Throwable>> failed = new ConcurrentHashMap<>();
      private final Map<TestIdentifier, Optional<Throwable>> aborted = new ConcurrentHashMap<>();
  
      private TestPlan testPlan;
      private long testPlanStartedAt = -1;
      private long testPlanEndedAt = -1;
      private final AtomicLong numTestsRun = new AtomicLong(0);
      private final AtomicLong numTestsFailed = new AtomicLong(0);
      private final AtomicLong numTestsSkipped = new AtomicLong(0);
      private final AtomicLong numTestsAborted = new AtomicLong(0);
      private boolean useLegacyReportingName = true;
  
  
      @Override
      public void testPlanExecutionStarted(final TestPlan testPlan) {
          this.testPlan = testPlan;
          this.testPlanStartedAt = System.currentTimeMillis();
      }
  
      @Override
      public void testPlanExecutionFinished(final TestPlan testPlan) {
          this.testPlanEndedAt = System.currentTimeMillis();
          // format and print out the result
          try {
              new XMLReportWriter().write();
          } catch (IOException | XMLStreamException e) {
              handleException(e);
          }
      }
  
      @Override
      public void dynamicTestRegistered(final TestIdentifier testIdentifier) {
          // nothing to do
      }
  
      @Override
      public void executionSkipped(final TestIdentifier testIdentifier, final String reason) {
          final long currentTime = System.currentTimeMillis();
          this.numTestsSkipped.incrementAndGet();
          this.skipped.put(testIdentifier, Optional.ofNullable(reason));
          // a skipped test is considered started and ended now
          final Stats stats = new Stats(testIdentifier, currentTime);
          stats.endedAt = currentTime;
          this.testIds.put(testIdentifier, stats);
      }
  
      @Override
      public void executionStarted(final TestIdentifier testIdentifier) {
          final long currentTime = System.currentTimeMillis();
          this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
          if (testIdentifier.isTest()) {
              this.numTestsRun.incrementAndGet();
          }
      }
  
      @Override
      public void executionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
          final long currentTime = System.currentTimeMillis();
          final Stats stats = this.testIds.get(testIdentifier);
          if (stats != null) {
              stats.endedAt = currentTime;
          }
          switch (testExecutionResult.getStatus()) {
              case SUCCESSFUL: {
                  break;
              }
              case ABORTED: {
                  this.numTestsAborted.incrementAndGet();
                  this.aborted.put(testIdentifier, testExecutionResult.getThrowable());
                  break;
              }
              case FAILED: {
                  this.numTestsFailed.incrementAndGet();
                  this.failed.put(testIdentifier, testExecutionResult.getThrowable());
                  break;
              }
          }
      }
  
      @Override
      public void reportingEntryPublished(final TestIdentifier testIdentifier, final ReportEntry entry) {
          // nothing to do
      }
  
      @Override
      public void setDestination(final OutputStream os) {
          this.outputStream = os;
      }
  
      @Override
      public void setUseLegacyReportingName(final boolean useLegacyReportingName) {
          this.useLegacyReportingName = useLegacyReportingName;
      }
  
      private final class Stats {
          @SuppressWarnings("unused")
          private final TestIdentifier testIdentifier;
          private final long startedAt;
          private long endedAt;
  
          private Stats(final TestIdentifier testIdentifier, final long startedAt) {
              this.testIdentifier = testIdentifier;
              this.startedAt = startedAt;
          }
      }
  
      private final class XMLReportWriter {
  
          private static final String ELEM_TESTSUITE = "testsuite";
          private static final String ELEM_PROPERTIES = "properties";
          private static final String ELEM_PROPERTY = "property";
          private static final String ELEM_TESTCASE = "testcase";
          private static final String ELEM_SKIPPED = "skipped";
          private static final String ELEM_FAILURE = "failure";
          private static final String ELEM_ABORTED = "aborted";
          private static final String ELEM_SYSTEM_OUT = "system-out";
          private static final String ELEM_SYSTEM_ERR = "system-err";
  
  
          private static final String ATTR_CLASSNAME = "classname";
          private static final String ATTR_NAME = "name";
          private static final String ATTR_VALUE = "value";
          private static final String ATTR_TIME = "time";
          private static final String ATTR_TIMESTAMP = "timestamp";
          private static final String ATTR_NUM_ABORTED = "aborted";
          private static final String ATTR_NUM_FAILURES = "failures";
          private static final String ATTR_NUM_TESTS = "tests";
          private static final String ATTR_NUM_SKIPPED = "skipped";
          private static final String ATTR_MESSAGE = "message";
          private static final String ATTR_TYPE = "type";
  
          void write() throws XMLStreamException, IOException {
              final XMLStreamWriter writer = XMLOutputFactory.newFactory().createXMLStreamWriter(outputStream, "UTF-8");
              try {
                  writer.writeStartDocument();
                  writeTestSuite(writer);
                  writer.writeEndDocument();
              } finally {
                  writer.close();
              }
          }
  
          void writeTestSuite(final XMLStreamWriter writer) throws XMLStreamException, IOException {
              // write the testsuite element
              writer.writeStartElement(ELEM_TESTSUITE);
              final String testsuiteName = determineTestSuiteName();
              writeAttribute(writer, ATTR_NAME, testsuiteName);
              // time taken for the tests execution
              writeAttribute(writer, ATTR_TIME, String.valueOf((testPlanEndedAt - testPlanStartedAt) / ONE_SECOND));
              // add the timestamp of report generation
              final String timestamp = DateUtils.format(new Date(), DateUtils.ISO8601_DATETIME_PATTERN);
              writeAttribute(writer, ATTR_TIMESTAMP, timestamp);
              writeAttribute(writer, ATTR_NUM_TESTS, String.valueOf(numTestsRun.longValue()));
              writeAttribute(writer, ATTR_NUM_FAILURES, String.valueOf(numTestsFailed.longValue()));
              writeAttribute(writer, ATTR_NUM_SKIPPED, String.valueOf(numTestsSkipped.longValue()));
              writeAttribute(writer, ATTR_NUM_ABORTED, String.valueOf(numTestsAborted.longValue()));
  
              // write the properties
              writeProperties(writer);
              // write the tests
              writeTestCase(writer);
              writeSysOut(writer);
              writeSysErr(writer);
              // end the testsuite
              writer.writeEndElement();
          }
  
          void writeProperties(final XMLStreamWriter writer) throws XMLStreamException {
              final Properties properties = LegacyXmlResultFormatter.this.context.getProperties();
              if (properties == null || properties.isEmpty()) {
                  return;
              }
              writer.writeStartElement(ELEM_PROPERTIES);
              for (final String prop : properties.stringPropertyNames()) {
                  writer.writeStartElement(ELEM_PROPERTY);
                  writeAttribute(writer, ATTR_NAME, prop);
                  writeAttribute(writer, ATTR_VALUE, properties.getProperty(prop));
                  writer.writeEndElement();
              }
              writer.writeEndElement();
          }
  
          void writeTestCase(final XMLStreamWriter writer) throws XMLStreamException {
              for (final Map.Entry<TestIdentifier, Stats> entry : testIds.entrySet()) {
                  final TestIdentifier testId = entry.getKey();
                  if (!testId.isTest() && !failed.containsKey(testId)) {
                      // only interested in test methods unless there was a failure,
                      // in which case we want the exception reported
                      // (https://bz.apache.org/bugzilla/show_bug.cgi?id=63850)
                      continue;
                  }
                  String classname = null;
                  // find the associated class of this test
                  final Optional<ClassSource> parentClassSource;
                  if (testId.isTest()) {
                      parentClassSource = findFirstParentClassSource(testId);
                  }
                  else {
                      parentClassSource = findFirstClassSource(testId);
                  }
                  if (parentClassSource.isPresent()) {
                      classname = parentClassSource.get().getClassName();
                  } else {
                      if (testId.getSource().isPresent()) {
                          final TestSource testSource = testId.getSource().get();
                          if (testSource instanceof MethodSource) {
                              // this can happen for the case where the test is generated dynamically
                              // through the use of @org.junit.jupiter.api.TestFactory. In such cases
                              // the method having that annotation is considered a test and its
                              // source is reported as a MethodSource
                              classname = ((MethodSource) testSource).getClassName();
                          }
                      }
                  }
                  if (classname == null) {
                      // as a last resort, use the test id as the classname,
                      // so that the failure does get reported instead of being ignored/invisible
                      classname = testId.getUniqueIdObject().toString();
                  }
                  writer.writeStartElement(ELEM_TESTCASE);
                  writeAttribute(writer, ATTR_CLASSNAME, classname);
                  writeAttribute(writer, ATTR_NAME, useLegacyReportingName ? testId.getLegacyReportingName()
                          : testId.getDisplayName());
                  final Stats stats = entry.getValue();
                  writeAttribute(writer, ATTR_TIME, String.valueOf((stats.endedAt - stats.startedAt) / ONE_SECOND));
                  // skipped element if the test was skipped
                  writeSkipped(writer, testId);
                  // failed element if the test failed
                  writeFailed(writer, testId);
                  // aborted element if the test was aborted
                  writeAborted(writer, testId);
  
                  writer.writeEndElement();
              }
          }
  
          private void writeSkipped(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
              if (!skipped.containsKey(testIdentifier)) {
                  return;
              }
              writer.writeStartElement(ELEM_SKIPPED);
              final Optional<String> reason = skipped.get(testIdentifier);
              if (reason.isPresent()) {
                  writeAttribute(writer, ATTR_MESSAGE, reason.get());
              }
              writer.writeEndElement();
          }
  
          private void writeFailed(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
              if (!failed.containsKey(testIdentifier)) {
                  return;
              }
              writer.writeStartElement(ELEM_FAILURE);
              final Optional<Throwable> cause = failed.get(testIdentifier);
              if (cause.isPresent()) {
                  final Throwable t = cause.get();
                  final String message = t.getMessage();
                  if (message != null && !message.trim().isEmpty()) {
                      writeAttribute(writer, ATTR_MESSAGE, message);
                  }
                  writeAttribute(writer, ATTR_TYPE, t.getClass().getName());
                  // write out the stacktrace
                  this.writeCDataSafely(writer, StringUtils.getStackTrace(t));
              }
              writer.writeEndElement();
          }
  
          private void writeAborted(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
              if (!aborted.containsKey(testIdentifier)) {
                  return;
              }
              writer.writeStartElement(ELEM_ABORTED);
              final Optional<Throwable> cause = aborted.get(testIdentifier);
              if (cause.isPresent()) {
                  final Throwable t = cause.get();
                  final String message = t.getMessage();
                  if (message != null && !message.trim().isEmpty()) {
                      writeAttribute(writer, ATTR_MESSAGE, message);
                  }
                  writeAttribute(writer, ATTR_TYPE, t.getClass().getName());
                  // write out the stacktrace
                  this.writeCDataSafely(writer, StringUtils.getStackTrace(t));
              }
              writer.writeEndElement();
          }
  
          private void writeSysOut(final XMLStreamWriter writer) throws XMLStreamException, IOException {
              if (!LegacyXmlResultFormatter.this.hasSysOut()) {
                  return;
              }
              writer.writeStartElement(ELEM_SYSTEM_OUT);
              try (final Reader reader = LegacyXmlResultFormatter.this.getSysOutReader()) {
                  writeCharactersFrom(reader, writer);
              }
              writer.writeEndElement();
          }
  
          private void writeSysErr(final XMLStreamWriter writer) throws XMLStreamException, IOException {
              if (!LegacyXmlResultFormatter.this.hasSysErr()) {
                  return;
              }
              writer.writeStartElement(ELEM_SYSTEM_ERR);
              try (final Reader reader = LegacyXmlResultFormatter.this.getSysErrReader()) {
                  writeCharactersFrom(reader, writer);
              }
              writer.writeEndElement();
          }
  
          /**
           * Write cdata safely (escape special sequence {@code "]]>"})
           * @param writer The xml writer to use
           * @param cdata The cdata to write
           * @see <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=65833">Bugzilla #65833</a>
           */
          private void writeCDataSafely(final XMLStreamWriter writer, final String cdata) throws XMLStreamException {
              writer.writeCData(cdata.replace("]]>", "]]]]><![CDATA[>"));
          }
  
          private void writeCharactersFrom(final Reader reader, final XMLStreamWriter writer) throws IOException, XMLStreamException {
              final char[] chars = new char[1024];
              int numRead = -1;
              while ((numRead = reader.read(chars)) != -1) {
                  writer.writeCharacters(encode(new String(chars, 0, numRead)));
              }
          }
  
          private void writeAttribute(final XMLStreamWriter writer, final String name, final String value)
              throws XMLStreamException {
              writer.writeAttribute(name, encode(value));
          }
  
          private String encode(final String s) {
              boolean changed = false;
              final StringBuilder sb = new StringBuilder();
              for (char c : s.toCharArray()) {
                  if (!DOMElementWriter.isLegalXmlCharacter(c)) {
                      changed = true;
                      sb.append("&#").append((int) c).append(';');
                  } else {
                      sb.append(c);
                  }
              }
              return changed ? sb.toString() : s;
          }
  
          private String determineTestSuiteName() {
              // this is really a hack to try and match the expectations of the XML report in JUnit4.x
              // world. In JUnit5, the TestPlan doesn't have a name and a TestPlan (for which this is a
              // listener) can have numerous tests within it
              final Set<TestIdentifier> roots = testPlan.getRoots();
              if (roots.isEmpty()) {
                  return "UNKNOWN";
              }
              for (final TestIdentifier root : roots) {
                  final Optional<ClassSource> classSource = findFirstClassSource(root);
                  if (classSource.isPresent()) {
                      return classSource.get().getClassName();
                  }
              }
              return "UNKNOWN";
          }
  
          private Optional<ClassSource> findFirstClassSource(final TestIdentifier root) {
              if (root.getSource().isPresent()) {
                  final TestSource source = root.getSource().get();
                  if (source instanceof ClassSource) {
                      return Optional.of((ClassSource) source);
                  }
              }
              for (final TestIdentifier child : testPlan.getChildren(root)) {
                  final Optional<ClassSource> classSource = findFirstClassSource(child);
                  if (classSource.isPresent()) {
                      return classSource;
                  }
              }
              return Optional.empty();
          }
  
          private Optional<ClassSource> findFirstParentClassSource(final TestIdentifier testId) {
              final Optional<TestIdentifier> parent = testPlan.getParent(testId);
              if (!parent.isPresent()) {
                  return Optional.empty();
              }
              if (!parent.get().getSource().isPresent()) {
                  // the source of the parent is unknown, so we move up the
                  // hierarchy and try and find a class source
                  return findFirstParentClassSource(parent.get());
              }
              final TestSource parentSource = parent.get().getSource().get();
              return parentSource instanceof ClassSource ? Optional.of((ClassSource) parentSource)
                      : findFirstParentClassSource(parent.get());
          }
      }
  
  }
  