diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java index ae94b4c3b4..ff17068fbb 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.lang.Nullable; * @author Juergen Hoeller * @author Phillip Webb * @author Andy Clement + * @author Sam Brannen * @since 3.0 * @see org.springframework.expression.spel.standard.SpelExpressionParser#SpelExpressionParser(SpelParserConfiguration) */ diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java index 0e2897497e..2f86fda382 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java @@ -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. @@ -16,7 +16,6 @@ package org.springframework.test.context.aot; -import org.springframework.aot.AotDetector; import org.springframework.lang.Nullable; /** @@ -27,7 +26,7 @@ import org.springframework.lang.Nullable; * 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. * *

For example, if a test component computes something at build time that @@ -44,7 +43,7 @@ import org.springframework.lang.Nullable; * — 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. * @@ -71,12 +70,12 @@ public interface AotTestAttributes { * @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); @@ -88,13 +87,13 @@ public interface AotTestAttributes { * @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)); @@ -104,8 +103,8 @@ public interface AotTestAttributes { * 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); diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java index 0ca8bb80cc..185b6a573c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java @@ -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. @@ -19,7 +19,6 @@ package org.springframework.test.context.aot; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.aot.AotDetector; import org.springframework.lang.Nullable; /** @@ -40,7 +39,7 @@ final class AotTestAttributesFactory { /** * Get the underlying attributes map. *

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. */ @@ -50,7 +49,7 @@ final class AotTestAttributesFactory { synchronized (AotTestAttributesFactory.class) { attrs = attributes; if (attrs == null) { - attrs = (AotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>()); + attrs = (TestAotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>()); attributes = attrs; } } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java index a377ce7c09..0dd5d91266 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java @@ -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. @@ -19,7 +19,6 @@ package org.springframework.test.context.aot; 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; @@ -30,7 +29,7 @@ import org.springframework.lang.Nullable; * *

Intended solely for internal use within the framework. * - *

If we are not running in {@linkplain AotDetector#useGeneratedArtifacts() + *

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}. diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java index 8700ced840..1e6685c109 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java @@ -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. @@ -19,7 +19,6 @@ package org.springframework.test.context.aot; 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; @@ -45,7 +44,7 @@ final class AotTestContextInitializersFactory { /** * Get the underlying map. *

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>> getContextInitializers() { @@ -54,7 +53,7 @@ final class AotTestContextInitializersFactory { synchronized (AotTestContextInitializersFactory.class) { initializers = contextInitializers; if (initializers == null) { - initializers = (AotDetector.useGeneratedArtifacts() ? loadContextInitializersMap() : Map.of()); + initializers = (TestAotDetector.useGeneratedArtifacts() ? loadContextInitializersMap() : Map.of()); contextInitializers = initializers; } } @@ -68,7 +67,7 @@ final class AotTestContextInitializersFactory { synchronized (AotTestContextInitializersFactory.class) { initializerClasses = contextInitializerClasses; if (initializerClasses == null) { - initializerClasses = (AotDetector.useGeneratedArtifacts() ? loadContextInitializerClassesMap() : Map.of()); + initializerClasses = (TestAotDetector.useGeneratedArtifacts() ? loadContextInitializerClassesMap() : Map.of()); contextInitializerClasses = initializerClasses; } } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java b/spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java index 2c5cee5c4d..ac3dbfa1bd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java @@ -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. @@ -18,7 +18,6 @@ package org.springframework.test.context.aot; import java.util.Map; -import org.springframework.aot.AotDetector; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -61,7 +60,7 @@ class DefaultAotTestAttributes implements AotTestAttributes { private static void assertNotInAotRuntime() { - if (AotDetector.useGeneratedArtifacts()) { + if (TestAotDetector.useGeneratedArtifacts()) { throw new UnsupportedOperationException( "AOT attributes cannot be modified during AOT run-time execution"); } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestAotDetector.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestAotDetector.java new file mode 100644 index 0000000000..f095d40ff1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestAotDetector.java @@ -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. + * + *

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. + *

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 GraalVM's ImageInfo.java + * @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())); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java index b8d40fb3ae..1909a5a056 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java @@ -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. @@ -26,7 +26,6 @@ import java.util.stream.Stream; 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; @@ -123,7 +122,7 @@ public class TestContextAotGenerator { * @throws TestContextAotException if an error occurs during AOT processing */ public void processAheadOfTime(Stream> 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(); diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java index 1278b3388e..a6ae78aa5c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java @@ -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. @@ -21,7 +21,6 @@ import java.util.List; 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; @@ -36,6 +35,7 @@ import org.springframework.test.context.MergedContextConfiguration; 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; @@ -248,7 +248,7 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext */ @SuppressWarnings("unchecked") private MergedContextConfiguration replaceIfNecessary(MergedContextConfiguration mergedConfig) { - if (AotDetector.useGeneratedArtifacts()) { + if (TestAotDetector.useGeneratedArtifacts()) { Class testClass = mergedConfig.getTestClass(); Class> contextInitializerClass = this.aotTestContextInitializers.getContextInitializerClass(testClass); diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorErrorCaseTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorErrorCaseTests.java new file mode 100644 index 0000000000..f886d96bd2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorErrorCaseTests.java @@ -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); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests.java index 517fe0a708..db63efb3b8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests.java @@ -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. @@ -18,7 +18,6 @@ package org.springframework.test.context.aot.samples.basic; import org.junit.runner.RunWith; -import org.springframework.aot.AotDetector; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; @@ -28,6 +27,7 @@ import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.aot.AotTestAttributes; +import org.springframework.test.context.aot.TestAotDetector; import org.springframework.test.context.aot.samples.basic.BasicSpringVintageTests.CustomXmlBootstrapper; import org.springframework.test.context.aot.samples.common.MessageService; import org.springframework.test.context.junit4.SpringRunner; @@ -78,7 +78,7 @@ public class BasicSpringVintageTests { String booleanKey1 = "@SpringBootConfiguration-" + mergedConfig.getTestClass().getName() + "-active1"; String booleanKey2 = "@SpringBootConfiguration-" + mergedConfig.getTestClass().getName() + "-active2"; AotTestAttributes aotAttributes = AotTestAttributes.getInstance(); - if (AotDetector.useGeneratedArtifacts()) { + if (TestAotDetector.useGeneratedArtifacts()) { assertThat(aotAttributes.getString(stringKey)) .as("AOT String attribute must already be present during AOT run-time execution") .isEqualTo("org.example.Main"); diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/ImportsContextCustomizerFactory.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/ImportsContextCustomizerFactory.java index 059a42cee0..431ff14489 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/ImportsContextCustomizerFactory.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/ImportsContextCustomizerFactory.java @@ -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. @@ -19,7 +19,6 @@ package org.springframework.test.context.aot.samples.basic; import java.util.Arrays; import java.util.List; -import org.springframework.aot.AotDetector; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; import org.springframework.context.annotation.Import; @@ -28,6 +27,7 @@ import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.aot.TestAotDetector; /** * Emulates {@code ImportsContextCustomizerFactory} from Spring Boot's testing support. @@ -41,7 +41,7 @@ class ImportsContextCustomizerFactory implements ContextCustomizerFactory { public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - if (AotDetector.useGeneratedArtifacts()) { + if (TestAotDetector.useGeneratedArtifacts()) { return null; } if (testClass.getName().startsWith("org.springframework.test.context.aot.samples") &&