Skip to content

Commit

Permalink
Use a "store" for sysout/syserr content, which first uses in-memory s…
Browse files Browse the repository at this point in the history
…ized limited buffer, before switching to a file based store, if the size limit is exceeded
  • Loading branch information
jaikiran committed Feb 23, 2018
1 parent 9704d63 commit b76c5d6
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,40 @@
import org.junit.platform.launcher.TestPlan;

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.OutputStream;
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;

/**
* Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
*/
abstract class AbstractJUnitResultFormatter implements TestResultFormatter {


protected static String NEW_LINE = System.getProperty("line.separator");
protected Path sysOutFilePath;
protected Path sysErrFilePath;
protected Task task;

private OutputStream sysOutStream;
private OutputStream sysErrStream;
private SysOutErrContentStore sysOutStore;
private SysOutErrContentStore sysErrStore;

@Override
public void sysOutAvailable(final byte[] data) {
if (this.sysOutStream == null) {
try {
this.sysOutFilePath = Files.createTempFile(null, "sysout");
this.sysOutFilePath.toFile().deleteOnExit();
this.sysOutStream = Files.newOutputStream(this.sysOutFilePath);
} catch (IOException e) {
handleException(e);
return;
}
if (this.sysOutStore == null) {
this.sysOutStore = new SysOutErrContentStore(true);
}
try {
this.sysOutStream.write(data);
this.sysOutStore.store(data);
} catch (IOException e) {
handleException(e);
return;
Expand All @@ -52,18 +51,11 @@ public void sysOutAvailable(final byte[] data) {

@Override
public void sysErrAvailable(final byte[] data) {
if (this.sysErrStream == null) {
try {
this.sysErrFilePath = Files.createTempFile(null, "syserr");
this.sysErrFilePath.toFile().deleteOnExit();
this.sysErrStream = Files.newOutputStream(this.sysErrFilePath);
} catch (IOException e) {
handleException(e);
return;
}
if (this.sysErrStore == null) {
this.sysErrStore = new SysOutErrContentStore(false);
}
try {
this.sysErrStream.write(data);
this.sysErrStore.store(data);
} catch (IOException e) {
handleException(e);
return;
Expand All @@ -75,12 +67,66 @@ public void setExecutingTask(final Task task) {
this.task = task;
}

protected void writeSysOut(final Writer writer) throws IOException {
this.writeFrom(this.sysOutFilePath, writer);
/**
* @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);
}

protected void writeSysErr(final Writer writer) throws IOException {
this.writeFrom(this.sysErrFilePath, 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) {
Expand All @@ -106,13 +152,10 @@ static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
return Optional.empty();
}

private void writeFrom(final Path path, final Writer writer) throws IOException {
private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
final char[] chars = new char[1024];
int numRead = -1;
// 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
try (final BufferedReader reader = new BufferedReader(new FileReader(path.toFile()))) {
try (final Reader reader = store.getReader()) {
while ((numRead = reader.read(chars)) != -1) {
writer.write(chars, 0, numRead);
}
Expand All @@ -121,12 +164,132 @@ private void writeFrom(final Path path, final Writer writer) throws IOException

@Override
public void close() throws IOException {
FileUtils.close(this.sysOutStream);
FileUtils.close(this.sysErrStream);
FileUtils.close(this.sysOutStore);
FileUtils.close(this.sysErrStore);
}

protected void handleException(final Throwable t) {
// we currently just log it and move on.
task.getProject().log("Exception in listener " + 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;

private SysOutErrContentStore(final boolean isSysOut) {
this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
}

private void store(final byte[] data) throws IOException {
if (this.usingFileStore) {
this.storeToFile(data);
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());
storeToFile(data);
// discard the in-memory store
this.inMemoryStore = null;
}

private void storeToFile(final byte[] data) throws IOException {
if (this.fileOutputStream == null) {
// no backing file was created so we can't do anything
return;
}
this.fileOutputStream.write(data);
}

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.
*/
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;
}
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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -84,14 +83,14 @@ public void testPlanExecutionFinished(final TestPlan testPlan) {
}
// write out sysout and syserr content if any
try {
if (this.sysOutFilePath != null && Files.exists(this.sysOutFilePath)) {
if (this.hasSysOut()) {
this.writer.write("------------- Standard Output ---------------");
this.writer.newLine();
writeSysOut(writer);
this.writer.write("------------- ---------------- ---------------");
this.writer.newLine();
}
if (this.sysErrFilePath != null && Files.exists(this.sysErrFilePath)) {
if (this.hasSysErr()) {
this.writer.write("------------- Standard Error ---------------");
this.writer.newLine();
writeSysErr(writer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.Reader;
import java.util.Date;
import java.util.Hashtable;
import java.util.Map;
Expand Down Expand Up @@ -305,38 +302,37 @@ private void writeAborted(final XMLStreamWriter writer, final TestIdentifier tes
}

private void writeSysOut(final XMLStreamWriter writer) throws XMLStreamException, IOException {
if (LegacyXmlResultFormatter.this.sysOutFilePath == null || !Files.exists(LegacyXmlResultFormatter.this.sysOutFilePath)) {
if (!LegacyXmlResultFormatter.this.hasSysOut()) {
return;
}
writer.writeStartElement(ELEM_SYSTEM_OUT);
writeCharactersFrom(LegacyXmlResultFormatter.this.sysOutFilePath, writer);
try (final Reader reader = LegacyXmlResultFormatter.this.getSysOutReader()) {
writeCharactersFrom(reader, writer);
}
writer.writeEndElement();
}

private void writeSysErr(final XMLStreamWriter writer) throws XMLStreamException, IOException {
if (LegacyXmlResultFormatter.this.sysErrFilePath == null || !Files.exists(LegacyXmlResultFormatter.this.sysErrFilePath)) {
if (!LegacyXmlResultFormatter.this.hasSysErr()) {
return;
}
writer.writeStartElement(ELEM_SYSTEM_ERR);
writeCharactersFrom(LegacyXmlResultFormatter.this.sysErrFilePath, writer);
try (final Reader reader = LegacyXmlResultFormatter.this.getSysErrReader()) {
writeCharactersFrom(reader, writer);
}
writer.writeEndElement();
}

private void writeCharactersFrom(final Path path, final XMLStreamWriter writer) throws IOException, XMLStreamException {
// 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
try (final BufferedReader reader = new BufferedReader(new FileReader(path.toFile()))) {
final char[] chars = new char[1024];
int numRead = -1;
while ((numRead = reader.read(chars)) != -1) {
// although it's called a DOMElementWriter, the encode method is just a
// straight forward XML util method which doesn't concern about whether
// DOM, SAX, StAX semantics.
// TODO: Perhaps make it a static method
final String encoded = new DOMElementWriter().encode(new String(chars, 0, numRead));
writer.writeCharacters(encoded);
}
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) {
// although it's called a DOMElementWriter, the encode method is just a
// straight forward XML util method which doesn't concern about whether
// DOM, SAX, StAX semantics.
// TODO: Perhaps make it a static method
final String encoded = new DOMElementWriter().encode(new String(chars, 0, numRead));
writer.writeCharacters(encoded);
}
}

Expand Down

0 comments on commit b76c5d6

Please sign in to comment.