Skip to content

Commit 176bc81

Browse files
authored
Allow specifying which Nodes require the global read lock (#5099)
Implementations of `HierarchicalTestEngine` can now specify which nodes require the global read lock by overriding the new `Node.isGlobalReadLockRequired()` method to return `false`. This allows for more fine-grained locks when intermediate container do not have any behavior of their own and thus do not require even the global read lock. Resolves #2982.
1 parent 9badb66 commit 176bc81

File tree

4 files changed

+134
-6
lines changed

4 files changed

+134
-6
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ repository on GitHub.
3131
* `OpenTestReportGeneratingListener` now supports redirecting XML events to a socket via
3232
the new `junit.platform.reporting.open.xml.socket` configuration parameter. When set to a
3333
port number, events are sent to `127.0.0.1:<port>` instead of being written to a file.
34+
* Allow implementations of `HierarchicalTestEngine` to specify which nodes require the
35+
global read lock by overriding the `Node.isGlobalReadLockRequired()` method to return
36+
`false`.
3437

3538

3639
[[release-notes-6.1.0-M1-junit-jupiter]]

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package org.junit.platform.engine.support.hierarchical;
1212

1313
import static java.util.Collections.emptySet;
14+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1415
import static org.apiguardian.api.API.Status.MAINTAINED;
1516
import static org.apiguardian.api.API.Status.STABLE;
1617

@@ -182,6 +183,23 @@ default Set<ExclusiveResource> getExclusiveResources() {
182183
return emptySet();
183184
}
184185

186+
/**
187+
* {@return whether this node requires the global read-write lock}
188+
*
189+
* <p>Engines should return {@code true} for all nodes that have behavior,
190+
* including tests but also containers with before/after lifecycle hooks.
191+
*
192+
* <p>The default implementation returns {@code true}. The value returned by
193+
* engine-level nodes is ignored. Otherwise, if a container node returns
194+
* {@code true}, the value returned by its descendants is ignored.
195+
*
196+
* @since 6.1
197+
*/
198+
@API(status = EXPERIMENTAL, since = "6.1")
199+
default boolean isGlobalReadLockRequired() {
200+
return true;
201+
}
202+
185203
/**
186204
* Get the preferred of {@linkplain ExecutionMode execution mode} for
187205
* parallel execution of this node.

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ;
1414
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ_WRITE;
1515
import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.SAME_THREAD;
16+
import static org.junit.platform.engine.support.hierarchical.NodeUtils.asNode;
1617

1718
import java.util.HashSet;
1819
import java.util.Set;
1920
import java.util.function.Consumer;
2021

22+
import org.jspecify.annotations.Nullable;
2123
import org.junit.platform.commons.util.Preconditions;
2224
import org.junit.platform.engine.TestDescriptor;
2325

@@ -44,26 +46,34 @@ NodeExecutionAdvisor walk(TestDescriptor rootDescriptor) {
4446
Preconditions.condition(getExclusiveResources(rootDescriptor).isEmpty(),
4547
"Engine descriptor must not declare exclusive resources");
4648
NodeExecutionAdvisor advisor = new NodeExecutionAdvisor();
47-
rootDescriptor.getChildren().forEach(child -> walk(child, child, advisor));
49+
rootDescriptor.getChildren().forEach(child -> walk(nullUnlessRequiresGlobalReadLock(child), child, advisor));
4850
return advisor;
4951
}
5052

51-
private void walk(TestDescriptor globalLockDescriptor, TestDescriptor testDescriptor,
53+
private void walk(@Nullable TestDescriptor globalLockDescriptor, TestDescriptor testDescriptor,
5254
NodeExecutionAdvisor advisor) {
5355

54-
if (advisor.getResourceLock(globalLockDescriptor) == globalReadWriteLock) {
56+
if (globalLockDescriptor != null && advisor.getResourceLock(globalLockDescriptor) == globalReadWriteLock) {
5557
// Global read-write lock is already being enforced, so no additional locks are needed
5658
return;
5759
}
5860

5961
Set<ExclusiveResource> exclusiveResources = getExclusiveResources(testDescriptor);
6062
if (exclusiveResources.isEmpty()) {
61-
if (globalLockDescriptor.equals(testDescriptor)) {
63+
if (globalLockDescriptor != null && globalLockDescriptor.equals(testDescriptor)) {
6264
advisor.useResourceLock(globalLockDescriptor, globalReadLock);
6365
}
64-
testDescriptor.getChildren().forEach(child -> walk(globalLockDescriptor, child, advisor));
66+
testDescriptor.getChildren().forEach(child -> {
67+
var newGlobalLockDescriptor = globalLockDescriptor == null //
68+
? nullUnlessRequiresGlobalReadLock(child) //
69+
: globalLockDescriptor;
70+
walk(newGlobalLockDescriptor, child, advisor);
71+
});
6572
}
6673
else {
74+
Preconditions.notNull(globalLockDescriptor,
75+
() -> "Node requiring exclusive resources must also require global read lock: " + testDescriptor);
76+
6777
Set<ExclusiveResource> allResources = new HashSet<>(exclusiveResources);
6878
if (isReadOnly(allResources)) {
6979
doForChildrenRecursively(testDescriptor, child -> allResources.addAll(getExclusiveResources(child)));
@@ -109,7 +119,11 @@ private boolean isReadOnly(Set<ExclusiveResource> exclusiveResources) {
109119
}
110120

111121
private Set<ExclusiveResource> getExclusiveResources(TestDescriptor testDescriptor) {
112-
return NodeUtils.asNode(testDescriptor).getExclusiveResources();
122+
return asNode(testDescriptor).getExclusiveResources();
123+
}
124+
125+
private static @Nullable TestDescriptor nullUnlessRequiresGlobalReadLock(TestDescriptor testDescriptor) {
126+
return asNode(testDescriptor).isGlobalReadLockRequired() ? testDescriptor : null;
113127
}
114128

115129
private void doForChildrenRecursively(TestDescriptor parent, Consumer<TestDescriptor> consumer) {

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
package org.junit.platform.engine.support.hierarchical;
1212

1313
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.assertj.core.api.InstanceOfAssertFactories.LIST;
15+
import static org.junit.platform.commons.test.PreconditionAssertions.assertPreconditionViolationFor;
1416
import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement;
1517
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
1618
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ;
@@ -20,10 +22,13 @@
2022
import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.SAME_THREAD;
2123
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
2224

25+
import java.util.LinkedHashSet;
2326
import java.util.List;
27+
import java.util.Set;
2428
import java.util.concurrent.locks.Lock;
2529
import java.util.function.Function;
2630

31+
import org.jspecify.annotations.NullMarked;
2732
import org.junit.jupiter.api.Nested;
2833
import org.junit.jupiter.api.Test;
2934
import org.junit.jupiter.api.parallel.ResourceAccessMode;
@@ -32,6 +37,8 @@
3237
import org.junit.jupiter.engine.JupiterTestEngine;
3338
import org.junit.platform.engine.TestDescriptor;
3439
import org.junit.platform.engine.UniqueId;
40+
import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor;
41+
import org.junit.platform.engine.support.descriptor.EngineDescriptor;
3542

3643
/**
3744
* @since 1.3
@@ -171,6 +178,55 @@ void coarsensGlobalLockToEngineDescriptorChild() {
171178
assertThat(advisor.getForcedExecutionMode(testMethodDescriptor)).contains(SAME_THREAD);
172179
}
173180

181+
@Test
182+
void putsGlobalReadLockOnFirstNodeThatRequiresIt() {
183+
var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("dummy"), "Dummy");
184+
185+
var containerWithoutBehavior = new NodeStub(engineDescriptor.getUniqueId().append("container", "1"),
186+
"Container 1") //
187+
.withGlobalReadLockRequired(false);
188+
var test1 = new NodeStub(containerWithoutBehavior.getUniqueId().append("test", "1"), "Test 1") //
189+
.withExclusiveResource(new ExclusiveResource("key1", READ_WRITE));
190+
containerWithoutBehavior.addChild(test1);
191+
192+
var containerWithBehavior = new NodeStub(engineDescriptor.getUniqueId().append("container", "2"), "Container 2") //
193+
.withGlobalReadLockRequired(true);
194+
var test2 = new NodeStub(containerWithBehavior.getUniqueId().append("test", "2"), "Test 2") //
195+
.withExclusiveResource(new ExclusiveResource("key2", READ_WRITE));
196+
containerWithBehavior.addChild(test2);
197+
198+
engineDescriptor.addChild(containerWithoutBehavior);
199+
engineDescriptor.addChild(containerWithBehavior);
200+
201+
var advisor = nodeTreeWalker.walk(engineDescriptor);
202+
203+
assertThat(advisor.getResourceLock(containerWithoutBehavior)) //
204+
.extracting(allLocks(), LIST) //
205+
.isEmpty();
206+
assertThat(advisor.getResourceLock(test1)) //
207+
.extracting(allLocks(), LIST) //
208+
.containsExactly(getLock(GLOBAL_READ), getReadWriteLock("key1"));
209+
210+
assertThat(advisor.getResourceLock(containerWithBehavior)) //
211+
.extracting(allLocks(), LIST) //
212+
.containsExactly(getLock(GLOBAL_READ));
213+
assertThat(advisor.getResourceLock(test2)) //
214+
.extracting(allLocks(), LIST) //
215+
.containsExactly(getReadWriteLock("key2"));
216+
}
217+
218+
@Test
219+
void doesNotAllowExclusiveResourcesWithoutRequiringGlobalReadLock() {
220+
var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("dummy"), "Dummy");
221+
var invalidNode = new NodeStub(engineDescriptor.getUniqueId().append("container", "1"), "Container") //
222+
.withGlobalReadLockRequired(false) //
223+
.withExclusiveResource(new ExclusiveResource("key", READ_WRITE));
224+
engineDescriptor.addChild(invalidNode);
225+
226+
assertPreconditionViolationFor(() -> nodeTreeWalker.walk(engineDescriptor)) //
227+
.withMessage("Node requiring exclusive resources must also require global read lock: " + invalidNode);
228+
}
229+
174230
private static Function<org.junit.platform.engine.support.hierarchical.ResourceLock, List<Lock>> allLocks() {
175231
return ResourceLockSupport::getLocks;
176232
}
@@ -262,4 +318,41 @@ static class TestCaseWithResourceReadLockOnClassAndReadClockOnTestCase {
262318
void test() {
263319
}
264320
}
321+
322+
@NullMarked
323+
static class NodeStub extends AbstractTestDescriptor implements Node<EngineExecutionContext> {
324+
325+
private final Set<ExclusiveResource> exclusiveResources = new LinkedHashSet<>();
326+
private boolean globalReadLockRequired = true;
327+
328+
NodeStub(UniqueId uniqueId, String displayName) {
329+
super(uniqueId, displayName);
330+
}
331+
332+
NodeStub withExclusiveResource(ExclusiveResource exclusiveResource) {
333+
exclusiveResources.add(exclusiveResource);
334+
return this;
335+
}
336+
337+
NodeStub withGlobalReadLockRequired(boolean globalReadLockRequired) {
338+
this.globalReadLockRequired = globalReadLockRequired;
339+
return this;
340+
}
341+
342+
@Override
343+
public boolean isGlobalReadLockRequired() {
344+
return globalReadLockRequired;
345+
}
346+
347+
@Override
348+
public Type getType() {
349+
throw new UnsupportedOperationException("should not be called");
350+
}
351+
352+
@Override
353+
public Set<ExclusiveResource> getExclusiveResources() {
354+
return exclusiveResources;
355+
}
356+
}
357+
265358
}

0 commit comments

Comments
 (0)