Skip to content

Commit 1113096

Browse files
committed
Support Test AOT processing with GraalVM tracing agent and NBT
Prior to this commit, test AOT processing failed when using the GraalVM tracing agent and GraalVM Native Build Tools (NBT) plugins for Maven and Gradle. The reason is that the AOT support in the TestContext framework (TCF) relied on AotDetector.useGeneratedArtifacts() which delegates internally to NativeDetector.inNativeImage() which does not differentiate between values stored in the "org.graalvm.nativeimage.imagecode" JVM system property. This commit addresses this issue by introducing a TestAotDetector utility that is specific to the TCF. This detector considers the current runtime to be in "AOT runtime mode" if the "spring.aot.enabled" Spring property is set to "true" or the GraalVM "org.graalvm.nativeimage.imagecode" JVM system property is set to any non-empty value other than "agent". Closes gh-30281
1 parent 89bcee6 commit 1113096

11 files changed

+165
-37
lines changed

Diff for: spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -16,7 +16,6 @@
1616

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

19-
import org.springframework.aot.AotDetector;
2019
import org.springframework.lang.Nullable;
2120

2221
/**
@@ -27,7 +26,7 @@
2726
* and run-time. At build time, test components can {@linkplain #setAttribute contribute}
2827
* attributes during the AOT processing phase. At run time, test components can
2928
* {@linkplain #getString(String) retrieve} attributes that were contributed at
30-
* build time. If {@link AotDetector#useGeneratedArtifacts()} returns {@code true},
29+
* build time. If {@link TestAotDetector#useGeneratedArtifacts()} returns {@code true},
3130
* run-time mode applies.
3231
*
3332
* <p>For example, if a test component computes something at build time that
@@ -44,7 +43,7 @@
4443
* &mdash; can choose to contribute an attribute at any point in time. Note that
4544
* contributing an attribute during standard JVM test execution will not have any
4645
* adverse side effect since AOT attributes will be ignored in that scenario. In
47-
* any case, you should use {@link AotDetector#useGeneratedArtifacts()} to determine
46+
* any case, you should use {@link TestAotDetector#useGeneratedArtifacts()} to determine
4847
* if invocations of {@link #setAttribute(String, String)} and
4948
* {@link #removeAttribute(String)} are permitted.
5049
*
@@ -71,12 +70,12 @@ static AotTestAttributes getInstance() {
7170
* @param name the unique attribute name
7271
* @param value the associated attribute value
7372
* @throws UnsupportedOperationException if invoked during
74-
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
73+
* {@linkplain TestAotDetector#useGeneratedArtifacts() AOT run-time execution}
7574
* @throws IllegalArgumentException if the provided value is {@code null} or
7675
* if an attempt is made to override an existing attribute
7776
* @see #setAttribute(String, boolean)
7877
* @see #removeAttribute(String)
79-
* @see AotDetector#useGeneratedArtifacts()
78+
* @see TestAotDetector#useGeneratedArtifacts()
8079
*/
8180
void setAttribute(String name, String value);
8281

@@ -88,13 +87,13 @@ static AotTestAttributes getInstance() {
8887
* @param name the unique attribute name
8988
* @param value the associated attribute value
9089
* @throws UnsupportedOperationException if invoked during
91-
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
90+
* {@linkplain TestAotDetector#useGeneratedArtifacts() AOT run-time execution}
9291
* @throws IllegalArgumentException if an attempt is made to override an
9392
* existing attribute
9493
* @see #setAttribute(String, String)
9594
* @see #removeAttribute(String)
9695
* @see Boolean#toString(boolean)
97-
* @see AotDetector#useGeneratedArtifacts()
96+
* @see TestAotDetector#useGeneratedArtifacts()
9897
*/
9998
default void setAttribute(String name, boolean value) {
10099
setAttribute(name, Boolean.toString(value));
@@ -104,8 +103,8 @@ default void setAttribute(String name, boolean value) {
104103
* Remove the attribute stored under the provided name.
105104
* @param name the unique attribute name
106105
* @throws UnsupportedOperationException if invoked during
107-
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
108-
* @see AotDetector#useGeneratedArtifacts()
106+
* {@linkplain TestAotDetector#useGeneratedArtifacts() AOT run-time execution}
107+
* @see TestAotDetector#useGeneratedArtifacts()
109108
* @see #setAttribute(String, String)
110109
*/
111110
void removeAttribute(String name);

Diff for: spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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,7 +19,6 @@
1919
import java.util.Map;
2020
import java.util.concurrent.ConcurrentHashMap;
2121

22-
import org.springframework.aot.AotDetector;
2322
import org.springframework.lang.Nullable;
2423

2524
/**
@@ -40,7 +39,7 @@ private AotTestAttributesFactory() {
4039
/**
4140
* Get the underlying attributes map.
4241
* <p>If the map is not already loaded, this method loads the map from the
43-
* generated class when running in {@linkplain AotDetector#useGeneratedArtifacts()
42+
* generated class when running in {@linkplain TestAotDetector#useGeneratedArtifacts()
4443
* AOT execution mode} and otherwise creates a new map for storing attributes
4544
* during the AOT processing phase.
4645
*/
@@ -50,7 +49,7 @@ static Map<String, String> getAttributes() {
5049
synchronized (AotTestAttributesFactory.class) {
5150
attrs = attributes;
5251
if (attrs == null) {
53-
attrs = (AotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>());
52+
attrs = (TestAotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>());
5453
attributes = attrs;
5554
}
5655
}

Diff for: spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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,7 +19,6 @@
1919
import java.util.Map;
2020
import java.util.function.Supplier;
2121

22-
import org.springframework.aot.AotDetector;
2322
import org.springframework.context.ApplicationContextInitializer;
2423
import org.springframework.context.ConfigurableApplicationContext;
2524
import org.springframework.lang.Nullable;
@@ -30,7 +29,7 @@
3029
*
3130
* <p>Intended solely for internal use within the framework.
3231
*
33-
* <p>If we are not running in {@linkplain AotDetector#useGeneratedArtifacts()
32+
* <p>If we are not running in {@linkplain TestAotDetector#useGeneratedArtifacts()
3433
* AOT mode} or if a test class is not {@linkplain #isSupportedTestClass(Class)
3534
* supported} in AOT mode, {@link #getContextInitializer(Class)} and
3635
* {@link #getContextInitializerClass(Class)} will return {@code null}.

Diff for: spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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,7 +19,6 @@
1919
import java.util.Map;
2020
import java.util.function.Supplier;
2121

22-
import org.springframework.aot.AotDetector;
2322
import org.springframework.context.ApplicationContextInitializer;
2423
import org.springframework.context.ConfigurableApplicationContext;
2524
import org.springframework.lang.Nullable;
@@ -45,7 +44,7 @@ private AotTestContextInitializersFactory() {
4544
/**
4645
* Get the underlying map.
4746
* <p>If the map is not already loaded, this method loads the map from the
48-
* generated class when running in {@linkplain AotDetector#useGeneratedArtifacts()
47+
* generated class when running in {@linkplain TestAotDetector#useGeneratedArtifacts()
4948
* AOT execution mode} and otherwise creates an immutable, empty map.
5049
*/
5150
static Map<String, Supplier<ApplicationContextInitializer<ConfigurableApplicationContext>>> getContextInitializers() {
@@ -54,7 +53,7 @@ static Map<String, Supplier<ApplicationContextInitializer<ConfigurableApplicatio
5453
synchronized (AotTestContextInitializersFactory.class) {
5554
initializers = contextInitializers;
5655
if (initializers == null) {
57-
initializers = (AotDetector.useGeneratedArtifacts() ? loadContextInitializersMap() : Map.of());
56+
initializers = (TestAotDetector.useGeneratedArtifacts() ? loadContextInitializersMap() : Map.of());
5857
contextInitializers = initializers;
5958
}
6059
}
@@ -68,7 +67,7 @@ static Map<String, Class<ApplicationContextInitializer<?>>> getContextInitialize
6867
synchronized (AotTestContextInitializersFactory.class) {
6968
initializerClasses = contextInitializerClasses;
7069
if (initializerClasses == null) {
71-
initializerClasses = (AotDetector.useGeneratedArtifacts() ? loadContextInitializerClassesMap() : Map.of());
70+
initializerClasses = (TestAotDetector.useGeneratedArtifacts() ? loadContextInitializerClassesMap() : Map.of());
7271
contextInitializerClasses = initializerClasses;
7372
}
7473
}

Diff for: spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -18,7 +18,6 @@
1818

1919
import java.util.Map;
2020

21-
import org.springframework.aot.AotDetector;
2221
import org.springframework.lang.Nullable;
2322
import org.springframework.util.Assert;
2423

@@ -61,7 +60,7 @@ public String getString(String name) {
6160

6261

6362
private static void assertNotInAotRuntime() {
64-
if (AotDetector.useGeneratedArtifacts()) {
63+
if (TestAotDetector.useGeneratedArtifacts()) {
6564
throw new UnsupportedOperationException(
6665
"AOT attributes cannot be modified during AOT run-time execution");
6766
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2002-2023 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 org.springframework.aot.AotDetector;
20+
import org.springframework.core.SpringProperties;
21+
import org.springframework.util.StringUtils;
22+
23+
/**
24+
* TestContext framework specific utility for determining if AOT-processed
25+
* optimizations must be used rather than the regular runtime.
26+
*
27+
* <p>Strictly for internal use within the framework.
28+
*
29+
* @author Sam Brannen
30+
* @since 6.0.9
31+
*/
32+
public abstract class TestAotDetector {
33+
34+
/**
35+
* Determine whether AOT optimizations must be considered at runtime.
36+
* <p>This can be triggered using the {@value AotDetector#AOT_ENABLED}
37+
* Spring property or via GraalVM's {@code "org.graalvm.nativeimage.imagecode"}
38+
* JVM system property (if set to any non-empty value other than {@code agent}).
39+
* @return {@code true} if AOT optimizations must be considered
40+
* @see <a href="https://github.com/oracle/graal/blob/master/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/ImageInfo.java">GraalVM's ImageInfo.java</a>
41+
* @see AotDetector#useGeneratedArtifacts()
42+
*/
43+
public static boolean useGeneratedArtifacts() {
44+
return (SpringProperties.getFlag(AotDetector.AOT_ENABLED) || inNativeImage());
45+
}
46+
47+
/**
48+
* Determine if we are currently running within a GraalVM native image from
49+
* the perspective of the TestContext framework.
50+
* @return {@code true} if the {@code org.graalvm.nativeimage.imagecode} JVM
51+
* system property has been set to any value other than {@code agent}.
52+
*/
53+
private static boolean inNativeImage() {
54+
String imageCode = System.getProperty("org.graalvm.nativeimage.imagecode");
55+
return (StringUtils.hasText(imageCode) && !"agent".equalsIgnoreCase(imageCode.trim()));
56+
}
57+
58+
}

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

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -26,7 +26,6 @@
2626
import org.apache.commons.logging.Log;
2727
import org.apache.commons.logging.LogFactory;
2828

29-
import org.springframework.aot.AotDetector;
3029
import org.springframework.aot.generate.ClassNameGenerator;
3130
import org.springframework.aot.generate.DefaultGenerationContext;
3231
import org.springframework.aot.generate.GeneratedClasses;
@@ -123,7 +122,7 @@ public final RuntimeHints getRuntimeHints() {
123122
* @throws TestContextAotException if an error occurs during AOT processing
124123
*/
125124
public void processAheadOfTime(Stream<Class<?>> testClasses) throws TestContextAotException {
126-
Assert.state(!AotDetector.useGeneratedArtifacts(), "Cannot perform AOT processing during AOT run-time execution");
125+
Assert.state(!TestAotDetector.useGeneratedArtifacts(), "Cannot perform AOT processing during AOT run-time execution");
127126
try {
128127
resetAotFactories();
129128

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -21,7 +21,6 @@
2121
import org.apache.commons.logging.Log;
2222
import org.apache.commons.logging.LogFactory;
2323

24-
import org.springframework.aot.AotDetector;
2524
import org.springframework.context.ApplicationContext;
2625
import org.springframework.context.ApplicationContextInitializer;
2726
import org.springframework.context.ConfigurableApplicationContext;
@@ -36,6 +35,7 @@
3635
import org.springframework.test.context.SmartContextLoader;
3736
import org.springframework.test.context.aot.AotContextLoader;
3837
import org.springframework.test.context.aot.AotTestContextInitializers;
38+
import org.springframework.test.context.aot.TestAotDetector;
3939
import org.springframework.test.context.aot.TestContextAotException;
4040
import org.springframework.test.context.util.TestContextSpringFactoriesUtils;
4141
import org.springframework.util.Assert;
@@ -248,7 +248,7 @@ private ContextLoader getContextLoader(MergedContextConfiguration mergedConfig)
248248
*/
249249
@SuppressWarnings("unchecked")
250250
private MergedContextConfiguration replaceIfNecessary(MergedContextConfiguration mergedConfig) {
251-
if (AotDetector.useGeneratedArtifacts()) {
251+
if (TestAotDetector.useGeneratedArtifacts()) {
252252
Class<?> testClass = mergedConfig.getTestClass();
253253
Class<? extends ApplicationContextInitializer<?>> contextInitializerClass =
254254
this.aotTestContextInitializers.getContextInitializerClass(testClass);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2002-2023 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+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.CsvSource;
24+
25+
import org.springframework.aot.generate.InMemoryGeneratedFiles;
26+
27+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
28+
import static org.assertj.core.api.Assertions.assertThatNoException;
29+
30+
/**
31+
* Tests for error cases in {@link TestContextAotGenerator}.
32+
*
33+
* @author Sam Brannen
34+
* @since 6.0.9
35+
*/
36+
class TestContextAotGeneratorErrorCaseTests {
37+
38+
@ParameterizedTest
39+
@CsvSource(delimiter = '=', textBlock = """
40+
'spring.aot.enabled' = 'true'
41+
'org.graalvm.nativeimage.imagecode' = 'buildtime'
42+
'org.graalvm.nativeimage.imagecode' = 'runtime'
43+
'org.graalvm.nativeimage.imagecode' = 'bogus'
44+
""")
45+
void attemptToProcessWhileRunningInAotMode(String property, String value) {
46+
try {
47+
System.setProperty(property, value);
48+
49+
assertThatIllegalStateException()
50+
.isThrownBy(() -> generator().processAheadOfTime(Stream.empty()))
51+
.withMessage("Cannot perform AOT processing during AOT run-time execution");
52+
}
53+
finally {
54+
System.clearProperty(property);
55+
}
56+
}
57+
58+
@Test
59+
void attemptToProcessWhileRunningInGraalVmNativeBuildToolsAgentMode() {
60+
final String IMAGECODE = "org.graalvm.nativeimage.imagecode";
61+
try {
62+
System.setProperty(IMAGECODE, "AgenT");
63+
64+
assertThatNoException().isThrownBy(() -> generator().processAheadOfTime(Stream.empty()));
65+
}
66+
finally {
67+
System.clearProperty(IMAGECODE);
68+
}
69+
}
70+
71+
private static TestContextAotGenerator generator() {
72+
InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles();
73+
return new TestContextAotGenerator(generatedFiles);
74+
}
75+
76+
}

0 commit comments

Comments
 (0)