mirror of https://github.com/apache/lucene.git
Speed up spill writer. Echo failed test output to disk.
This commit is contained in:
parent
14dd5a5e4d
commit
85d261339b
|
@ -1,14 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
package org.apache.lucene.gradle;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
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.FullExceptionFormatter;
|
||||||
import org.gradle.api.internal.tasks.testing.logging.TestExceptionFormatter;
|
import org.gradle.api.internal.tasks.testing.logging.TestExceptionFormatter;
|
||||||
import org.gradle.api.logging.Logger;
|
import org.gradle.api.logging.Logger;
|
||||||
|
@ -32,22 +49,17 @@ public class ErrorReportingTestListener implements TestOutputListener, TestListe
|
||||||
private final TestExceptionFormatter formatter;
|
private final TestExceptionFormatter formatter;
|
||||||
private final Map<TestKey, OutputHandler> outputHandlers = new ConcurrentHashMap<>();
|
private final Map<TestKey, OutputHandler> outputHandlers = new ConcurrentHashMap<>();
|
||||||
private final Path spillDir;
|
private final Path spillDir;
|
||||||
|
private final Path outputsDir;
|
||||||
|
|
||||||
public ErrorReportingTestListener(TestLogging testLogging, Path spillDir) {
|
public ErrorReportingTestListener(TestLogging testLogging, Path spillDir, Path outputsDir) {
|
||||||
this.formatter = new FullExceptionFormatter(testLogging);
|
this.formatter = new FullExceptionFormatter(testLogging);
|
||||||
this.spillDir = spillDir;
|
this.spillDir = spillDir;
|
||||||
|
this.outputsDir = outputsDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOutput(TestDescriptor testDescriptor, TestOutputEvent outputEvent) {
|
public void onOutput(TestDescriptor testDescriptor, TestOutputEvent outputEvent) {
|
||||||
TestDescriptor suite = testDescriptor.getParent();
|
handlerFor(testDescriptor).write(outputEvent);
|
||||||
|
|
||||||
// Check if this is output from the test suite itself (e.g. afterTest or beforeTest)
|
|
||||||
if (testDescriptor.isComposite()) {
|
|
||||||
suite = testDescriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlerFor(suite).write(outputEvent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -62,51 +74,89 @@ public class ErrorReportingTestListener implements TestOutputListener, TestListe
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterSuite(final TestDescriptor suite, TestResult result) {
|
public void afterSuite(final TestDescriptor suite, TestResult result) {
|
||||||
|
if (suite.getParent() == null || suite.getName().startsWith("Gradle")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
TestKey key = TestKey.of(suite);
|
TestKey key = TestKey.of(suite);
|
||||||
try {
|
try {
|
||||||
// if the test suite failed, report all captured output
|
OutputHandler outputHandler = outputHandlers.get(key);
|
||||||
if (Objects.equals(result.getResultType(), TestResult.ResultType.FAILURE)) {
|
if (outputHandler != null) {
|
||||||
reportFailure(suite, outputHandlers.get(key));
|
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; // Force output dumping.
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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) {
|
} catch (IOException e) {
|
||||||
throw new UncheckedIOException("Error reading test suite output", e);
|
throw new UncheckedIOException(e);
|
||||||
} finally {
|
} finally {
|
||||||
OutputHandler writer = outputHandlers.remove(key);
|
OutputHandler handler = outputHandlers.remove(key);
|
||||||
if (writer != null) {
|
if (handler != null) {
|
||||||
try {
|
try {
|
||||||
writer.close();
|
handler.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.error("Failed to close test suite's event writer for: " + key, e);
|
LOGGER.error("Failed to close output handler for: " + key, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reportFailure(TestDescriptor suite, OutputHandler outputHandler) throws IOException {
|
private static Pattern SANITIZE = Pattern.compile("[^a-zA-Z .\\-_0-9]+");
|
||||||
if (outputHandler != null) {
|
|
||||||
synchronized (this) {
|
public static String getOutputLogName(TestDescriptor suite) {
|
||||||
System.out.println("");
|
return SANITIZE.matcher("OUTPUT-" + suite.getName() + ".txt").replaceAll("_");
|
||||||
System.out.println(suite.getClassName() + " > test suite's output copied below:");
|
|
||||||
outputHandler.copyTo(System.out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterTest(TestDescriptor testDescriptor, TestResult result) {
|
public void afterTest(TestDescriptor testDescriptor, TestResult result) {
|
||||||
// Include test failure exception stacktrace(s) in test output log.
|
// Include test failure exception stacktrace(s) in test output log.
|
||||||
if (result.getResultType() == TestResult.ResultType.FAILURE) {
|
if (result.getResultType() == TestResult.ResultType.FAILURE) {
|
||||||
if (testDescriptor.getParent() != null) {
|
if (result.getExceptions().size() > 0) {
|
||||||
if (result.getExceptions().size() > 0) {
|
String message = formatter.format(testDescriptor, result.getExceptions());
|
||||||
String message = formatter.format(testDescriptor, result.getExceptions());
|
handlerFor(testDescriptor).write(message);
|
||||||
handlerFor(testDescriptor.getParent()).write(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private OutputHandler handlerFor(TestDescriptor suite) {
|
private OutputHandler handlerFor(TestDescriptor descriptor) {
|
||||||
return outputHandlers.computeIfAbsent(TestKey.of(suite), (key) -> new OutputHandler());
|
// 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 {
|
public static class TestKey {
|
||||||
|
@ -183,6 +233,10 @@ public class ErrorReportingTestListener implements TestOutputListener, TestListe
|
||||||
write(sint, message);
|
write(sint, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long length() throws IOException {
|
||||||
|
return buffer.length();
|
||||||
|
}
|
||||||
|
|
||||||
private void write(PrefixedWriter out, String message) {
|
private void write(PrefixedWriter out, String message) {
|
||||||
try {
|
try {
|
||||||
if (out != last) {
|
if (out != last) {
|
||||||
|
@ -195,10 +249,9 @@ public class ErrorReportingTestListener implements TestOutputListener, TestListe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void copyTo(PrintStream out) throws IOException {
|
public void copyTo(Writer out) throws IOException {
|
||||||
flush();
|
flush();
|
||||||
buffer.copyTo(out);
|
buffer.copyTo(out);
|
||||||
out.println();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void flush() throws IOException {
|
public void flush() throws IOException {
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
package org.apache.lucene.gradle;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
package org.apache.lucene.gradle;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
@ -27,6 +42,44 @@ public class SpillWriter extends Writer {
|
||||||
getSink(len).write(cbuf, off, len);
|
getSink(len).write(cbuf, off, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(int c) throws IOException {
|
||||||
|
getSink(1).write(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(char[] cbuf) throws IOException {
|
||||||
|
getSink(cbuf.length).write(cbuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(String str) throws IOException {
|
||||||
|
getSink(str.length()).write(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(String str, int off, int len) throws IOException {
|
||||||
|
getSink(len).write(str, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Writer append(CharSequence csq) throws IOException {
|
||||||
|
getSink(csq.length()).append(csq);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Writer append(CharSequence csq, int start, int end) throws IOException {
|
||||||
|
getSink(Math.max(0, end - start)).append(csq, start, end);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Writer append(char c) throws IOException {
|
||||||
|
getSink(1).append(c);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
private Writer getSink(int expectedWriteChars) throws IOException {
|
private Writer getSink(int expectedWriteChars) throws IOException {
|
||||||
if (spill == null) {
|
if (spill == null) {
|
||||||
if (buffer.getBuffer().length() + expectedWriteChars <= MAX_BUFFERED) {
|
if (buffer.getBuffer().length() + expectedWriteChars <= MAX_BUFFERED) {
|
||||||
|
@ -56,18 +109,23 @@ public class SpillWriter extends Writer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void copyTo(PrintStream ps) throws IOException {
|
public void copyTo(Writer writer) throws IOException {
|
||||||
if (spill != null) {
|
if (spill != null) {
|
||||||
flush();
|
flush();
|
||||||
char [] buf = new char [MAX_BUFFERED];
|
|
||||||
try (Reader reader = Files.newBufferedReader(spillPath, StandardCharsets.UTF_8)) {
|
try (Reader reader = Files.newBufferedReader(spillPath, StandardCharsets.UTF_8)) {
|
||||||
int len;
|
reader.transferTo(writer);
|
||||||
while ((len = reader.read(buf)) >= 0) {
|
|
||||||
ps.print(new String(buf, 0, len));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ps.append(buffer.getBuffer());
|
writer.append(buffer.getBuffer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long length() throws IOException {
|
||||||
|
flush();
|
||||||
|
if (spill != null) {
|
||||||
|
return Files.size(spillPath);
|
||||||
|
} else {
|
||||||
|
return buffer.getBuffer().length();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,8 +98,13 @@ allprojects {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up custom test output handler.
|
// Set up custom test output handler.
|
||||||
|
def testOutputsDir = file("${reports.junitXml.destination}/outputs")
|
||||||
|
doFirst {
|
||||||
|
project.delete testOutputsDir
|
||||||
|
}
|
||||||
|
|
||||||
def spillDir = getTemporaryDir().toPath()
|
def spillDir = getTemporaryDir().toPath()
|
||||||
def listener = new ErrorReportingTestListener(test.testLogging, spillDir)
|
def listener = new ErrorReportingTestListener(test.testLogging, spillDir, testOutputsDir.toPath())
|
||||||
addTestOutputListener(listener)
|
addTestOutputListener(listener)
|
||||||
addTestListener(listener)
|
addTestListener(listener)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue