Skip to content

Commit 3997952

Browse files
authored
Attribute non-test thread output to most recent test thread (#4200)
When using frameworks or running external processes, test output is often written on other threads than the test. When tests are executed sequentially that output can be attributed unambiguously to the current test. If tests are run in parallel, picking the right test to attribute output to becomes much harder, though.
1 parent 779ee68 commit 3997952

File tree

3 files changed

+58
-14
lines changed

3 files changed

+58
-14
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ JUnit repository on GitHub.
5252
- If <<../user-guide/index.adoc#running-tests-capturing-output, output capturing>> is
5353
enabled, the captured output written to `System.out` and `System.err` is now included
5454
in the XML report.
55+
* Output written to `System.out` and `System.err` from non-test threads is now attributed
56+
to the most recent test or container that was started or has written output.
5557
* Introduced contracts for Kotlin-specific assertion methods.
5658

5759

junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptor.java

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
import java.util.ArrayDeque;
1616
import java.util.Deque;
1717
import java.util.Optional;
18+
import java.util.concurrent.ConcurrentLinkedDeque;
1819
import java.util.function.Consumer;
1920

2021
/**
2122
* @since 1.3
2223
*/
2324
class StreamInterceptor extends PrintStream {
2425

26+
private final Deque<RewindableByteArrayOutputStream> mostRecentOutputs = new ConcurrentLinkedDeque<>();
27+
2528
private final PrintStream originalStream;
2629
private final Consumer<PrintStream> unregisterAction;
2730
private final int maxNumberOfBytesPerThread;
@@ -56,11 +59,18 @@ private StreamInterceptor(PrintStream originalStream, Consumer<PrintStream> unre
5659
}
5760

5861
void capture() {
59-
output.get().mark();
62+
RewindableByteArrayOutputStream out = output.get();
63+
out.mark();
64+
pushToTop(out);
6065
}
6166

6267
String consume() {
63-
return output.get().rewind();
68+
RewindableByteArrayOutputStream out = output.get();
69+
String result = out.rewind();
70+
if (!out.isMarked()) {
71+
mostRecentOutputs.remove(out);
72+
}
73+
return result;
6474
}
6575

6676
void unregister() {
@@ -69,8 +79,9 @@ void unregister() {
6979

7080
@Override
7181
public void write(int b) {
72-
RewindableByteArrayOutputStream out = output.get();
73-
if (out.isMarked() && out.size() < maxNumberOfBytesPerThread) {
82+
RewindableByteArrayOutputStream out = getOutput();
83+
if (out != null && out.size() < maxNumberOfBytesPerThread) {
84+
pushToTop(out);
7485
out.write(b);
7586
}
7687
super.write(b);
@@ -83,16 +94,29 @@ public void write(byte[] b) {
8394

8495
@Override
8596
public void write(byte[] buf, int off, int len) {
86-
RewindableByteArrayOutputStream out = output.get();
87-
if (out.isMarked()) {
97+
RewindableByteArrayOutputStream out = getOutput();
98+
if (out != null) {
8899
int actualLength = Math.max(0, Math.min(len, maxNumberOfBytesPerThread - out.size()));
89100
if (actualLength > 0) {
101+
pushToTop(out);
90102
out.write(buf, off, actualLength);
91103
}
92104
}
93105
super.write(buf, off, len);
94106
}
95107

108+
private void pushToTop(RewindableByteArrayOutputStream out) {
109+
if (!out.equals(mostRecentOutputs.peek())) {
110+
mostRecentOutputs.remove(out);
111+
mostRecentOutputs.push(out);
112+
}
113+
}
114+
115+
private RewindableByteArrayOutputStream getOutput() {
116+
RewindableByteArrayOutputStream out = output.get();
117+
return out.isMarked() ? out : mostRecentOutputs.peek();
118+
}
119+
96120
static class RewindableByteArrayOutputStream extends ByteArrayOutputStream {
97121

98122
private final Deque<Integer> markedPositions = new ArrayDeque<>();

platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptorTests.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,27 @@
2020
import java.io.PrintStream;
2121
import java.util.stream.IntStream;
2222

23+
import org.junit.jupiter.api.AutoClose;
2324
import org.junit.jupiter.api.Test;
2425

2526
/**
2627
* @since 1.3
2728
*/
2829
class StreamInterceptorTests {
2930

30-
private ByteArrayOutputStream originalOut = new ByteArrayOutputStream();
31-
private PrintStream targetStream = new PrintStream(originalOut);
31+
final ByteArrayOutputStream originalOut = new ByteArrayOutputStream();
32+
PrintStream targetStream = new PrintStream(originalOut);
33+
34+
@AutoClose
35+
StreamInterceptor streamInterceptor;
3236

3337
@Test
3438
void interceptsWriteOperationsToStreamPerThread() {
35-
var streamInterceptor = StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream,
39+
streamInterceptor = StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream,
3640
3).orElseThrow(RuntimeException::new);
3741
// @formatter:off
3842
IntStream.range(0, 1000)
3943
.parallel()
40-
.peek(i -> targetStream.println(i))
4144
.mapToObj(String::valueOf)
4245
.peek(i -> streamInterceptor.capture())
4346
.peek(i -> targetStream.println(i))
@@ -49,7 +52,7 @@ void interceptsWriteOperationsToStreamPerThread() {
4952
void unregisterRestoresOriginalStream() {
5053
var originalStream = targetStream;
5154

52-
var streamInterceptor = StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream,
55+
streamInterceptor = StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream,
5356
3).orElseThrow(RuntimeException::new);
5457
assertSame(streamInterceptor, targetStream);
5558

@@ -61,8 +64,8 @@ void unregisterRestoresOriginalStream() {
6164
void writeForwardsOperationsToOriginalStream() throws IOException {
6265
var originalStream = targetStream;
6366

64-
StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream, 2).orElseThrow(
65-
RuntimeException::new);
67+
streamInterceptor = StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream,
68+
2).orElseThrow(RuntimeException::new);
6669
assertNotSame(originalStream, targetStream);
6770

6871
targetStream.write('a');
@@ -73,7 +76,7 @@ void writeForwardsOperationsToOriginalStream() throws IOException {
7376

7477
@Test
7578
void handlesNestedCaptures() {
76-
var streamInterceptor = StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream,
79+
streamInterceptor = StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream,
7780
100).orElseThrow(RuntimeException::new);
7881

7982
String outermost, inner, innermost;
@@ -100,4 +103,19 @@ void handlesNestedCaptures() {
100103
() -> assertEquals("innermost", innermost) //
101104
);
102105
}
106+
107+
@Test
108+
void capturesOutputFromNonTestThreads() throws Exception {
109+
streamInterceptor = StreamInterceptor.register(targetStream, newStream -> this.targetStream = newStream,
110+
100).orElseThrow(RuntimeException::new);
111+
112+
streamInterceptor.capture();
113+
var thread = new Thread(() -> {
114+
targetStream.println("from non-test thread");
115+
});
116+
thread.start();
117+
thread.join();
118+
119+
assertEquals("from non-test thread", streamInterceptor.consume().trim());
120+
}
103121
}

0 commit comments

Comments
 (0)