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.Project;
  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.FileReader;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  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 TestExecutionContext context;
  
      private SysOutErrContentStore sysOutStore;
      private SysOutErrContentStore sysErrStore;
  
      @Override
      public void sysOutAvailable(final byte[] data) {
          if (this.sysOutStore == null) {
              this.sysOutStore = new SysOutErrContentStore(context, true);
          }
          try {
              this.sysOutStore.store(data);
          } catch (IOException e) {
              handleException(e);
          }
      }
  
      @Override
      public void sysErrAvailable(final byte[] data) {
          if (this.sysErrStore == null) {
              this.sysErrStore = new SysOutErrContentStore(context, false);
          }
          try {
              this.sysErrStore.store(data);
          } catch (IOException e) {
              handleException(e);
          }
      }
  
      @Override
      public void setContext(final TestExecutionContext context) {
          this.context = context;
      }
  
      /**
       * @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.
          this.context.getProject().ifPresent((p) -> p.log("Exception in listener "
                  + AbstractJUnitResultFormatter.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 TestExecutionContext context;
          private final String tmpFileSuffix;
          private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
          private boolean usingFileStore = false;
          private Path filePath;
          private OutputStream fileOutputStream;
  
          private SysOutErrContentStore(final TestExecutionContext context, final boolean isSysOut) {
              this.context = context;
              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 OutputStream createFileStore() throws IOException {
              this.filePath = FileUtils.getFileUtils()
                  .createTempFile(context.getProject().orElse(null), null, this.tmpFileSuffix, null, true, true)
                  .toPath();
              return Files.newOutputStream(this.filePath);
          }
  
          /*
           * Returns a Reader for reading the sysout/syserr content. If there's no data
           * available in this store, then this returns a Reader which when used for read operations,
           * will immediately indicate an EOF.
           */
          private Reader getReader() throws IOException {
              if (this.usingFileStore && this.filePath != null) {
                  // we use a FileReader here so that we can use the system default character
                  // encoding for reading the contents on sysout/syserr stream, since that's the
                  // encoding that System.out/System.err uses to write out the messages
                  return new BufferedReader(new FileReader(this.filePath.toFile()));
              }
              if (this.inMemoryStore != null) {
                  return new InputStreamReader(new ByteArrayInputStream(this.inMemoryStore.array(), 0, this.inMemoryStore.position()));
              }
              // no data to read, so we return an "empty" reader
              return EMPTY_READER;
          }
  
          /*
           *  Returns true if this store has any data (either in-memory or in a file). Else
           *  returns false.
           */
          private boolean hasData() {
              if (this.inMemoryStore != null && this.inMemoryStore.position() > 0) {
                  return true;
              }
              return this.usingFileStore && this.filePath != null;
          }
  
          @Override
          public void close() throws IOException {
              this.inMemoryStore = null;
              FileUtils.close(this.fileOutputStream);
              FileUtils.delete(this.filePath.toFile());
          }
      }
  }
  