diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc
index 46da6e279efd..33fb2881d2b5 100644
--- a/documentation/src/docs/asciidoc/link-attributes.adoc
+++ b/documentation/src/docs/asciidoc/link-attributes.adoc
@@ -128,6 +128,7 @@ endif::[]
:Execution: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Execution.html[@Execution]
:Isolated: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Isolated.html[@Isolated]
:ResourceLock: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/ResourceLock.html[@ResourceLock]
+:ResourceLockTarget: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/ResourceLockTarget.html[ResourceLockTarget]
:ResourceLocksProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/ResourceLocksProvider.html[ResourceLocksProvider]
:Resources: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Resources.html[Resources]
// Jupiter Extension APIs
diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc
index e3802369cccc..4eeb9af0f4f5 100644
--- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc
@@ -89,6 +89,9 @@ JUnit repository on GitHub.
the absence of invocations is expected in some cases and should not cause a test failure.
* Allow determining "shared resources" at runtime via the new `@ResourceLock#providers`
attribute that accepts implementations of `ResourceLocksProvider`.
+* Allow declaring "shared resources" for _direct_ child nodes via the new
+ `@ResourceLock(target = CHILDREN)` attribute. This may improve parallelization when
+ a test class declares a `READ` lock, but only a few methods hold a `READ_WRITE` lock.
* Extensions that implement `TestInstancePreConstructCallback`, `TestInstanceFactory`,
`TestInstancePostProcessor`, `ParameterResolver`, or `InvocationInterceptor` may
override the `getTestInstantiationExtensionContextScope()` method to enable receiving
diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
index 7b2914a95216..200673894b7a 100644
--- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
+++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
@@ -2992,7 +2992,7 @@ Note that resources declared statically with `{ResourceLock}` annotation are com
resources added dynamically by `{ResourceLocksProvider}` implementations.
If the tests in the following example were run in parallel _without_ the use of
-{ResourceLock}, they would be _flaky_. Sometimes they would pass, and at other times they
+`{ResourceLock}`, they would be _flaky_. Sometimes they would pass, and at other times they
would fail due to the inherent race condition of writing and then reading the same JVM
System Property.
@@ -3029,6 +3029,28 @@ include::{testDir}/example/sharedresources/StaticSharedResourcesDemo.java[tags=u
include::{testDir}/example/sharedresources/DynamicSharedResourcesDemo.java[tags=user_guide]
----
+Also, "static" shared resources can be declared for _direct_ child nodes via the `target`
+attribute in the `{ResourceLock}` annotation, the attribute accepts a value from
+the `{ResourceLockTarget}` enum.
+
+Specifying `target = CHILDREN` in a class-level `{ResourceLock}` 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.
+
+Tests in the following example would run in the `SAME_THREAD` if the `{ResourceLock}`
+didn't have `target = CHILDREN`. This is because the test class declares a `READ`
+shared resource, but one test method holds a `READ_WRITE` lock,
+which would force the `SAME_THREAD` execution mode for all the test methods.
+
+[source,java]
+.Declaring shared resources for child nodes with `target` attribute
+----
+include::{testDir}/example/sharedresources/ChildrenSharedResourcesDemo.java[tags=user_guide]
+----
+
[[writing-tests-built-in-extensions]]
=== Built-in Extensions
diff --git a/documentation/src/test/java/example/sharedresources/ChildrenSharedResourcesDemo.java b/documentation/src/test/java/example/sharedresources/ChildrenSharedResourcesDemo.java
new file mode 100644
index 000000000000..5350b9ff02a0
--- /dev/null
+++ b/documentation/src/test/java/example/sharedresources/ChildrenSharedResourcesDemo.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015-2024 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package example.sharedresources;
+
+import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
+import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ;
+import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE;
+import static org.junit.jupiter.api.parallel.ResourceLockTarget.CHILDREN;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ResourceLock;
+
+// tag::user_guide[]
+@Execution(CONCURRENT)
+@ResourceLock(value = "a", mode = READ, target = CHILDREN)
+public class ChildrenSharedResourcesDemo {
+
+ @ResourceLock(value = "a", mode = READ_WRITE)
+ @Test
+ void test1() throws InterruptedException {
+ Thread.sleep(2000L);
+ }
+
+ @Test
+ void test2() throws InterruptedException {
+ Thread.sleep(2000L);
+ }
+
+ @Test
+ void test3() throws InterruptedException {
+ Thread.sleep(2000L);
+ }
+
+ @Test
+ void test4() throws InterruptedException {
+ Thread.sleep(2000L);
+ }
+
+ @Test
+ void test5() throws InterruptedException {
+ Thread.sleep(2000L);
+ }
+
+}
+// end::user_guide[]
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java
index f32012e6871e..e78abdcb647d 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java
@@ -46,11 +46,14 @@
*
*
This annotation can be repeated to declare the use of multiple shared resources.
*
+ *
Uniqueness of a shared resource is identified by both {@link #value()} and
+ * {@link #mode()}. Duplicated shared resources do not cause errors.
+ *
*
Since JUnit Jupiter 5.4, this annotation is {@linkplain Inherited inherited}
* within class hierarchies.
*
*
Since JUnit Jupiter 5.12, this annotation supports adding shared resources
- * dynamically at runtime via {@link ResourceLock#providers}.
+ * dynamically at runtime via {@link #providers}.
*
*
Resources declared "statically" using {@link #value()} and {@link #mode()}
* are combined with "dynamic" resources added via {@link #providers()}.
@@ -58,9 +61,29 @@
* and resource "B" via a provider returning {@code new Lock("B")} will result
* in two shared resources "A" and "B".
*
+ *
Since JUnit Jupiter 5.12, this annotation supports declaring "static"
+ * shared resources for direct child nodes via the {@link #target()}
+ * attribute.
+ *
+ *
Using the {@link ResourceLockTarget#CHILDREN} in a class-level
+ * annotation has the same semantics as adding an annotation with the same
+ * {@link #value()} and {@link #mode()} to each test method and nested test
+ * class declared in this class.
+ *
+ *
This may improve parallelization when a test class declares a
+ * {@link ResourceAccessMode#READ READ} lock, but only a few methods hold
+ * {@link ResourceAccessMode#READ_WRITE READ_WRITE} lock.
+ *
+ *
Note that the {@code target = CHILDREN} means that
+ * {@link #value()} and {@link #mode()} no longer apply to a node
+ * declaring the annotation. However, the {@link #providers()} attribute
+ * remains applicable, and the target of "dynamic" shared resources
+ * added via implementations of {@link ResourceLocksProvider} is not changed.
+ *
* @see Isolated
* @see Resources
* @see ResourceAccessMode
+ * @see ResourceLockTarget
* @see ResourceLocks
* @see ResourceLocksProvider
* @since 5.3
@@ -92,6 +115,20 @@
*/
ResourceAccessMode mode() default ResourceAccessMode.READ_WRITE;
+ /**
+ * The target of a resource created from {@link #value()} and {@link #mode()}.
+ *
+ *
Defaults to {@link ResourceLockTarget#SELF SELF}.
+ *
+ *
Note that using {@link ResourceLockTarget#CHILDREN} in
+ * a method-level annotation results in an exception.
+ *
+ * @see ResourceLockTarget
+ * @since 5.12
+ */
+ @API(status = EXPERIMENTAL, since = "5.12")
+ ResourceLockTarget target() default ResourceLockTarget.SELF;
+
/**
* An array of one or more classes implementing {@link ResourceLocksProvider}.
*
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLockTarget.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLockTarget.java
new file mode 100644
index 000000000000..89aa97a67bf1
--- /dev/null
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLockTarget.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015-2024 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.jupiter.api.parallel;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import org.apiguardian.api.API;
+
+/**
+ * {@code ResourceLockTarget} is used to define the target of a shared resource.
+ *
+ * @since 5.12
+ * @see ResourceLock#target()
+ */
+@API(status = EXPERIMENTAL, since = "5.12")
+public enum ResourceLockTarget {
+
+ /**
+ * Add a shared resource to the current node.
+ */
+ SELF,
+
+ /**
+ * Add a shared resource to the direct children of the current node.
+ *
+ *
Examples of "parent - child" relationship in the context of
+ * {@link ResourceLockTarget}:
+ *
+ * - a test class
+ * - test methods and nested test classes declared in the class.
+ * - a nested test class
+ * - test methods and nested test classes declared in the nested class.
+ *
+ * - a test method
+ * - considered to have no children. Using {@code CHILDREN} for
+ * a test method results in an exception.
+ *
+ */
+ CHILDREN
+
+}
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExclusiveResourceCollector.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExclusiveResourceCollector.java
index 42b1295b2221..53757a4be9e6 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExclusiveResourceCollector.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExclusiveResourceCollector.java
@@ -10,6 +10,7 @@
package org.junit.jupiter.engine.descriptor;
+import static org.junit.jupiter.api.parallel.ResourceLockTarget.SELF;
import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations;
import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList;
@@ -22,6 +23,7 @@
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.ResourceLockTarget;
import org.junit.jupiter.api.parallel.ResourceLocksProvider;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.util.ReflectionUtils;
@@ -42,7 +44,7 @@ Stream getAllExclusiveResources(
}
@Override
- public Stream getStaticResources() {
+ Stream getStaticResourcesFor(ResourceLockTarget target) {
return Stream.empty();
}
@@ -55,10 +57,10 @@ Stream getDynamicResources(
Stream getAllExclusiveResources(
Function> providerToLocks) {
- return Stream.concat(getStaticResources(), getDynamicResources(providerToLocks));
+ return Stream.concat(getStaticResourcesFor(SELF), getDynamicResources(providerToLocks));
}
- abstract Stream getStaticResources();
+ abstract Stream getStaticResourcesFor(ResourceLockTarget target);
abstract Stream getDynamicResources(
Function> providerToLocks);
@@ -78,9 +80,10 @@ private static class DefaultExclusiveResourceCollector extends ExclusiveResource
}
@Override
- public Stream getStaticResources() {
+ Stream getStaticResourcesFor(ResourceLockTarget target) {
return annotations.stream() //
.filter(annotation -> StringUtils.isNotBlank(annotation.value())) //
+ .filter(annotation -> annotation.target() == target) //
.map(annotation -> new ExclusiveResource(annotation.value(), toLockMode(annotation.mode())));
}
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java
index 9c8e4bcb1ce3..95d9ecbf6035 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java
@@ -11,6 +11,7 @@
package org.junit.jupiter.engine.descriptor;
import static org.apiguardian.api.API.Status.INTERNAL;
+import static org.junit.jupiter.api.parallel.ResourceLockTarget.CHILDREN;
import static org.junit.jupiter.engine.descriptor.DisplayNameUtils.determineDisplayNameForMethod;
import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder;
@@ -27,6 +28,7 @@
import org.junit.jupiter.api.parallel.ResourceLocksProvider;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
+import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.util.ClassUtils;
@@ -82,7 +84,15 @@ public final Set getTags() {
@Override
public ExclusiveResourceCollector getExclusiveResourceCollector() {
// There's no need to cache this as this method should only be called once
- return ExclusiveResourceCollector.from(getTestMethod());
+ ExclusiveResourceCollector collector = ExclusiveResourceCollector.from(getTestMethod());
+
+ if (collector.getStaticResourcesFor(CHILDREN).findAny().isPresent()) {
+ String message = "'ResourceLockTarget.CHILDREN' is not supported for methods." + //
+ " Invalid method: " + getTestMethod();
+ throw new JUnitException(message);
+ }
+
+ return collector;
}
@Override
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ResourceLockAware.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ResourceLockAware.java
index 858644d48520..8c50b188d0d2 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ResourceLockAware.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ResourceLockAware.java
@@ -10,6 +10,8 @@
package org.junit.jupiter.engine.descriptor;
+import static org.junit.jupiter.api.parallel.ResourceLockTarget.CHILDREN;
+
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Set;
@@ -37,11 +39,15 @@ default Stream determineExclusiveResources() {
return determineOwnExclusiveResources();
}
+ Stream parentStaticResourcesForChildren = ancestors.getLast() //
+ .getExclusiveResourceCollector().getStaticResourcesFor(CHILDREN);
+
Stream ancestorDynamicResources = ancestors.stream() //
.map(ResourceLockAware::getExclusiveResourceCollector) //
.flatMap(collector -> collector.getDynamicResources(this::evaluateResourceLocksProvider));
- return Stream.concat(ancestorDynamicResources, determineOwnExclusiveResources());
+ return Stream.of(ancestorDynamicResources, parentStaticResourcesForChildren, determineOwnExclusiveResources())//
+ .flatMap(s -> s);
}
default Stream determineOwnExclusiveResources() {
diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/ResourceLockAnnotationTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/ResourceLockAnnotationTests.java
index e5f3d65fb099..6ba68bac7677 100644
--- a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/ResourceLockAnnotationTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/ResourceLockAnnotationTests.java
@@ -11,27 +11,46 @@
package org.junit.platform.engine.support.descriptor;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.util.Throwables.getRootCause;
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode;
+import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
+import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
+import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
+import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.lang.reflect.Method;
import java.util.Set;
+import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.ResourceLockTarget;
import org.junit.jupiter.api.parallel.ResourceLocksProvider;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.descriptor.ClassTestDescriptor;
import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor;
import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.engine.support.hierarchical.ExclusiveResource;
+import org.junit.platform.testkit.engine.EngineTestKit;
+import org.junit.platform.testkit.engine.Event;
+import org.junit.platform.testkit.engine.Events;
/**
* Integration tests for {@link ResourceLock} and {@link ResourceLocksProvider}.
@@ -64,7 +83,6 @@ void noSharedResources() {
assertThat(methodResources).isEmpty();
var nestedClassResources = getNestedClassResources(
- NoSharedResourcesTestCase.class,
NoSharedResourcesTestCase.NestedClass.class
);
assertThat(nestedClassResources).isEmpty();
@@ -86,16 +104,23 @@ void addSharedResourcesViaAnnotationValue() {
SharedResourcesViaAnnotationValueTestCase.class
);
assertThat(methodResources).containsExactlyInAnyOrder(
+ new ExclusiveResource("a3", LockMode.READ_WRITE),
new ExclusiveResource("b1", LockMode.READ),
new ExclusiveResource("b2", LockMode.READ_WRITE)
);
var nestedClassResources = getNestedClassResources(
- SharedResourcesViaAnnotationValueTestCase.class,
SharedResourcesViaAnnotationValueTestCase.NestedClass.class
);
assertThat(nestedClassResources).containsExactlyInAnyOrder(
- new ExclusiveResource("c1", LockMode.READ),
+ new ExclusiveResource("a3", LockMode.READ_WRITE),
+ new ExclusiveResource("c1", LockMode.READ)
+ );
+
+ var nestedClassMethodResources = getMethodResources(
+ SharedResourcesViaAnnotationValueTestCase.NestedClass.class
+ );
+ assertThat(nestedClassMethodResources).containsExactlyInAnyOrder(
new ExclusiveResource("c2", LockMode.READ)
);
// @formatter:on
@@ -121,7 +146,6 @@ void addSharedResourcesViaAnnotationProviders() {
);
var nestedClassResources = getNestedClassResources(
- SharedResourcesViaAnnotationProvidersTestCase.class,
SharedResourcesViaAnnotationProvidersTestCase.NestedClass.class
);
assertThat(nestedClassResources).containsExactlyInAnyOrder(
@@ -139,22 +163,23 @@ void addSharedResourcesViaAnnotationValueAndProviders() {
);
assertThat(classResources).containsExactlyInAnyOrder(
new ExclusiveResource("a1", LockMode.READ_WRITE),
- new ExclusiveResource("a2", LockMode.READ)
+ new ExclusiveResource("a3", LockMode.READ)
);
var methodResources = getMethodResources(
SharedResourcesViaAnnotationValueAndProvidersTestCase.class
);
assertThat(methodResources).containsExactlyInAnyOrder(
+ new ExclusiveResource("a2", LockMode.READ_WRITE),
new ExclusiveResource("b1", LockMode.READ),
new ExclusiveResource("b2", LockMode.READ)
);
var nestedClassResources = getNestedClassResources(
- SharedResourcesViaAnnotationValueAndProvidersTestCase.class,
SharedResourcesViaAnnotationValueAndProvidersTestCase.NestedClass.class
);
assertThat(nestedClassResources).containsExactlyInAnyOrder(
+ new ExclusiveResource("a2", LockMode.READ_WRITE),
new ExclusiveResource("c1", LockMode.READ_WRITE),
new ExclusiveResource("c2", LockMode.READ_WRITE),
new ExclusiveResource("c3", LockMode.READ_WRITE)
@@ -162,6 +187,52 @@ void addSharedResourcesViaAnnotationValueAndProviders() {
// @formatter:on
}
+ @Test
+ void sharedResourcesHavingTheSameValueAndModeAreDeduplicated() {
+ // @formatter:off
+ var methodResources = getMethodResources(
+ SharedResourcesHavingTheSameValueAndModeAreDeduplicatedTestCase.class
+ );
+ assertThat(methodResources).containsExactlyInAnyOrder(
+ new ExclusiveResource("a1", LockMode.READ_WRITE)
+ );
+ // @formatter:on
+ }
+
+ @Test
+ void sharedResourcesHavingTheSameValueButDifferentModeAreNotDeduplicated() {
+ // @formatter:off
+ var methodResources = getMethodResources(
+ SharedResourcesHavingTheSameValueButDifferentModeAreNotDeduplicatedTestCase.class
+ );
+ assertThat(methodResources).containsExactlyInAnyOrder(
+ new ExclusiveResource("a1", LockMode.READ),
+ new ExclusiveResource("a1", LockMode.READ_WRITE)
+ );
+ // @formatter:on
+ }
+
+ static Stream> testMethodsCanNotDeclareSharedResourcesForChildrenArguments() {
+ // @formatter:off
+ return Stream.of(
+ TestCanNotDeclareSharedResourcesForChildrenTestCase.class,
+ ParameterizedTestCanNotDeclareSharedResourcesForChildrenTestCase.class,
+ RepeatedTestCanNotDeclareSharedResourcesForChildrenTestCase.class,
+ TestFactoryCanNotDeclareSharedResourcesForChildrenTestCase.class
+ );
+ // @formatter:on
+ }
+
+ @ParameterizedTest
+ @MethodSource("testMethodsCanNotDeclareSharedResourcesForChildrenArguments")
+ void testMethodsCanNotDeclareSharedResourcesForChildren(Class> testClass) {
+ var messageTemplate = "'ResourceLockTarget.CHILDREN' is not supported for methods. Invalid method: %s";
+ assertThrowsJunitExceptionWithMessage( //
+ testClass, //
+ messageTemplate.formatted(getDeclaredTestMethod(testClass)) //
+ );
+ }
+
@Test
void emptyAnnotation() {
// @formatter:off
@@ -176,7 +247,6 @@ void emptyAnnotation() {
assertThat(methodResources).isEmpty();
var nestedClassResources = getNestedClassResources(
- EmptyAnnotationTestCase.class,
EmptyAnnotationTestCase.NestedClass.class
);
assertThat(nestedClassResources).isEmpty();
@@ -192,26 +262,49 @@ private ClassTestDescriptor getClassTestDescriptor(Class> testClass) {
}
private Set getMethodResources(Class> testClass) {
+ var descriptor = new TestMethodTestDescriptor( //
+ uniqueId, testClass, getDeclaredTestMethod(testClass), configuration //
+ );
+ descriptor.setParent(getClassTestDescriptor(testClass));
+ return descriptor.getExclusiveResources();
+ }
+
+ private static Method getDeclaredTestMethod(Class> testClass) {
try {
- // @formatter:off
- var descriptor = new TestMethodTestDescriptor(
- uniqueId, testClass, testClass.getDeclaredMethod("test"), configuration
- );
- // @formatter:on
- descriptor.setParent(getClassTestDescriptor(testClass));
- return descriptor.getExclusiveResources();
+ return testClass.getDeclaredMethod("test");
}
catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
- private Set getNestedClassResources(Class> testClass, Class> nestedClass) {
- var descriptor = new NestedClassTestDescriptor(uniqueId, nestedClass, configuration);
- descriptor.setParent(getClassTestDescriptor(testClass));
+ private Set getNestedClassResources(Class> testClass) {
+ var descriptor = new NestedClassTestDescriptor(uniqueId, testClass, configuration);
+ descriptor.setParent(getClassTestDescriptor(testClass.getEnclosingClass()));
return descriptor.getExclusiveResources();
}
+ private static void assertThrowsJunitExceptionWithMessage(Class> testClass, String message) {
+ // @formatter:off
+ var events = execute(testClass);
+ assertThat(events.filter(finishedWithFailure(instanceOf(JUnitException.class))::matches))
+ .hasSize(1)
+ .map(Event::getPayload)
+ .map(payload -> (TestExecutionResult) payload.orElseThrow())
+ .map(payload -> getRootCause(payload.getThrowable().orElseThrow()))
+ .first()
+ .is(instanceOf(JUnitException.class))
+ .has(message(message));
+ // @formatter:on
+ }
+
+ private static Events execute(Class> testCase) {
+ var discoveryRequests = request() //
+ .selectors(DiscoverySelectors.selectClass(testCase)) //
+ .build();
+ return EngineTestKit.execute("junit-jupiter", discoveryRequests).allEvents();
+ }
+
// -------------------------------------------------------------------------
@SuppressWarnings("JUnitMalformedDeclaration")
@@ -229,18 +322,23 @@ class NestedClass {
@SuppressWarnings("JUnitMalformedDeclaration")
@ResourceLock("a1")
@ResourceLock(value = "a2", mode = ResourceAccessMode.READ_WRITE)
+ @ResourceLock(value = "a3", mode = ResourceAccessMode.READ_WRITE, target = ResourceLockTarget.CHILDREN)
static class SharedResourcesViaAnnotationValueTestCase {
@Test
@ResourceLock(value = "b1", mode = ResourceAccessMode.READ)
- @ResourceLock("b2")
+ @ResourceLock(value = "b2", target = ResourceLockTarget.SELF)
void test() {
}
@Nested
@ResourceLock(value = "c1", mode = ResourceAccessMode.READ)
- @ResourceLock(value = "c2", mode = ResourceAccessMode.READ)
+ @ResourceLock(value = "c2", mode = ResourceAccessMode.READ, target = ResourceLockTarget.CHILDREN)
class NestedClass {
+
+ @Test
+ void test() {
+ }
}
}
@@ -302,8 +400,12 @@ public Set provideForNestedClass(Class> testClass) {
@SuppressWarnings("JUnitMalformedDeclaration")
@ResourceLock( //
value = "a1", //
- mode = ResourceAccessMode.READ_WRITE, //
- providers = SharedResourcesViaAnnotationValueAndProvidersTestCase.ClassLevelProvider.class //
+ providers = SharedResourcesViaAnnotationValueAndProvidersTestCase.FirstClassLevelProvider.class //
+ )
+ @ResourceLock( //
+ value = "a2", //
+ target = ResourceLockTarget.CHILDREN, //
+ providers = SharedResourcesViaAnnotationValueAndProvidersTestCase.SecondClassLevelProvider.class //
)
static class SharedResourcesViaAnnotationValueAndProvidersTestCase {
@@ -318,12 +420,15 @@ void test() {
class NestedClass {
}
- static class ClassLevelProvider implements ResourceLocksProvider {
+ static class FirstClassLevelProvider implements ResourceLocksProvider {
@Override
public Set provideForClass(Class> testClass) {
- return Set.of(new Lock("a2", ResourceAccessMode.READ));
+ return Set.of(new Lock("a3", ResourceAccessMode.READ));
}
+ }
+
+ static class SecondClassLevelProvider implements ResourceLocksProvider {
@Override
public Set provideForMethod(Class> testClass, Method testMethod) {
@@ -346,51 +451,85 @@ public Set provideForNestedClass(Class> testClass) {
}
@SuppressWarnings("JUnitMalformedDeclaration")
- @ResourceLock
- static class EmptyAnnotationTestCase {
+ @ResourceLock( //
+ value = "a1", //
+ target = ResourceLockTarget.CHILDREN, //
+ providers = SharedResourcesHavingTheSameValueAndModeAreDeduplicatedTestCase.Provider.class //
+ )
+ static class SharedResourcesHavingTheSameValueAndModeAreDeduplicatedTestCase {
@Test
- @ResourceLock
+ @ResourceLock(value = "a1")
void test() {
}
- @Nested
- @ResourceLock
- class NestedClass {
+ static class Provider implements ResourceLocksProvider {
+
+ @Override
+ public Set provideForMethod(Class> testClass, Method testMethod) {
+ return Set.of(new Lock("a1"));
+ }
}
}
- static class NestedNestedTestCase {
+ @SuppressWarnings("JUnitMalformedDeclaration")
+ @ResourceLock(value = "a1", mode = ResourceAccessMode.READ_WRITE, target = ResourceLockTarget.CHILDREN)
+ static class SharedResourcesHavingTheSameValueButDifferentModeAreNotDeduplicatedTestCase {
- @SuppressWarnings("JUnitMalformedDeclaration")
- @Nested
- @ResourceLock(providers = NestedNestedTestCase.Provider.class)
- static class NestedClass {
+ @Test
+ @ResourceLock(value = "a1", mode = ResourceAccessMode.READ)
+ void test() {
+ }
+ }
- @Nested
- class NestedClassTwo {
+ @SuppressWarnings("JUnitMalformedDeclaration")
+ static class TestCanNotDeclareSharedResourcesForChildrenTestCase {
- @Test
- void test() {
- }
- }
+ @Test
+ @ResourceLock(value = "a1", target = ResourceLockTarget.CHILDREN)
+ void test() {
}
+ }
- static class Provider implements ResourceLocksProvider {
- @Override
- public Set provideForClass(Class> testClass) {
- return ResourceLocksProvider.super.provideForClass(testClass);
- }
+ static class ParameterizedTestCanNotDeclareSharedResourcesForChildrenTestCase {
- @Override
- public Set provideForNestedClass(Class> testClass) {
- return ResourceLocksProvider.super.provideForNestedClass(testClass);
- }
+ @ParameterizedTest
+ @ValueSource(ints = { 1, 2, 3 })
+ @ResourceLock(value = "a1", target = ResourceLockTarget.CHILDREN)
+ void test() {
+ }
+ }
- @Override
- public Set provideForMethod(Class> testClass, Method testMethod) {
- return ResourceLocksProvider.super.provideForMethod(testClass, testMethod);
- }
+ static class RepeatedTestCanNotDeclareSharedResourcesForChildrenTestCase {
+
+ @RepeatedTest(5)
+ @ResourceLock(value = "a1", target = ResourceLockTarget.CHILDREN)
+ void test() {
+ }
+ }
+
+ static class TestFactoryCanNotDeclareSharedResourcesForChildrenTestCase {
+
+ @TestFactory
+ @ResourceLock(value = "a1", target = ResourceLockTarget.CHILDREN)
+ Stream test() {
+ return Stream.of(DynamicTest.dynamicTest("Dynamic test", () -> {
+ }));
+ }
+ }
+
+ @SuppressWarnings("JUnitMalformedDeclaration")
+ @ResourceLock
+ static class EmptyAnnotationTestCase {
+
+ @Test
+ @ResourceLock
+ void test() {
+ }
+
+ @Nested
+ @ResourceLock
+ class NestedClass {
}
}