Skip to content

Commit

Permalink
Support Test AOT processing with GraalVM tracing agent and NBT
Browse files Browse the repository at this point in the history
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 spring-projectsgh-30281
  • Loading branch information
sbrannen committed May 10, 2023
1 parent 89bcee6 commit 1113096
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -16,7 +16,6 @@

package org.springframework.test.context.aot;

import org.springframework.aot.AotDetector;
import org.springframework.lang.Nullable;

/**
Expand All @@ -27,7 +26,7 @@
* and run-time. At build time, test components can {@linkplain #setAttribute contribute}
* attributes during the AOT processing phase. At run time, test components can
* {@linkplain #getString(String) retrieve} attributes that were contributed at
* build time. If {@link AotDetector#useGeneratedArtifacts()} returns {@code true},
* build time. If {@link TestAotDetector#useGeneratedArtifacts()} returns {@code true},
* run-time mode applies.
*
* <p>For example, if a test component computes something at build time that
Expand All @@ -44,7 +43,7 @@
* &mdash; can choose to contribute an attribute at any point in time. Note that
* contributing an attribute during standard JVM test execution will not have any
* adverse side effect since AOT attributes will be ignored in that scenario. In
* any case, you should use {@link AotDetector#useGeneratedArtifacts()} to determine
* any case, you should use {@link TestAotDetector#useGeneratedArtifacts()} to determine
* if invocations of {@link #setAttribute(String, String)} and
* {@link #removeAttribute(String)} are permitted.
*
Expand All @@ -71,12 +70,12 @@ static AotTestAttributes getInstance() {
* @param name the unique attribute name
* @param value the associated attribute value
* @throws UnsupportedOperationException if invoked during
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
* {@linkplain TestAotDetector#useGeneratedArtifacts() AOT run-time execution}
* @throws IllegalArgumentException if the provided value is {@code null} or
* if an attempt is made to override an existing attribute
* @see #setAttribute(String, boolean)
* @see #removeAttribute(String)
* @see AotDetector#useGeneratedArtifacts()
* @see TestAotDetector#useGeneratedArtifacts()
*/
void setAttribute(String name, String value);

Expand All @@ -88,13 +87,13 @@ static AotTestAttributes getInstance() {
* @param name the unique attribute name
* @param value the associated attribute value
* @throws UnsupportedOperationException if invoked during
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
* {@linkplain TestAotDetector#useGeneratedArtifacts() AOT run-time execution}
* @throws IllegalArgumentException if an attempt is made to override an
* existing attribute
* @see #setAttribute(String, String)
* @see #removeAttribute(String)
* @see Boolean#toString(boolean)
* @see AotDetector#useGeneratedArtifacts()
* @see TestAotDetector#useGeneratedArtifacts()
*/
default void setAttribute(String name, boolean value) {
setAttribute(name, Boolean.toString(value));
Expand All @@ -104,8 +103,8 @@ default void setAttribute(String name, boolean value) {
* Remove the attribute stored under the provided name.
* @param name the unique attribute name
* @throws UnsupportedOperationException if invoked during
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
* @see AotDetector#useGeneratedArtifacts()
* {@linkplain TestAotDetector#useGeneratedArtifacts() AOT run-time execution}
* @see TestAotDetector#useGeneratedArtifacts()
* @see #setAttribute(String, String)
*/
void removeAttribute(String name);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -19,7 +19,6 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.aot.AotDetector;
import org.springframework.lang.Nullable;

/**
Expand All @@ -40,7 +39,7 @@ private AotTestAttributesFactory() {
/**
* Get the underlying attributes map.
* <p>If the map is not already loaded, this method loads the map from the
* generated class when running in {@linkplain AotDetector#useGeneratedArtifacts()
* generated class when running in {@linkplain TestAotDetector#useGeneratedArtifacts()
* AOT execution mode} and otherwise creates a new map for storing attributes
* during the AOT processing phase.
*/
Expand All @@ -50,7 +49,7 @@ static Map<String, String> getAttributes() {
synchronized (AotTestAttributesFactory.class) {
attrs = attributes;
if (attrs == null) {
attrs = (AotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>());
attrs = (TestAotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>());
attributes = attrs;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -19,7 +19,6 @@
import java.util.Map;
import java.util.function.Supplier;

import org.springframework.aot.AotDetector;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
Expand All @@ -30,7 +29,7 @@
*
* <p>Intended solely for internal use within the framework.
*
* <p>If we are not running in {@linkplain AotDetector#useGeneratedArtifacts()
* <p>If we are not running in {@linkplain TestAotDetector#useGeneratedArtifacts()
* AOT mode} or if a test class is not {@linkplain #isSupportedTestClass(Class)
* supported} in AOT mode, {@link #getContextInitializer(Class)} and
* {@link #getContextInitializerClass(Class)} will return {@code null}.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -19,7 +19,6 @@
import java.util.Map;
import java.util.function.Supplier;

import org.springframework.aot.AotDetector;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
Expand All @@ -45,7 +44,7 @@ private AotTestContextInitializersFactory() {
/**
* Get the underlying map.
* <p>If the map is not already loaded, this method loads the map from the
* generated class when running in {@linkplain AotDetector#useGeneratedArtifacts()
* generated class when running in {@linkplain TestAotDetector#useGeneratedArtifacts()
* AOT execution mode} and otherwise creates an immutable, empty map.
*/
static Map<String, Supplier<ApplicationContextInitializer<ConfigurableApplicationContext>>> getContextInitializers() {
Expand All @@ -54,7 +53,7 @@ static Map<String, Supplier<ApplicationContextInitializer<ConfigurableApplicatio
synchronized (AotTestContextInitializersFactory.class) {
initializers = contextInitializers;
if (initializers == null) {
initializers = (AotDetector.useGeneratedArtifacts() ? loadContextInitializersMap() : Map.of());
initializers = (TestAotDetector.useGeneratedArtifacts() ? loadContextInitializersMap() : Map.of());
contextInitializers = initializers;
}
}
Expand All @@ -68,7 +67,7 @@ static Map<String, Class<ApplicationContextInitializer<?>>> getContextInitialize
synchronized (AotTestContextInitializersFactory.class) {
initializerClasses = contextInitializerClasses;
if (initializerClasses == null) {
initializerClasses = (AotDetector.useGeneratedArtifacts() ? loadContextInitializerClassesMap() : Map.of());
initializerClasses = (TestAotDetector.useGeneratedArtifacts() ? loadContextInitializerClassesMap() : Map.of());
contextInitializerClasses = initializerClasses;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -18,7 +18,6 @@

import java.util.Map;

import org.springframework.aot.AotDetector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

Expand Down Expand Up @@ -61,7 +60,7 @@ public String getString(String name) {


private static void assertNotInAotRuntime() {
if (AotDetector.useGeneratedArtifacts()) {
if (TestAotDetector.useGeneratedArtifacts()) {
throw new UnsupportedOperationException(
"AOT attributes cannot be modified during AOT run-time execution");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2002-2023 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 org.springframework.aot.AotDetector;
import org.springframework.core.SpringProperties;
import org.springframework.util.StringUtils;

/**
* TestContext framework specific utility for determining if AOT-processed
* optimizations must be used rather than the regular runtime.
*
* <p>Strictly for internal use within the framework.
*
* @author Sam Brannen
* @since 6.0.9
*/
public abstract class TestAotDetector {

/**
* Determine whether AOT optimizations must be considered at runtime.
* <p>This can be triggered using the {@value AotDetector#AOT_ENABLED}
* Spring property or via GraalVM's {@code "org.graalvm.nativeimage.imagecode"}
* JVM system property (if set to any non-empty value other than {@code agent}).
* @return {@code true} if AOT optimizations must be considered
* @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>
* @see AotDetector#useGeneratedArtifacts()
*/
public static boolean useGeneratedArtifacts() {
return (SpringProperties.getFlag(AotDetector.AOT_ENABLED) || inNativeImage());
}

/**
* Determine if we are currently running within a GraalVM native image from
* the perspective of the TestContext framework.
* @return {@code true} if the {@code org.graalvm.nativeimage.imagecode} JVM
* system property has been set to any value other than {@code agent}.
*/
private static boolean inNativeImage() {
String imageCode = System.getProperty("org.graalvm.nativeimage.imagecode");
return (StringUtils.hasText(imageCode) && !"agent".equalsIgnoreCase(imageCode.trim()));
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -26,7 +26,6 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.aot.AotDetector;
import org.springframework.aot.generate.ClassNameGenerator;
import org.springframework.aot.generate.DefaultGenerationContext;
import org.springframework.aot.generate.GeneratedClasses;
Expand Down Expand Up @@ -123,7 +122,7 @@ public final RuntimeHints getRuntimeHints() {
* @throws TestContextAotException if an error occurs during AOT processing
*/
public void processAheadOfTime(Stream<Class<?>> testClasses) throws TestContextAotException {
Assert.state(!AotDetector.useGeneratedArtifacts(), "Cannot perform AOT processing during AOT run-time execution");
Assert.state(!TestAotDetector.useGeneratedArtifacts(), "Cannot perform AOT processing during AOT run-time execution");
try {
resetAotFactories();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -21,7 +21,6 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.aot.AotDetector;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
Expand All @@ -36,6 +35,7 @@
import org.springframework.test.context.SmartContextLoader;
import org.springframework.test.context.aot.AotContextLoader;
import org.springframework.test.context.aot.AotTestContextInitializers;
import org.springframework.test.context.aot.TestAotDetector;
import org.springframework.test.context.aot.TestContextAotException;
import org.springframework.test.context.util.TestContextSpringFactoriesUtils;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -248,7 +248,7 @@ private ContextLoader getContextLoader(MergedContextConfiguration mergedConfig)
*/
@SuppressWarnings("unchecked")
private MergedContextConfiguration replaceIfNecessary(MergedContextConfiguration mergedConfig) {
if (AotDetector.useGeneratedArtifacts()) {
if (TestAotDetector.useGeneratedArtifacts()) {
Class<?> testClass = mergedConfig.getTestClass();
Class<? extends ApplicationContextInitializer<?>> contextInitializerClass =
this.aotTestContextInitializers.getContextInitializerClass(testClass);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2002-2023 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.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import org.springframework.aot.generate.InMemoryGeneratedFiles;

import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;

/**
* Tests for error cases in {@link TestContextAotGenerator}.
*
* @author Sam Brannen
* @since 6.0.9
*/
class TestContextAotGeneratorErrorCaseTests {

@ParameterizedTest
@CsvSource(delimiter = '=', textBlock = """
'spring.aot.enabled' = 'true'
'org.graalvm.nativeimage.imagecode' = 'buildtime'
'org.graalvm.nativeimage.imagecode' = 'runtime'
'org.graalvm.nativeimage.imagecode' = 'bogus'
""")
void attemptToProcessWhileRunningInAotMode(String property, String value) {
try {
System.setProperty(property, value);

assertThatIllegalStateException()
.isThrownBy(() -> generator().processAheadOfTime(Stream.empty()))
.withMessage("Cannot perform AOT processing during AOT run-time execution");
}
finally {
System.clearProperty(property);
}
}

@Test
void attemptToProcessWhileRunningInGraalVmNativeBuildToolsAgentMode() {
final String IMAGECODE = "org.graalvm.nativeimage.imagecode";
try {
System.setProperty(IMAGECODE, "AgenT");

assertThatNoException().isThrownBy(() -> generator().processAheadOfTime(Stream.empty()));
}
finally {
System.clearProperty(IMAGECODE);
}
}

private static TestContextAotGenerator generator() {
InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles();
return new TestContextAotGenerator(generatedFiles);
}

}
Loading

0 comments on commit 1113096

Please sign in to comment.