Browse Source

Allow local @⁠BootstrapWith annotation to override a meta-annotation

This commit revises the resolveExplicitTestContextBootstrapper()
algorithm in BootstrapUtils to allow a local @⁠BootstrapWith annotation
to override a meta-annotation within the same composed annotation.

Closes gh-35938
pull/36467/head
Sam Brannen 2 weeks ago
parent
commit
293a9c0ee3
  1. 48
      spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java
  2. 8
      spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java

48
spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java

@ -19,15 +19,21 @@ package org.springframework.test.context; @@ -19,15 +19,21 @@ package org.springframework.test.context;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.log.LogMessage;
import org.springframework.test.context.TestContextAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.ClassUtils;
/**
@ -169,25 +175,33 @@ public abstract class BootstrapUtils { @@ -169,25 +175,33 @@ public abstract class BootstrapUtils {
}
private static @Nullable Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {
Set<BootstrapWith> annotations = new LinkedHashSet<>();
AnnotationDescriptor<BootstrapWith> descriptor =
TestContextAnnotationUtils.findAnnotationDescriptor(testClass, BootstrapWith.class);
while (descriptor != null) {
annotations.addAll(descriptor.findAllLocalMergedAnnotations());
descriptor = descriptor.next();
}
if (annotations.isEmpty()) {
Map<Integer, Set<BootstrapWith>> distanceToAnnotationsMap = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
.withEnclosingClasses(TestContextAnnotationUtils::searchEnclosingClass)
.from(testClass)
.stream(BootstrapWith.class)
// The following effectively filters out annotations in the type and
// enclosing class hierarchies once annotations have already been found
// on a particular class or interface.
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
// Grouping by "meta-distance" enables us to allow a directly-present
// annotation to override annotations that are meta-present.
// Collecting synthesized annotations for each meta-distance enables
// us to filter out duplicates.
.collect(Collectors.groupingBy(MergedAnnotation::getDistance, TreeMap::new,
Collectors.mapping(MergedAnnotation::synthesize, Collectors.toCollection(LinkedHashSet::new))));
if (distanceToAnnotationsMap.isEmpty()) {
return null;
}
if (annotations.size() == 1) {
return annotations.iterator().next().value();
}
// Allow directly-present annotation to override annotations that are meta-present.
BootstrapWith bootstrapWith = testClass.getDeclaredAnnotation(BootstrapWith.class);
if (bootstrapWith != null) {
return bootstrapWith.value();
Set<BootstrapWith> annotations = new LinkedHashSet<>();
for (Set<BootstrapWith> currentAnnotations: distanceToAnnotationsMap.values()) {
// If we have found a single, non-competing @BootstrapWith annotation, return it.
if (annotations.isEmpty() && currentAnnotations.size() == 1) {
return currentAnnotations.iterator().next().value();
}
// Otherwise, track all discovered annotations for error reporting.
annotations.addAll(currentAnnotations);
}
throw new IllegalStateException(String.format(

8
spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java

@ -146,13 +146,7 @@ class BootstrapUtilsTests { @@ -146,13 +146,7 @@ class BootstrapUtilsTests {
*/
@Test // gh-35938
void resolveTestContextBootstrapperWithMetaBootstrapWithAnnotationThatOverridesMetaMetaBootstrapWithAnnotation() {
BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext(
MetaAndMetaMetaBootstrapWithAnnotationsClass.class, delegate);
assertThatIllegalStateException()
.isThrownBy(() -> resolveTestContextBootstrapper(bootstrapContext))
.withMessageContaining("Configuration error: found multiple declarations of @BootstrapWith")
.withMessageContaining(FooBootstrapper.class.getSimpleName())
.withMessageContaining(BarBootstrapper.class.getSimpleName());
assertBootstrapper(MetaAndMetaMetaBootstrapWithAnnotationsClass.class, BarBootstrapper.class);
}
/**

Loading…
Cancel
Save