diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextRuntimeHints.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextRuntimeHints.java
new file mode 100644
index 00000000000..90dbc813f45
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextRuntimeHints.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.aot;
+
+import java.lang.annotation.Annotation;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.aot.hint.support.RuntimeHintsUtils;
+import org.springframework.util.ClassUtils;
+
+/**
+ * {@link RuntimeHintsRegistrar} implementation that makes types and annotations
+ * from the Spring TestContext Framework available at runtime.
+ *
+ * @author Sam Brannen
+ * @since 6.0
+ */
+public class TestContextRuntimeHints implements RuntimeHintsRegistrar {
+
+ @Override
+ public void registerHints(RuntimeHints runtimeHints, ClassLoader classLoader) {
+ ReflectionHints reflectionHints = runtimeHints.reflection();
+ boolean txPresent = ClassUtils.isPresent("org.springframework.transaction.annotation.Transactional", classLoader);
+ boolean servletPresent = ClassUtils.isPresent("jakarta.servlet.Servlet", classLoader);
+ boolean groovyPresent = ClassUtils.isPresent("groovy.lang.Closure", classLoader);
+
+ registerPublicConstructors(reflectionHints,
+ org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.class,
+ org.springframework.test.context.support.DefaultBootstrapContext.class,
+ org.springframework.test.context.support.DelegatingSmartContextLoader.class
+ );
+
+ registerDeclaredConstructors(reflectionHints,
+ org.springframework.test.context.support.DefaultTestContextBootstrapper.class
+ );
+
+ if (servletPresent) {
+ registerPublicConstructors(reflectionHints,
+ "org.springframework.test.context.web.WebDelegatingSmartContextLoader"
+ );
+ registerDeclaredConstructors(reflectionHints,
+ "org.springframework.test.context.web.WebTestContextBootstrapper"
+ );
+ }
+
+ if (groovyPresent) {
+ registerDeclaredConstructors(reflectionHints,
+ "org.springframework.test.context.support.GenericGroovyXmlContextLoader"
+ );
+ if (servletPresent) {
+ registerDeclaredConstructors(reflectionHints,
+ "org.springframework.test.context.web.GenericGroovyXmlWebContextLoader"
+ );
+ }
+ }
+
+ registerSynthesizedAnnotation(runtimeHints,
+ // Legacy and JUnit 4
+ org.springframework.test.annotation.Commit.class,
+ org.springframework.test.annotation.DirtiesContext.class,
+ org.springframework.test.annotation.IfProfileValue.class,
+ org.springframework.test.annotation.ProfileValueSourceConfiguration.class,
+ org.springframework.test.annotation.Repeat.class,
+ org.springframework.test.annotation.Rollback.class,
+ org.springframework.test.annotation.Timed.class,
+
+ // Core TestContext framework
+ org.springframework.test.context.ActiveProfiles.class,
+ org.springframework.test.context.BootstrapWith.class,
+ org.springframework.test.context.ContextConfiguration.class,
+ org.springframework.test.context.ContextHierarchy.class,
+ org.springframework.test.context.DynamicPropertySource.class,
+ org.springframework.test.context.NestedTestConfiguration.class,
+ org.springframework.test.context.TestConstructor.class,
+ org.springframework.test.context.TestExecutionListeners.class,
+ org.springframework.test.context.TestPropertySource.class,
+ org.springframework.test.context.TestPropertySources.class,
+
+ // Application Events
+ org.springframework.test.context.event.RecordApplicationEvents.class,
+
+ // JUnit Jupiter
+ org.springframework.test.context.junit.jupiter.EnabledIf.class,
+ org.springframework.test.context.junit.jupiter.DisabledIf.class,
+ org.springframework.test.context.junit.jupiter.SpringJUnitConfig.class,
+ org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig.class,
+
+ // Web
+ org.springframework.test.context.web.WebAppConfiguration.class
+ );
+
+ if (txPresent) {
+ registerSynthesizedAnnotation(runtimeHints,
+ org.springframework.test.context.jdbc.Sql.class,
+ org.springframework.test.context.jdbc.SqlConfig.class,
+ org.springframework.test.context.jdbc.SqlGroup.class,
+ org.springframework.test.context.jdbc.SqlMergeMode.class,
+ org.springframework.test.context.transaction.AfterTransaction.class,
+ org.springframework.test.context.transaction.BeforeTransaction.class
+ );
+ }
+ }
+
+ private static void registerPublicConstructors(ReflectionHints reflectionHints, Class>... types) {
+ reflectionHints.registerTypes(TypeReference.listOf(types),
+ builder -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
+ }
+
+ private static void registerPublicConstructors(ReflectionHints reflectionHints, String... classNames) {
+ reflectionHints.registerTypes(listOf(classNames),
+ builder -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
+ }
+
+ private static void registerDeclaredConstructors(ReflectionHints reflectionHints, Class>... types) {
+ reflectionHints.registerTypes(TypeReference.listOf(types),
+ builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
+ }
+
+ private static void registerDeclaredConstructors(ReflectionHints reflectionHints, String... classNames) {
+ reflectionHints.registerTypes(listOf(classNames),
+ builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
+ }
+
+ private static List listOf(String... classNames) {
+ return Arrays.stream(classNames).map(TypeReference::of).toList();
+ }
+
+ @SafeVarargs
+ @SuppressWarnings("unchecked")
+ private static void registerSynthesizedAnnotation(RuntimeHints runtimeHints, Class extends Annotation>... annotationTypes) {
+ for (Class extends Annotation> annotationType : annotationTypes) {
+ registerAnnotation(runtimeHints.reflection(), annotationType);
+ RuntimeHintsUtils.registerSynthesizedAnnotation(runtimeHints, annotationType);
+ }
+ }
+
+ private static void registerAnnotation(ReflectionHints reflectionHints, Class extends Annotation> annotationType) {
+ reflectionHints.registerType(annotationType,
+ builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_METHODS));
+ }
+
+}
diff --git a/spring-test/src/main/resources/META-INF/spring/aot.factories b/spring-test/src/main/resources/META-INF/spring/aot.factories
new file mode 100644
index 00000000000..97ad484d8b2
--- /dev/null
+++ b/spring-test/src/main/resources/META-INF/spring/aot.factories
@@ -0,0 +1,2 @@
+org.springframework.aot.hint.RuntimeHintsRegistrar=\
+org.springframework.test.context.aot.TestContextRuntimeHints
diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java
index 775921c7f4a..9f1bfbc1639 100644
--- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java
@@ -16,23 +16,27 @@
package org.springframework.test.context.aot;
+import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.DefaultGenerationContext;
import org.springframework.aot.generate.GeneratedFiles.Kind;
import org.springframework.aot.generate.InMemoryGeneratedFiles;
+import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.MemberCategory;
-import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.test.generator.compile.CompileWithTargetClassAccess;
import org.springframework.aot.test.generator.compile.TestCompiler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.annotation.SynthesizedAnnotation;
import org.springframework.javapoet.ClassName;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests;
@@ -52,6 +56,11 @@ import org.springframework.web.context.WebApplicationContext;
import static java.util.Comparator.comparing;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS;
+import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_METHODS;
+import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS;
+import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_METHODS;
+import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -83,10 +92,7 @@ class TestContextAotGeneratorTests extends AbstractAotTests {
generator.processAheadOfTime(testClasses.stream().sorted(comparing(Class::getName)));
- ReflectionHints reflectionHints = generator.getRuntimeHints().reflection();
- assertThat(reflectionHints.getTypeHint(TypeReference.of(AotTestMappings.GENERATED_MAPPINGS_CLASS_NAME)))
- .satisfies(typeHint ->
- assertThat(typeHint.getMemberCategories()).containsExactly(MemberCategory.INVOKE_PUBLIC_METHODS));
+ assertRuntimeHints(generator.getRuntimeHints());
List sourceFiles = generatedFiles.getGeneratedFiles(Kind.SOURCE).keySet().stream().toList();
assertThat(sourceFiles).containsExactlyInAnyOrder(expectedSourceFilesForBasicSpringTests);
@@ -105,6 +111,101 @@ class TestContextAotGeneratorTests extends AbstractAotTests {
}));
}
+ private static void assertRuntimeHints(RuntimeHints runtimeHints) {
+ assertReflectionRegistered(runtimeHints, AotTestMappings.GENERATED_MAPPINGS_CLASS_NAME, INVOKE_PUBLIC_METHODS);
+
+ Set.of(
+ org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.class,
+ org.springframework.test.context.support.DefaultBootstrapContext.class,
+ org.springframework.test.context.support.DelegatingSmartContextLoader.class,
+ org.springframework.test.context.web.WebDelegatingSmartContextLoader.class
+ ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_PUBLIC_CONSTRUCTORS));
+
+ Set.of(
+ org.springframework.test.context.support.DefaultTestContextBootstrapper.class,
+ org.springframework.test.context.web.WebTestContextBootstrapper.class,
+ org.springframework.test.context.support.GenericGroovyXmlContextLoader.class,
+ org.springframework.test.context.web.GenericGroovyXmlWebContextLoader.class
+ ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS));
+
+ Set.of(
+ // Legacy and JUnit 4
+ org.springframework.test.annotation.Commit.class,
+ org.springframework.test.annotation.DirtiesContext.class,
+ org.springframework.test.annotation.IfProfileValue.class,
+ org.springframework.test.annotation.ProfileValueSourceConfiguration.class,
+ org.springframework.test.annotation.Repeat.class,
+ org.springframework.test.annotation.Rollback.class,
+ org.springframework.test.annotation.Timed.class,
+
+ // Core TestContext framework
+ org.springframework.test.context.ActiveProfiles.class,
+ org.springframework.test.context.BootstrapWith.class,
+ org.springframework.test.context.ContextConfiguration.class,
+ org.springframework.test.context.ContextHierarchy.class,
+ org.springframework.test.context.DynamicPropertySource.class,
+ org.springframework.test.context.NestedTestConfiguration.class,
+ org.springframework.test.context.TestConstructor.class,
+ org.springframework.test.context.TestExecutionListeners.class,
+ org.springframework.test.context.TestPropertySource.class,
+ org.springframework.test.context.TestPropertySources.class,
+
+ // Application Events
+ org.springframework.test.context.event.RecordApplicationEvents.class,
+
+ // JUnit Jupiter
+ org.springframework.test.context.junit.jupiter.EnabledIf.class,
+ org.springframework.test.context.junit.jupiter.DisabledIf.class,
+ org.springframework.test.context.junit.jupiter.SpringJUnitConfig.class,
+ org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig.class,
+
+ // Web
+ org.springframework.test.context.web.WebAppConfiguration.class
+ ).forEach(type -> assertAnnotationRegistered(runtimeHints, type));
+
+ // TestExecutionListener
+ Set.of(
+ org.springframework.test.context.event.ApplicationEventsTestExecutionListener.class,
+ org.springframework.test.context.event.EventPublishingTestExecutionListener.class,
+ org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.class,
+ org.springframework.test.context.support.DependencyInjectionTestExecutionListener.class,
+ org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener.class,
+ org.springframework.test.context.support.DirtiesContextTestExecutionListener.class,
+ org.springframework.test.context.transaction.TransactionalTestExecutionListener.class,
+ org.springframework.test.context.web.ServletTestExecutionListener.class
+ ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS));
+
+ // ContextCustomizerFactory
+ Set.of(
+ "org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactory",
+ "org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory"
+ ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS));
+ }
+
+ private static void assertReflectionRegistered(RuntimeHints runtimeHints, String type, MemberCategory memberCategory) {
+ assertThat(reflection().onType(TypeReference.of(type)).withMemberCategory(memberCategory))
+ .as("Reflection hint for %s with category %s", type, memberCategory)
+ .accepts(runtimeHints);
+ }
+
+ private static void assertReflectionRegistered(RuntimeHints runtimeHints, Class> type, MemberCategory memberCategory) {
+ assertThat(reflection().onType(type).withMemberCategory(memberCategory))
+ .as("Reflection hint for %s with category %s", type.getSimpleName(), memberCategory)
+ .accepts(runtimeHints);
+ }
+
+ private static void assertAnnotationRegistered(RuntimeHints runtimeHints, Class extends Annotation> annotationType) {
+ assertReflectionRegistered(runtimeHints, annotationType, INVOKE_DECLARED_METHODS);
+ assertThat(runtimeHints.proxies().jdkProxies())
+ .as("Proxy hint for annotation @%s", annotationType.getSimpleName())
+ .anySatisfy(annotationProxy(annotationType));
+ }
+
+ private static Consumer annotationProxy(Class extends Annotation> type) {
+ return jdkProxyHint -> assertThat(jdkProxyHint.getProxiedInterfaces())
+ .containsExactly(TypeReference.of(type), TypeReference.of(SynthesizedAnnotation.class));
+ }
+
@Test
void processAheadOfTimeWithBasicTests() {
// We cannot parameterize with the test classes, since @CompileWithTargetClassAccess