Skip to content

Commit 8a6c1ba

Browse files
committed
Introduce AOT run-time support in the TestContext framework
This commit introduces initial AOT run-time support in the Spring TestContext Framework. - DefaultCacheAwareContextLoaderDelegate: when running in AOT mode, now loads a test's ApplicationContext via the AotContextLoader SPI instead of via the standard SmartContextLoader and ContextLoader SPIs. - DependencyInjectionTestExecutionListener: when running in AOT mode, now injects dependencies into a test instance using a local instance of AutowiredAnnotationBeanPostProcessor instead of relying on AutowireCapableBeanFactory support. Closes gh-28205
1 parent ada0880 commit 8a6c1ba

File tree

4 files changed

+221
-15
lines changed

4 files changed

+221
-15
lines changed

spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,21 @@
1919
import org.apache.commons.logging.Log;
2020
import org.apache.commons.logging.LogFactory;
2121

22+
import org.springframework.aot.AotDetector;
2223
import org.springframework.context.ApplicationContext;
24+
import org.springframework.context.ApplicationContextInitializer;
25+
import org.springframework.context.ConfigurableApplicationContext;
26+
import org.springframework.context.support.GenericApplicationContext;
27+
import org.springframework.core.log.LogMessage;
2328
import org.springframework.lang.Nullable;
2429
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
2530
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
2631
import org.springframework.test.context.ContextLoader;
2732
import org.springframework.test.context.MergedContextConfiguration;
2833
import org.springframework.test.context.SmartContextLoader;
34+
import org.springframework.test.context.aot.AotContextLoader;
35+
import org.springframework.test.context.aot.AotTestMappings;
36+
import org.springframework.test.context.aot.TestContextAotException;
2937
import org.springframework.util.Assert;
3038

3139
/**
@@ -48,6 +56,8 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
4856
*/
4957
static final ContextCache defaultContextCache = new DefaultContextCache();
5058

59+
private final AotTestMappings aotTestMappings = getAotTestMappings();
60+
5161
private final ContextCache contextCache;
5262

5363

@@ -87,7 +97,12 @@ public ApplicationContext loadContext(MergedContextConfiguration mergedContextCo
8797
ApplicationContext context = this.contextCache.get(mergedContextConfiguration);
8898
if (context == null) {
8999
try {
90-
context = loadContextInternal(mergedContextConfiguration);
100+
if (runningInAotMode(mergedContextConfiguration.getTestClass())) {
101+
context = loadContextInAotMode(mergedContextConfiguration);
102+
}
103+
else {
104+
context = loadContextInternal(mergedContextConfiguration);
105+
}
91106
if (logger.isDebugEnabled()) {
92107
logger.debug(String.format("Storing ApplicationContext [%s] in cache under key [%s]",
93108
System.identityHashCode(context), mergedContextConfiguration));
@@ -149,4 +164,45 @@ protected ApplicationContext loadContextInternal(MergedContextConfiguration merg
149164
}
150165
}
151166

167+
protected ApplicationContext loadContextInAotMode(MergedContextConfiguration mergedConfig) throws Exception {
168+
Class<?> testClass = mergedConfig.getTestClass();
169+
ApplicationContextInitializer<ConfigurableApplicationContext> contextInitializer =
170+
this.aotTestMappings.getContextInitializer(testClass);
171+
Assert.state(contextInitializer != null,
172+
() -> "Failed to load AOT ApplicationContextInitializer for test class [%s]"
173+
.formatted(testClass.getName()));
174+
logger.info(LogMessage.format("Loading ApplicationContext in AOT mode for %s", mergedConfig));
175+
ContextLoader contextLoader = mergedConfig.getContextLoader();
176+
if (!((contextLoader instanceof AotContextLoader aotContextLoader) &&
177+
(aotContextLoader.loadContextForAotRuntime(mergedConfig, contextInitializer)
178+
instanceof GenericApplicationContext gac))) {
179+
throw new TestContextAotException("""
180+
Cannot load ApplicationContext for AOT runtime for %s. The configured \
181+
ContextLoader [%s] must be an AotContextLoader and must create a \
182+
GenericApplicationContext."""
183+
.formatted(mergedConfig, contextLoader.getClass().getName()));
184+
}
185+
gac.registerShutdownHook();
186+
return gac;
187+
}
188+
189+
/**
190+
* Determine if we are running in AOT mode for the supplied test class.
191+
*/
192+
private boolean runningInAotMode(Class<?> testClass) {
193+
return (this.aotTestMappings != null && this.aotTestMappings.isSupportedTestClass(testClass));
194+
}
195+
196+
private static AotTestMappings getAotTestMappings() {
197+
if (AotDetector.useGeneratedArtifacts()) {
198+
try {
199+
return new AotTestMappings();
200+
}
201+
catch (Exception ex) {
202+
throw new IllegalStateException("Failed to instantiate AotTestMappings", ex);
203+
}
204+
}
205+
return null;
206+
}
207+
152208
}

spring-test/src/main/java/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.java

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,9 +19,15 @@
1919
import org.apache.commons.logging.Log;
2020
import org.apache.commons.logging.LogFactory;
2121

22+
import org.springframework.aot.AotDetector;
23+
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
2224
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
25+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
26+
import org.springframework.context.ApplicationContext;
27+
import org.springframework.context.support.GenericApplicationContext;
2328
import org.springframework.core.Conventions;
2429
import org.springframework.test.context.TestContext;
30+
import org.springframework.test.context.aot.AotTestMappings;
2531

2632
/**
2733
* {@code TestExecutionListener} which provides support for dependency
@@ -53,6 +59,8 @@ public class DependencyInjectionTestExecutionListener extends AbstractTestExecut
5359

5460
private static final Log logger = LogFactory.getLog(DependencyInjectionTestExecutionListener.class);
5561

62+
private final AotTestMappings aotTestMappings = getAotTestMappings();
63+
5664

5765
/**
5866
* Returns {@code 2000}.
@@ -78,9 +86,14 @@ public final int getOrder() {
7886
@Override
7987
public void prepareTestInstance(TestContext testContext) throws Exception {
8088
if (logger.isDebugEnabled()) {
81-
logger.debug("Performing dependency injection for test context [" + testContext + "].");
89+
logger.debug("Performing dependency injection for test context " + testContext);
90+
}
91+
if (runningInAotMode(testContext.getTestClass())) {
92+
injectDependenciesInAotMode(testContext);
93+
}
94+
else {
95+
injectDependencies(testContext);
8296
}
83-
injectDependencies(testContext);
8497
}
8598

8699
/**
@@ -96,7 +109,12 @@ public void beforeTestMethod(TestContext testContext) throws Exception {
96109
if (logger.isDebugEnabled()) {
97110
logger.debug("Reinjecting dependencies for test context [" + testContext + "].");
98111
}
99-
injectDependencies(testContext);
112+
if (runningInAotMode(testContext.getTestClass())) {
113+
injectDependenciesInAotMode(testContext);
114+
}
115+
else {
116+
injectDependencies(testContext);
117+
}
100118
}
101119
}
102120

@@ -121,4 +139,40 @@ protected void injectDependencies(TestContext testContext) throws Exception {
121139
testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
122140
}
123141

142+
private void injectDependenciesInAotMode(TestContext testContext) throws Exception {
143+
ApplicationContext applicationContext = testContext.getApplicationContext();
144+
if (!(applicationContext instanceof GenericApplicationContext gac)) {
145+
throw new IllegalStateException("AOT ApplicationContext must be a GenericApplicationContext instead of " +
146+
applicationContext.getClass().getName());
147+
}
148+
149+
Object bean = testContext.getTestInstance();
150+
Class<?> clazz = testContext.getTestClass();
151+
ConfigurableListableBeanFactory beanFactory = gac.getBeanFactory();
152+
AutowiredAnnotationBeanPostProcessor beanPostProcessor = new AutowiredAnnotationBeanPostProcessor();
153+
beanPostProcessor.setBeanFactory(beanFactory);
154+
beanPostProcessor.processInjection(bean);
155+
beanFactory.initializeBean(bean, clazz.getName() + AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX);
156+
testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
157+
}
158+
159+
/**
160+
* Determine if we are running in AOT mode for the supplied test class.
161+
*/
162+
private boolean runningInAotMode(Class<?> testClass) {
163+
return (this.aotTestMappings != null && this.aotTestMappings.isSupportedTestClass(testClass));
164+
}
165+
166+
private static AotTestMappings getAotTestMappings() {
167+
if (AotDetector.useGeneratedArtifacts()) {
168+
try {
169+
return new AotTestMappings();
170+
}
171+
catch (Exception ex) {
172+
throw new IllegalStateException("Failed to instantiate AotTestMappings", ex);
173+
}
174+
}
175+
return null;
176+
}
177+
124178
}

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

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,139 @@
1616

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

19+
20+
import java.nio.file.Path;
21+
import java.nio.file.Paths;
22+
import java.util.Arrays;
1923
import java.util.List;
24+
import java.util.Set;
2025
import java.util.stream.Stream;
2126

2227
import org.junit.jupiter.api.Test;
28+
import org.junit.platform.launcher.LauncherDiscoveryRequest;
29+
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
30+
import org.junit.platform.launcher.core.LauncherFactory;
31+
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
32+
import org.junit.platform.launcher.listeners.TestExecutionSummary;
33+
import org.junit.platform.launcher.listeners.TestExecutionSummary.Failure;
34+
import org.opentest4j.MultipleFailuresError;
2335

36+
import org.springframework.aot.AotDetector;
2437
import org.springframework.aot.generate.GeneratedFiles.Kind;
2538
import org.springframework.aot.generate.InMemoryGeneratedFiles;
39+
import org.springframework.aot.test.generator.compile.CompileWithTargetClassAccess;
2640
import org.springframework.aot.test.generator.compile.TestCompiler;
41+
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests;
42+
import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests;
2743

2844
import static org.assertj.core.api.Assertions.assertThat;
45+
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
46+
import static org.junit.platform.launcher.EngineFilter.includeEngines;
2947

3048
/**
3149
* Smoke tests for AOT support in the TestContext framework.
3250
*
3351
* @author Sam Brannen
3452
* @since 6.0
3553
*/
54+
@CompileWithTargetClassAccess
3655
class AotSmokeTests extends AbstractAotTests {
3756

57+
private static final String CLASSPATH_ROOT = "AotSmokeTests.classpath_root";
58+
59+
// We have to determine the classpath root and store it in a system property
60+
// since @CompileWithTargetClassAccess uses a custom ClassLoader that does
61+
// not support CodeSource.
62+
//
63+
// The system property will only be set when this class is loaded by the
64+
// original ClassLoader used to launch the JUnit Platform. The attempt to
65+
// access the CodeSource will fail when the tests are executed in the
66+
// nested JUnit Platform launched by the CompileWithTargetClassAccessExtension.
67+
static {
68+
try {
69+
Path classpathRoot = Paths.get(AotSmokeTests.class.getProtectionDomain().getCodeSource().getLocation().toURI());
70+
System.setProperty(CLASSPATH_ROOT, classpathRoot.toFile().getCanonicalPath());
71+
}
72+
catch (Exception ex) {
73+
// ignore
74+
}
75+
}
76+
77+
3878
@Test
39-
// Using @CompileWithTargetClassAccess results in the following exception in classpathRoots():
40-
// java.lang.NullPointerException: Cannot invoke "java.net.URL.toURI()" because the return
41-
// value of "java.security.CodeSource.getLocation()" is null
42-
void scanClassPathThenGenerateSourceFilesAndCompileThem() {
43-
Stream<Class<?>> testClasses = scan("org.springframework.test.context.aot.samples.basic");
79+
void endToEndTests() {
80+
// AOT BUILD-TIME: CLASSPATH SCANNING
81+
Stream<Class<?>> testClasses = createTestClassScanner()
82+
.scan("org.springframework.test.context.aot.samples.basic")
83+
// This test focuses solely on JUnit Jupiter tests
84+
.filter(sourceFile -> sourceFile.getName().contains("Jupiter"));
85+
86+
// AOT BUILD-TIME: PROCESSING
4487
InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles();
4588
TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles);
46-
4789
generator.processAheadOfTime(testClasses);
4890

4991
List<String> sourceFiles = generatedFiles.getGeneratedFiles(Kind.SOURCE).keySet().stream().toList();
50-
assertThat(sourceFiles).containsExactlyInAnyOrder(expectedSourceFilesForBasicSpringTests);
92+
assertThat(sourceFiles).containsExactlyInAnyOrder(expectedSourceFilesForBasicSpringJupiterTests);
5193

94+
// AOT BUILD-TIME: COMPILATION
5295
TestCompiler.forSystem().withFiles(generatedFiles)
5396
// .printFiles(System.out)
54-
.compile(compiled -> {
55-
// just make sure compilation completes without errors
56-
});
97+
.compile(compiled ->
98+
// AOT RUN-TIME: EXECUTION
99+
runTestsInAotMode(BasicSpringJupiterTests.class, BasicSpringJupiterSharedConfigTests.class));
57100
}
58101

102+
103+
private static void runTestsInAotMode(Class<?>... testClasses) {
104+
try {
105+
System.setProperty(AotDetector.AOT_ENABLED, "true");
106+
107+
LauncherDiscoveryRequestBuilder builder = LauncherDiscoveryRequestBuilder.request()
108+
.filters(includeEngines("junit-jupiter"));
109+
Arrays.stream(testClasses).forEach(testClass -> builder.selectors(selectClass(testClass)));
110+
LauncherDiscoveryRequest request = builder.build();
111+
SummaryGeneratingListener listener = new SummaryGeneratingListener();
112+
LauncherFactory.create().execute(request, listener);
113+
TestExecutionSummary summary = listener.getSummary();
114+
if (summary.getTotalFailureCount() > 0) {
115+
List<Throwable> exceptions = summary.getFailures().stream().map(Failure::getException).toList();
116+
throw new MultipleFailuresError("Test execution failures", exceptions);
117+
}
118+
}
119+
finally {
120+
System.clearProperty(AotDetector.AOT_ENABLED);
121+
}
122+
}
123+
124+
private static TestClassScanner createTestClassScanner() {
125+
String classpathRoot = System.getProperty(CLASSPATH_ROOT);
126+
assertThat(classpathRoot).as(CLASSPATH_ROOT).isNotNull();
127+
Set<Path> classpathRoots = Set.of(Paths.get(classpathRoot));
128+
return new TestClassScanner(classpathRoots);
129+
}
130+
131+
private static final String[] expectedSourceFilesForBasicSpringJupiterTests = {
132+
// Global
133+
"org/springframework/test/context/aot/AotTestMappings__Generated.java",
134+
// BasicSpringJupiterSharedConfigTests
135+
"org/springframework/context/event/DefaultEventListenerFactory__TestContext001_BeanDefinitions.java",
136+
"org/springframework/context/event/EventListenerMethodProcessor__TestContext001_BeanDefinitions.java",
137+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_ApplicationContextInitializer.java",
138+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_BeanFactoryRegistrations.java",
139+
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java",
140+
// BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
141+
// "org/springframework/context/event/DefaultEventListenerFactory__TestContext00?_BeanDefinitions.java",
142+
// "org/springframework/context/event/EventListenerMethodProcessor__TestContext00?_BeanDefinitions.java",
143+
// "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests__TestContext00?_ApplicationContextInitializer.java",
144+
// "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests__TestContext00?_BeanFactoryRegistrations.java",
145+
// "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext00?_BeanDefinitions.java",
146+
// BasicSpringJupiterTests.NestedTests
147+
"org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java",
148+
"org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java",
149+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_ApplicationContextInitializer.java",
150+
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_BeanFactoryRegistrations.java",
151+
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
152+
};
153+
59154
}

spring-test/src/test/resources/log4j2-test.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<Logger name="org.springframework.test.context.ContextLoaderUtils" level="warn" />
1717
<Logger name="org.springframework.test.context.aot" level="debug" />
1818
<Logger name="org.springframework.test.context.cache" level="warn" />
19+
<Logger name="org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate" level="info" />
1920
<Logger name="org.springframework.test.context.junit4.rules" level="warn" />
2021
<Logger name="org.springframework.test.context.transaction.TransactionalTestExecutionListener" level="warn" />
2122
<Logger name="org.springframework.test.context.web" level="warn" />

0 commit comments

Comments
 (0)