Skip to content

Commit ab94140

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 9658fac commit ab94140

File tree

4 files changed

+181
-33
lines changed

4 files changed

+181
-33
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.11.2.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ on GitHub.
1616
[[release-notes-5.11.2-junit-platform-bug-fixes]]
1717
==== Bug Fixes
1818

19-
* ❓
19+
* Fix regression in parallel execution that was introduced in 5.10.4/5.11.1 regarding
20+
global read-write locks. When such a lock was declared on descendants of top-level nodes
21+
in the test tree, such as Cucumber scenarios, test execution failed.
2022

2123
[[release-notes-5.11.2-junit-platform-deprecations-and-breaking-changes]]
2224
==== Deprecations and Breaking Changes

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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 13 additions & 3 deletions
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

Lines changed: 161 additions & 29 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy