Ignore @NestedTestConfiguration on enclosing class for nested interface

Closes gh-25917
This commit is contained in:
Sam Brannen 2020-10-14 14:11:10 +02:00
parent 69af56c4e9
commit a271a0a2bf
3 changed files with 34 additions and 22 deletions

View File

@ -36,8 +36,11 @@ import org.springframework.lang.Nullable;
* <p>If {@code @NestedTestConfiguration} is not <em>present</em> or * <p>If {@code @NestedTestConfiguration} is not <em>present</em> or
* <em>meta-present</em> on a test class, in its super type hierarchy, or in its * <em>meta-present</em> on a test class, in its super type hierarchy, or in its
* enclosing class hierarchy, the default <em>enclosing configuration inheritance * enclosing class hierarchy, the default <em>enclosing configuration inheritance
* mode</em> will be used. See {@link #ENCLOSING_CONFIGURATION_PROPERTY_NAME} for * mode</em> will be used. A {@code @NestedTestConfiguration} declaration on an
* details on how to change the default mode. * enclosing class for a nested interface will be ignored when searching for the
* annotation on classes that implement the interface. See
* {@link #ENCLOSING_CONFIGURATION_PROPERTY_NAME} for details on how to change
* the default mode.
* *
* <p>When the {@link EnclosingConfiguration#INHERIT INHERIT} mode is in use, * <p>When the {@link EnclosingConfiguration#INHERIT INHERIT} mode is in use,
* configuration from an enclosing test class will be inherited by inner test * configuration from an enclosing test class will be inherited by inner test

View File

@ -19,6 +19,7 @@ package org.springframework.test.util;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import org.springframework.core.SpringProperties; import org.springframework.core.SpringProperties;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
@ -93,7 +94,15 @@ public abstract class MetaAnnotationUtils {
*/ */
@Nullable @Nullable
public static <T extends Annotation> T findMergedAnnotation(Class<?> clazz, Class<T> annotationType) { public static <T extends Annotation> T findMergedAnnotation(Class<?> clazz, Class<T> annotationType) {
AnnotationDescriptor<T> descriptor = findAnnotationDescriptor(clazz, annotationType); return findMergedAnnotation(clazz, annotationType, MetaAnnotationUtils::searchEnclosingClass);
}
@Nullable
private static <T extends Annotation> T findMergedAnnotation(Class<?> clazz, Class<T> annotationType,
Predicate<Class<?>> searchEnclosingClass) {
AnnotationDescriptor<T> descriptor =
findAnnotationDescriptor(clazz, annotationType, searchEnclosingClass, new HashSet<>());
return (descriptor != null ? descriptor.synthesizeAnnotation() : null); return (descriptor != null ? descriptor.synthesizeAnnotation() : null);
} }
@ -125,7 +134,8 @@ public abstract class MetaAnnotationUtils {
public static <T extends Annotation> AnnotationDescriptor<T> findAnnotationDescriptor( public static <T extends Annotation> AnnotationDescriptor<T> findAnnotationDescriptor(
Class<?> clazz, Class<T> annotationType) { Class<?> clazz, Class<T> annotationType) {
return findAnnotationDescriptor(clazz, annotationType, new HashSet<>()); return findAnnotationDescriptor(clazz, annotationType, MetaAnnotationUtils::searchEnclosingClass,
new HashSet<>());
} }
/** /**
@ -140,7 +150,8 @@ public abstract class MetaAnnotationUtils {
*/ */
@Nullable @Nullable
private static <T extends Annotation> AnnotationDescriptor<T> findAnnotationDescriptor( private static <T extends Annotation> AnnotationDescriptor<T> findAnnotationDescriptor(
@Nullable Class<?> clazz, Class<T> annotationType, Set<Annotation> visited) { @Nullable Class<?> clazz, Class<T> annotationType, Predicate<Class<?>> searchEnclosingClass,
Set<Annotation> visited) {
Assert.notNull(annotationType, "Annotation type must not be null"); Assert.notNull(annotationType, "Annotation type must not be null");
if (clazz == null || Object.class == clazz) { if (clazz == null || Object.class == clazz) {
@ -152,11 +163,13 @@ public abstract class MetaAnnotationUtils {
return new AnnotationDescriptor<>(clazz, clazz.getAnnotation(annotationType)); return new AnnotationDescriptor<>(clazz, clazz.getAnnotation(annotationType));
} }
AnnotationDescriptor<T> descriptor = null;
// Declared on a composed annotation (i.e., as a meta-annotation)? // Declared on a composed annotation (i.e., as a meta-annotation)?
for (Annotation composedAnn : clazz.getDeclaredAnnotations()) { for (Annotation composedAnn : clazz.getDeclaredAnnotations()) {
Class<? extends Annotation> composedType = composedAnn.annotationType(); Class<? extends Annotation> composedType = composedAnn.annotationType();
if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedType.getName()) && visited.add(composedAnn)) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedType.getName()) && visited.add(composedAnn)) {
AnnotationDescriptor<T> descriptor = findAnnotationDescriptor(composedType, annotationType, visited); descriptor = findAnnotationDescriptor(composedType, annotationType, searchEnclosingClass, visited);
if (descriptor != null) { if (descriptor != null) {
return new AnnotationDescriptor<>( return new AnnotationDescriptor<>(
clazz, descriptor.getDeclaringClass(), composedAnn, descriptor.getAnnotation()); clazz, descriptor.getDeclaringClass(), composedAnn, descriptor.getAnnotation());
@ -166,7 +179,7 @@ public abstract class MetaAnnotationUtils {
// Declared on an interface? // Declared on an interface?
for (Class<?> ifc : clazz.getInterfaces()) { for (Class<?> ifc : clazz.getInterfaces()) {
AnnotationDescriptor<T> descriptor = findAnnotationDescriptor(ifc, annotationType, visited); descriptor = findAnnotationDescriptor(ifc, annotationType, searchEnclosingClass, visited);
if (descriptor != null) { if (descriptor != null) {
return new AnnotationDescriptor<>(clazz, descriptor.getDeclaringClass(), return new AnnotationDescriptor<>(clazz, descriptor.getDeclaringClass(),
descriptor.getComposedAnnotation(), descriptor.getAnnotation()); descriptor.getComposedAnnotation(), descriptor.getAnnotation());
@ -174,15 +187,14 @@ public abstract class MetaAnnotationUtils {
} }
// Declared on a superclass? // Declared on a superclass?
AnnotationDescriptor<T> descriptor = descriptor = findAnnotationDescriptor(clazz.getSuperclass(), annotationType, searchEnclosingClass, visited);
findAnnotationDescriptor(clazz.getSuperclass(), annotationType, visited);
if (descriptor != null) { if (descriptor != null) {
return descriptor; return descriptor;
} }
// Declared on an enclosing class of an inner class? // Declared on an enclosing class of an inner class?
if (searchEnclosingClass(clazz)) { if (searchEnclosingClass.test(clazz)) {
descriptor = findAnnotationDescriptor(clazz.getEnclosingClass(), annotationType, visited); descriptor = findAnnotationDescriptor(clazz.getEnclosingClass(), annotationType, searchEnclosingClass, visited);
if (descriptor != null) { if (descriptor != null) {
return descriptor; return descriptor;
} }
@ -301,6 +313,7 @@ public abstract class MetaAnnotationUtils {
* class should be searched * class should be searched
* @since 5.3 * @since 5.3
* @see ClassUtils#isInnerClass(Class) * @see ClassUtils#isInnerClass(Class)
* @see NestedTestConfiguration @NestedTestConfiguration
*/ */
public static boolean searchEnclosingClass(Class<?> clazz) { public static boolean searchEnclosingClass(Class<?> clazz) {
return (ClassUtils.isInnerClass(clazz) && return (ClassUtils.isInnerClass(clazz) &&
@ -319,11 +332,13 @@ public abstract class MetaAnnotationUtils {
} }
private static EnclosingConfiguration lookUpEnclosingConfiguration(Class<?> clazz) { private static EnclosingConfiguration lookUpEnclosingConfiguration(Class<?> clazz) {
return MergedAnnotations.from(clazz, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES) // @NestedTestConfiguration should not be discovered on an enclosing class
.stream(NestedTestConfiguration.class) // for a nested interface (which is always static), so our predicate simply
.map(mergedAnnotation -> mergedAnnotation.getEnum("value", EnclosingConfiguration.class)) // ensures that the candidate class is an inner class.
.findFirst() Predicate<Class<?>> searchEnclosingClass = ClassUtils::isInnerClass;
.orElseGet(MetaAnnotationUtils::getDefaultEnclosingConfigurationMode); NestedTestConfiguration nestedTestConfiguration =
findMergedAnnotation(clazz, NestedTestConfiguration.class, searchEnclosingClass);
return (nestedTestConfiguration != null ? nestedTestConfiguration.value() : getDefaultEnclosingConfigurationMode());
} }
private static EnclosingConfiguration getDefaultEnclosingConfigurationMode() { private static EnclosingConfiguration getDefaultEnclosingConfigurationMode() {

View File

@ -161,12 +161,6 @@ class TestPropertySourceNestedTests {
} }
@Nested @Nested
// The following explicit INHERIT is necessary since this nested
// test class implements an interface whose enclosing class is
// annotated with @NestedTestConfiguration(OVERRIDE). In other
// words, the local declaration overrides the declaration
// "inherited" via the interface.
@NestedTestConfiguration(INHERIT)
class L5WithInheritedConfigAndTestInterfaceTests implements TestInterface { class L5WithInheritedConfigAndTestInterfaceTests implements TestInterface {
@Autowired @Autowired