Skip to content

Commit 741ee96

Browse files
committed
Register runtime hints for TestContext framework classes and annotations
This commit introduces TestContextRuntimeHints which is a RuntimeHintsRegistrar implementation that makes core types and annotations from the Spring TestContext Framework available at runtime within a GraalVM native image. TestContextRuntimeHints is registered automatically via the "META-INF/spring/aot.factories" file in spring-test. This commit also modifies TestContextAotGeneratorTests to assert the expected runtime hints registered by TestContextRuntimeHints as well as runtime hints for TestExecutionListener and ContextCustomizerFactory implementations registered by SpringFactoriesLoaderRuntimeHints. Closes gh-29028 Closes gh-29044
1 parent 8a7e839 commit 741ee96

File tree

3 files changed

+270
-5
lines changed

3 files changed

+270
-5
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.aot;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
import org.springframework.aot.hint.MemberCategory;
24+
import org.springframework.aot.hint.ReflectionHints;
25+
import org.springframework.aot.hint.RuntimeHints;
26+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
27+
import org.springframework.aot.hint.TypeReference;
28+
import org.springframework.aot.hint.support.RuntimeHintsUtils;
29+
import org.springframework.util.ClassUtils;
30+
31+
/**
32+
* {@link RuntimeHintsRegistrar} implementation that makes types and annotations
33+
* from the <em>Spring TestContext Framework</em> available at runtime.
34+
*
35+
* @author Sam Brannen
36+
* @since 6.0
37+
*/
38+
public class TestContextRuntimeHints implements RuntimeHintsRegistrar {
39+
40+
@Override
41+
public void registerHints(RuntimeHints runtimeHints, ClassLoader classLoader) {
42+
ReflectionHints reflectionHints = runtimeHints.reflection();
43+
boolean txPresent = ClassUtils.isPresent("org.springframework.transaction.annotation.Transactional", classLoader);
44+
boolean servletPresent = ClassUtils.isPresent("jakarta.servlet.Servlet", classLoader);
45+
boolean groovyPresent = ClassUtils.isPresent("groovy.lang.Closure", classLoader);
46+
47+
registerPublicConstructors(reflectionHints,
48+
org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.class,
49+
org.springframework.test.context.support.DefaultBootstrapContext.class,
50+
org.springframework.test.context.support.DelegatingSmartContextLoader.class
51+
);
52+
53+
registerDeclaredConstructors(reflectionHints,
54+
org.springframework.test.context.support.DefaultTestContextBootstrapper.class
55+
);
56+
57+
if (servletPresent) {
58+
registerPublicConstructors(reflectionHints,
59+
"org.springframework.test.context.web.WebDelegatingSmartContextLoader"
60+
);
61+
registerDeclaredConstructors(reflectionHints,
62+
"org.springframework.test.context.web.WebTestContextBootstrapper"
63+
);
64+
}
65+
66+
if (groovyPresent) {
67+
registerDeclaredConstructors(reflectionHints,
68+
"org.springframework.test.context.support.GenericGroovyXmlContextLoader"
69+
);
70+
if (servletPresent) {
71+
registerDeclaredConstructors(reflectionHints,
72+
"org.springframework.test.context.web.GenericGroovyXmlWebContextLoader"
73+
);
74+
}
75+
}
76+
77+
registerSynthesizedAnnotation(runtimeHints,
78+
// Legacy and JUnit 4
79+
org.springframework.test.annotation.Commit.class,
80+
org.springframework.test.annotation.DirtiesContext.class,
81+
org.springframework.test.annotation.IfProfileValue.class,
82+
org.springframework.test.annotation.ProfileValueSourceConfiguration.class,
83+
org.springframework.test.annotation.Repeat.class,
84+
org.springframework.test.annotation.Rollback.class,
85+
org.springframework.test.annotation.Timed.class,
86+
87+
// Core TestContext framework
88+
org.springframework.test.context.ActiveProfiles.class,
89+
org.springframework.test.context.BootstrapWith.class,
90+
org.springframework.test.context.ContextConfiguration.class,
91+
org.springframework.test.context.ContextHierarchy.class,
92+
org.springframework.test.context.DynamicPropertySource.class,
93+
org.springframework.test.context.NestedTestConfiguration.class,
94+
org.springframework.test.context.TestConstructor.class,
95+
org.springframework.test.context.TestExecutionListeners.class,
96+
org.springframework.test.context.TestPropertySource.class,
97+
org.springframework.test.context.TestPropertySources.class,
98+
99+
// Application Events
100+
org.springframework.test.context.event.RecordApplicationEvents.class,
101+
102+
// JUnit Jupiter
103+
org.springframework.test.context.junit.jupiter.EnabledIf.class,
104+
org.springframework.test.context.junit.jupiter.DisabledIf.class,
105+
org.springframework.test.context.junit.jupiter.SpringJUnitConfig.class,
106+
org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig.class,
107+
108+
// Web
109+
org.springframework.test.context.web.WebAppConfiguration.class
110+
);
111+
112+
if (txPresent) {
113+
registerSynthesizedAnnotation(runtimeHints,
114+
org.springframework.test.context.jdbc.Sql.class,
115+
org.springframework.test.context.jdbc.SqlConfig.class,
116+
org.springframework.test.context.jdbc.SqlGroup.class,
117+
org.springframework.test.context.jdbc.SqlMergeMode.class,
118+
org.springframework.test.context.transaction.AfterTransaction.class,
119+
org.springframework.test.context.transaction.BeforeTransaction.class
120+
);
121+
}
122+
}
123+
124+
private static void registerPublicConstructors(ReflectionHints reflectionHints, Class<?>... types) {
125+
reflectionHints.registerTypes(TypeReference.listOf(types),
126+
builder -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
127+
}
128+
129+
private static void registerPublicConstructors(ReflectionHints reflectionHints, String... classNames) {
130+
reflectionHints.registerTypes(listOf(classNames),
131+
builder -> builder.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
132+
}
133+
134+
private static void registerDeclaredConstructors(ReflectionHints reflectionHints, Class<?>... types) {
135+
reflectionHints.registerTypes(TypeReference.listOf(types),
136+
builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
137+
}
138+
139+
private static void registerDeclaredConstructors(ReflectionHints reflectionHints, String... classNames) {
140+
reflectionHints.registerTypes(listOf(classNames),
141+
builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
142+
}
143+
144+
private static List<TypeReference> listOf(String... classNames) {
145+
return Arrays.stream(classNames).map(TypeReference::of).toList();
146+
}
147+
148+
@SafeVarargs
149+
@SuppressWarnings("unchecked")
150+
private static void registerSynthesizedAnnotation(RuntimeHints runtimeHints, Class<? extends Annotation>... annotationTypes) {
151+
for (Class<? extends Annotation> annotationType : annotationTypes) {
152+
registerAnnotation(runtimeHints.reflection(), annotationType);
153+
RuntimeHintsUtils.registerSynthesizedAnnotation(runtimeHints, annotationType);
154+
}
155+
}
156+
157+
private static void registerAnnotation(ReflectionHints reflectionHints, Class<? extends Annotation> annotationType) {
158+
reflectionHints.registerType(annotationType,
159+
builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_METHODS));
160+
}
161+
162+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.aot.hint.RuntimeHintsRegistrar=\
2+
org.springframework.test.context.aot.TestContextRuntimeHints

spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,27 @@
1616

1717
package org.springframework.test.context.aot;
1818

19+
import java.lang.annotation.Annotation;
1920
import java.util.ArrayList;
2021
import java.util.List;
2122
import java.util.Set;
23+
import java.util.function.Consumer;
2224

2325
import org.junit.jupiter.api.Test;
2426

2527
import org.springframework.aot.generate.DefaultGenerationContext;
2628
import org.springframework.aot.generate.GeneratedFiles.Kind;
2729
import org.springframework.aot.generate.InMemoryGeneratedFiles;
30+
import org.springframework.aot.hint.JdkProxyHint;
2831
import org.springframework.aot.hint.MemberCategory;
29-
import org.springframework.aot.hint.ReflectionHints;
32+
import org.springframework.aot.hint.RuntimeHints;
3033
import org.springframework.aot.hint.TypeReference;
3134
import org.springframework.aot.test.generator.compile.CompileWithTargetClassAccess;
3235
import org.springframework.aot.test.generator.compile.TestCompiler;
3336
import org.springframework.context.ApplicationContext;
3437
import org.springframework.context.ApplicationContextInitializer;
3538
import org.springframework.context.ConfigurableApplicationContext;
39+
import org.springframework.core.annotation.SynthesizedAnnotation;
3640
import org.springframework.javapoet.ClassName;
3741
import org.springframework.test.context.MergedContextConfiguration;
3842
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests;
@@ -52,6 +56,11 @@
5256

5357
import static java.util.Comparator.comparing;
5458
import static org.assertj.core.api.Assertions.assertThat;
59+
import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS;
60+
import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_METHODS;
61+
import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS;
62+
import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_METHODS;
63+
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
5564
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
5665
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
5766
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -83,10 +92,7 @@ void processAheadOfTimeAndGenerateAotTestMappings() {
8392

8493
generator.processAheadOfTime(testClasses.stream().sorted(comparing(Class::getName)));
8594

86-
ReflectionHints reflectionHints = generator.getRuntimeHints().reflection();
87-
assertThat(reflectionHints.getTypeHint(TypeReference.of(AotTestMappings.GENERATED_MAPPINGS_CLASS_NAME)))
88-
.satisfies(typeHint ->
89-
assertThat(typeHint.getMemberCategories()).containsExactly(MemberCategory.INVOKE_PUBLIC_METHODS));
95+
assertRuntimeHints(generator.getRuntimeHints());
9096

9197
List<String> sourceFiles = generatedFiles.getGeneratedFiles(Kind.SOURCE).keySet().stream().toList();
9298
assertThat(sourceFiles).containsExactlyInAnyOrder(expectedSourceFilesForBasicSpringTests);
@@ -105,6 +111,101 @@ void processAheadOfTimeAndGenerateAotTestMappings() {
105111
}));
106112
}
107113

114+
private static void assertRuntimeHints(RuntimeHints runtimeHints) {
115+
assertReflectionRegistered(runtimeHints, AotTestMappings.GENERATED_MAPPINGS_CLASS_NAME, INVOKE_PUBLIC_METHODS);
116+
117+
Set.of(
118+
org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.class,
119+
org.springframework.test.context.support.DefaultBootstrapContext.class,
120+
org.springframework.test.context.support.DelegatingSmartContextLoader.class,
121+
org.springframework.test.context.web.WebDelegatingSmartContextLoader.class
122+
).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_PUBLIC_CONSTRUCTORS));
123+
124+
Set.of(
125+
org.springframework.test.context.support.DefaultTestContextBootstrapper.class,
126+
org.springframework.test.context.web.WebTestContextBootstrapper.class,
127+
org.springframework.test.context.support.GenericGroovyXmlContextLoader.class,
128+
org.springframework.test.context.web.GenericGroovyXmlWebContextLoader.class
129+
).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS));
130+
131+
Set.of(
132+
// Legacy and JUnit 4
133+
org.springframework.test.annotation.Commit.class,
134+
org.springframework.test.annotation.DirtiesContext.class,
135+
org.springframework.test.annotation.IfProfileValue.class,
136+
org.springframework.test.annotation.ProfileValueSourceConfiguration.class,
137+
org.springframework.test.annotation.Repeat.class,
138+
org.springframework.test.annotation.Rollback.class,
139+
org.springframework.test.annotation.Timed.class,
140+
141+
// Core TestContext framework
142+
org.springframework.test.context.ActiveProfiles.class,
143+
org.springframework.test.context.BootstrapWith.class,
144+
org.springframework.test.context.ContextConfiguration.class,
145+
org.springframework.test.context.ContextHierarchy.class,
146+
org.springframework.test.context.DynamicPropertySource.class,
147+
org.springframework.test.context.NestedTestConfiguration.class,
148+
org.springframework.test.context.TestConstructor.class,
149+
org.springframework.test.context.TestExecutionListeners.class,
150+
org.springframework.test.context.TestPropertySource.class,
151+
org.springframework.test.context.TestPropertySources.class,
152+
153+
// Application Events
154+
org.springframework.test.context.event.RecordApplicationEvents.class,
155+
156+
// JUnit Jupiter
157+
org.springframework.test.context.junit.jupiter.EnabledIf.class,
158+
org.springframework.test.context.junit.jupiter.DisabledIf.class,
159+
org.springframework.test.context.junit.jupiter.SpringJUnitConfig.class,
160+
org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig.class,
161+
162+
// Web
163+
org.springframework.test.context.web.WebAppConfiguration.class
164+
).forEach(type -> assertAnnotationRegistered(runtimeHints, type));
165+
166+
// TestExecutionListener
167+
Set.of(
168+
org.springframework.test.context.event.ApplicationEventsTestExecutionListener.class,
169+
org.springframework.test.context.event.EventPublishingTestExecutionListener.class,
170+
org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.class,
171+
org.springframework.test.context.support.DependencyInjectionTestExecutionListener.class,
172+
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener.class,
173+
org.springframework.test.context.support.DirtiesContextTestExecutionListener.class,
174+
org.springframework.test.context.transaction.TransactionalTestExecutionListener.class,
175+
org.springframework.test.context.web.ServletTestExecutionListener.class
176+
).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS));
177+
178+
// ContextCustomizerFactory
179+
Set.of(
180+
"org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactory",
181+
"org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory"
182+
).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS));
183+
}
184+
185+
private static void assertReflectionRegistered(RuntimeHints runtimeHints, String type, MemberCategory memberCategory) {
186+
assertThat(reflection().onType(TypeReference.of(type)).withMemberCategory(memberCategory))
187+
.as("Reflection hint for %s with category %s", type, memberCategory)
188+
.accepts(runtimeHints);
189+
}
190+
191+
private static void assertReflectionRegistered(RuntimeHints runtimeHints, Class<?> type, MemberCategory memberCategory) {
192+
assertThat(reflection().onType(type).withMemberCategory(memberCategory))
193+
.as("Reflection hint for %s with category %s", type.getSimpleName(), memberCategory)
194+
.accepts(runtimeHints);
195+
}
196+
197+
private static void assertAnnotationRegistered(RuntimeHints runtimeHints, Class<? extends Annotation> annotationType) {
198+
assertReflectionRegistered(runtimeHints, annotationType, INVOKE_DECLARED_METHODS);
199+
assertThat(runtimeHints.proxies().jdkProxies())
200+
.as("Proxy hint for annotation @%s", annotationType.getSimpleName())
201+
.anySatisfy(annotationProxy(annotationType));
202+
}
203+
204+
private static Consumer<JdkProxyHint> annotationProxy(Class<? extends Annotation> type) {
205+
return jdkProxyHint -> assertThat(jdkProxyHint.getProxiedInterfaces())
206+
.containsExactly(TypeReference.of(type), TypeReference.of(SynthesizedAnnotation.class));
207+
}
208+
108209
@Test
109210
void processAheadOfTimeWithBasicTests() {
110211
// We cannot parameterize with the test classes, since @CompileWithTargetClassAccess

0 commit comments

Comments
 (0)