Skip to content

Commit ce82b18

Browse files
Make SummaryGeneratingListener thread-safe (#2549)
Prior to this commit, concurrently reported failures were stored in an unguarded `ArrayList` which caused a race condition that sometimes caused failures to get lost and hence not reported. Now the list of failures is wrapped in `synchronizedList` to make it thread-safe. Fixes #2546. Co-authored-by: Christian Stein <[email protected]>
1 parent e884aa6 commit ce82b18

File tree

8 files changed

+101
-34
lines changed

8 files changed

+101
-34
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ on GitHub.
1515

1616
==== Bug Fixes
1717

18-
* ❓
18+
* `SummaryGeneratingListener` is now thread-safe and can handle concurrently failing
19+
tests.
1920

2021
==== Deprecations and Breaking Changes
2122

junit-jupiter-engine/junit-jupiter-engine.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies {
1717
testImplementation(project(":junit-platform-launcher"))
1818
testImplementation(project(":junit-platform-runner"))
1919
testImplementation(project(":junit-platform-testkit"))
20+
testImplementation(testFixtures(project(":junit-platform-commons")))
2021
testImplementation("org.jetbrains.kotlin:kotlin-stdlib")
2122
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
2223
testImplementation("org.codehaus.groovy:groovy")

junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/execution/ExtensionValuesStoreTests.java

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,16 @@
1010

1111
package org.junit.jupiter.engine.execution;
1212

13-
import static java.util.stream.Collectors.toList;
1413
import static org.assertj.core.api.Assertions.assertThat;
1514
import static org.junit.jupiter.api.Assertions.assertEquals;
1615
import static org.junit.jupiter.api.Assertions.assertNotEquals;
1716
import static org.junit.jupiter.api.Assertions.assertNull;
1817
import static org.junit.jupiter.api.Assertions.assertThrows;
18+
import static org.junit.platform.commons.test.ConcurrencyTestingUtils.executeConcurrently;
1919

20-
import java.util.ArrayList;
2120
import java.util.List;
22-
import java.util.concurrent.CompletableFuture;
23-
import java.util.concurrent.CountDownLatch;
24-
import java.util.concurrent.ExecutorService;
25-
import java.util.concurrent.Executors;
2621
import java.util.concurrent.atomic.AtomicInteger;
2722
import java.util.function.Function;
28-
import java.util.function.Supplier;
2923

3024
import org.junit.jupiter.api.Nested;
3125
import org.junit.jupiter.api.Test;
@@ -300,7 +294,7 @@ void removeNullValueWithTypeSafety() {
300294
}
301295

302296
@Test
303-
void simulateRaceConditionInGetOrComputeIfAbsent() {
297+
void simulateRaceConditionInGetOrComputeIfAbsent() throws Exception {
304298
int threads = 10;
305299
AtomicInteger counter = new AtomicInteger();
306300
ExtensionValuesStore localStore = new ExtensionValuesStore(null);
@@ -313,30 +307,6 @@ void simulateRaceConditionInGetOrComputeIfAbsent() {
313307
}
314308
}
315309

316-
private <T> List<T> executeConcurrently(int threads, Supplier<T> supplier) {
317-
ExecutorService executorService = Executors.newFixedThreadPool(threads);
318-
try {
319-
CountDownLatch latch = new CountDownLatch(threads);
320-
List<CompletableFuture<T>> futures = new ArrayList<>();
321-
for (int i = 0; i < threads; i++) {
322-
futures.add(CompletableFuture.supplyAsync(() -> {
323-
latch.countDown();
324-
try {
325-
latch.await();
326-
}
327-
catch (InterruptedException e) {
328-
Thread.currentThread().interrupt();
329-
}
330-
return supplier.get();
331-
}, executorService));
332-
}
333-
return futures.stream().map(CompletableFuture::join).collect(toList());
334-
}
335-
finally {
336-
executorService.shutdown();
337-
}
338-
}
339-
340310
@Nested
341311
class InheritedValuesTests {
342312

junit-platform-commons/junit-platform-commons.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ plugins {
44
`java-library-conventions`
55
`java-multi-release-sources`
66
`java-repackage-jars`
7+
`java-test-fixtures`
78
}
89

910
description = "JUnit Platform Commons"
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2015-2021 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.commons.test;
12+
13+
import static java.util.concurrent.TimeUnit.SECONDS;
14+
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import java.util.concurrent.Callable;
18+
import java.util.concurrent.CompletableFuture;
19+
import java.util.concurrent.CompletionException;
20+
import java.util.concurrent.CountDownLatch;
21+
import java.util.concurrent.ExecutorService;
22+
import java.util.concurrent.Executors;
23+
24+
public class ConcurrencyTestingUtils {
25+
26+
public static void executeConcurrently(int threads, Runnable action) throws Exception {
27+
executeConcurrently(threads, () -> {
28+
action.run();
29+
return null;
30+
});
31+
}
32+
33+
public static <T> List<T> executeConcurrently(int threads, Callable<T> action) throws Exception {
34+
ExecutorService executorService = Executors.newFixedThreadPool(threads);
35+
try {
36+
CountDownLatch latch = new CountDownLatch(threads);
37+
List<CompletableFuture<T>> futures = new ArrayList<>();
38+
for (int i = 0; i < threads; i++) {
39+
futures.add(CompletableFuture.supplyAsync(() -> {
40+
try {
41+
latch.countDown();
42+
latch.await();
43+
return action.call();
44+
}
45+
catch (InterruptedException e) {
46+
Thread.currentThread().interrupt();
47+
throw new CompletionException(e);
48+
}
49+
catch (Exception e) {
50+
throw new CompletionException("Action failed", e);
51+
}
52+
}, executorService));
53+
}
54+
List<T> list = new ArrayList<>();
55+
for (CompletableFuture<T> future : futures) {
56+
list.add(future.get(5, SECONDS));
57+
}
58+
return list;
59+
}
60+
finally {
61+
executorService.shutdownNow();
62+
var terminated = executorService.awaitTermination(5, SECONDS);
63+
if (!terminated) {
64+
//noinspection ThrowFromFinallyBlock
65+
throw new AssertionError("ExecutorService did not cleanly shut down");
66+
}
67+
}
68+
}
69+
}

junit-platform-launcher/src/main/java/org/junit/platform/launcher/listeners/MutableTestExecutionSummary.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package org.junit.platform.launcher.listeners;
1212

1313
import static java.lang.String.join;
14+
import static java.util.Collections.synchronizedList;
1415

1516
import java.io.PrintWriter;
1617
import java.util.ArrayList;
@@ -54,7 +55,7 @@ class MutableTestExecutionSummary implements TestExecutionSummary {
5455
final AtomicLong testsFailed = new AtomicLong();
5556

5657
private final TestPlan testPlan;
57-
private final List<Failure> failures = new ArrayList<>();
58+
private final List<Failure> failures = synchronizedList(new ArrayList<>());
5859
private final long timeStarted;
5960
long timeFinished;
6061

platform-tests/platform-tests.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies {
1818
// --- Things we are testing with ---------------------------------------------
1919
testImplementation(project(":junit-platform-runner"))
2020
testImplementation(project(":junit-platform-testkit"))
21+
testImplementation(testFixtures(project(":junit-platform-commons")))
2122
testImplementation(testFixtures(project(":junit-platform-engine")))
2223
testImplementation(testFixtures(project(":junit-platform-launcher")))
2324
testImplementation(project(":junit-jupiter-engine"))

platform-tests/src/test/java/org/junit/platform/launcher/listeners/SummaryGenerationTests.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
import static org.junit.jupiter.api.Assertions.assertEquals;
1616
import static org.junit.jupiter.api.Assertions.assertFalse;
1717
import static org.junit.jupiter.api.Assertions.assertTrue;
18+
import static org.junit.platform.commons.test.ConcurrencyTestingUtils.executeConcurrently;
1819

1920
import java.io.PrintWriter;
2021
import java.io.StringWriter;
2122
import java.util.List;
2223
import java.util.Optional;
2324

25+
import org.junit.jupiter.api.RepeatedTest;
2426
import org.junit.jupiter.api.Test;
2527
import org.junit.platform.engine.TestExecutionResult;
2628
import org.junit.platform.engine.TestSource;
@@ -220,6 +222,27 @@ public Optional<TestSource> getSource() {
220222
);
221223
}
222224

225+
@RepeatedTest(10)
226+
void reportingConcurrentlyFinishedTests() throws Exception {
227+
var numThreads = 250;
228+
var testIdentifier = TestIdentifier.from(new TestDescriptorStub(UniqueId.root("root", "2"), "failingTest") {
229+
@Override
230+
public Optional<TestSource> getSource() {
231+
return Optional.of(ClassSource.from(Object.class));
232+
}
233+
});
234+
var result = TestExecutionResult.failed(new RuntimeException());
235+
236+
listener.testPlanExecutionStarted(testPlan);
237+
executeConcurrently(numThreads, () -> {
238+
listener.executionStarted(testIdentifier);
239+
listener.executionFinished(testIdentifier, result);
240+
});
241+
listener.testPlanExecutionFinished(testPlan);
242+
243+
assertThat(listener.getSummary().getFailures()).hasSize(numThreads);
244+
}
245+
223246
@SuppressWarnings("deprecation")
224247
private TestIdentifier createTestIdentifier(String uniqueId) {
225248
var identifier = TestIdentifier.from(new TestDescriptorStub(UniqueId.root("test", uniqueId), uniqueId));

0 commit comments

Comments
 (0)