@ -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 (