| /******************************************************************************* |
| * Copyright (c) 2018, 2020 Red Hat Inc. and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Lucas Bullen (Red Hat Inc.) - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.test; |
| |
| import java.io.BufferedReader; |
| import java.io.ByteArrayInputStream; |
| import java.io.Closeable; |
| import java.io.FileOutputStream; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.io.Writer; |
| import java.nio.BufferOverflowException; |
| import java.nio.ByteBuffer; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.Objects; |
| import java.util.Optional; |
| |
| import org.apache.tools.ant.Project; |
| import org.apache.tools.ant.taskdefs.optional.junitlauncher.TestExecutionContext; |
| import org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter; |
| 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; |
| |
| /** |
| * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s |
| */ |
| abstract class AbstractJUnitResultFormatter implements TestResultFormatter { |
| |
| |
| protected static String NEW_LINE = System.lineSeparator(); |
| protected TestExecutionContext context; |
| |
| private SysOutErrContentStore sysOutStore; |
| private SysOutErrContentStore sysErrStore; |
| |
| @Override |
| public void sysOutAvailable(final byte[] data) { |
| if (this.sysOutStore == null) { |
| this.sysOutStore = new SysOutErrContentStore(true); |
| } |
| try { |
| this.sysOutStore.store(data); |
| } catch (IOException e) { |
| handleException(e); |
| return; |
| } |
| } |
| |
| @Override |
| public void sysErrAvailable(final byte[] data) { |
| if (this.sysErrStore == null) { |
| this.sysErrStore = new SysOutErrContentStore(false); |
| } |
| try { |
| this.sysErrStore.store(data); |
| } catch (IOException e) { |
| handleException(e); |
| return; |
| } |
| } |
| |
| @Override |
| public void 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 String tmpFileSuffix; |
| private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES); |
| private boolean usingFileStore = false; |
| private Path filePath; |
| private FileOutputStream fileOutputStream; |
| |
| SysOutErrContentStore(final boolean isSysOut) { |
| this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr"; |
| } |
| |
| void store(final byte[] data) throws IOException { |
| if (this.usingFileStore) { |
| this.storeToFile(data, 0, data.length); |
| return; |
| } |
| // we haven't yet created a file store and the data can fit in memory, |
| // so we write it in our buffer |
| try { |
| this.inMemoryStore.put(data); |
| return; |
| } catch (BufferOverflowException boe) { |
| // the buffer capacity can't hold this incoming data, so this |
| // incoming data hasn't been transferred to the buffer. let's |
| // now fall back to a file store |
| this.usingFileStore = true; |
| } |
| // since the content couldn't be transferred into in-memory buffer, |
| // we now create a file and transfer already (previously) stored in-memory |
| // content into that file, before finally transferring this new content |
| // into the file too. We then finally discard this in-memory buffer and |
| // just keep using the file store instead |
| this.fileOutputStream = createFileStore(); |
| // first the existing in-memory content |
| storeToFile(this.inMemoryStore.array(), 0, this.inMemoryStore.position()); |
| storeToFile(data, 0, data.length); |
| // discard the in-memory store |
| this.inMemoryStore = null; |
| } |
| |
| private void storeToFile(final byte[] data, final int offset, final int length) throws IOException { |
| if (this.fileOutputStream == null) { |
| // no backing file was created so we can't do anything |
| return; |
| } |
| this.fileOutputStream.write(data, offset, length); |
| } |
| |
| private FileOutputStream createFileStore() throws IOException { |
| this.filePath = Files.createTempFile(null, this.tmpFileSuffix); |
| this.filePath.toFile().deleteOnExit(); |
| return new FileOutputStream(this.filePath.toFile()); |
| } |
| |
| /* |
| * 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. |
| */ |
| 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. |
| */ |
| boolean hasData() { |
| if (this.inMemoryStore != null && this.inMemoryStore.position() > 0) { |
| return true; |
| } |
| if (this.usingFileStore && this.filePath != null) { |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public void close() throws IOException { |
| this.inMemoryStore = null; |
| FileUtils.close(this.fileOutputStream); |
| FileUtils.delete(this.filePath.toFile()); |
| } |
| } |
| } |