Provide AOT support for @⁠Nested classes in a @⁠ParameterizedClass

This commit adds AOT support for discovering @⁠Nested test classes
within a @⁠ClassTemplate test class, which includes
@⁠ParameterizedClass test classes.

Closes gh-35744
This commit is contained in:
Sam Brannen
2025-11-01 17:33:38 +01:00
parent 096303c477
commit 1fc446b3fc
7 changed files with 329 additions and 31 deletions
@@ -17,10 +17,12 @@
package org.springframework.test.context.aot;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
@@ -82,8 +84,10 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
class TestClassScanner {
// JUnit Jupiter
private static final String EXTEND_WITH_ANNOTATION_NAME = "org.junit.jupiter.api.extension.ExtendWith";
private static final String SPRING_EXTENSION_NAME = "org.springframework.test.context.junit.jupiter.SpringExtension";
private static final String EXTEND_WITH_ANNOTATION_NAME = "org.junit.jupiter.api.extension.ExtendWith";
private static final String CLASS_TEMPLATE_ANNOTATION_NAME = "org.junit.jupiter.api.ClassTemplate";
private static final String NESTED_ANNOTATION_NAME = "org.junit.jupiter.api.Nested";
// JUnit 4
private static final String RUN_WITH_ANNOTATION_NAME = "org.junit.runner.RunWith";
@@ -161,6 +165,7 @@ class TestClassScanner {
.map(this::getJavaClass)
.flatMap(Optional::stream)
.filter(this::isSpringTestClass)
.flatMap(this::expandJupiterClassTemplateIfNecessary)
.distinct()
.sorted(Comparator.comparing(Class::getName));
}
@@ -184,6 +189,45 @@ class TestClassScanner {
return isSpringTestClass;
}
/**
* Expand the supplied test class into a stream containing the supplied test
* class. If the supplied class is a JUnit Jupiter {@code @ClassTemplate}
* (such as a {@code @ParameterizedClass}), the returned stream will also
* contain {@code @Nested} test classes associated with the supplied test
* class.
* @since 7.0
*/
private Stream<Class<?>> expandJupiterClassTemplateIfNecessary(Class<?> testClass) {
if (isJupiterClassTemplate(testClass)) {
Set<Class<?>> testClasses = new HashSet<>();
collectNestedTestClasses(testClass, testClasses, new HashSet<>());
testClasses.add(testClass);
return testClasses.stream();
}
return Stream.of(testClass);
}
/**
* Collect all {@code @Nested} test classes declared in the superclass hierarchy
* of the supplied test class as well as {@code @Nested} test classes declared
* in the supplied test class itself, recursively.
* @since 7.0
*/
private static void collectNestedTestClasses(Class<?> testClass, Set<Class<?>> testClasses, Set<Class<?>> visited) {
if (visited.add(testClass)) {
Class<?> superclass = testClass.getSuperclass();
if (superclass != null && superclass != Object.class) {
collectNestedTestClasses(superclass, testClasses, visited);
}
for (Class<?> nestedClass : testClass.getDeclaredClasses()) {
if (isJupiterNestedClass(nestedClass)) {
testClasses.add(nestedClass);
collectNestedTestClasses(nestedClass, testClasses, visited);
}
}
}
}
private static boolean isJupiterSpringTestClass(Class<?> clazz) {
return MergedAnnotations.search(TYPE_HIERARCHY)
.withEnclosingClasses(ClassUtils::isInnerClass)
@@ -195,6 +239,15 @@ class TestClassScanner {
.anyMatch(SPRING_EXTENSION_NAME::equals);
}
private static boolean isJupiterClassTemplate(Class<?> clazz) {
return MergedAnnotations.from(clazz, TYPE_HIERARCHY).isPresent(CLASS_TEMPLATE_ANNOTATION_NAME);
}
private static boolean isJupiterNestedClass(Class<?> clazz) {
return (!Modifier.isAbstract(clazz.getModifiers()) && ClassUtils.isInnerClass(clazz) &&
MergedAnnotations.from(clazz, TYPE_HIERARCHY).isPresent(NESTED_ANNOTATION_NAME));
}
private static boolean isJUnit4SpringTestClass(Class<?> clazz) {
MergedAnnotation<Annotation> mergedAnnotation =
MergedAnnotations.from(clazz, INHERITED_ANNOTATIONS).get(RUN_WITH_ANNOTATION_NAME);