From 745f989e18d5201ca7fdb49611c8d30b8f147698 Mon Sep 17 00:00:00 2001 From: Vladimir Dmitrienko Date: Wed, 31 Jul 2024 18:09:21 +0200 Subject: [PATCH] Introduce adding 'resource locks' programmatically. Issue: #2677 --- .../api/parallel/ResourceLocksFrom.java | 31 +++++++ .../api/parallel/ResourceLocksProvider.java | 85 +++++++++++++++++++ .../descriptor/ClassBasedTestDescriptor.java | 13 ++- .../descriptor/JupiterTestDescriptor.java | 26 +++++- .../descriptor/MethodBasedTestDescriptor.java | 12 ++- .../descriptor/NestedClassTestDescriptor.java | 16 ++++ .../TestTemplateInvocationTestDescriptor.java | 2 +- 7 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksFrom.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksProvider.java diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksFrom.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksFrom.java new file mode 100644 index 000000000000..c12fca116f87 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksFrom.java @@ -0,0 +1,31 @@ +/* + * 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.STABLE; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +@API(status = STABLE, since = "5.10") +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface ResourceLocksFrom { + + Class[] value(); + +} 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 new file mode 100644 index 000000000000..7a9ced97595f --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLocksProvider.java @@ -0,0 +1,85 @@ +/* + * 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 java.util.Collections.emptySet; +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.reflect.Method; +import java.util.Objects; +import java.util.Set; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.ToStringBuilder; + +@API(status = STABLE, since = "5.10") +public interface ResourceLocksProvider { + default Set provideForClass(Class testClass) { + return emptySet(); + } + + default Set provideForNestedClass(Class testClass) { + return emptySet(); + } + + default Set provideForMethod(Class testClass, Method testMethod) { + return emptySet(); + } + + final class Lock { + + private final String key; + + private final ResourceAccessMode accessMode; + + public Lock(String key) { + this(key, ResourceAccessMode.READ_WRITE); + } + + public Lock(String key, ResourceAccessMode accessMode) { + this.key = Preconditions.notBlank(key, "key must not be blank"); + this.accessMode = Preconditions.notNull(accessMode, "accessMode must not be null"); + } + + public String getKey() { + return key; + } + + public ResourceAccessMode getAccessMode() { + return accessMode; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Lock lock = (Lock) o; + return Objects.equals(key, lock.key) && accessMode == lock.accessMode; + } + + @Override + public int hashCode() { + return Objects.hash(key, accessMode); + } + + @Override + public String toString() { + return new ToStringBuilder(this) // + .append("key", key) // + .append("accessMode", accessMode) // + .toString(); + } + } + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index baa884c18e7e..d9971829c04e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -11,6 +11,7 @@ package org.junit.jupiter.engine.descriptor; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromConstructorParameters; @@ -34,6 +35,7 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; import org.apiguardian.api.API; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -66,6 +68,7 @@ import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.jupiter.engine.extension.MutableExtensionRegistry; import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.util.CollectionUtils; import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.commons.util.StringUtils; @@ -142,7 +145,15 @@ public void setDefaultChildExecutionMode(ExecutionMode defaultChildExecutionMode @Override public Set getExclusiveResources() { - return getExclusiveResourcesFromAnnotation(getTestClass()); + // @formatter:off + return Stream.concat( + getExclusiveResourcesFromAnnotation(getTestClass()), + getExclusiveResourcesFromProvider( + getTestClass(), + provider -> provider.provideForClass(getTestClass()) + ) + ).collect(toSet()); + // @formatter:on } @Override 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 4d975ce4b684..c719a365b377 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 @@ -12,19 +12,21 @@ import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toCollection; -import static java.util.stream.Collectors.toSet; import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.descriptor.DisplayNameUtils.determineDisplayName; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations; import java.lang.reflect.AnnotatedElement; +import java.util.Collection; import java.util.Collections; 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 java.util.stream.Stream; import org.apiguardian.api.API; import org.junit.jupiter.api.Tag; @@ -33,6 +35,8 @@ 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.ResourceLocksFrom; +import org.junit.jupiter.api.parallel.ResourceLocksProvider; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.ConditionEvaluator; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; @@ -41,6 +45,7 @@ import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.ExceptionUtils; +import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.commons.util.UnrecoverableExceptions; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestSource; @@ -180,11 +185,24 @@ public static ExecutionMode toExecutionMode(org.junit.jupiter.api.parallel.Execu throw new JUnitException("Unknown ExecutionMode: " + mode); } - Set getExclusiveResourcesFromAnnotation(AnnotatedElement element) { + Stream getExclusiveResourcesFromAnnotation(AnnotatedElement element) { // @formatter:off return findRepeatableAnnotations(element, ResourceLock.class).stream() - .map(resource -> new ExclusiveResource(resource.value(), toLockMode(resource.mode()))) - .collect(toSet()); + .map(resource -> new ExclusiveResource(resource.value(), toLockMode(resource.mode()))); + // @formatter:on + } + + @SuppressWarnings("Convert2MethodRef") + Stream getExclusiveResourcesFromProvider(Class testClass, + Function> providerToLocks) { + // @formatter:off + return findAnnotation(testClass, ResourceLocksFrom.class) + .map(annotation -> Stream.of(annotation.value())) + .orElseGet(Stream::empty) + .map(providerClass -> ReflectionUtils.newInstance(providerClass)) + .map(providerToLocks) + .flatMap(Collection::stream) + .map(lock -> new ExclusiveResource(lock.getKey(), toLockMode(lock.getAccessMode()))); // @formatter:on } 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 8c851955c5b2..85e3dc0f7c8c 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 @@ -10,6 +10,7 @@ package org.junit.jupiter.engine.descriptor; +import static java.util.stream.Collectors.toSet; import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.descriptor.DisplayNameUtils.determineDisplayNameForMethod; import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder; @@ -20,6 +21,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import java.util.stream.Stream; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ExtensionContext; @@ -81,7 +83,15 @@ public final Set getTags() { @Override public Set getExclusiveResources() { - return getExclusiveResourcesFromAnnotation(getTestMethod()); + // @formatter:off + return Stream.concat( + getExclusiveResourcesFromAnnotation(getTestMethod()), + getExclusiveResourcesFromProvider( + getTestClass(), + provider -> provider.provideForMethod(getTestClass(), getTestMethod()) + ) + ).collect(toSet()); + // @formatter:on } @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 84ce0645b7d3..9878793d7fb8 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 @@ -11,6 +11,7 @@ package org.junit.jupiter.engine.descriptor; import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toSet; import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.descriptor.DisplayNameUtils.createDisplayNameSupplierForNestedClass; @@ -19,6 +20,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ExtensionContext; @@ -30,6 +32,7 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource; import org.junit.platform.engine.support.hierarchical.ThrowableCollector; /** @@ -87,4 +90,17 @@ protected TestInstances instantiateTestClass(JupiterEngineExecutionContext paren return instantiateTestClass(Optional.of(outerInstances), registry, extensionContext); } + @Override + public Set getExclusiveResources() { + // @formatter:off + return Stream.concat( + getExclusiveResourcesFromAnnotation(getTestClass()), + getExclusiveResourcesFromProvider( + getTestClass(), + provider -> provider.provideForNestedClass(getTestClass()) + ) + ).collect(toSet()); + // @formatter:on + } + } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java index 32d627dff049..acc31605ebcb 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java @@ -53,7 +53,7 @@ public class TestTemplateInvocationTestDescriptor extends TestMethodTestDescript @Override public Set getExclusiveResources() { - // @ResourceLock annotations are already collected and returned by the enclosing container + // Resources are already collected and returned by the enclosing container return emptySet(); }