Skip to content

Commit 3444740

Browse files
author
Vladimir Dmitrienko
committed
Allow declaring exclusive resources for child nodes
Issue: #3102
1 parent ad0ef2e commit 3444740

File tree

10 files changed

+382
-61
lines changed

10 files changed

+382
-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
@@ -89,6 +89,9 @@ JUnit repository on GitHub.
8989
the absence of arguments is expected in some cases and should not cause a test failure.
9090
* Allow determining "shared resources" at runtime via the new `@ResourceLock#providers`
9191
attribute that accepts implementations of `ResourceLocksProvider`.
92+
* Allow declaring "shared resources" for _direct_ child nodes via the new
93+
`@ResourceLock(target = CHILDREN)` attribute. It may improve parallelization when
94+
a test class declares a `READ` lock, but only a few methods hold a `READ_WRITE` lock.
9295
* Extensions that implement `TestInstancePreConstructCallback`, `TestInstanceFactory`,
9396
`TestInstancePostProcessor`, `ParameterResolver`, or `InvocationInterceptor` may
9497
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+
Using the `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 declared in this class.
3039+
3040+
It 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+
doesn't have the `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 forces 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 declared
71+
* in this class.
72+
*
73+
* <p>It 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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 declared in the nested class.</li>
41+
* <li>a test method - considered to have no children.</li>
42+
* </ul>
43+
*/
44+
CHILDREN
45+
46+
}

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)