diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java index f3a5d9c377d..2d6ace8f5d8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java @@ -29,7 +29,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotationCollectors; -import org.springframework.core.annotation.MergedAnnotationPredicates; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.RepeatableContainers; @@ -571,14 +570,21 @@ public abstract class TestContextAnnotationUtils { * that are present or meta-present on the {@linkplain #getRootDeclaringClass() * root declaring class} of this descriptor or on any interfaces that the * root declaring class implements. + *

Annotations are returned in the order they are discovered: annotations + * on the root declaring class appear first, followed by annotations on + * implemented interfaces in the order they are declared in the + * {@code implements} clause. * @return the set of all merged, synthesized {@code Annotations} found, * or an empty set if none were found */ public Set findAllLocalMergedAnnotations() { - SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY; - return MergedAnnotations.from(getRootDeclaringClass(), searchStrategy, RepeatableContainers.none()) + Class rootDeclaringClass = getRootDeclaringClass(); + Set> localSources = new HashSet<>(); + localSources.add(rootDeclaringClass); + Collections.addAll(localSources, rootDeclaringClass.getInterfaces()); + return MergedAnnotations.from(rootDeclaringClass, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none()) .stream(getAnnotationType()) - .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .filter(mergedAnnotation -> localSources.contains(mergedAnnotation.getSource())) .collect(MergedAnnotationCollectors.toAnnotationSet()); } diff --git a/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java index 258e591577d..d9f9a2bb067 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java @@ -274,6 +274,73 @@ class TestContextAnnotationUtilsTests { } + @Nested + @DisplayName("findAllLocalMergedAnnotations() tests") + class FindAllLocalMergedAnnotationsTests { + + @Test + void annotationOnClassOnly() { + var descriptor = findAnnotationDescriptor(AnnotatedClass.class, ContextConfiguration.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.findAllLocalMergedAnnotations()) + .singleElement() + .satisfies(config -> assertThat(config.classes()).containsExactly(Config1.class)); + } + + @Test + void annotationOnInterfaceOnly() { + var descriptor = findAnnotationDescriptor(ClassImplementingAnnotatedInterface.class, ContextConfiguration.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.findAllLocalMergedAnnotations()) + .singleElement() + .satisfies(config -> assertThat(config.classes()).containsExactly(Config2.class)); + } + + @Test + void annotationOnClassAndInterface() { + var descriptor = findAnnotationDescriptor(AnnotatedClassImplementingAnnotatedInterface.class, ContextConfiguration.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.findAllLocalMergedAnnotations()) + .satisfiesExactly( + config1 -> assertThat(config1.classes()).containsExactly(Config1.class), + config2 -> assertThat(config2.classes()).containsExactly(Config2.class) + ); + } + + @Test + void annotationOnTwoInterfaces() { + var descriptor = findAnnotationDescriptor(ClassImplementingTwoAnnotatedInterfaces.class, ContextConfiguration.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.findAllLocalMergedAnnotations()) + .satisfiesExactly( + config1 -> assertThat(config1.classes()).containsExactly(Config2.class), + config2 -> assertThat(config2.classes()).containsExactly(Config3.class) + ); + } + + @Test + void annotationOnSuperclassIsExcluded() { + var descriptor = findAnnotationDescriptor(SubAnnotatedClass.class, ContextConfiguration.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getRootDeclaringClass()).isEqualTo(AnnotatedClass.class); + assertThat(descriptor.findAllLocalMergedAnnotations()) + .singleElement() + .satisfies(config -> assertThat(config.classes()).containsExactly(Config1.class)); + } + + @Test + void metaAnnotationOnClassAndAnnotationOnInterface() { + var descriptor = findAnnotationDescriptor(MetaAnnotatedClassImplementingAnnotatedInterface.class, ContextConfiguration.class); + assertThat(descriptor).isNotNull(); + assertThat(descriptor.findAllLocalMergedAnnotations()) + .satisfiesExactly( + config1 -> assertThat(config1.classes()).containsExactly(DevConfig.class, ProductionConfig.class), + config2 -> assertThat(config2.classes()).containsExactly(Config2.class) + ); + } + + } + @Nested @DisplayName("findAnnotationDescriptorForTypes() tests") class FindAnnotationDescriptorForTypesTests { @@ -377,7 +444,7 @@ class TestContextAnnotationUtilsTests { assertThat(descriptor.getAnnotationType()).isEqualTo(annotationType); assertThat(((ContextConfiguration) descriptor.getAnnotation()).value()).isEmpty(); assertThat(((ContextConfiguration) descriptor.getAnnotation()).classes()) - .containsExactly(MetaConfig.DevConfig.class, MetaConfig.ProductionConfig.class); + .containsExactly(DevConfig.class, ProductionConfig.class); } @Test @@ -559,12 +626,12 @@ class TestContextAnnotationUtilsTests { @AliasFor(annotation = ContextConfiguration.class) Class[] classes() default { DevConfig.class, ProductionConfig.class }; - class DevConfig { - } + } - class ProductionConfig { - } + static class DevConfig { + } + static class ProductionConfig { } // ------------------------------------------------------------------------- @@ -668,4 +735,45 @@ class TestContextAnnotationUtilsTests { } } + // ------------------------------------------------------------------------- + // Fixtures for FindAllLocalMergedAnnotationsTests + + static class Config1 { + } + + static class Config2 { + } + + static class Config3 { + } + + @ContextConfiguration(classes = Config2.class) + interface AnnotatedInterface { + } + + @ContextConfiguration(classes = Config3.class) + interface AnotherAnnotatedInterface { + } + + @ContextConfiguration(classes = Config1.class) + static class AnnotatedClass { + } + + static class SubAnnotatedClass extends AnnotatedClass { + } + + static class ClassImplementingAnnotatedInterface implements AnnotatedInterface { + } + + @ContextConfiguration(classes = Config1.class) + static class AnnotatedClassImplementingAnnotatedInterface implements AnnotatedInterface { + } + + static class ClassImplementingTwoAnnotatedInterfaces implements AnnotatedInterface, AnotherAnnotatedInterface { + } + + @MetaConfig + static class MetaAnnotatedClassImplementingAnnotatedInterface implements AnnotatedInterface { + } + }