mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Pick up all @TypeExcludeFilters annotations on test classes
Previously, TypeExcludeFiltersContextCustomizerFactory only discovered the first @TypeExcludeFilters annotation. This commit updates it to discover all @TypeExcludeFilters annotations instead of stopping at the first. Signed-off-by: Dmytro Nosan <dimanosan@gmail.com> See gh-51489
This commit is contained in:
committed by
Andy Wilkinson
parent
133572f6cb
commit
e78471699c
+5
-2
@@ -27,6 +27,7 @@ import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -55,8 +56,10 @@ public abstract class StandardAnnotationCustomizableTypeExcludeFilter<A extends
|
||||
private final MergedAnnotation<A> annotation;
|
||||
|
||||
protected StandardAnnotationCustomizableTypeExcludeFilter(Class<?> testClass) {
|
||||
this.annotation = MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS)
|
||||
.get(getAnnotationType());
|
||||
this.annotation = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
|
||||
.withEnclosingClasses(TestContextAnnotationUtils::searchEnclosingClass)
|
||||
.from(testClass)
|
||||
.get(this.getAnnotationType());
|
||||
}
|
||||
|
||||
protected final MergedAnnotation<A> getAnnotation() {
|
||||
|
||||
+14
-12
@@ -19,16 +19,20 @@ package org.springframework.boot.test.context.filter.annotation;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.aot.AotDetector;
|
||||
import org.springframework.boot.context.TypeExcludeFilter;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.test.context.ContextConfigurationAttributes;
|
||||
import org.springframework.test.context.ContextCustomizer;
|
||||
import org.springframework.test.context.ContextCustomizerFactory;
|
||||
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||
import org.springframework.test.context.TestContextAnnotationUtils.AnnotationDescriptor;
|
||||
|
||||
/**
|
||||
* {@link ContextCustomizerFactory} to support
|
||||
@@ -45,19 +49,17 @@ class TypeExcludeFiltersContextCustomizerFactory implements ContextCustomizerFac
|
||||
if (AotDetector.useGeneratedArtifacts()) {
|
||||
return null;
|
||||
}
|
||||
AnnotationDescriptor<TypeExcludeFilters> descriptor = TestContextAnnotationUtils
|
||||
.findAnnotationDescriptor(testClass, TypeExcludeFilters.class);
|
||||
if (descriptor == null) {
|
||||
Set<Class<? extends TypeExcludeFilter>> filterClasses = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
|
||||
.withEnclosingClasses(TestContextAnnotationUtils::searchEnclosingClass)
|
||||
.from(testClass)
|
||||
.stream(TypeExcludeFilters.class)
|
||||
.map(MergedAnnotation::synthesize)
|
||||
.flatMap((annotation) -> Arrays.stream(annotation.value()))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (filterClasses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Class<?>[] filterClasses = descriptor.getAnnotation().value();
|
||||
return createContextCustomizer(descriptor.getRootDeclaringClass(), filterClasses);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ContextCustomizer createContextCustomizer(Class<?> testClass, Class<?>[] filterClasses) {
|
||||
return new TypeExcludeFiltersContextCustomizer(testClass,
|
||||
new LinkedHashSet<>(Arrays.asList((Class<? extends TypeExcludeFilter>[]) filterClasses)));
|
||||
return new TypeExcludeFiltersContextCustomizer(testClass, filterClasses);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+204
-13
@@ -16,20 +16,26 @@
|
||||
|
||||
package org.springframework.boot.test.context.filter.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.context.TypeExcludeFilter;
|
||||
import org.springframework.boot.test.context.filter.annotation.TypeExcludeFiltersContextCustomizerFactoryTests.EnclosingClass.WithEnclosingClassExcludeFilters;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
|
||||
import org.springframework.test.context.ContextCustomizer;
|
||||
import org.springframework.test.context.MergedContextConfiguration;
|
||||
import org.springframework.test.context.NestedTestConfiguration;
|
||||
import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -63,11 +69,18 @@ class TypeExcludeFiltersContextCustomizerFactoryTests {
|
||||
|
||||
@Test
|
||||
void getContextCustomizerWhenEnclosingClassHasAnnotationShouldReturnCustomizer() {
|
||||
ContextCustomizer customizer = this.factory.createContextCustomizer(WithEnclosingClassExcludeFilters.class,
|
||||
Collections.emptyList());
|
||||
ContextCustomizer customizer = this.factory
|
||||
.createContextCustomizer(EnclosingClass.WithEnclosingClassExcludeFilters.class, Collections.emptyList());
|
||||
assertThat(customizer).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContextCustomizerWhenEnclosingClassHasAnnotationButNestedConfigurationIsOverrideShouldReturnNull() {
|
||||
ContextCustomizer customizer = this.factory
|
||||
.createContextCustomizer(EnclosingWithOverride.InnerWithOverride.class, Collections.emptyList());
|
||||
assertThat(customizer).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hashCodeAndEquals() {
|
||||
ContextCustomizer customizer1 = this.factory.createContextCustomizer(WithExcludeFilters.class,
|
||||
@@ -82,19 +95,54 @@ class TypeExcludeFiltersContextCustomizerFactoryTests {
|
||||
|
||||
@Test
|
||||
void getContextCustomizerShouldAddExcludeFilters() throws Exception {
|
||||
ContextCustomizer customizer = this.factory.createContextCustomizer(WithExcludeFilters.class,
|
||||
typeExcludeFiltersFor(WithExcludeFilters.class).doesNotMatch(NoAnnotation.class)
|
||||
.matches(SimpleExclude.class, TestClassAwareExclude.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContextCustomizerWhenEnclosingClassHasAnnotationShouldAddExcludeFilters() throws Exception {
|
||||
typeExcludeFiltersFor(EnclosingClass.WithEnclosingClassExcludeFilters.class).matches(SimpleExclude.class,
|
||||
TestClassAwareExclude.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContextCustomizerWhenHasDuplicateSliceExcludeFilterShouldInstantiateItOnce() {
|
||||
ContextCustomizer customizer = this.factory.createContextCustomizer(WithDuplicateSliceExclude.class,
|
||||
Collections.emptyList());
|
||||
assertThat(customizer).extracting("filters", InstanceOfAssertFactories.collection(TypeExcludeFilter.class))
|
||||
.hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContextCustomizerWhenEnclosingClassHasAnnotationsTypeExcludeFilters() throws Exception {
|
||||
typeExcludeFiltersFor(WithMultipleExcludeFilterAnnotations.WithEnclosingClassExcludeFilters.class)
|
||||
.matches(FirstSliceExclude.class, SecondSliceExclude.class);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContextCustomizerWhenSuperclassHasAnnotationShouldIncludeTypeExcludeFilters() throws Exception {
|
||||
typeExcludeFiltersFor(WithMixedInheritance.class).matches(TestClassAwareExclude.class, FirstSliceExclude.class,
|
||||
SecondSliceExclude.class, ThirdSliceExclude.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContextCustomizerWhenHasNestedComposedAnnotationShouldIncludeTypeExcludeFilters() throws Exception {
|
||||
typeExcludeFiltersFor(WithComposedAnnotation.class).matches(FirstSliceExclude.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContextCustomizerWhenDeeplyNestedShouldIncludeAllEnclosingExcludeFilters() throws Exception {
|
||||
typeExcludeFiltersFor(GrandparentEnclosing.ParentEnclosing.DeepInnerClass.class)
|
||||
.matches(FirstSliceExclude.class, SecondSliceExclude.class, ThirdSliceExclude.class);
|
||||
}
|
||||
|
||||
private TypeExcludeFilterAssert typeExcludeFiltersFor(Class<?> testClass) {
|
||||
ContextCustomizer customizer = this.factory.createContextCustomizer(testClass, Collections.emptyList());
|
||||
assertThat(customizer).isNotNull();
|
||||
customizer.customizeContext(this.context, this.mergedContextConfiguration);
|
||||
this.context.refresh();
|
||||
TypeExcludeFilter filter = this.context.getBean(TypeExcludeFilter.class);
|
||||
MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
|
||||
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NoAnnotation.class.getName());
|
||||
assertThat(filter.match(metadataReader, metadataReaderFactory)).isFalse();
|
||||
metadataReader = metadataReaderFactory.getMetadataReader(SimpleExclude.class.getName());
|
||||
assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue();
|
||||
metadataReader = metadataReaderFactory.getMetadataReader(TestClassAwareExclude.class.getName());
|
||||
assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue();
|
||||
return new TypeExcludeFilterAssert(this.context.getBean(TypeExcludeFilter.class));
|
||||
}
|
||||
|
||||
static class NoAnnotation {
|
||||
@@ -152,4 +200,147 @@ class TypeExcludeFiltersContextCustomizerFactoryTests {
|
||||
|
||||
}
|
||||
|
||||
@FirstTestSlice
|
||||
@TypeExcludeFilters(SecondSliceExclude.class)
|
||||
static class WithMultipleExcludeFilterAnnotations {
|
||||
|
||||
class WithEnclosingClassExcludeFilters {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TypeExcludeFilters(TestClassAwareExclude.class)
|
||||
static class WithMixedInheritance extends WithFirstTestSliceExclude {
|
||||
|
||||
}
|
||||
|
||||
@FirstTestSlice
|
||||
static class WithFirstTestSliceExclude implements WithSecondTestSliceExclude {
|
||||
|
||||
}
|
||||
|
||||
@SecondTestSlice
|
||||
interface WithSecondTestSliceExclude extends WithThirdTestSliceExclude {
|
||||
|
||||
}
|
||||
|
||||
@TypeExcludeFilters(ThirdSliceExclude.class)
|
||||
interface WithThirdTestSliceExclude {
|
||||
|
||||
}
|
||||
|
||||
@ComposedFirstTestSlice
|
||||
static class WithComposedAnnotation {
|
||||
|
||||
}
|
||||
|
||||
@FirstTestSlice
|
||||
@TypeExcludeFilters(FirstSliceExclude.class)
|
||||
static class WithDuplicateSliceExclude {
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@TypeExcludeFilters(FirstSliceExclude.class)
|
||||
@interface FirstTestSlice {
|
||||
|
||||
}
|
||||
|
||||
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@TypeExcludeFilters(SecondSliceExclude.class)
|
||||
@interface SecondTestSlice {
|
||||
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@FirstTestSlice
|
||||
@interface ComposedFirstTestSlice {
|
||||
|
||||
}
|
||||
|
||||
@TypeExcludeFilters(FirstSliceExclude.class)
|
||||
static class EnclosingWithOverride {
|
||||
|
||||
@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)
|
||||
class InnerWithOverride {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TypeExcludeFilters(FirstSliceExclude.class)
|
||||
static class GrandparentEnclosing {
|
||||
|
||||
@TypeExcludeFilters(SecondSliceExclude.class)
|
||||
class ParentEnclosing {
|
||||
|
||||
@TypeExcludeFilters(ThirdSliceExclude.class)
|
||||
class DeepInnerClass {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class FirstSliceExclude extends TestClassAwareExclude {
|
||||
|
||||
FirstSliceExclude(Class<?> testClass) {
|
||||
super(testClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class SecondSliceExclude extends TestClassAwareExclude {
|
||||
|
||||
SecondSliceExclude(Class<?> testClass) {
|
||||
super(testClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class ThirdSliceExclude extends TestClassAwareExclude {
|
||||
|
||||
ThirdSliceExclude(Class<?> testClass) {
|
||||
super(testClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class TypeExcludeFilterAssert {
|
||||
|
||||
private final TypeExcludeFilter filter;
|
||||
|
||||
private final MetadataReaderFactory metadataReaderFactory = MetadataReaderFactory
|
||||
.create(new DefaultResourceLoader());
|
||||
|
||||
private TypeExcludeFilterAssert(TypeExcludeFilter filter) {
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
TypeExcludeFilterAssert matches(Class<?>... types) throws Exception {
|
||||
for (Class<?> type : types) {
|
||||
assertThat(matches(type)).as("Filter should match %s", type.getName()).isTrue();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
TypeExcludeFilterAssert doesNotMatch(Class<?>... types) throws Exception {
|
||||
for (Class<?> type : types) {
|
||||
assertThat(matches(type)).as("Filter should not match %s", type.getName()).isFalse();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean matches(Class<?> type) throws Exception {
|
||||
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(type.getName());
|
||||
return this.filter.match(metadataReader, this.metadataReaderFactory);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user