/*
* Copyright 2013 the original author or authors.
*
* Licensed 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
*
* http://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.gradle.api.internal.tasks.testing.junit.result;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.google.common.collect.ImmutableMap;
import org.gradle.api.UncheckedIOException;
import org.gradle.api.tasks.testing.TestOutputEvent;
import org.gradle.internal.io.RandomAccessFileInputStream;
import org.gradle.internal.UncheckedException;
import org.gradle.internal.serialize.kryo.KryoBackedDecoder;
import org.gradle.internal.serialize.kryo.KryoBackedEncoder;
import java.io.*;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;
public class TestOutputStore {
private final File resultsDir;
private final Charset messageStorageCharset;
public TestOutputStore(File resultsDir) {
this.resultsDir = resultsDir;
this.messageStorageCharset = Charset.forName("UTF-8");
}
File getOutputsFile() {
return new File(resultsDir, "output.bin");
}
File getIndexFile() {
return new File(resultsDir, getOutputsFile().getName() + ".idx");
}
private static class Region {
long start;
long stop;
private Region() {
start = -1;
stop = -1;
}
private Region(long start, long stop) {
this.start = start;
this.stop = stop;
}
}
private static class TestCaseRegion {
Region stdOutRegion = new Region();
Region stdErrRegion = new Region();
}
public class Writer implements Closeable {
private final KryoBackedEncoder output;
private final Map<Long, Map<Long, TestCaseRegion>> index = new LinkedHashMap<Long, Map<Long, TestCaseRegion>>();
public Writer() {
try {
output = new KryoBackedEncoder(new FileOutputStream(getOutputsFile()));
} catch (FileNotFoundException e) {
throw new UncheckedIOException(e);
}
}
@Override
public void close() {
output.close();
writeIndex();
}
public void onOutput(long classId, TestOutputEvent outputEvent) {
onOutput(classId, 0, outputEvent);
}
public void onOutput(long classId, long testId, TestOutputEvent outputEvent) {
boolean stdout = outputEvent.getDestination() == TestOutputEvent.Destination.StdOut;
mark(classId, testId, stdout);
output.writeBoolean(stdout);
output.writeSmallLong(classId);
output.writeSmallLong(testId);
byte[] bytes;
try {
bytes = outputEvent.getMessage().getBytes(messageStorageCharset.name());
} catch (UnsupportedEncodingException e) {
throw UncheckedException.throwAsUncheckedException(e);
}
output.writeSmallInt(bytes.length);
output.writeBytes(bytes, 0, bytes.length);
}
private void mark(long classId, long testId, boolean isStdout) {
if (!index.containsKey(classId)) {
index.put(classId, new LinkedHashMap<Long, TestCaseRegion>());
}
Map<Long, TestCaseRegion> testCaseRegions = index.get(classId);
if (!testCaseRegions.containsKey(testId)) {
TestCaseRegion region = new TestCaseRegion();
testCaseRegions.put(testId, region);
}
TestCaseRegion region = testCaseRegions.get(testId);
Region streamRegion = isStdout ? region.stdOutRegion : region.stdErrRegion;
int total = output.getWritePosition();
if (streamRegion.start < 0) {
streamRegion.start = total;
}
streamRegion.stop = total;
}
private void writeIndex() {
Output indexOutput;
try {
indexOutput = new Output(new FileOutputStream(getIndexFile()));
} catch (FileNotFoundException e) {
throw new UncheckedIOException(e);
}
try {
indexOutput.writeInt(index.size(), true);
for (Map.Entry<Long, Map<Long, TestCaseRegion>> classEntry : index.entrySet()) {
Long classId = classEntry.getKey();
Map<Long, TestCaseRegion> regions = classEntry.getValue();
indexOutput.writeLong(classId, true);
indexOutput.writeInt(regions.size(), true);
for (Map.Entry<Long, TestCaseRegion> testCaseEntry : regions.entrySet()) {
long id = testCaseEntry.getKey();
TestCaseRegion region = testCaseEntry.getValue();
indexOutput.writeLong(id, true);
indexOutput.writeLong(region.stdOutRegion.start);
indexOutput.writeLong(region.stdOutRegion.stop);
indexOutput.writeLong(region.stdErrRegion.start);
indexOutput.writeLong(region.stdErrRegion.stop);
}
}
} finally {
indexOutput.close();
}
}
}
public Writer writer() {
return new Writer();
}
private static class Index {
final ImmutableMap<Long, Index> children;
final Region stdOut;
final Region stdErr;
private Index(Region stdOut, Region stdErr) {
this.children = ImmutableMap.of();
this.stdOut = stdOut;
this.stdErr = stdErr;
}
private Index(ImmutableMap<Long, Index> children, Region stdOut, Region stdErr) {
this.children = children;
this.stdOut = stdOut;
this.stdErr = stdErr;
}
}
private static class IndexBuilder {
final Region stdOut = new Region();
final Region stdErr = new Region();
private final ImmutableMap.Builder<Long, Index> children = ImmutableMap.builder();
void add(long key, Index index) {
if (stdOut.start < 0) {
stdOut.start = index.stdOut.start;
}
if (stdErr.start < 0) {
stdErr.start = index.stdErr.start;
}
if (index.stdOut.stop > stdOut.stop) {
stdOut.stop = index.stdOut.stop;
}
if (index.stdErr.stop > stdErr.stop) {
stdErr.stop = index.stdErr.stop;
}
children.put(key, index);
}
Index build() {
return new Index(children.build(), stdOut, stdErr);
}
}
public class Reader implements Closeable {
private final Index index;
private final RandomAccessFile dataFile;
public Reader() {
File indexFile = getIndexFile();
File outputsFile = getOutputsFile();
if (outputsFile.exists()) {
if (!indexFile.exists()) {
throw new IllegalStateException(String.format("Test outputs data file '%s' exists but the index file '%s' does not", outputsFile, indexFile));
}
Input input;
try {
input = new Input(new FileInputStream(indexFile));
} catch (FileNotFoundException e) {
throw new UncheckedIOException(e);
}
IndexBuilder rootBuilder = null;
try {
int numClasses = input.readInt(true);
rootBuilder = new IndexBuilder();
for (int classCounter = 0; classCounter < numClasses; ++classCounter) {
long classId = input.readLong(true);
IndexBuilder classBuilder = new IndexBuilder();
int numEntries = input.readInt(true);
for (int entryCounter = 0; entryCounter < numEntries; ++entryCounter) {
long testId = input.readLong(true);
Region stdOut = new Region(input.readLong(), input.readLong());
Region stdErr = new Region(input.readLong(), input.readLong());
classBuilder.add(testId, new Index(stdOut, stdErr));
}
rootBuilder.add(classId, classBuilder.build());
}
} finally {
input.close();
}
index = rootBuilder.build();
try {
dataFile = new RandomAccessFile(getOutputsFile(), "r");
} catch (FileNotFoundException e) {
throw new UncheckedIOException(e);
}
} else { // no outputs file
if (indexFile.exists()) {
throw new IllegalStateException(String.format("Test outputs data file '%s' does not exist but the index file '%s' does", outputsFile, indexFile));
}
index = null;
dataFile = null;
}
}
@Override
public void close() throws IOException {
if (dataFile != null) {
dataFile.close();
}
}
public boolean hasOutput(long classId, TestOutputEvent.Destination destination) {
if (dataFile == null) {
return false;
}
Index classIndex = index.children.get(classId);
if (classIndex == null) {
return false;
} else {
Region region = destination == TestOutputEvent.Destination.StdOut ? classIndex.stdOut : classIndex.stdErr;
return region.start >= 0;
}
}
public void writeAllOutput(long classId, TestOutputEvent.Destination destination, java.io.Writer writer) {
doRead(classId, 0, true, destination, writer);
}
public void writeNonTestOutput(long classId, TestOutputEvent.Destination destination, java.io.Writer writer) {
doRead(classId, 0, false, destination, writer);
}
public void writeTestOutput(long classId, long testId, TestOutputEvent.Destination destination, java.io.Writer writer) {
doRead(classId, testId, false, destination, writer);
}
private void doRead(long classId, long testId, boolean allClassOutput, TestOutputEvent.Destination destination, java.io.Writer writer) {
if (dataFile == null) {
return;
}
Index targetIndex = index.children.get(classId);
if (targetIndex != null && testId != 0) {
targetIndex = targetIndex.children.get(testId);
}
if (targetIndex == null) {
return;
}
boolean stdout = destination == TestOutputEvent.Destination.StdOut;
Region region = stdout ? targetIndex.stdOut : targetIndex.stdErr;
if (region.start < 0) {
return;
}
boolean ignoreClassLevel = !allClassOutput && testId != 0;
boolean ignoreTestLevel = !allClassOutput && testId == 0;
try {
dataFile.seek(region.start);
long maxPos = region.stop - region.start;
KryoBackedDecoder decoder = new KryoBackedDecoder(new RandomAccessFileInputStream(dataFile));
while (decoder.getReadPosition() <= maxPos) {
boolean readStdout = decoder.readBoolean();
long readClassId = decoder.readSmallLong();
long readTestId = decoder.readSmallLong();
int readLength = decoder.readSmallInt();
boolean isClassLevel = readTestId == 0;
if (stdout != readStdout || classId != readClassId) {
decoder.skipBytes(readLength);
continue;
}
if (ignoreClassLevel && isClassLevel) {
decoder.skipBytes(readLength);
continue;
}
if (ignoreTestLevel && !isClassLevel) {
decoder.skipBytes(readLength);
continue;
}
if (testId == 0 || testId == readTestId) {
byte[] stringBytes = new byte[readLength];
decoder.readBytes(stringBytes);
String message;
try {
message = new String(stringBytes, messageStorageCharset.name());
} catch (UnsupportedEncodingException e) {
// shouldn't happen
throw UncheckedException.throwAsUncheckedException(e);
}
writer.write(message);
} else {
decoder.skipBytes(readLength);
}
}
} catch (IOException e1) {
throw new UncheckedIOException(e1);
}
}
}
// IMPORTANT: return must be closed when done with.
public Reader reader() {
return new Reader();
}
}