Skip to content

Commit ae3bec5

Browse files
committed
Allow test classes to provide runtime hints via declarative mechanisms
Prior to this commit, it was possible to register hints for individual test classes programmatically via the org.springframework.test.context.aot.TestRuntimeHintsRegistrar SPI; however, that requires that a custom TestRuntimeHintsRegistrar be registered via "META-INF/spring/aot.factories". In addition, implementing a TestRuntimeHintsRegistrar is more cumbersome than using the core mechanisms such as @Reflective, @ImportRuntimeHints, and @RegisterReflectionForBinding. This commit address this by introducing support for @Reflective and @ImportRuntimeHints on test classes. @RegisterReflectionForBinding support is available automatically since it is an extension of the @Reflective mechanism. Closes gh-29455
1 parent 1b61217 commit ae3bec5

File tree

3 files changed

+190
-1
lines changed

3 files changed

+190
-1
lines changed

spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

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

19+
import java.util.Arrays;
20+
import java.util.LinkedHashSet;
1921
import java.util.Map;
22+
import java.util.Set;
2023
import java.util.concurrent.atomic.AtomicInteger;
2124
import java.util.stream.Stream;
2225

@@ -30,12 +33,19 @@
3033
import org.springframework.aot.generate.GeneratedFiles;
3134
import org.springframework.aot.generate.GenerationContext;
3235
import org.springframework.aot.hint.RuntimeHints;
36+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
3337
import org.springframework.aot.hint.TypeReference;
38+
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
39+
import org.springframework.beans.BeanUtils;
3440
import org.springframework.beans.factory.aot.AotServices;
3541
import org.springframework.context.ApplicationContext;
3642
import org.springframework.context.ApplicationContextInitializer;
43+
import org.springframework.context.annotation.ImportRuntimeHints;
3744
import org.springframework.context.aot.ApplicationContextAotGenerator;
3845
import org.springframework.context.support.GenericApplicationContext;
46+
import org.springframework.core.annotation.MergedAnnotation;
47+
import org.springframework.core.annotation.MergedAnnotations;
48+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
3949
import org.springframework.core.log.LogMessage;
4050
import org.springframework.javapoet.ClassName;
4151
import org.springframework.test.context.BootstrapUtils;
@@ -117,16 +127,31 @@ public void processAheadOfTime(Stream<Class<?>> testClasses) throws TestContextA
117127
try {
118128
resetAotFactories();
119129

130+
Set<Class<? extends RuntimeHintsRegistrar>> coreRuntimeHintsRegistrarClasses = new LinkedHashSet<>();
131+
ReflectiveRuntimeHintsRegistrar reflectiveRuntimeHintsRegistrar = new ReflectiveRuntimeHintsRegistrar();
132+
120133
MultiValueMap<MergedContextConfiguration, Class<?>> mergedConfigMappings = new LinkedMultiValueMap<>();
121134
ClassLoader classLoader = getClass().getClassLoader();
122135
testClasses.forEach(testClass -> {
123136
MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass);
124137
mergedConfigMappings.add(mergedConfig, testClass);
138+
collectRuntimeHintsRegistrarClasses(testClass, coreRuntimeHintsRegistrarClasses);
139+
reflectiveRuntimeHintsRegistrar.registerRuntimeHints(this.runtimeHints, testClass);
125140
this.testRuntimeHintsRegistrars.forEach(registrar ->
126141
registrar.registerHints(this.runtimeHints, testClass, classLoader));
127142
});
128-
MultiValueMap<ClassName, Class<?>> initializerClassMappings = processAheadOfTime(mergedConfigMappings);
129143

144+
coreRuntimeHintsRegistrarClasses.stream()
145+
.map(BeanUtils::instantiateClass)
146+
.forEach(registrar -> {
147+
if (logger.isTraceEnabled()) {
148+
logger.trace("Processing RuntimeHints contribution from test class [%s]"
149+
.formatted(registrar.getClass().getCanonicalName()));
150+
}
151+
registrar.registerHints(this.runtimeHints, classLoader);
152+
});
153+
154+
MultiValueMap<ClassName, Class<?>> initializerClassMappings = processAheadOfTime(mergedConfigMappings);
130155
generateAotTestContextInitializerMappings(initializerClassMappings);
131156
generateAotTestAttributeMappings();
132157
}
@@ -135,6 +160,25 @@ public void processAheadOfTime(Stream<Class<?>> testClasses) throws TestContextA
135160
}
136161
}
137162

163+
/**
164+
* Collect all {@link RuntimeHintsRegistrar} classes declared via
165+
* {@link ImportRuntimeHints @ImportRuntimeHints} on the supplied test class
166+
* and add them to the supplied {@link Set}.
167+
* @param testClass the test class on which to search for {@code @ImportRuntimeHints}
168+
* @param coreRuntimeHintsRegistrarClasses the set of registrar classes
169+
*/
170+
private void collectRuntimeHintsRegistrarClasses(
171+
Class<?> testClass, Set<Class<? extends RuntimeHintsRegistrar>> coreRuntimeHintsRegistrarClasses) {
172+
173+
MergedAnnotations.from(testClass, SearchStrategy.TYPE_HIERARCHY)
174+
.stream(ImportRuntimeHints.class)
175+
.filter(MergedAnnotation::isPresent)
176+
.map(MergedAnnotation::synthesize)
177+
.map(ImportRuntimeHints::value)
178+
.flatMap(Arrays::stream)
179+
.forEach(coreRuntimeHintsRegistrarClasses::add);
180+
}
181+
138182
private void resetAotFactories() {
139183
AotTestAttributesFactory.reset();
140184
AotTestContextInitializersFactory.reset();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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.util.stream.Stream;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.aot.generate.InMemoryGeneratedFiles;
24+
import org.springframework.aot.hint.RuntimeHints;
25+
import org.springframework.test.context.aot.samples.hints.DeclarativeRuntimeHintsSpringJupiterTests;
26+
import org.springframework.test.context.aot.samples.hints.DeclarativeRuntimeHintsSpringJupiterTests.SampleClassWithGetter;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
30+
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.resource;
31+
32+
/**
33+
* Tests for declarative support for registering run-time hints for tests, tested
34+
* via the {@link TestContextAotGenerator}
35+
*
36+
* @author Sam Brannen
37+
* @since 6.0
38+
*/
39+
class DeclarativeRuntimeHintsTests extends AbstractAotTests {
40+
41+
private final RuntimeHints runtimeHints = new RuntimeHints();
42+
43+
private final TestContextAotGenerator generator =
44+
new TestContextAotGenerator(new InMemoryGeneratedFiles(), this.runtimeHints);
45+
46+
47+
@Test
48+
void declarativeRuntimeHints() {
49+
Class<?> testClass = DeclarativeRuntimeHintsSpringJupiterTests.class;
50+
51+
this.generator.processAheadOfTime(Stream.of(testClass));
52+
53+
// @Reflective
54+
assertReflectionRegistered(testClass);
55+
56+
// @@RegisterReflectionForBinding
57+
assertReflectionRegistered(SampleClassWithGetter.class);
58+
assertReflectionRegistered(String.class);
59+
assertThat(reflection().onMethod(SampleClassWithGetter.class, "getName")).accepts(this.runtimeHints);
60+
61+
// @ImportRuntimeHints
62+
assertThat(resource().forResource("org/example/config/enigma.txt")).accepts(this.runtimeHints);
63+
}
64+
65+
private void assertReflectionRegistered(Class<?> type) {
66+
assertThat(reflection().onType(type)).as("Reflection hint for %s", type).accepts(this.runtimeHints);
67+
}
68+
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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.samples.hints;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.aot.hint.RuntimeHints;
22+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
23+
import org.springframework.aot.hint.annotation.Reflective;
24+
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.context.annotation.ImportRuntimeHints;
29+
import org.springframework.test.context.aot.samples.hints.DeclarativeRuntimeHintsSpringJupiterTests.DemoHints;
30+
import org.springframework.test.context.aot.samples.hints.DeclarativeRuntimeHintsSpringJupiterTests.SampleClassWithGetter;
31+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
35+
/**
36+
* @author Sam Brannen
37+
* @since 6.0
38+
*/
39+
@SpringJUnitConfig
40+
@Reflective
41+
@RegisterReflectionForBinding(SampleClassWithGetter.class)
42+
@ImportRuntimeHints(DemoHints.class)
43+
public class DeclarativeRuntimeHintsSpringJupiterTests {
44+
45+
@Test
46+
void test(@Autowired String foo) {
47+
assertThat(foo).isEqualTo("bar");
48+
}
49+
50+
51+
@Configuration(proxyBeanMethods = false)
52+
static class Config {
53+
54+
@Bean
55+
String foo() {
56+
return "bar";
57+
}
58+
}
59+
60+
static class DemoHints implements RuntimeHintsRegistrar {
61+
62+
@Override
63+
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
64+
hints.resources().registerPattern("org/example/config/*.txt");
65+
}
66+
67+
}
68+
69+
public static class SampleClassWithGetter {
70+
71+
public String getName() {
72+
return null;
73+
}
74+
}
75+
76+
}

0 commit comments

Comments
 (0)