From 511ab7d04dd5deec372157381b37a42003778b18 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Sun, 26 Jan 2025 19:23:11 +0100 Subject: [PATCH] Provide runtime enclosing instance types to `ResourceLocksProviders` Make API consistent with `DisplayNameGenerator` prior to releasing it in 5.12. Resolves #4163. --------- Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> --- .../DynamicSharedResourcesDemo.java | 4 +- .../api/parallel/ResourceLocksProvider.java | 32 +++++++++++--- .../descriptor/ClassTestDescriptor.java | 5 ++- .../descriptor/MethodBasedTestDescriptor.java | 17 ++++++- .../descriptor/NestedClassTestDescriptor.java | 7 ++- .../engine/descriptor/ResourceLockAware.java | 37 +++++++++++++--- .../parallel/ResourceLockAnnotationTests.java | 18 +++++--- .../parallel/ResourceLocksProviderTests.java | 44 ++++++++++++++----- 8 files changed, 128 insertions(+), 36 deletions(-) diff --git a/documentation/src/test/java/example/sharedresources/DynamicSharedResourcesDemo.java b/documentation/src/test/java/example/sharedresources/DynamicSharedResourcesDemo.java index 31f4c37e6bb3..4abe7297e357 100644 --- a/documentation/src/test/java/example/sharedresources/DynamicSharedResourcesDemo.java +++ b/documentation/src/test/java/example/sharedresources/DynamicSharedResourcesDemo.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.Collections; +import java.util.List; import java.util.Properties; import java.util.Set; @@ -68,7 +69,8 @@ void canSetCustomPropertyToBanana() { static class Provider implements ResourceLocksProvider { @Override - public Set provideForMethod(Class testClass, Method testMethod) { + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { ResourceAccessMode mode = testMethod.getName().startsWith("canSet") ? READ_WRITE : READ; return Collections.singleton(new Lock(SYSTEM_PROPERTIES, mode)); } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksProvider.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksProvider.java index d94024d99f3b..2cffc5d6101f 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksProvider.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksProvider.java @@ -14,6 +14,7 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; import java.lang.reflect.Method; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -43,7 +44,7 @@ public interface ResourceLocksProvider { /** - * Add shared resources to a test class. + * Add shared resources for a test class. * *

Invoked in case a test class or its parent class is annotated with * {@code @ResourceLock(providers)}. @@ -60,8 +61,8 @@ default Set provideForClass(Class testClass) { } /** - * Add shared resources to a {@linkplain org.junit.jupiter.api.Nested @Nested} - * test class. + * Add shared resources for a + * {@link org.junit.jupiter.api.Nested @Nested} test class. * *

Invoked in case: *

    @@ -75,16 +76,24 @@ default Set provideForClass(Class testClass) { * the same semantics as annotating a nested test class with an analogous * {@code @ResourceLock(value, mode)} declaration. * + * @implNote The classes supplied as {@code enclosingInstanceTypes} may + * differ from the classes returned from invocations of + * {@link Class#getEnclosingClass()} — for example, when a nested test + * class is inherited from a superclass. + * + * @param enclosingInstanceTypes the runtime types of the enclosing + * instances for the test class, ordered from outermost to innermost, + * excluding {@code testClass}; never {@code null} * @param testClass a nested test class for which to add shared resources * @return a set of {@link Lock}; may be empty * @see org.junit.jupiter.api.Nested @Nested */ - default Set provideForNestedClass(Class testClass) { + default Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { return emptySet(); } /** - * Add shared resources to a test method. + * Add shared resources for a test method. * *

    Invoked in case: *

      @@ -97,13 +106,24 @@ default Set provideForNestedClass(Class testClass) { * has the same semantics as annotating a test method * with analogous {@code @ResourceLock(value, mode)}. * + * @implNote The classes supplied as {@code enclosingInstanceTypes} may + * differ from the classes returned from invocations of + * {@link Class#getEnclosingClass()} — for example, when a nested test + * class is inherited from a superclass. Similarly, the class instance + * supplied as {@code testClass} may differ from the class returned by + * {@code testMethod.getDeclaringClass()} — for example, when a test + * method is inherited from a superclass. + * + * @param enclosingInstanceTypes the runtime types of the enclosing + * instances for the test class, ordered from outermost to innermost, + * excluding {@code testClass}; never {@code null} * @param testClass the test class or {@link org.junit.jupiter.api.Nested @Nested} * test class that contains the {@code testMethod} * @param testMethod a test method for which to add shared resources * @return a set of {@link Lock}; may be empty * @see org.junit.jupiter.api.Nested @Nested */ - default Set provideForMethod(Class testClass, Method testMethod) { + default Set provideForMethod(List> enclosingInstanceTypes, Class testClass, Method testMethod) { return emptySet(); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java index 7c2a5571694c..e935db38e782 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.TestInstances; @@ -79,8 +80,8 @@ protected TestInstances instantiateTestClass(JupiterEngineExecutionContext paren } @Override - public Set evaluateResourceLocksProvider(ResourceLocksProvider provider) { - return provider.provideForClass(getTestClass()); + public Function> getResourceLocksProviderEvaluator() { + return provider -> provider.provideForClass(getTestClass()); } } 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 52bf3c4ef8b9..3a5b785591f8 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 @@ -13,14 +13,17 @@ 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.jupiter.engine.descriptor.ResourceLockAware.enclosingInstanceTypesDependentResourceLocksProviderEvaluator; import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder; import java.lang.reflect.Method; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import org.apiguardian.api.API; @@ -97,8 +100,18 @@ public ExclusiveResourceCollector getExclusiveResourceCollector() { } @Override - public Set evaluateResourceLocksProvider(ResourceLocksProvider provider) { - return provider.provideForMethod(getTestClass(), getTestMethod()); + public Function> getResourceLocksProviderEvaluator() { + return enclosingInstanceTypesDependentResourceLocksProviderEvaluator(this::getEnclosingTestClasses, + (provider, enclosingInstanceTypes) -> provider.provideForMethod(enclosingInstanceTypes, getTestClass(), + getTestMethod())); + } + + private List> getEnclosingTestClasses() { + return getParent() // + .filter(ClassBasedTestDescriptor.class::isInstance) // + .map(ClassBasedTestDescriptor.class::cast) // + .map(ClassBasedTestDescriptor::getEnclosingTestClasses) // + .orElseGet(Collections::emptyList); } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java index 72d11ce5b09e..b0619fa09cff 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java @@ -13,12 +13,14 @@ import static java.util.Collections.emptyList; import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.descriptor.DisplayNameUtils.createDisplayNameSupplierForNestedClass; +import static org.junit.jupiter.engine.descriptor.ResourceLockAware.enclosingInstanceTypesDependentResourceLocksProviderEvaluator; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.function.Supplier; import org.apiguardian.api.API; @@ -94,8 +96,9 @@ protected TestInstances instantiateTestClass(JupiterEngineExecutionContext paren } @Override - public Set evaluateResourceLocksProvider(ResourceLocksProvider provider) { - return provider.provideForNestedClass(getTestClass()); + public Function> getResourceLocksProviderEvaluator() { + return enclosingInstanceTypesDependentResourceLocksProviderEvaluator(this::getEnclosingTestClasses, (provider, + enclosingInstanceTypes) -> provider.provideForNestedClass(enclosingInstanceTypes, getTestClass())); } } 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 e7aa88edea52..c4f427989036 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 @@ -14,7 +14,11 @@ import java.util.ArrayDeque; import java.util.Deque; +import java.util.List; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Stream; import org.junit.jupiter.api.parallel.ResourceLocksProvider; @@ -35,8 +39,10 @@ default Stream determineExclusiveResources() { parent = parent.getParent().orElse(null); } + Function> evaluator = getResourceLocksProviderEvaluator(); + if (ancestors.isEmpty()) { - return determineOwnExclusiveResources(); + return determineOwnExclusiveResources(evaluator); } Stream parentStaticResourcesForChildren = ancestors.getLast() // @@ -44,18 +50,37 @@ default Stream determineExclusiveResources() { Stream ancestorDynamicResources = ancestors.stream() // .map(ResourceLockAware::getExclusiveResourceCollector) // - .flatMap(collector -> collector.getDynamicResources(this::evaluateResourceLocksProvider)); + .flatMap(collector -> collector.getDynamicResources(evaluator)); - return Stream.of(ancestorDynamicResources, parentStaticResourcesForChildren, determineOwnExclusiveResources())// + return Stream.of(ancestorDynamicResources, parentStaticResourcesForChildren, + determineOwnExclusiveResources(evaluator))// .flatMap(s -> s); } - default Stream determineOwnExclusiveResources() { - return this.getExclusiveResourceCollector().getAllExclusiveResources(this::evaluateResourceLocksProvider); + default Stream determineOwnExclusiveResources( + Function> providerToLocks) { + return this.getExclusiveResourceCollector().getAllExclusiveResources(providerToLocks); } ExclusiveResourceCollector getExclusiveResourceCollector(); - Set evaluateResourceLocksProvider(ResourceLocksProvider provider); + Function> getResourceLocksProviderEvaluator(); + + static Function> enclosingInstanceTypesDependentResourceLocksProviderEvaluator( + Supplier>> enclosingInstanceTypesSupplier, + BiFunction>, Set> evaluator) { + return new Function>() { + + private List> enclosingInstanceTypes; + + @Override + public Set apply(ResourceLocksProvider provider) { + if (this.enclosingInstanceTypes == null) { + this.enclosingInstanceTypes = enclosingInstanceTypesSupplier.get(); + } + return evaluator.apply(provider, this.enclosingInstanceTypes); + } + }; + } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java index b095a8d28e7c..60e7c36b808f 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java @@ -361,7 +361,8 @@ public Set provideForClass(Class testClass) { } @Override - public Set provideForMethod(Class testClass, Method testMethod) { + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { return Set.of(new Lock("b1")); } } @@ -369,7 +370,8 @@ public Set provideForMethod(Class testClass, Method testMethod) { static class MethodLevelProvider implements ResourceLocksProvider { @Override - public Set provideForMethod(Class testClass, Method testMethod) { + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { return Set.of(new Lock("b2")); } } @@ -377,7 +379,7 @@ public Set provideForMethod(Class testClass, Method testMethod) { static class NestedClassLevelProvider implements ResourceLocksProvider { @Override - public Set provideForNestedClass(Class testClass) { + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { return Set.of(new Lock("c1"), new Lock("c2", ResourceAccessMode.READ)); } } @@ -417,12 +419,13 @@ public Set provideForClass(Class testClass) { static class SecondClassLevelProvider implements ResourceLocksProvider { @Override - public Set provideForMethod(Class testClass, Method testMethod) { + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { return Set.of(new Lock("b2", ResourceAccessMode.READ)); } @Override - public Set provideForNestedClass(Class testClass) { + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { return Set.of(new Lock("c2")); } } @@ -430,7 +433,7 @@ public Set provideForNestedClass(Class testClass) { static class NestedClassLevelProvider implements ResourceLocksProvider { @Override - public Set provideForNestedClass(Class testClass) { + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { return Set.of(new Lock("c3")); } } @@ -452,7 +455,8 @@ void test() { static class Provider implements ResourceLocksProvider { @Override - public Set provideForMethod(Class testClass, Method testMethod) { + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { return Set.of(new Lock("a1")); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLocksProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLocksProviderTests.java index 8a73be42b8ee..f5bc7a17b77b 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLocksProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLocksProviderTests.java @@ -20,6 +20,7 @@ import static org.junit.platform.testkit.engine.EventConditions.test; import java.lang.reflect.Method; +import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -60,6 +61,12 @@ void methodLevelProviderInNestedClass() { assertThat(events.filter(event(test(), finishedSuccessfully())::matches)).hasSize(2); } + @Test + void providesAccessToRuntimeEnclosingInstances() { + var events = execute(SubClassLevelProviderTestCase.class); + assertThat(events.filter(event(test(), finishedSuccessfully())::matches)).hasSize(2); + } + private Stream execute(Class testCase) { return executeTestsForClass(testCase).allEvents().stream(); } @@ -108,28 +115,35 @@ static class Provider implements ResourceLocksProvider { private static boolean isProvideForNestedClassCalled = false; private static boolean isProvideForNestedTestMethodCalled = false; + private Class testClass; + @Override public Set provideForClass(Class testClass) { + this.testClass = testClass; isProvideForClassCalled = true; - assertEquals(ClassLevelProviderTestCase.class, testClass); + assertThat(testClass).isAssignableTo(ClassLevelProviderTestCase.class); return emptySet(); } @Override - public Set provideForNestedClass(Class testClass) { + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { isProvideForNestedClassCalled = true; + assertEquals(List.of(this.testClass), enclosingInstanceTypes); assertEquals(ClassLevelProviderTestCase.NestedClass.class, testClass); return emptySet(); } @Override - public Set provideForMethod(Class testClass, Method testMethod) { - if (testClass == ClassLevelProviderTestCase.class) { + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { + if (ClassLevelProviderTestCase.class.isAssignableFrom(testClass)) { + assertEquals(List.of(), enclosingInstanceTypes); assertEquals("test", testMethod.getName()); isProvideForTestMethodCalled = true; return emptySet(); } if (testClass == ClassLevelProviderTestCase.NestedClass.class) { + assertEquals(List.of(this.testClass), enclosingInstanceTypes); assertEquals("nestedTest", testMethod.getName()); isProvideForNestedTestMethodCalled = true; return emptySet(); @@ -140,6 +154,9 @@ public Set provideForMethod(Class testClass, Method testMethod) { } } + static class SubClassLevelProviderTestCase extends ClassLevelProviderTestCase { + } + @SuppressWarnings("JUnitMalformedDeclaration") static class NestedClassLevelProviderTestCase { @@ -177,15 +194,18 @@ public Set provideForClass(Class testClass) { } @Override - public Set provideForNestedClass(Class testClass) { + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { isProvideForNestedClassCalled = true; + assertEquals(List.of(NestedClassLevelProviderTestCase.class), enclosingInstanceTypes); assertEquals(NestedClass.class, testClass); return emptySet(); } @Override - public Set provideForMethod(Class testClass, Method testMethod) { + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { isProvideForMethodCalled = true; + assertEquals(List.of(NestedClassLevelProviderTestCase.class), enclosingInstanceTypes); assertEquals(NestedClassLevelProviderTestCase.NestedClass.class, testClass); assertEquals("nestedTest", testMethod.getName()); return emptySet(); @@ -226,14 +246,16 @@ public Set provideForClass(Class testClass) { } @Override - public Set provideForNestedClass(Class testClass) { + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { fail("'provideForNestedClass' should not be called"); return emptySet(); } @Override - public Set provideForMethod(Class testClass, Method testMethod) { + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { isProvideForMethodCalled = true; + assertEquals(List.of(), enclosingInstanceTypes); assertEquals(MethodLevelProviderTestCase.class, testClass); assertEquals("test", testMethod.getName()); return emptySet(); @@ -274,14 +296,16 @@ public Set provideForClass(Class testClass) { } @Override - public Set provideForNestedClass(Class testClass) { + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { fail("'provideForNestedClass' should not be called"); return emptySet(); } @Override - public Set provideForMethod(Class testClass, Method testMethod) { + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { isProvideForMethodCalled = true; + assertEquals(List.of(MethodLevelProviderInNestedClassTestCase.class), enclosingInstanceTypes); assertEquals(MethodLevelProviderInNestedClassTestCase.NestedClass.class, testClass); assertEquals("nestedTest", testMethod.getName()); return emptySet();