blob: 1a3755fd5a8a970813b2aa32fc51a516d2fa7b5f [file] [log] [blame]
/*******************************************************************************
* 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());
}
}
}