Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ImportsContextCustomizer does not support AliasFor #34917

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 @@ -16,13 +16,12 @@

package org.springframework.boot.test.context;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
Expand All @@ -42,14 +41,19 @@
import org.springframework.context.annotation.ImportSelector;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationFilter;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.Order;
import org.springframework.core.style.ToStringCreator;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.ReflectionUtils;

import static org.springframework.core.annotation.AnnotationFilter.packages;

/**
* {@link ContextCustomizer} to allow {@code @Import} annotations to be used directly on
* test classes.
Expand Down Expand Up @@ -214,80 +218,43 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
*/
static class ContextCustomizerKey {

private static final Class<?>[] NO_IMPORTS = {};

private static final Set<AnnotationFilter> ANNOTATION_FILTERS;

static {
Set<AnnotationFilter> filters = new HashSet<>();
filters.add(new JavaLangAnnotationFilter());
filters.add(new KotlinAnnotationFilter());
filters.add(new SpockAnnotationFilter());
filters.add(new JUnitAnnotationFilter());
ANNOTATION_FILTERS = Collections.unmodifiableSet(filters);
}
private static final AnnotationFilter ANNOTATION_FILTERS = or(
packages("java.lang.annotation"),
packages("org.spockframework", "spock"),
or(isEqualTo("kotlin.Metadata"), packages("kotlin.annotation")),
packages(("org.junit")));

private final Set<Object> key;
private final Object key;

ContextCustomizerKey(Class<?> testClass) {
Set<Annotation> annotations = new HashSet<>();
Set<Class<?>> seen = new HashSet<>();
collectClassAnnotations(testClass, annotations, seen);
Set<Object> determinedImports = determineImports(annotations, testClass);
this.key = Collections.unmodifiableSet((determinedImports != null) ? determinedImports : annotations);
}

private void collectClassAnnotations(Class<?> classType, Set<Annotation> annotations, Set<Class<?>> seen) {
if (seen.add(classType)) {
collectElementAnnotations(classType, annotations, seen);
for (Class<?> interfaceType : classType.getInterfaces()) {
collectClassAnnotations(interfaceType, annotations, seen);
}
if (classType.getSuperclass() != null) {
collectClassAnnotations(classType.getSuperclass(), annotations, seen);
}
MergedAnnotations mergedAnnotations =
MergedAnnotations.search(MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
.withAnnotationFilter(ANNOTATION_FILTERS)
.from(testClass);
Set<Object> determinedImports = determineImports(mergedAnnotations, testClass);
if (determinedImports != null) {
this.key = determinedImports;
}
}

private void collectElementAnnotations(AnnotatedElement element, Set<Annotation> annotations,
Set<Class<?>> seen) {
for (Annotation annotation : element.getDeclaredAnnotations()) {
if (!isIgnoredAnnotation(annotation)) {
annotations.add(annotation);
collectClassAnnotations(annotation.annotationType(), annotations, seen);
}
}
}

private boolean isIgnoredAnnotation(Annotation annotation) {
for (AnnotationFilter annotationFilter : ANNOTATION_FILTERS) {
if (annotationFilter.isIgnored(annotation)) {
return true;
}
else {
this.key = AnnotatedElementUtils.findAllMergedAnnotations(testClass,
mergedAnnotations.stream().map(MergedAnnotation::getType).collect(Collectors.toSet()));
}
return false;
}

private Set<Object> determineImports(Set<Annotation> annotations, Class<?> testClass) {
Set<Object> determinedImports = new LinkedHashSet<>();
private Set<Object> determineImports(MergedAnnotations mergedAnnotations, Class<?> testClass) {
AnnotationMetadata testClassMetadata = AnnotationMetadata.introspect(testClass);
for (Annotation annotation : annotations) {
for (Class<?> source : getImports(annotation)) {
Set<Object> determinedSourceImports = determineImports(source, testClassMetadata);
if (determinedSourceImports == null) {
return mergedAnnotations.stream(Import.class)
.flatMap((ma) -> Stream.of(ma.getClassArray("value")))
.map((source) -> determineImports(source, testClassMetadata))
.reduce(new HashSet<>(), (a, b) -> {
if (a == null || b == null) {
return null;
}
determinedImports.addAll(determinedSourceImports);
}
}
return determinedImports;
}

private Class<?>[] getImports(Annotation annotation) {
if (annotation instanceof Import importAnnotation) {
return importAnnotation.value();
}
return NO_IMPORTS;
else {
a.add(b);
return a;
}
});
}

private Set<Object> determineImports(Class<?> source, AnnotationMetadata metadata) {
Expand Down Expand Up @@ -335,67 +302,13 @@ public String toString() {

}

/**
* Filter used to limit considered annotations.
*/
private interface AnnotationFilter {

boolean isIgnored(Annotation annotation);

static AnnotationFilter or(AnnotationFilter... filters) {
return typeName ->
Stream.of(filters)
.anyMatch(filter -> filter.matches(typeName));
}

/**
* {@link AnnotationFilter} for {@literal java.lang} annotations.
*/
private static final class JavaLangAnnotationFilter implements AnnotationFilter {

@Override
public boolean isIgnored(Annotation annotation) {
return AnnotationUtils.isInJavaLangAnnotationPackage(annotation);
}

static AnnotationFilter isEqualTo(String expectedTypeName) {
return typeName -> typeName.equals(expectedTypeName);
}

/**
* {@link AnnotationFilter} for Kotlin annotations.
*/
private static final class KotlinAnnotationFilter implements AnnotationFilter {

@Override
public boolean isIgnored(Annotation annotation) {
return "kotlin.Metadata".equals(annotation.annotationType().getName())
|| isInKotlinAnnotationPackage(annotation);
}

private boolean isInKotlinAnnotationPackage(Annotation annotation) {
return annotation.annotationType().getName().startsWith("kotlin.annotation.");
}

}

/**
* {@link AnnotationFilter} for Spock annotations.
*/
private static final class SpockAnnotationFilter implements AnnotationFilter {

@Override
public boolean isIgnored(Annotation annotation) {
return annotation.annotationType().getName().startsWith("org.spockframework.")
|| annotation.annotationType().getName().startsWith("spock.");
}

}

/**
* {@link AnnotationFilter} for JUnit annotations.
*/
private static final class JUnitAnnotationFilter implements AnnotationFilter {

@Override
public boolean isIgnored(Annotation annotation) {
return annotation.annotationType().getName().startsWith("org.junit.");
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.type.AnnotationMetadata;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -80,6 +81,30 @@ void customizersForTestClassesWithDifferentJUnitAnnotationsAreEqual() {
.isEqualTo(new ImportsContextCustomizer(SecondJUnitAnnotatedTestClass.class));
}

@Test
void customizersForClassesWithDifferentImportsAreNotEqual() {
assertThat(new ImportsContextCustomizer(FirstAnnotatedTestClass.class))
.isNotEqualTo(new ImportsContextCustomizer(SecondAnnotatedTestClass.class));
}

@Test
void customizersForClassesWithDifferentMetaImportsAreNotEqual() {
assertThat(new ImportsContextCustomizer(FirstMetaAnnotatedTestClass.class))
.isNotEqualTo(new ImportsContextCustomizer(SecondMetaAnnotatedTestClass.class));
}

@Test
void customizersForClassesWithDifferentAliasedImportsAreNotEqual() {
assertThat(new ImportsContextCustomizer(FirstAliasAnnotatedTestClass.class))
.isNotEqualTo(new ImportsContextCustomizer(SecondAliasAnnotatedTestClass.class));
}

@Test
void importsCanBeScatteredOnMultipleAnnotations() {
assertThat(new ImportsContextCustomizer(SingleImportAnnotationTestClass.class))
.isEqualTo(new ImportsContextCustomizer(MultipleImportAnnotationTestClass.class));
}

@Import(TestImportSelector.class)
@Indicator1
static class FirstImportSelectorAnnotatedClass {
Expand Down Expand Up @@ -152,6 +177,17 @@ static class SecondJUnitAnnotatedTestClass {

}

@Import({ FirstImportedClass.class, SecondImportedClass.class })
static class SingleImportAnnotationTestClass {

}

@FirstMetaImport
@Import(SecondImportedClass.class)
static class MultipleImportAnnotationTestClass {

}

@Retention(RetentionPolicy.RUNTIME)
@interface Indicator1 {

Expand All @@ -162,6 +198,65 @@ static class SecondJUnitAnnotatedTestClass {

}

@Retention(RetentionPolicy.RUNTIME)
@Import(AliasFor.class)
public @interface AliasedImport {

@AliasFor(annotation = Import.class)
Class<?>[] value();

}

@Retention(RetentionPolicy.RUNTIME)
@Import(FirstImportedClass.class)
public @interface FirstMetaImport {

}

@Retention(RetentionPolicy.RUNTIME)
@Import(SecondImportedClass.class)
public @interface SecondMetaImport {

}

static class FirstImportedClass {

}

static class SecondImportedClass {

}

@AliasedImport(FirstImportedClass.class)
static class FirstAliasAnnotatedTestClass {

}

@AliasedImport(SecondImportedClass.class)
static class SecondAliasAnnotatedTestClass {

}

@FirstMetaImport
static class FirstMetaAnnotatedTestClass {

}

@SecondMetaImport
static class SecondMetaAnnotatedTestClass {

}

@Import(FirstImportedClass.class)
static class FirstAnnotatedTestClass {

}

@Import(SecondImportedClass.class)
static class SecondAnnotatedTestClass {

}

static class TestImportSelector implements ImportSelector {

@Override
Expand Down