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}: + *

+ */ + 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 { } }