diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc
index 2f8ebd6bf778..b3b89692951b 100644
--- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc
@@ -36,6 +36,9 @@ repository on GitHub.
of the former. Please refer to the
<<../user-guide/index.adoc#launcher-api-launcher-interceptors-custom, User Guide>> for
details.
+* Introduced `@ResourceLock(target = SELF | CHILDREN)` where the target defaults to SELF to
+ preserve existing behavior. Using CHILDREN has the same effect as declaring @ResourceLock
+ on every @Test method of a test class. Please refer to the <<../user-guide/writing-tests.adoc#specifying-target-for-resource-lock, User Guide. Writing Tests>> for details.
[[release-notes-5.10.0-M1-junit-jupiter]]
=== JUnit Jupiter
diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
index a0fc45f2934c..556663bc824d 100644
--- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
+++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
@@ -2490,6 +2490,16 @@ to the same shared resource is running.
include::{testDir}/example/SharedResourcesDemo.java[tags=user_guide]
----
+[[specifying-target-for-resource-lock]]
+You can declare a resource lock on the class level which will apply only to its children,
+and will not be applied to the class itself. This is useful if you want to keep `CONCURRENT`
+execution mode for each method.
+
+[source,java]
+----
+include::{testDir}/example/SharedResourcesChildrenTargetDemo.java[tags=user_guide]
+----
+
[[writing-tests-built-in-extensions]]
=== Built-in Extensions
diff --git a/documentation/src/test/java/example/SharedResourcesChildrenTargetDemo.java b/documentation/src/test/java/example/SharedResourcesChildrenTargetDemo.java
new file mode 100644
index 000000000000..2bb570260237
--- /dev/null
+++ b/documentation/src/test/java/example/SharedResourcesChildrenTargetDemo.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2015-2023 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+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.Resources.SYSTEM_OUT;
+import static org.junit.jupiter.api.parallel.Resources.SYSTEM_PROPERTIES;
+import static org.junit.jupiter.api.parallel.Resources.TIME_ZONE;
+
+import java.util.Properties;
+import java.util.TimeZone;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.ResourceLockTarget;
+
+// tag::user_guide[]
+@Execution(CONCURRENT)
+@ResourceLock(value = TIME_ZONE, mode = READ, target = ResourceLockTarget.CHILDREN)
+class SharedResourcesChildrenTargetDemo {
+
+ private Properties backup;
+
+ @BeforeEach
+ void backup() {
+ backup = new Properties();
+ backup.putAll(System.getProperties());
+ }
+
+ @AfterEach
+ void restore() {
+ System.setProperties(backup);
+ }
+
+ @Test
+ @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ)
+ void usePropertiesAndTimeZoneWithoutModification() {
+ assertNull(System.getProperty("my.prop"));
+ }
+
+ @Test
+ @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ)
+ void usePropertiesAndTimeZoneWithoutModificationAgain() {
+ assertNull(System.getProperty("my.prop"));
+ }
+
+ @Test
+ @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
+ void canSetCustomPropertyToTimeZone() {
+ String timezone = TimeZone.getDefault().getDisplayName();
+ System.setProperty("my.timezone", timezone);
+ assertEquals(timezone, System.getProperty("my.timezone"));
+ }
+
+}
+// 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 4f7f97d403fb..a84c8ee7b506 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
@@ -40,6 +40,9 @@
*
Since JUnit Jupiter 5.4, this annotation is {@linkplain Inherited inherited}
* within class hierarchies.
*
+ *
Since JUnit Jupiter 5.10, this annotation can be used to specify the target
+ * of the lock with {@link ResourceLockTarget}.
+ *
* @see Isolated
* @see Resources
* @see ResourceAccessMode
@@ -69,4 +72,13 @@
*/
ResourceAccessMode mode() default ResourceAccessMode.READ_WRITE;
+ /**
+ * Resource lock target.
+ *
+ *
Defaults to {@link ResourceLockTarget#SELF SELF}.
+ *
+ * @see ResourceLockTarget
+ */
+ ResourceLockTarget target() default ResourceLockTarget.SELF;
+
}
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..22db67df6236
--- /dev/null
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLockTarget.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015-2023 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;
+
+/**
+ * Indicates the target of a {@link ResourceLock}.
+ *
+ * @since 5.10
+ * @see ResourceLock
+ */
+@API(status = EXPERIMENTAL, since = "5.10")
+public enum ResourceLockTarget {
+
+ /**
+ * Point to the test descriptor itself
+ */
+ SELF,
+
+ /**
+ * Skip the test descriptor itself and apply annotation {@link ResourceLock} to all its children
+ */
+ CHILDREN
+
+}
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java
index 541844b0da8f..8f14d3a9191d 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java
@@ -33,6 +33,7 @@
import org.junit.jupiter.api.parallel.Execution;
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.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.ConditionEvaluator;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
@@ -49,6 +50,7 @@
import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor;
import org.junit.platform.engine.support.hierarchical.ExclusiveResource;
import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode;
+import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockScope;
import org.junit.platform.engine.support.hierarchical.Node;
/**
@@ -183,7 +185,11 @@ public static ExecutionMode toExecutionMode(org.junit.jupiter.api.parallel.Execu
Set getExclusiveResourcesFromAnnotation(AnnotatedElement element) {
// @formatter:off
return findRepeatableAnnotations(element, ResourceLock.class).stream()
- .map(resource -> new ExclusiveResource(resource.value(), toLockMode(resource.mode())))
+ .map(resource -> new ExclusiveResource(
+ resource.value(),
+ toLockMode(resource.mode()),
+ toLockScope(resource.target()))
+ )
.collect(toSet());
// @formatter:on
}
@@ -198,6 +204,16 @@ private static LockMode toLockMode(ResourceAccessMode mode) {
throw new JUnitException("Unknown ResourceAccessMode: " + mode);
}
+ private static LockScope toLockScope(ResourceLockTarget lockTarget) {
+ switch (lockTarget) {
+ case SELF:
+ return LockScope.SELF;
+ case CHILDREN:
+ return LockScope.CHILDREN;
+ }
+ throw new JUnitException("Unknown ResourceLockTarget: " + lockTarget);
+ }
+
@Override
public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) throws Exception {
context.getThrowableCollector().assertEmpty();
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExclusiveResource.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExclusiveResource.java
index 1674101c4c4c..b6a8363c9764 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExclusiveResource.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ExclusiveResource.java
@@ -52,18 +52,34 @@ public class ExclusiveResource {
private final String key;
private final LockMode lockMode;
+ private final LockScope lockScope;
private int hash;
/**
- * Create a new {@code ExclusiveResource}.
+ * Create a new {@code ExclusiveResource} with a default lock scope {@link LockScope#SELF}
*
* @param key the identifier of the resource; never {@code null} or blank
* @param lockMode the lock mode to use to synchronize access to the
* resource; never {@code null}
+ *
*/
public ExclusiveResource(String key, LockMode lockMode) {
+ this(key, lockMode, LockScope.SELF);
+ }
+
+ /**
+ * Create a new {@code ExclusiveResource}.
+ *
+ * @param key the identifier of the resource; never {@code null} or blank
+ * @param lockMode the lock mode to use to synchronize access to the
+ * resource; never {@code null}
+ * @param lockScope the lock scope to use to synchronize access to the
+ * resource; never {@code null}
+ */
+ public ExclusiveResource(String key, LockMode lockMode, LockScope lockScope) {
this.key = Preconditions.notBlank(key, "key must not be blank");
this.lockMode = Preconditions.notNull(lockMode, "lockMode must not be null");
+ this.lockScope = Preconditions.notNull(lockScope, "lockScope must not be null");
}
/**
@@ -80,6 +96,13 @@ public LockMode getLockMode() {
return lockMode;
}
+ /**
+ * Get the lock scope of this resource.
+ */
+ public LockScope getLockScope() {
+ return lockScope;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -89,21 +112,26 @@ public boolean equals(Object o) {
return false;
}
ExclusiveResource that = (ExclusiveResource) o;
- return Objects.equals(key, that.key) && lockMode == that.lockMode;
+ return Objects.equals(key, that.key) && lockMode == that.lockMode && lockScope == that.lockScope;
}
@Override
public int hashCode() {
int h = hash;
if (h == 0) {
- h = hash = Objects.hash(key, lockMode);
+ h = hash = Objects.hash(key, lockMode, lockScope);
}
return h;
}
@Override
public String toString() {
- return new ToStringBuilder(this).append("key", key).append("lockMode", lockMode).toString();
+ return new ToStringBuilder(this).append("key", key).append("lockMode", lockMode).append("lockScope",
+ lockScope).toString();
+ }
+
+ public ExclusiveResource convertToSelfTarget() {
+ return new ExclusiveResource(key, lockMode, LockScope.SELF);
}
/**
@@ -131,4 +159,21 @@ public enum LockMode {
}
+ /**
+ * {@code LockTarget} defines the scope of the lock.
+ */
+ public enum LockScope {
+
+ /**
+ * Lock the resource for the node itself.
+ */
+ SELF,
+
+ /**
+ * Lock the resource for all children of the node. Bypass the node itself.
+ */
+ CHILDREN
+
+ }
+
}
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTreeWalker.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTreeWalker.java
index 2bb59d5099f9..3f37f00c9f5f 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTreeWalker.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTreeWalker.java
@@ -14,18 +14,24 @@
import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_READ_WRITE;
import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.SAME_THREAD;
+import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode;
+import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockScope;
/**
* @since 1.3
*/
class NodeTreeWalker {
+ private static final Set NO_RESOURCES_INHERITED_AT_ROOT = Collections.emptySet();
+
private final LockManager lockManager;
private final ResourceLock globalReadLock;
private final ResourceLock globalReadWriteLock;
@@ -44,41 +50,62 @@ NodeExecutionAdvisor walk(TestDescriptor rootDescriptor) {
Preconditions.condition(getExclusiveResources(rootDescriptor).isEmpty(),
"Engine descriptor must not declare exclusive resources");
NodeExecutionAdvisor advisor = new NodeExecutionAdvisor();
- rootDescriptor.getChildren().forEach(child -> walk(child, child, advisor));
+ rootDescriptor.getChildren().forEach(child -> walk(child, child, advisor, NO_RESOURCES_INHERITED_AT_ROOT));
return advisor;
}
- private void walk(TestDescriptor globalLockDescriptor, TestDescriptor testDescriptor,
- NodeExecutionAdvisor advisor) {
+ private void walk(TestDescriptor globalLockDescriptor, TestDescriptor testDescriptor, NodeExecutionAdvisor advisor,
+ Set inheritedExclusiveResources) {
+
Set exclusiveResources = getExclusiveResources(testDescriptor);
- if (exclusiveResources.isEmpty()) {
- advisor.useResourceLock(testDescriptor, globalReadLock);
- testDescriptor.getChildren().forEach(child -> walk(globalLockDescriptor, child, advisor));
+ exclusiveResources.addAll(inheritedExclusiveResources);
+
+ if (hasAnyExclusiveResourceWithSelfScope(exclusiveResources)) {
+ assignLocksForDescriptor(globalLockDescriptor, testDescriptor, advisor, exclusiveResources);
}
else {
- Set allResources = new HashSet<>(exclusiveResources);
- if (isReadOnly(allResources)) {
- doForChildrenRecursively(testDescriptor, child -> allResources.addAll(getExclusiveResources(child)));
- if (!isReadOnly(allResources)) {
- forceDescendantExecutionModeRecursively(advisor, testDescriptor);
- }
- }
- else {
- advisor.forceDescendantExecutionMode(testDescriptor, SAME_THREAD);
- doForChildrenRecursively(testDescriptor, child -> {
- allResources.addAll(getExclusiveResources(child));
- advisor.forceDescendantExecutionMode(child, SAME_THREAD);
- });
- }
- if (!globalLockDescriptor.equals(testDescriptor) && allResources.contains(GLOBAL_READ_WRITE)) {
- forceDescendantExecutionModeRecursively(advisor, globalLockDescriptor);
- advisor.useResourceLock(globalLockDescriptor, globalReadWriteLock);
- }
- if (globalLockDescriptor.equals(testDescriptor) && !allResources.contains(GLOBAL_READ_WRITE)) {
- allResources.add(GLOBAL_READ);
- }
- advisor.useResourceLock(testDescriptor, lockManager.getLockForResources(allResources));
+ assignReadLockAndWalkOverChildren(globalLockDescriptor, testDescriptor, advisor, exclusiveResources);
+ }
+ }
+
+ private static boolean hasAnyExclusiveResourceWithSelfScope(Set exclusiveResources) {
+ return exclusiveResources.stream().anyMatch(resource -> resource.getLockScope() == LockScope.SELF);
+ }
+
+ private void assignLocksForDescriptor(TestDescriptor globalLockDescriptor, TestDescriptor testDescriptor,
+ NodeExecutionAdvisor advisor, Set exclusiveResources) {
+ Set allResources = new HashSet<>(exclusiveResources);
+ doForChildrenRecursively(testDescriptor, child -> allResources.addAll(getExclusiveResources(child)));
+
+ if (hasReadWriteLockMode(allResources)) {
+ forceDescendantExecutionModeRecursively(advisor, testDescriptor);
+ }
+
+ if (!globalLockDescriptor.equals(testDescriptor) && allResources.contains(GLOBAL_READ_WRITE)) {
+ forceDescendantExecutionModeRecursively(advisor, globalLockDescriptor);
+ advisor.useResourceLock(globalLockDescriptor, globalReadWriteLock);
}
+
+ if (globalLockDescriptor.equals(testDescriptor) && !allResources.contains(GLOBAL_READ_WRITE)) {
+ allResources.add(GLOBAL_READ);
+ }
+
+ advisor.useResourceLock(testDescriptor, lockManager.getLockForResources(allResources));
+ }
+
+ private void assignReadLockAndWalkOverChildren(TestDescriptor globalLockDescriptor, TestDescriptor testDescriptor,
+ NodeExecutionAdvisor advisor, Set exclusiveResources) {
+ advisor.useResourceLock(testDescriptor, globalReadLock);
+ Set resourcesToInherit = convertChildrenScopeToSelf(exclusiveResources);
+ testDescriptor.getChildren().forEach(child -> walk(globalLockDescriptor, child, advisor, resourcesToInherit));
+ }
+
+ private static Set convertChildrenScopeToSelf(Set exclusiveResources) {
+ return exclusiveResources.stream().map(ExclusiveResource::convertToSelfTarget).collect(Collectors.toSet());
+ }
+
+ private boolean hasReadWriteLockMode(Set allResources) {
+ return allResources.stream().anyMatch(resource -> resource.getLockMode() == LockMode.READ_WRITE);
}
private void forceDescendantExecutionModeRecursively(NodeExecutionAdvisor advisor, TestDescriptor testDescriptor) {
@@ -86,11 +113,6 @@ private void forceDescendantExecutionModeRecursively(NodeExecutionAdvisor adviso
doForChildrenRecursively(testDescriptor, child -> advisor.forceDescendantExecutionMode(child, SAME_THREAD));
}
- private boolean isReadOnly(Set exclusiveResources) {
- return exclusiveResources.stream().allMatch(
- exclusiveResource -> exclusiveResource.getLockMode() == ExclusiveResource.LockMode.READ);
- }
-
private Set getExclusiveResources(TestDescriptor testDescriptor) {
return NodeUtils.asNode(testDescriptor).getExclusiveResources();
}
diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/NodeTreeWalkerIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/NodeTreeWalkerIntegrationTests.java
index e78138018f75..1c0e4f1d2fbb 100644
--- a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/NodeTreeWalkerIntegrationTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/NodeTreeWalkerIntegrationTests.java
@@ -23,11 +23,13 @@
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.function.Function;
+import java.util.stream.Collectors;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
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.engine.JupiterTestEngine;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.UniqueId;
@@ -170,6 +172,37 @@ void coarsensGlobalLockToEngineDescriptorChild() {
assertThat(advisor.getForcedExecutionMode(testMethodDescriptor)).contains(SAME_THREAD);
}
+ @Test
+ void acquireLocksForMethodsButDoNotForceSameThreadForAllMethods() {
+ var engineDescriptor = discover(
+ TestCaseWithResourceReadLockOnClassButTargetChildrenAndReadWriteLockOnTestCase.class);
+
+ var advisor = nodeTreeWalker.walk(engineDescriptor);
+
+ var testClassDescriptor = getOnlyElement(engineDescriptor.getChildren());
+ assertThat(advisor.getResourceLock(testClassDescriptor)).extracting(allLocks()).isEqualTo(
+ List.of(getLock(GLOBAL_READ)));
+
+ assertThat(advisor.getForcedExecutionMode(testClassDescriptor)).isEmpty();
+
+ var testMethods = testClassDescriptor.getChildren().stream().collect(
+ Collectors.groupingBy(TestDescriptor::getDisplayName));
+
+ TestDescriptor readOnlyMethod1 = testMethods.get("readOnlyMethod1()").get(0);
+ TestDescriptor readOnlyMethod2 = testMethods.get("readOnlyMethod2()").get(0);
+ TestDescriptor readWriteMethod = testMethods.get("greedyMethod()").get(0);
+
+ assertThat(advisor.getResourceLock(readOnlyMethod1)).extracting(allLocks()).isEqualTo(
+ List.of(getReadLock("a"), getReadLock("b")));
+ assertThat(advisor.getResourceLock(readOnlyMethod2)).extracting(allLocks()).isEqualTo(
+ List.of(getReadLock("a"), getReadLock("b")));
+ assertThat(advisor.getResourceLock(readWriteMethod)).extracting(allLocks()).isEqualTo(
+ List.of(getReadLock("a"), getReadWriteLock("b")));
+ assertThat(advisor.getForcedExecutionMode(readOnlyMethod1)).isEmpty();
+ assertThat(advisor.getForcedExecutionMode(readOnlyMethod2)).isEmpty();
+ assertThat(advisor.getForcedExecutionMode(readWriteMethod)).isEmpty();
+ }
+
private static Function> allLocks() {
return ResourceLockSupport::getLocks;
}
@@ -254,4 +287,22 @@ static class TestCaseWithResourceReadLockOnClassAndReadClockOnTestCase {
void test() {
}
}
+
+ @ResourceLock(value = "a", mode = ResourceAccessMode.READ, target = ResourceLockTarget.CHILDREN)
+ static class TestCaseWithResourceReadLockOnClassButTargetChildrenAndReadWriteLockOnTestCase {
+ @Test
+ @ResourceLock(value = "b", mode = ResourceAccessMode.READ)
+ void readOnlyMethod1() {
+ }
+
+ @Test
+ @ResourceLock(value = "b", mode = ResourceAccessMode.READ)
+ void readOnlyMethod2() {
+ }
+
+ @Test
+ @ResourceLock(value = "b", mode = ResourceAccessMode.READ_WRITE)
+ void greedyMethod() {
+ }
+ }
}
diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java
index ca7433377bf1..d2d75c7ba134 100644
--- a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java
@@ -63,7 +63,9 @@
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.Isolated;
+import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.ResourceLockTarget;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.engine.reporting.ReportEntry;
@@ -106,6 +108,20 @@ void successfulTestWithMethodLock() {
assertThat(ThreadReporter.getThreadNames(events)).hasSize(3);
}
+ @Test
+ void successfulTestWithClassLockChildrenTarget() {
+ var events = executeConcurrently(3, SuccessfulWithClassLockChildrenTargetTestCase.class);
+ assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
+ assertThat(ThreadReporter.getThreadNames(events)).hasSize(3);
+ }
+
+ @Test
+ void successfulTestWithClassLockChildrenTargetAndPerMethodLocks() {
+ var events = executeConcurrently(3, SuccessfulWithClassLockChildrenTargetAndTwoLocksTestCase.class);
+ assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
+ assertThat(ThreadReporter.getThreadNames(events)).hasSize(3);
+ }
+
@Test
void successfulTestWithClassLock() {
var events = executeConcurrently(3, SuccessfulWithClassLockTestCase.class);
@@ -491,6 +507,92 @@ void thirdTest() throws Exception {
}
}
+ @ExtendWith(ThreadReporter.class)
+ @ResourceLock(value = "sharedResource", mode = ResourceAccessMode.READ, target = ResourceLockTarget.CHILDREN)
+ static class SuccessfulWithClassLockChildrenTargetTestCase {
+
+ static AtomicInteger sharedResource;
+ static CountDownLatch countDownLatch;
+ static CountDownLatch assertionCountDownLatch;
+
+ @BeforeAll
+ static void initialize() {
+ sharedResource = new AtomicInteger();
+ countDownLatch = new CountDownLatch(2);
+ assertionCountDownLatch = new CountDownLatch(2);
+ }
+
+ @Test
+ void test1() throws Exception {
+ sharedResource.incrementAndGet();
+ countDownLatch.countDown();
+ countDownLatch.await(estimateSimulatedTestDurationInMiliseconds(), MILLISECONDS);
+ assertEquals(2, sharedResource.get());
+ assertionCountDownLatch.countDown();
+ assertionCountDownLatch.await(estimateSimulatedTestDurationInMiliseconds(), MILLISECONDS);
+ sharedResource.decrementAndGet();
+ }
+
+ @Test
+ void test2() throws Exception {
+ sharedResource.incrementAndGet();
+ countDownLatch.countDown();
+ countDownLatch.await(estimateSimulatedTestDurationInMiliseconds(), MILLISECONDS);
+ assertEquals(2, sharedResource.get());
+ assertionCountDownLatch.countDown();
+ assertionCountDownLatch.await(estimateSimulatedTestDurationInMiliseconds(), MILLISECONDS);
+ sharedResource.decrementAndGet();
+ }
+
+ @Test
+ @ResourceLock(value = "sharedResource", mode = ResourceAccessMode.READ_WRITE)
+ void test3() throws Exception {
+ int readWrite = sharedResource.addAndGet(5);
+ assertEquals(5, readWrite);
+ sharedResource.addAndGet(-5);
+ }
+
+ }
+
+ @ExtendWith(ThreadReporter.class)
+ @ResourceLock(value = "sharedResource", mode = ResourceAccessMode.READ, target = ResourceLockTarget.CHILDREN)
+ static class SuccessfulWithClassLockChildrenTargetAndTwoLocksTestCase {
+
+ static AtomicInteger sharedResource;
+ static CountDownLatch countDownLatch;
+ static CountDownLatch assertionCountDownLatch;
+
+ @BeforeAll
+ static void initialize() {
+ sharedResource = new AtomicInteger();
+ countDownLatch = new CountDownLatch(2);
+ assertionCountDownLatch = new CountDownLatch(2);
+ }
+
+ @Test
+ @ResourceLock(value = "otherSharedResource", mode = ResourceAccessMode.READ)
+ void test1() throws Exception {
+ assertExecutionInParallelWithAnotherReadLockBasedMethod(sharedResource, countDownLatch,
+ assertionCountDownLatch);
+ }
+
+ @Test
+ @ResourceLock(value = "otherSharedResource", mode = ResourceAccessMode.READ)
+ void test2() throws Exception {
+ assertExecutionInParallelWithAnotherReadLockBasedMethod(sharedResource, countDownLatch,
+ assertionCountDownLatch);
+ }
+
+ @Test
+ @ResourceLock(value = "otherSharedResource", mode = ResourceAccessMode.READ_WRITE)
+ void test3() throws Exception {
+ int readWrite = sharedResource.addAndGet(5);
+ assertEquals(5, readWrite);
+ sharedResource.addAndGet(-5);
+ }
+
+ }
+
@ExtendWith(ThreadReporter.class)
@ResourceLock("sharedResource")
static class SuccessfulWithClassLockTestCase {
@@ -782,6 +884,17 @@ private static void incrementBlockAndCheck(AtomicInteger sharedResource, CountDo
assertEquals(value, sharedResource.get());
}
+ private static void assertExecutionInParallelWithAnotherReadLockBasedMethod(AtomicInteger sharedResource,
+ CountDownLatch countDownLatch, CountDownLatch assertionCountDownLatch) throws InterruptedException {
+ sharedResource.incrementAndGet();
+ countDownLatch.countDown();
+ countDownLatch.await(estimateSimulatedTestDurationInMiliseconds(), MILLISECONDS);
+ assertEquals(2, sharedResource.get());
+ assertionCountDownLatch.countDown();
+ assertionCountDownLatch.await(estimateSimulatedTestDurationInMiliseconds(), MILLISECONDS);
+ sharedResource.decrementAndGet();
+ }
+
private static int incrementAndBlock(AtomicInteger sharedResource, CountDownLatch countDownLatch)
throws InterruptedException {
var value = sharedResource.incrementAndGet();