Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
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.test.context.TestContextAnnotationUtils.AnnotationDescriptor;
import org.springframework.test.context.TestContextBootstrapper;
import org.springframework.util.Assert;

Expand Down Expand Up @@ -53,11 +52,13 @@ protected TestSliceTestContextBootstrapper() {

@Override
protected String @Nullable [] getProperties(Class<?> testClass) {
MergedAnnotation<T> annotation = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
.withEnclosingClasses(TestContextAnnotationUtils::searchEnclosingClass)
.from(testClass)
.get(this.annotationType);
return annotation.isPresent() ? annotation.getStringArray("properties") : null;
AnnotationDescriptor<T> descriptor = TestContextAnnotationUtils.findAnnotationDescriptor(testClass,
this.annotationType);
if (descriptor == null) {
return null;
}
MergedAnnotation<T> annotation = MergedAnnotation.from(descriptor.getAnnotation());
return annotation.getStringArray("properties");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
import org.springframework.context.annotation.ComponentScan.Filter;
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.test.context.TestContextAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;

/**
Expand Down Expand Up @@ -55,8 +55,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());
AnnotationDescriptor<A> descriptor = TestContextAnnotationUtils.findAnnotationDescriptor(testClass,
getAnnotationType());
Assert.state(descriptor != null, "No " + getAnnotationType().getSimpleName() + " found");
this.annotation = MergedAnnotation.from(descriptor.getAnnotation());
}

protected final MergedAnnotation<A> getAnnotation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.jspecify.annotations.Nullable;
Expand All @@ -44,16 +45,17 @@ class TypeExcludeFiltersContextCustomizer implements ContextCustomizer {

private final Set<TypeExcludeFilter> filters;

TypeExcludeFiltersContextCustomizer(Class<?> testClass, Set<Class<? extends TypeExcludeFilter>> filterClasses) {
this.filters = instantiateTypeExcludeFilters(testClass, filterClasses);
TypeExcludeFiltersContextCustomizer(Map<Class<?>, Set<Class<? extends TypeExcludeFilter>>> filtersByTestClass) {
this.filters = instantiateTypeExcludeFilters(filtersByTestClass);
}

private Set<TypeExcludeFilter> instantiateTypeExcludeFilters(Class<?> testClass,
Set<Class<? extends TypeExcludeFilter>> filterClasses) {
private Set<TypeExcludeFilter> instantiateTypeExcludeFilters(
Map<Class<?>, Set<Class<? extends TypeExcludeFilter>>> filtersByTestClass) {
Set<TypeExcludeFilter> filters = new LinkedHashSet<>();
for (Class<? extends TypeExcludeFilter> filterClass : filterClasses) {
filters.add(instantiateTypeExcludeFilter(testClass, filterClass));
}
filtersByTestClass.forEach((testClass,
filterClasses) -> filters.addAll(filterClasses.stream()
.map((filterClass) -> instantiateTypeExcludeFilter(testClass, filterClass))
.toList()));
return Collections.unmodifiableSet(filters);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@
package org.springframework.boot.test.context.filter.annotation;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
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.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
Expand All @@ -47,17 +52,24 @@ class TypeExcludeFiltersContextCustomizerFactory implements ContextCustomizerFac
}
AnnotationDescriptor<TypeExcludeFilters> descriptor = TestContextAnnotationUtils
.findAnnotationDescriptor(testClass, TypeExcludeFilters.class);
if (descriptor == null) {
Map<Class<?>, Set<Class<? extends TypeExcludeFilter>>> filtersByTestClass = new LinkedHashMap<>();
while (descriptor != null) {
filtersByTestClass.put(descriptor.getRootDeclaringClass(),
getTypeExcludeFilterClasses(descriptor.findAllLocalMergedAnnotations()));
descriptor = descriptor.next();
}
if (filtersByTestClass.isEmpty()) {
return null;
}
Class<?>[] filterClasses = descriptor.getAnnotation().value();
return createContextCustomizer(descriptor.getRootDeclaringClass(), filterClasses);
return new TypeExcludeFiltersContextCustomizer(filtersByTestClass);
}

@SuppressWarnings("unchecked")
private ContextCustomizer createContextCustomizer(Class<?> testClass, Class<?>[] filterClasses) {
return new TypeExcludeFiltersContextCustomizer(testClass,
new LinkedHashSet<>(Arrays.asList((Class<? extends TypeExcludeFilter>[]) filterClasses)));
private Set<Class<? extends TypeExcludeFilter>> getTypeExcludeFilterClasses(Set<TypeExcludeFilters> annotations) {
return annotations.stream()
.map(MergedAnnotation::from)
.map(MergedAnnotation::synthesize)
.flatMap((annotation) -> Arrays.stream(annotation.value()))
.collect(Collectors.toCollection(LinkedHashSet::new));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,11 +69,18 @@ void getContextCustomizerWhenHasAnnotationShouldReturnCustomizer() {

@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,
Expand All @@ -82,19 +95,54 @@ void hashCodeAndEquals() {

@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 {
Expand Down Expand Up @@ -152,4 +200,147 @@ static class TestClassAwareExclude extends SimpleExclude {

}

@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);
}

}

}
Loading