blob: 44cd09b33f4dbbf2f56f5ad10997c12b05374159 [file] [log] [blame]
/*
* 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
*
* 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.apache.lucene.gradle;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.gradle.api.internal.tasks.testing.logging.FullExceptionFormatter;
import org.gradle.api.internal.tasks.testing.logging.TestExceptionFormatter;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.tasks.testing.TestDescriptor;
import org.gradle.api.tasks.testing.TestListener;
import org.gradle.api.tasks.testing.TestOutputEvent;
import org.gradle.api.tasks.testing.TestOutputListener;
import org.gradle.api.tasks.testing.TestResult;
import org.gradle.api.tasks.testing.logging.TestLogging;
/**
* An error reporting listener that queues test output streams and displays them
* on failure.
* <p>
* Heavily inspired by Elasticsearch's ErrorReportingTestListener (ASL 2.0 licensed).
*/
public class ErrorReportingTestListener implements TestOutputListener, TestListener {
private static final Logger LOGGER = Logging.getLogger(ErrorReportingTestListener.class);
private final TestExceptionFormatter formatter;
private final Map<TestKey, OutputHandler> outputHandlers = new ConcurrentHashMap<>();
private final Path spillDir;
private final Path outputsDir;
private final boolean verboseMode;
public ErrorReportingTestListener(TestLogging testLogging, Path spillDir, Path outputsDir, boolean verboseMode) {
this.formatter = new FullExceptionFormatter(testLogging);
this.spillDir = spillDir;
this.outputsDir = outputsDir;
this.verboseMode = verboseMode;
}
@Override
public void onOutput(TestDescriptor testDescriptor, TestOutputEvent outputEvent) {
handlerFor(testDescriptor).write(outputEvent);
}
@Override
public void beforeSuite(TestDescriptor suite) {
// noop.
}
@Override
public void beforeTest(TestDescriptor testDescriptor) {
// Noop.
}
@Override
public void afterSuite(final TestDescriptor suite, TestResult result) {
if (suite.getParent() == null || suite.getName().startsWith("Gradle")) {
return;
}
TestKey key = TestKey.of(suite);
try {
OutputHandler outputHandler = outputHandlers.get(key);
if (outputHandler != null) {
long length = outputHandler.length();
if (length > 1024 * 1024 * 10) {
LOGGER.warn(String.format(Locale.ROOT, "WARNING: Test %s wrote %,d bytes of output.",
suite.getName(),
length));
}
}
boolean echoOutput = Objects.equals(result.getResultType(), TestResult.ResultType.FAILURE);
boolean dumpOutput = echoOutput;
// If the test suite failed, report output.
if (dumpOutput || echoOutput) {
Files.createDirectories(outputsDir);
Path outputLog = outputsDir.resolve(getOutputLogName(suite));
// Save the output of a failing test to disk.
try (Writer w = Files.newBufferedWriter(outputLog, StandardCharsets.UTF_8)) {
if (outputHandler != null) {
outputHandler.copyTo(w);
}
}
if (echoOutput && !verboseMode) {
synchronized (this) {
System.out.println("");
System.out.println(suite.getClassName() + " > test suite's output saved to " + outputLog + ", copied below:");
try (BufferedReader reader = Files.newBufferedReader(outputLog, StandardCharsets.UTF_8)) {
char[] buf = new char[1024];
int len;
while ((len = reader.read(buf)) >= 0) {
System.out.print(new String(buf, 0, len));
}
System.out.println();
}
}
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
OutputHandler handler = outputHandlers.remove(key);
if (handler != null) {
try {
handler.close();
} catch (IOException e) {
LOGGER.error("Failed to close output handler for: " + key, e);
}
}
}
}
private static Pattern SANITIZE = Pattern.compile("[^a-zA-Z .\\-_0-9]+");
public static String getOutputLogName(TestDescriptor suite) {
return SANITIZE.matcher("OUTPUT-" + suite.getName() + ".txt").replaceAll("_");
}
@Override
public void afterTest(TestDescriptor testDescriptor, TestResult result) {
// Include test failure exception stacktrace(s) in test output log.
if (result.getResultType() == TestResult.ResultType.FAILURE) {
if (result.getExceptions().size() > 0) {
String message = formatter.format(testDescriptor, result.getExceptions());
handlerFor(testDescriptor).write(message);
}
}
}
private OutputHandler handlerFor(TestDescriptor descriptor) {
// Attach output of leaves (individual tests) to their parent.
if (!descriptor.isComposite()) {
descriptor = descriptor.getParent();
}
return outputHandlers.computeIfAbsent(TestKey.of(descriptor), (key) -> new OutputHandler());
}
public static class TestKey {
private final String key;
private TestKey(String key) {
this.key = key;
}
public static TestKey of(TestDescriptor d) {
StringBuilder key = new StringBuilder();
key.append(d.getClassName());
key.append("::");
key.append(d.getName());
key.append("::");
key.append(d.getParent() == null ? "-" : d.getParent().toString());
return new TestKey(key.toString());
}
@Override
public boolean equals(Object o) {
return o != null &&
o.getClass() == this.getClass() &&
Objects.equals(((TestKey) o).key, key);
}
@Override
public int hashCode() {
return key.hashCode();
}
@Override
public String toString() {
return key;
}
}
private class OutputHandler implements Closeable {
// Max single-line buffer before automatic wrap occurs.
private static final int MAX_LINE_WIDTH = 1024 * 4;
private final SpillWriter buffer;
// internal stream.
private final PrefixedWriter sint;
// stdout
private final PrefixedWriter sout;
// stderr
private final PrefixedWriter serr;
// last used stream (so that we can flush it properly and prefixes are not screwed up).
private PrefixedWriter last;
public OutputHandler() {
buffer = new SpillWriter(() -> {
try {
return Files.createTempFile(spillDir, "spill-", ".tmp");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
Writer sink = buffer;
if (verboseMode) {
sink = new StdOutTeeWriter(buffer);
}
sint = new PrefixedWriter(" > ", sink, MAX_LINE_WIDTH);
sout = new PrefixedWriter(" 1> ", sink, MAX_LINE_WIDTH);
serr = new PrefixedWriter(" 2> ", sink, MAX_LINE_WIDTH);
last = sint;
}
public void write(TestOutputEvent event) {
write((event.getDestination() == TestOutputEvent.Destination.StdOut ? sout : serr), event.getMessage());
}
public void write(String message) {
write(sint, message);
}
public long length() throws IOException {
return buffer.length();
}
private void write(PrefixedWriter out, String message) {
try {
if (out != last) {
last.completeLine();
last = out;
}
out.write(message);
} catch (IOException e) {
throw new UncheckedIOException("Unable to write to test output.", e);
}
}
public void copyTo(Writer out) throws IOException {
flush();
buffer.copyTo(out);
}
public void flush() throws IOException {
sout.completeLine();
serr.completeLine();
buffer.flush();
}
@Override
public void close() throws IOException {
buffer.close();
}
}
}