Skip to content

Commit ffc2547

Browse files
committed
Fix global read-write lock handling when not declared on top level
Prior to this change additional locks were not cleared from siblings when discovering the global read-write lock on a test descriptor which led to incompatible locks and caused test execution to fail. Fixes #4027. (cherry picked from commit 871e800)
1 parent 8dfcdac commit ffc2547

File tree

3 files changed

+178
-32
lines changed

3 files changed

+178
-32
lines changed

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeExecutionAdvisor.java

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ void useResourceLock(TestDescriptor testDescriptor, ResourceLock resourceLock) {
3333
resourceLocksByTestDescriptor.put(testDescriptor, resourceLock);
3434
}
3535

36+
void removeResourceLock(TestDescriptor testDescriptor) {
37+
resourceLocksByTestDescriptor.remove(testDescriptor);
38+
}
39+
3640
Optional<ExecutionMode> getForcedExecutionMode(TestDescriptor testDescriptor) {
3741
return testDescriptor.getParent().flatMap(this::lookupExecutionModeForcedByAncestor);
3842
}

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTreeWalker.java

+13-3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ NodeExecutionAdvisor walk(TestDescriptor rootDescriptor) {
5050

5151
private void walk(TestDescriptor globalLockDescriptor, TestDescriptor testDescriptor,
5252
NodeExecutionAdvisor advisor) {
53+
54+
if (advisor.getResourceLock(globalLockDescriptor) == globalReadWriteLock) {
55+
// Global read-write lock is already being enforced, so no additional locks are needed
56+
return;
57+
}
58+
5359
Set<ExclusiveResource> exclusiveResources = getExclusiveResources(testDescriptor);
5460
if (exclusiveResources.isEmpty()) {
5561
if (globalLockDescriptor.equals(testDescriptor)) {
@@ -73,7 +79,12 @@ private void walk(TestDescriptor globalLockDescriptor, TestDescriptor testDescri
7379
});
7480
}
7581
if (allResources.contains(GLOBAL_READ_WRITE)) {
76-
forceDescendantExecutionModeRecursively(advisor, globalLockDescriptor);
82+
advisor.forceDescendantExecutionMode(globalLockDescriptor, SAME_THREAD);
83+
doForChildrenRecursively(globalLockDescriptor, child -> {
84+
advisor.forceDescendantExecutionMode(child, SAME_THREAD);
85+
// Remove any locks that may have been set for siblings or their descendants
86+
advisor.removeResourceLock(child);
87+
});
7788
advisor.useResourceLock(globalLockDescriptor, globalReadWriteLock);
7889
}
7990
else {
@@ -94,8 +105,7 @@ private void forceDescendantExecutionModeRecursively(NodeExecutionAdvisor adviso
94105
}
95106

96107
private boolean isReadOnly(Set<ExclusiveResource> exclusiveResources) {
97-
return exclusiveResources.stream().allMatch(
98-
exclusiveResource -> exclusiveResource.getLockMode() == ExclusiveResource.LockMode.READ);
108+
return exclusiveResources.stream().allMatch(it -> it.getLockMode() == ExclusiveResource.LockMode.READ);
99109
}
100110

101111
private Set<ExclusiveResource> getExclusiveResources(TestDescriptor testDescriptor) {

platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java

+161-29
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
1919
import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
2020
import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD;
21+
import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE;
2122
import static org.junit.jupiter.engine.Constants.DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME;
2223
import static org.junit.jupiter.engine.Constants.DEFAULT_PARALLEL_EXECUTION_MODE;
2324
import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME;
2425
import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME;
2526
import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_STRATEGY_PROPERTY_NAME;
2627
import static org.junit.jupiter.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME;
2728
import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement;
29+
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY;
2830
import static org.junit.platform.testkit.engine.EventConditions.container;
2931
import static org.junit.platform.testkit.engine.EventConditions.event;
3032
import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
@@ -65,6 +67,8 @@
6567
import org.junit.jupiter.api.parallel.Execution;
6668
import org.junit.jupiter.api.parallel.Isolated;
6769
import org.junit.jupiter.api.parallel.ResourceLock;
70+
import org.junit.jupiter.params.ParameterizedTest;
71+
import org.junit.jupiter.params.provider.ValueSource;
6872
import org.junit.platform.engine.TestDescriptor;
6973
import org.junit.platform.engine.discovery.ClassSelector;
7074
import org.junit.platform.engine.discovery.DiscoverySelectors;
@@ -73,6 +77,7 @@
7377
import org.junit.platform.testkit.engine.EngineExecutionResults;
7478
import org.junit.platform.testkit.engine.EngineTestKit;
7579
import org.junit.platform.testkit.engine.Event;
80+
import org.junit.platform.testkit.engine.Events;
7681

7782
/**
7883
* @since 1.3
@@ -82,7 +87,7 @@ class ParallelExecutionIntegrationTests {
8287

8388
@Test
8489
void successfulParallelTest(TestReporter reporter) {
85-
var events = executeConcurrently(3, SuccessfulParallelTestCase.class);
90+
var events = executeConcurrentlySuccessfully(3, SuccessfulParallelTestCase.class).list();
8691

8792
var startedTimestamps = getTimestampsFor(events, event(test(), started()));
8893
var finishedTimestamps = getTimestampsFor(events, event(test(), finishedSuccessfully()));
@@ -98,29 +103,29 @@ void successfulParallelTest(TestReporter reporter) {
98103

99104
@Test
100105
void failingTestWithoutLock() {
101-
var events = executeConcurrently(3, FailingWithoutLockTestCase.class);
106+
var events = executeConcurrently(3, FailingWithoutLockTestCase.class).list();
102107
assertThat(events.stream().filter(event(test(), finishedWithFailure())::matches)).hasSize(2);
103108
}
104109

105110
@Test
106111
void successfulTestWithMethodLock() {
107-
var events = executeConcurrently(3, SuccessfulWithMethodLockTestCase.class);
112+
var events = executeConcurrentlySuccessfully(3, SuccessfulWithMethodLockTestCase.class).list();
108113

109114
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
110115
assertThat(ThreadReporter.getThreadNames(events)).hasSize(3);
111116
}
112117

113118
@Test
114119
void successfulTestWithClassLock() {
115-
var events = executeConcurrently(3, SuccessfulWithClassLockTestCase.class);
120+
var events = executeConcurrentlySuccessfully(3, SuccessfulWithClassLockTestCase.class).list();
116121

117122
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
118123
assertThat(ThreadReporter.getThreadNames(events)).hasSize(1);
119124
}
120125

121126
@Test
122127
void testCaseWithFactory() {
123-
var events = executeConcurrently(3, TestCaseWithTestFactory.class);
128+
var events = executeConcurrentlySuccessfully(3, TestCaseWithTestFactory.class).list();
124129

125130
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
126131
assertThat(ThreadReporter.getThreadNames(events)).hasSize(1);
@@ -133,7 +138,7 @@ void customContextClassLoader() {
133138
var smilingLoader = new URLClassLoader("(-:", new URL[0], ClassLoader.getSystemClassLoader());
134139
currentThread.setContextClassLoader(smilingLoader);
135140
try {
136-
var events = executeConcurrently(3, SuccessfulWithMethodLockTestCase.class);
141+
var events = executeConcurrentlySuccessfully(3, SuccessfulWithMethodLockTestCase.class).list();
137142

138143
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
139144
assertThat(ThreadReporter.getThreadNames(events)).hasSize(3);
@@ -146,23 +151,24 @@ void customContextClassLoader() {
146151

147152
@RepeatedTest(10)
148153
void mixingClassAndMethodLevelLocks() {
149-
var events = executeConcurrently(4, TestCaseWithSortedLocks.class, TestCaseWithUnsortedLocks.class);
154+
var events = executeConcurrentlySuccessfully(4, TestCaseWithSortedLocks.class,
155+
TestCaseWithUnsortedLocks.class).list();
150156

151157
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(6);
152158
assertThat(ThreadReporter.getThreadNames(events).count()).isLessThanOrEqualTo(2);
153159
}
154160

155161
@RepeatedTest(10)
156162
void locksOnNestedTests() {
157-
var events = executeConcurrently(3, TestCaseWithNestedLocks.class);
163+
var events = executeConcurrentlySuccessfully(3, TestCaseWithNestedLocks.class).list();
158164

159165
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(6);
160166
assertThat(ThreadReporter.getThreadNames(events)).hasSize(1);
161167
}
162168

163169
@Test
164170
void afterHooksAreCalledAfterConcurrentDynamicTestsAreFinished() {
165-
var events = executeConcurrently(3, ConcurrentDynamicTestCase.class);
171+
var events = executeConcurrentlySuccessfully(3, ConcurrentDynamicTestCase.class).list();
166172

167173
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(1);
168174
var timestampedEvents = ConcurrentDynamicTestCase.events;
@@ -175,14 +181,14 @@ void afterHooksAreCalledAfterConcurrentDynamicTestsAreFinished() {
175181
*/
176182
@Test
177183
void threadInterruptedByUserCode() {
178-
var events = executeConcurrently(3, InterruptedThreadTestCase.class);
184+
var events = executeConcurrentlySuccessfully(3, InterruptedThreadTestCase.class).list();
179185

180186
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(4);
181187
}
182188

183189
@Test
184190
void executesTestTemplatesWithResourceLocksInSameThread() {
185-
var events = executeConcurrently(2, ConcurrentTemplateTestCase.class);
191+
var events = executeConcurrentlySuccessfully(2, ConcurrentTemplateTestCase.class).list();
186192

187193
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(10);
188194
assertThat(ThreadReporter.getThreadNames(events)).hasSize(1);
@@ -228,30 +234,22 @@ void executesMethodsInParallelIfEnabledViaConfigurationParameter() {
228234

229235
@Test
230236
void canRunTestsIsolatedFromEachOther() {
231-
var events = executeConcurrently(2, IsolatedTestCase.class);
232-
233-
assertThat(events.stream().filter(event(test(), finishedWithFailure())::matches)).isEmpty();
237+
executeConcurrentlySuccessfully(2, IsolatedTestCase.class);
234238
}
235239

236240
@Test
237241
void canRunTestsIsolatedFromEachOtherWithNestedCases() {
238-
var events = executeConcurrently(4, NestedIsolatedTestCase.class);
239-
240-
assertThat(events.stream().filter(event(test(), finishedWithFailure())::matches)).isEmpty();
242+
executeConcurrentlySuccessfully(4, NestedIsolatedTestCase.class);
241243
}
242244

243245
@Test
244246
void canRunTestsIsolatedFromEachOtherAcrossClasses() {
245-
var events = executeConcurrently(4, IndependentClasses.A.class, IndependentClasses.B.class);
246-
247-
assertThat(events.stream().filter(event(test(), finishedWithFailure())::matches)).isEmpty();
247+
executeConcurrentlySuccessfully(4, IndependentClasses.A.class, IndependentClasses.B.class);
248248
}
249249

250250
@RepeatedTest(10)
251251
void canRunTestsIsolatedFromEachOtherAcrossClassesWithOtherResourceLocks() {
252-
var events = executeConcurrently(4, IndependentClasses.B.class, IndependentClasses.C.class);
253-
254-
assertThat(events.stream().filter(event(test(), finishedWithFailure())::matches)).isEmpty();
252+
executeConcurrentlySuccessfully(4, IndependentClasses.B.class, IndependentClasses.C.class);
255253
}
256254

257255
@Test
@@ -262,9 +260,8 @@ void runsIsolatedTestsLastToMaximizeParallelism() {
262260
);
263261
Class<?>[] testClasses = { IsolatedTestCase.class, SuccessfulParallelTestCase.class };
264262
var events = executeWithFixedParallelism(3, configParams, testClasses) //
265-
.allEvents();
266-
267-
assertThat(events.stream().filter(event(test(), finishedWithFailure())::matches)).isEmpty();
263+
.allEvents() //
264+
.assertStatistics(it -> it.failed(0));
268265

269266
List<Event> parallelTestMethodEvents = events.reportingEntryPublished() //
270267
.filter(e -> e.getTestDescriptor().getSource() //
@@ -283,6 +280,15 @@ void runsIsolatedTestsLastToMaximizeParallelism() {
283280
assertThat(isolatedClassStart).isAfterOrEqualTo(parallelClassFinish);
284281
}
285282

283+
@ParameterizedTest
284+
@ValueSource(classes = { IsolatedMethodFirstTestCase.class, IsolatedMethodLastTestCase.class,
285+
IsolatedNestedMethodFirstTestCase.class, IsolatedNestedMethodLastTestCase.class })
286+
void canRunTestsIsolatedFromEachOtherWhenDeclaredOnMethodLevel(Class<?> testClass) {
287+
List<Event> events = executeConcurrentlySuccessfully(1, testClass).list();
288+
289+
assertThat(ThreadReporter.getThreadNames(events)).hasSize(1);
290+
}
291+
286292
@Isolated("testing")
287293
static class IsolatedTestCase {
288294
static AtomicInteger sharedResource;
@@ -355,6 +361,122 @@ void b() throws Exception {
355361
}
356362
}
357363

364+
@ExtendWith(ThreadReporter.class)
365+
static class IsolatedMethodFirstTestCase {
366+
367+
static AtomicInteger sharedResource;
368+
static CountDownLatch countDownLatch;
369+
370+
@BeforeAll
371+
static void initialize() {
372+
sharedResource = new AtomicInteger();
373+
countDownLatch = new CountDownLatch(2);
374+
}
375+
376+
@Test
377+
@ResourceLock(value = GLOBAL_KEY, mode = READ_WRITE) // effectively @Isolated
378+
void test1() throws InterruptedException {
379+
incrementBlockAndCheck(sharedResource, countDownLatch);
380+
}
381+
382+
@Test
383+
@ResourceLock(value = "b", mode = READ_WRITE)
384+
void test2() throws InterruptedException {
385+
incrementBlockAndCheck(sharedResource, countDownLatch);
386+
}
387+
}
388+
389+
@ExtendWith(ThreadReporter.class)
390+
static class IsolatedMethodLastTestCase {
391+
392+
static AtomicInteger sharedResource;
393+
static CountDownLatch countDownLatch;
394+
395+
@BeforeAll
396+
static void initialize() {
397+
sharedResource = new AtomicInteger();
398+
countDownLatch = new CountDownLatch(2);
399+
}
400+
401+
@Test
402+
@ResourceLock(value = "b", mode = READ_WRITE)
403+
void test1() throws InterruptedException {
404+
incrementBlockAndCheck(sharedResource, countDownLatch);
405+
}
406+
407+
@Test
408+
@ResourceLock(value = GLOBAL_KEY, mode = READ_WRITE) // effectively @Isolated
409+
void test2() throws InterruptedException {
410+
incrementBlockAndCheck(sharedResource, countDownLatch);
411+
}
412+
}
413+
414+
@ExtendWith(ThreadReporter.class)
415+
static class IsolatedNestedMethodFirstTestCase {
416+
417+
static AtomicInteger sharedResource;
418+
static CountDownLatch countDownLatch;
419+
420+
@BeforeAll
421+
static void initialize() {
422+
sharedResource = new AtomicInteger();
423+
countDownLatch = new CountDownLatch(2);
424+
}
425+
426+
@Nested
427+
class Test1 {
428+
429+
@Test
430+
@ResourceLock(value = GLOBAL_KEY, mode = READ_WRITE) // effectively @Isolated
431+
void test1() throws InterruptedException {
432+
incrementBlockAndCheck(sharedResource, countDownLatch);
433+
}
434+
}
435+
436+
@Nested
437+
class Test2 {
438+
439+
@Test
440+
@ResourceLock(value = "b", mode = READ_WRITE)
441+
void test2() throws InterruptedException {
442+
incrementBlockAndCheck(sharedResource, countDownLatch);
443+
}
444+
}
445+
}
446+
447+
@ExtendWith(ThreadReporter.class)
448+
static class IsolatedNestedMethodLastTestCase {
449+
450+
static AtomicInteger sharedResource;
451+
static CountDownLatch countDownLatch;
452+
453+
@BeforeAll
454+
static void initialize() {
455+
sharedResource = new AtomicInteger();
456+
countDownLatch = new CountDownLatch(2);
457+
}
458+
459+
@Nested
460+
class Test1 {
461+
462+
@Test
463+
@ResourceLock(value = "b", mode = READ_WRITE)
464+
void test1() throws InterruptedException {
465+
incrementBlockAndCheck(sharedResource, countDownLatch);
466+
}
467+
}
468+
469+
@Nested
470+
class Test2 {
471+
472+
@Test
473+
@ResourceLock(value = GLOBAL_KEY, mode = READ_WRITE) // effectively @Isolated
474+
void test2() throws InterruptedException {
475+
incrementBlockAndCheck(sharedResource, countDownLatch);
476+
}
477+
}
478+
}
479+
358480
static class IndependentClasses {
359481
static AtomicInteger sharedResource = new AtomicInteger();
360482
static CountDownLatch countDownLatch = new CountDownLatch(4);
@@ -416,11 +538,21 @@ private List<Instant> getTimestampsFor(List<Event> events, Condition<Event> cond
416538
// @formatter:on
417539
}
418540

419-
private List<Event> executeConcurrently(int parallelism, Class<?>... testClasses) {
541+
private Events executeConcurrentlySuccessfully(int parallelism, Class<?>... testClasses) {
542+
var events = executeConcurrently(parallelism, testClasses);
543+
try {
544+
return events.assertStatistics(it -> it.failed(0));
545+
}
546+
catch (AssertionError error) {
547+
events.debug();
548+
throw error;
549+
}
550+
}
551+
552+
private Events executeConcurrently(int parallelism, Class<?>... testClasses) {
420553
Map<String, String> configParams = Map.of(DEFAULT_PARALLEL_EXECUTION_MODE, "concurrent");
421554
return executeWithFixedParallelism(parallelism, configParams, testClasses) //
422-
.allEvents() //
423-
.list();
555+
.allEvents();
424556
}
425557

426558
private EngineExecutionResults executeWithFixedParallelism(int parallelism, Map<String, String> configParams,

0 commit comments

Comments
 (0)