Skip to content

Commit af6b502

Browse files
Allow declaring exclusive resources for child nodes (#4151)
Allow declaring "shared resources" for direct child nodes via the new `@ResourceLock(target = CHILDREN)` attribute. Using the `@ResourceLock(target = CHILDREN)` in a class-level annotation has the same semantics as adding an annotation with the same value and mode to each test method and nested test class declared in this class. This may improve parallelization when a test class declares a `READ` lock, but only a few methods hold a `READ_WRITE` lock. Resolves #3102. --------- Co-authored-by: Marc Philipp <[email protected]>
1 parent b8b5dc4 commit af6b502

File tree

10 files changed

+385
-61
lines changed

10 files changed

+385
-61
lines changed

documentation/src/docs/asciidoc/link-attributes.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ endif::[]
128128
:Execution: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Execution.html[@Execution]
129129
:Isolated: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Isolated.html[@Isolated]
130130
:ResourceLock: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/ResourceLock.html[@ResourceLock]
131+
:ResourceLockTarget: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/ResourceLockTarget.html[ResourceLockTarget]
131132
:ResourceLocksProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/ResourceLocksProvider.html[ResourceLocksProvider]
132133
:Resources: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Resources.html[Resources]
133134
// Jupiter Extension APIs

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ JUnit repository on GitHub.
9191
the absence of invocations is expected in some cases and should not cause a test failure.
9292
* Allow determining "shared resources" at runtime via the new `@ResourceLock#providers`
9393
attribute that accepts implementations of `ResourceLocksProvider`.
94+
* Allow declaring "shared resources" for _direct_ child nodes via the new
95+
`@ResourceLock(target = CHILDREN)` attribute. This may improve parallelization when
96+
a test class declares a `READ` lock, but only a few methods hold a `READ_WRITE` lock.
9497
* Extensions that implement `TestInstancePreConstructCallback`, `TestInstanceFactory`,
9598
`TestInstancePostProcessor`, `ParameterResolver`, or `InvocationInterceptor` may
9699
override the `getTestInstantiationExtensionContextScope()` method to enable receiving

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2992,7 +2992,7 @@ Note that resources declared statically with `{ResourceLock}` annotation are com
29922992
resources added dynamically by `{ResourceLocksProvider}` implementations.
29932993

29942994
If the tests in the following example were run in parallel _without_ the use of
2995-
{ResourceLock}, they would be _flaky_. Sometimes they would pass, and at other times they
2995+
`{ResourceLock}`, they would be _flaky_. Sometimes they would pass, and at other times they
29962996
would fail due to the inherent race condition of writing and then reading the same JVM
29972997
System Property.
29982998

@@ -3029,6 +3029,28 @@ include::{testDir}/example/sharedresources/StaticSharedResourcesDemo.java[tags=u
30293029
include::{testDir}/example/sharedresources/DynamicSharedResourcesDemo.java[tags=user_guide]
30303030
----
30313031

3032+
Also, "static" shared resources can be declared for _direct_ child nodes via the `target`
3033+
attribute in the `{ResourceLock}` annotation, the attribute accepts a value from
3034+
the `{ResourceLockTarget}` enum.
3035+
3036+
Specifying `target = CHILDREN` in a class-level `{ResourceLock}` annotation
3037+
has the same semantics as adding an annotation with the same `value` and `mode`
3038+
to each test method and nested test class declared in this class.
3039+
3040+
This may improve parallelization when a test class declares a `READ` lock,
3041+
but only a few methods hold a `READ_WRITE` lock.
3042+
3043+
Tests in the following example would run in the `SAME_THREAD` if the `{ResourceLock}`
3044+
didn't have `target = CHILDREN`. This is because the test class declares a `READ`
3045+
shared resource, but one test method holds a `READ_WRITE` lock,
3046+
which would force the `SAME_THREAD` execution mode for all the test methods.
3047+
3048+
[source,java]
3049+
.Declaring shared resources for child nodes with `target` attribute
3050+
----
3051+
include::{testDir}/example/sharedresources/ChildrenSharedResourcesDemo.java[tags=user_guide]
3052+
----
3053+
30323054

30333055
[[writing-tests-built-in-extensions]]
30343056
=== Built-in Extensions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2015-2024 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 example.sharedresources;
12+
13+
import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
14+
import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ;
15+
import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE;
16+
import static org.junit.jupiter.api.parallel.ResourceLockTarget.CHILDREN;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.parallel.Execution;
20+
import org.junit.jupiter.api.parallel.ResourceLock;
21+
22+
// tag::user_guide[]
23+
@Execution(CONCURRENT)
24+
@ResourceLock(value = "a", mode = READ, target = CHILDREN)
25+
public class ChildrenSharedResourcesDemo {
26+
27+
@ResourceLock(value = "a", mode = READ_WRITE)
28+
@Test
29+
void test1() throws InterruptedException {
30+
Thread.sleep(2000L);
31+
}
32+
33+
@Test
34+
void test2() throws InterruptedException {
35+
Thread.sleep(2000L);
36+
}
37+
38+
@Test
39+
void test3() throws InterruptedException {
40+
Thread.sleep(2000L);
41+
}
42+
43+
@Test
44+
void test4() throws InterruptedException {
45+
Thread.sleep(2000L);
46+
}
47+
48+
@Test
49+
void test5() throws InterruptedException {
50+
Thread.sleep(2000L);
51+
}
52+
53+
}
54+
// end::user_guide[]

junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,44 @@
4646
*
4747
* <p>This annotation can be repeated to declare the use of multiple shared resources.
4848
*
49+
* <p>Uniqueness of a shared resource is identified by both {@link #value()} and
50+
* {@link #mode()}. Duplicated shared resources do not cause errors.
51+
*
4952
* <p>Since JUnit Jupiter 5.4, this annotation is {@linkplain Inherited inherited}
5053
* within class hierarchies.
5154
*
5255
* <p>Since JUnit Jupiter 5.12, this annotation supports adding shared resources
53-
* dynamically at runtime via {@link ResourceLock#providers}.
56+
* dynamically at runtime via {@link #providers}.
5457
*
5558
* <p>Resources declared "statically" using {@link #value()} and {@link #mode()}
5659
* are combined with "dynamic" resources added via {@link #providers()}.
5760
* For example, declaring resource "A" via {@code @ResourceLock("A")}
5861
* and resource "B" via a provider returning {@code new Lock("B")} will result
5962
* in two shared resources "A" and "B".
6063
*
64+
* <p>Since JUnit Jupiter 5.12, this annotation supports declaring "static"
65+
* shared resources for <em>direct</em> child nodes via the {@link #target()}
66+
* attribute.
67+
*
68+
* <p>Using the {@link ResourceLockTarget#CHILDREN} in a class-level
69+
* annotation has the same semantics as adding an annotation with the same
70+
* {@link #value()} and {@link #mode()} to each test method and nested test
71+
* class declared in this class.
72+
*
73+
* <p>This may improve parallelization when a test class declares a
74+
* {@link ResourceAccessMode#READ READ} lock, but only a few methods hold
75+
* {@link ResourceAccessMode#READ_WRITE READ_WRITE} lock.
76+
*
77+
* <p>Note that the {@code target = CHILDREN} means that
78+
* {@link #value()} and {@link #mode()} no longer apply to a node
79+
* declaring the annotation. However, the {@link #providers()} attribute
80+
* remains applicable, and the target of "dynamic" shared resources
81+
* added via implementations of {@link ResourceLocksProvider} is not changed.
82+
*
6183
* @see Isolated
6284
* @see Resources
6385
* @see ResourceAccessMode
86+
* @see ResourceLockTarget
6487
* @see ResourceLocks
6588
* @see ResourceLocksProvider
6689
* @since 5.3
@@ -92,6 +115,20 @@
92115
*/
93116
ResourceAccessMode mode() default ResourceAccessMode.READ_WRITE;
94117

118+
/**
119+
* The target of a resource created from {@link #value()} and {@link #mode()}.
120+
*
121+
* <p>Defaults to {@link ResourceLockTarget#SELF SELF}.
122+
*
123+
* <p>Note that using {@link ResourceLockTarget#CHILDREN} in
124+
* a method-level annotation results in an exception.
125+
*
126+
* @see ResourceLockTarget
127+
* @since 5.12
128+
*/
129+
@API(status = EXPERIMENTAL, since = "5.12")
130+
ResourceLockTarget target() default ResourceLockTarget.SELF;
131+
95132
/**
96133
* An array of one or more classes implementing {@link ResourceLocksProvider}.
97134
*
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2015-2024 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.jupiter.api.parallel;
12+
13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
15+
import org.apiguardian.api.API;
16+
17+
/**
18+
* {@code ResourceLockTarget} is used to define the target of a shared resource.
19+
*
20+
* @since 5.12
21+
* @see ResourceLock#target()
22+
*/
23+
@API(status = EXPERIMENTAL, since = "5.12")
24+
public enum ResourceLockTarget {
25+
26+
/**
27+
* Add a shared resource to the current node.
28+
*/
29+
SELF,
30+
31+
/**
32+
* Add a shared resource to the <em>direct</em> children of the current node.
33+
*
34+
* <p>Examples of "parent - child" relationship in the context of
35+
* {@link ResourceLockTarget}:
36+
* <ul>
37+
* <li>a test class
38+
* - test methods and nested test classes declared in the class.</li>
39+
* <li>a nested test class
40+
* - test methods and nested test classes declared in the nested class.
41+
* </li>
42+
* <li>a test method
43+
* - considered to have no children. Using {@code CHILDREN} for
44+
* a test method results in an exception.</li>
45+
* </ul>
46+
*/
47+
CHILDREN
48+
49+
}

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExclusiveResourceCollector.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

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

13+
import static org.junit.jupiter.api.parallel.ResourceLockTarget.SELF;
1314
import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations;
1415
import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList;
1516

@@ -22,6 +23,7 @@
2223

2324
import org.junit.jupiter.api.parallel.ResourceAccessMode;
2425
import org.junit.jupiter.api.parallel.ResourceLock;
26+
import org.junit.jupiter.api.parallel.ResourceLockTarget;
2527
import org.junit.jupiter.api.parallel.ResourceLocksProvider;
2628
import org.junit.platform.commons.JUnitException;
2729
import org.junit.platform.commons.util.ReflectionUtils;
@@ -42,7 +44,7 @@ Stream<ExclusiveResource> getAllExclusiveResources(
4244
}
4345

4446
@Override
45-
public Stream<ExclusiveResource> getStaticResources() {
47+
Stream<ExclusiveResource> getStaticResourcesFor(ResourceLockTarget target) {
4648
return Stream.empty();
4749
}
4850

@@ -55,10 +57,10 @@ Stream<ExclusiveResource> getDynamicResources(
5557

5658
Stream<ExclusiveResource> getAllExclusiveResources(
5759
Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> providerToLocks) {
58-
return Stream.concat(getStaticResources(), getDynamicResources(providerToLocks));
60+
return Stream.concat(getStaticResourcesFor(SELF), getDynamicResources(providerToLocks));
5961
}
6062

61-
abstract Stream<ExclusiveResource> getStaticResources();
63+
abstract Stream<ExclusiveResource> getStaticResourcesFor(ResourceLockTarget target);
6264

6365
abstract Stream<ExclusiveResource> getDynamicResources(
6466
Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> providerToLocks);
@@ -78,9 +80,10 @@ private static class DefaultExclusiveResourceCollector extends ExclusiveResource
7880
}
7981

8082
@Override
81-
public Stream<ExclusiveResource> getStaticResources() {
83+
Stream<ExclusiveResource> getStaticResourcesFor(ResourceLockTarget target) {
8284
return annotations.stream() //
8385
.filter(annotation -> StringUtils.isNotBlank(annotation.value())) //
86+
.filter(annotation -> annotation.target() == target) //
8487
.map(annotation -> new ExclusiveResource(annotation.value(), toLockMode(annotation.mode())));
8588
}
8689

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package org.junit.jupiter.engine.descriptor;
1212

1313
import static org.apiguardian.api.API.Status.INTERNAL;
14+
import static org.junit.jupiter.api.parallel.ResourceLockTarget.CHILDREN;
1415
import static org.junit.jupiter.engine.descriptor.DisplayNameUtils.determineDisplayNameForMethod;
1516
import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder;
1617

@@ -27,6 +28,7 @@
2728
import org.junit.jupiter.api.parallel.ResourceLocksProvider;
2829
import org.junit.jupiter.engine.config.JupiterConfiguration;
2930
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
31+
import org.junit.platform.commons.JUnitException;
3032
import org.junit.platform.commons.logging.Logger;
3133
import org.junit.platform.commons.logging.LoggerFactory;
3234
import org.junit.platform.commons.util.ClassUtils;
@@ -82,7 +84,15 @@ public final Set<TestTag> getTags() {
8284
@Override
8385
public ExclusiveResourceCollector getExclusiveResourceCollector() {
8486
// There's no need to cache this as this method should only be called once
85-
return ExclusiveResourceCollector.from(getTestMethod());
87+
ExclusiveResourceCollector collector = ExclusiveResourceCollector.from(getTestMethod());
88+
89+
if (collector.getStaticResourcesFor(CHILDREN).findAny().isPresent()) {
90+
String message = "'ResourceLockTarget.CHILDREN' is not supported for methods." + //
91+
" Invalid method: " + getTestMethod();
92+
throw new JUnitException(message);
93+
}
94+
95+
return collector;
8696
}
8797

8898
@Override

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ResourceLockAware.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

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

13+
import static org.junit.jupiter.api.parallel.ResourceLockTarget.CHILDREN;
14+
1315
import java.util.ArrayDeque;
1416
import java.util.Deque;
1517
import java.util.Set;
@@ -37,11 +39,15 @@ default Stream<ExclusiveResource> determineExclusiveResources() {
3739
return determineOwnExclusiveResources();
3840
}
3941

42+
Stream<ExclusiveResource> parentStaticResourcesForChildren = ancestors.getLast() //
43+
.getExclusiveResourceCollector().getStaticResourcesFor(CHILDREN);
44+
4045
Stream<ExclusiveResource> ancestorDynamicResources = ancestors.stream() //
4146
.map(ResourceLockAware::getExclusiveResourceCollector) //
4247
.flatMap(collector -> collector.getDynamicResources(this::evaluateResourceLocksProvider));
4348

44-
return Stream.concat(ancestorDynamicResources, determineOwnExclusiveResources());
49+
return Stream.of(ancestorDynamicResources, parentStaticResourcesForChildren, determineOwnExclusiveResources())//
50+
.flatMap(s -> s);
4551
}
4652

4753
default Stream<ExclusiveResource> determineOwnExclusiveResources() {

0 commit comments

Comments
 (0)