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();