From b0ee513db635e7939f256d548b0c0008d5f47429 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 7 Sep 2022 17:11:57 +0200 Subject: [PATCH] Introduce AotTestAttributes mechanism in the TestContext framework For certain use cases it is beneficial to be able to compute something during AOT build-time processing and then retrieve the result of that computation during AOT run-time execution, without having to deal with code generation on your own. To support such use cases, this commit introduces an AotTestAttributes mechanism in the Spring TestContext Framework with the following feature set. - conceptually similar to org.springframework.core.AttributeAccessor in the sense that attributes are generic metadata - allows an AOT-aware test component to contribute a key-value pair during the AOT build-time processing phase, where the key is a String and the value is a String - provides convenience methods for storing and retrieving attributes as boolean values - generates the necessary source code during the AOT build-time processing phase in the TestContext framework to create a persistent map of the attributes - provides a mechanism for accessing the stored attributes during AOT run-time execution Closes gh-29100 --- .../test/context/aot/AotTestAttributes.java | 136 ++++++++++++++++++ .../aot/AotTestAttributesCodeGenerator.java | 98 +++++++++++++ .../context/aot/AotTestAttributesFactory.java | 95 ++++++++++++ .../context/aot/DefaultAotTestAttributes.java | 70 +++++++++ .../context/aot/TestContextAotGenerator.java | 34 ++++- .../test/context/aot/AbstractAotTests.java | 1 + .../aot/TestContextAotGeneratorTests.java | 58 +++++--- .../basic/BasicSpringVintageTests.java | 31 ++++ 8 files changed, 502 insertions(+), 21 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesCodeGenerator.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java 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 new file mode 100644 index 00000000000..0e2897497e8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2022 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.lang.Nullable; + +/** + * Holder for metadata specific to ahead-of-time (AOT) support in the Spring + * TestContext Framework. + * + *

AOT test attributes are supported in two modes of operation: build-time + * 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}, + * run-time mode applies. + * + *

For example, if a test component computes something at build time that + * cannot be computed at run time, the result of the build-time computation can + * be stored as an AOT attribute and retrieved at run time without repeating the + * computation. + * + *

An {@link AotContextLoader} would typically contribute an attribute in + * {@link AotContextLoader#loadContextForAotProcessing loadContextForAotProcessing()}; + * whereas, an {@link AotTestExecutionListener} would typically contribute an attribute + * in {@link AotTestExecutionListener#processAheadOfTime processAheadOfTime()}. + * Any other test component — such as a + * {@link org.springframework.test.context.TestContextBootstrapper TestContextBootstrapper} + * — 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 + * if invocations of {@link #setAttribute(String, String)} and + * {@link #removeAttribute(String)} are permitted. + * + * @author Sam Brannen + * @since 6.0 + */ +public interface AotTestAttributes { + + /** + * Get the current instance of {@code AotTestAttributes} to use. + *

See the class-level {@link AotTestAttributes Javadoc} for details on + * the two supported modes. + */ + static AotTestAttributes getInstance() { + return new DefaultAotTestAttributes(AotTestAttributesFactory.getAttributes()); + } + + + /** + * Set a {@code String} attribute for later retrieval during AOT run-time execution. + *

In general, users should take care to prevent overlaps with other + * metadata attributes by using fully-qualified names, perhaps using a + * class or package name as a prefix. + * @param name the unique attribute name + * @param value the associated attribute value + * @throws UnsupportedOperationException if invoked during + * {@linkplain AotDetector#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() + */ + void setAttribute(String name, String value); + + /** + * Set a {@code boolean} attribute for later retrieval during AOT run-time execution. + *

In general, users should take care to prevent overlaps with other + * metadata attributes by using fully-qualified names, perhaps using a + * class or package name as a prefix. + * @param name the unique attribute name + * @param value the associated attribute value + * @throws UnsupportedOperationException if invoked during + * {@linkplain AotDetector#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() + */ + default void setAttribute(String name, boolean value) { + setAttribute(name, Boolean.toString(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() + * @see #setAttribute(String, String) + */ + void removeAttribute(String name); + + /** + * Retrieve the attribute value for the given name as a {@link String}. + * @param name the unique attribute name + * @return the associated attribute value, or {@code null} if not found + * @see #getBoolean(String) + * @see #setAttribute(String, String) + */ + @Nullable + String getString(String name); + + /** + * Retrieve the attribute value for the given name as a {@code boolean}. + * @param name the unique attribute name + * @return {@code true} if the attribute is set to "true" (ignoring case), + * {@code} false otherwise + * @see #getString(String) + * @see #setAttribute(String, String) + * @see Boolean#parseBoolean(String) + */ + default boolean getBoolean(String name) { + return Boolean.parseBoolean(getString(name)); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesCodeGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesCodeGenerator.java new file mode 100644 index 00000000000..1feace365f8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesCodeGenerator.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2022 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.HashMap; +import java.util.Map; + +import javax.lang.model.element.Modifier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedClasses; +import org.springframework.core.log.LogMessage; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; +import org.springframework.javapoet.TypeName; +import org.springframework.javapoet.TypeSpec; + +/** + * Internal code generator for {@link AotTestAttributes}. + * + * @author Sam Brannen + * @since 6.0 + */ +class AotTestAttributesCodeGenerator { + + private static final Log logger = LogFactory.getLog(AotTestAttributesCodeGenerator.class); + + // Map + private static final TypeName MAP_TYPE = ParameterizedTypeName.get(Map.class, String.class, String.class); + + private static final String GENERATED_SUFFIX = "Generated"; + + static final String GENERATED_ATTRIBUTES_CLASS_NAME = AotTestAttributes.class.getName() + "__" + GENERATED_SUFFIX; + + static final String GENERATED_ATTRIBUTES_METHOD_NAME = "getAttributes"; + + + private final Map attributes; + + private final GeneratedClass generatedClass; + + + AotTestAttributesCodeGenerator(Map attributes, GeneratedClasses generatedClasses) { + this.attributes = attributes; + this.generatedClass = generatedClasses.addForFeature(GENERATED_SUFFIX, this::generateType); + } + + + GeneratedClass getGeneratedClass() { + return this.generatedClass; + } + + private void generateType(TypeSpec.Builder type) { + logger.debug(LogMessage.format("Generating AOT test attributes in %s", + this.generatedClass.getName().reflectionName())); + type.addJavadoc("Generated map for {@link $T}.", AotTestAttributes.class); + type.addModifiers(Modifier.PUBLIC); + type.addMethod(generateMethod()); + } + + private MethodSpec generateMethod() { + MethodSpec.Builder method = MethodSpec.methodBuilder(GENERATED_ATTRIBUTES_METHOD_NAME); + method.addModifiers(Modifier.PUBLIC, Modifier.STATIC); + method.returns(MAP_TYPE); + method.addCode(generateCode()); + return method.build(); + } + + private CodeBlock generateCode() { + CodeBlock.Builder code = CodeBlock.builder(); + code.addStatement("$T map = new $T<>()", MAP_TYPE, HashMap.class); + this.attributes.forEach((key, value) -> { + logger.trace(LogMessage.format("Storing AOT test attribute: %s = %s", key, value)); + code.addStatement("map.put($S, $S)", key, value); + }); + code.addStatement("return map"); + return code.build(); + } + +} 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 new file mode 100644 index 00000000000..5f86413a3e6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2022 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.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.aot.AotDetector; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Factory for {@link AotTestAttributes}. + * + * @author Sam Brannen + * @since 6.0 + */ +final class AotTestAttributesFactory { + + @Nullable + private static volatile Map attributes; + + + private 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() + * AOT execution mode} and otherwise creates a new map for storing attributes + * during the AOT processing phase. + */ + static Map getAttributes() { + Map attrs = attributes; + if (attrs == null) { + synchronized (AotTestAttributesFactory.class) { + attrs = attributes; + if (attrs == null) { + attrs = (AotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>()); + attributes = attrs; + } + } + } + return attrs; + } + + /** + * Reset AOT test attributes. + *

Only for internal use. + */ + static void reset() { + synchronized (AotTestAttributesFactory.class) { + attributes = null; + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static Map loadAttributesMap() { + String className = AotTestAttributesCodeGenerator.GENERATED_ATTRIBUTES_CLASS_NAME; + String methodName = AotTestAttributesCodeGenerator.GENERATED_ATTRIBUTES_METHOD_NAME; + try { + Class clazz = ClassUtils.forName(className, null); + Method method = ReflectionUtils.findMethod(clazz, methodName); + Assert.state(method != null, () -> "No %s() method found in %s".formatted(methodName, clazz.getName())); + Map attributes = (Map) ReflectionUtils.invokeMethod(method, null); + return Collections.unmodifiableMap(attributes); + } + catch (IllegalStateException ex) { + throw ex; + } + catch (Exception ex) { + throw new IllegalStateException("Failed to invoke %s() method on %s".formatted(methodName, className), ex); + } + } + +} 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 new file mode 100644 index 00000000000..2c5cee5c4dd --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2022 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.Map; + +import org.springframework.aot.AotDetector; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link AotTestAttributes} backed by a {@link Map}. + * + * @author Sam Brannen + * @since 6.0 + */ +class DefaultAotTestAttributes implements AotTestAttributes { + + private final Map attributes; + + + DefaultAotTestAttributes(Map attributes) { + this.attributes = attributes; + } + + + @Override + public void setAttribute(String name, String value) { + assertNotInAotRuntime(); + Assert.notNull(value, "'value' must not be null"); + Assert.isTrue(!this.attributes.containsKey(name), + () -> "AOT attributes cannot be overridden. Name '%s' is already in use.".formatted(name)); + this.attributes.put(name, value); + } + + @Override + public void removeAttribute(String name) { + assertNotInAotRuntime(); + this.attributes.remove(name); + } + + @Override + @Nullable + public String getString(String name) { + return this.attributes.get(name); + } + + + private static void assertNotInAotRuntime() { + if (AotDetector.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/TestContextAotGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java index 4d0f9467750..185cd908a91 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 @@ -17,6 +17,7 @@ package org.springframework.test.context.aot; import java.util.Collections; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; @@ -108,10 +109,21 @@ public class TestContextAotGenerator { * @throws TestContextAotException if an error occurs during AOT processing */ public void processAheadOfTime(Stream> testClasses) throws TestContextAotException { - MultiValueMap> mergedConfigMappings = new LinkedMultiValueMap<>(); - testClasses.forEach(testClass -> mergedConfigMappings.add(buildMergedContextConfiguration(testClass), testClass)); - MultiValueMap> initializerClassMappings = processAheadOfTime(mergedConfigMappings); - generateTestAotMappings(initializerClassMappings); + try { + // Make sure AOT attributes are cleared before processing + AotTestAttributesFactory.reset(); + + MultiValueMap> mergedConfigMappings = new LinkedMultiValueMap<>(); + testClasses.forEach(testClass -> mergedConfigMappings.add(buildMergedContextConfiguration(testClass), testClass)); + MultiValueMap> initializerClassMappings = processAheadOfTime(mergedConfigMappings); + + generateTestAotMappings(initializerClassMappings); + generateAotTestAttributes(); + } + finally { + // Clear AOT attributes after processing + AotTestAttributesFactory.reset(); + } } private MultiValueMap> processAheadOfTime(MultiValueMap> mergedConfigMappings) { @@ -240,6 +252,20 @@ public class TestContextAotGenerator { registerPublicMethods(className); } + private void generateAotTestAttributes() { + ClassNameGenerator classNameGenerator = new ClassNameGenerator(AotTestAttributes.class); + DefaultGenerationContext generationContext = + new DefaultGenerationContext(classNameGenerator, this.generatedFiles, this.runtimeHints); + GeneratedClasses generatedClasses = generationContext.getGeneratedClasses(); + + Map attributes = AotTestAttributesFactory.getAttributes(); + AotTestAttributesCodeGenerator codeGenerator = + new AotTestAttributesCodeGenerator(attributes, generatedClasses); + generationContext.writeGeneratedContent(); + String className = codeGenerator.getGeneratedClass().getName().reflectionName(); + registerPublicMethods(className); + } + private void registerPublicMethods(String className) { this.runtimeHints.reflection().registerType(TypeReference.of(className), INVOKE_PUBLIC_METHODS); } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java index ea98ff704ef..142142949e0 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java @@ -32,6 +32,7 @@ abstract class AbstractAotTests { static final String[] expectedSourceFilesForBasicSpringTests = { // Global "org/springframework/test/context/aot/TestAotMappings__Generated.java", + "org/springframework/test/context/aot/AotTestAttributes__Generated.java", // BasicSpringJupiterSharedConfigTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext001_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext001_BeanDefinitions.java", diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java index faec46178ce..8b7a33bd83b 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java @@ -26,6 +26,7 @@ import javax.sql.DataSource; import org.junit.jupiter.api.Test; +import org.springframework.aot.AotDetector; import org.springframework.aot.generate.DefaultGenerationContext; import org.springframework.aot.generate.GeneratedFiles.Kind; import org.springframework.aot.generate.InMemoryGeneratedFiles; @@ -60,6 +61,7 @@ import org.springframework.web.context.WebApplicationContext; import static java.util.Comparator.comparing; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS; import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_METHODS; import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS; @@ -73,7 +75,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC /** * Tests for {@link TestContextAotGenerator}, {@link TestAotMappings}, - * {@link AotContextLoader}, and run-time hints. + * {@link AotTestAttributes}, {@link AotContextLoader}, and run-time hints. * * @author Sam Brannen * @since 6.0 @@ -109,29 +111,50 @@ class TestContextAotGeneratorTests extends AbstractAotTests { assertThat(sourceFiles).containsExactlyInAnyOrder(expectedSourceFiles); TestCompiler.forSystem().withFiles(generatedFiles).compile(ThrowingConsumer.of(compiled -> { - TestAotMappings aotTestMappings = new TestAotMappings(); - for (Class testClass : testClasses) { - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); - ApplicationContextInitializer contextInitializer = - aotTestMappings.getContextInitializer(testClass); - assertThat(contextInitializer).isNotNull(); - ApplicationContext context = ((AotContextLoader) mergedConfig.getContextLoader()) - .loadContextForAotRuntime(mergedConfig, contextInitializer); - if (context instanceof WebApplicationContext wac) { - assertContextForWebTests(wac); - } - else if (testClass.getPackageName().contains("jdbc")) { - assertContextForJdbcTests(context); - } - else { - assertContextForBasicTests(context); + try { + System.setProperty(AotDetector.AOT_ENABLED, "true"); + AotTestAttributesFactory.reset(); + + AotTestAttributes aotAttributes = AotTestAttributes.getInstance(); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> aotAttributes.setAttribute("foo", "bar")) + .withMessage("AOT attributes cannot be modified during AOT run-time execution"); + String key = "@SpringBootConfiguration-" + BasicSpringVintageTests.class.getName(); + assertThat(aotAttributes.getString(key)).isEqualTo("org.example.Main"); + assertThat(aotAttributes.getBoolean(key + "-active1")).isTrue(); + assertThat(aotAttributes.getBoolean(key + "-active2")).isTrue(); + assertThat(aotAttributes.getString("bogus")).isNull(); + assertThat(aotAttributes.getBoolean("bogus")).isFalse(); + + TestAotMappings testAotMappings = new TestAotMappings(); + for (Class testClass : testClasses) { + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + ApplicationContextInitializer contextInitializer = + testAotMappings.getContextInitializer(testClass); + assertThat(contextInitializer).isNotNull(); + ApplicationContext context = ((AotContextLoader) mergedConfig.getContextLoader()) + .loadContextForAotRuntime(mergedConfig, contextInitializer); + if (context instanceof WebApplicationContext wac) { + assertContextForWebTests(wac); + } + else if (testClass.getPackageName().contains("jdbc")) { + assertContextForJdbcTests(context); + } + else { + assertContextForBasicTests(context); + } } } + finally { + System.clearProperty(AotDetector.AOT_ENABLED); + AotTestAttributesFactory.reset(); + } })); } private static void assertRuntimeHints(RuntimeHints runtimeHints) { assertReflectionRegistered(runtimeHints, TestAotMappings.GENERATED_MAPPINGS_CLASS_NAME, INVOKE_PUBLIC_METHODS); + assertReflectionRegistered(runtimeHints, AotTestAttributesCodeGenerator.GENERATED_ATTRIBUTES_CLASS_NAME, INVOKE_PUBLIC_METHODS); Stream.of( org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.class, @@ -334,6 +357,7 @@ class TestContextAotGeneratorTests extends AbstractAotTests { private static final String[] expectedSourceFiles = { // Global "org/springframework/test/context/aot/TestAotMappings__Generated.java", + "org/springframework/test/context/aot/AotTestAttributes__Generated.java", // BasicSpringJupiterSharedConfigTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext001_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext001_BeanDefinitions.java", 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 13f298c6207..517fe0a7086 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 @@ -18,13 +18,16 @@ 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; import org.springframework.test.context.BootstrapWith; import org.springframework.test.context.ContextConfiguration; 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.samples.basic.BasicSpringVintageTests.CustomXmlBootstrapper; import org.springframework.test.context.aot.samples.common.MessageService; import org.springframework.test.context.junit4.SpringRunner; @@ -63,10 +66,38 @@ public class BasicSpringVintageTests { } public static class CustomXmlBootstrapper extends DefaultTestContextBootstrapper { + @Override protected Class getDefaultContextLoaderClass(Class testClass) { return GenericXmlContextLoader.class; } + + @Override + protected MergedContextConfiguration processMergedContextConfiguration(MergedContextConfiguration mergedConfig) { + String stringKey = "@SpringBootConfiguration-" + mergedConfig.getTestClass().getName(); + String booleanKey1 = "@SpringBootConfiguration-" + mergedConfig.getTestClass().getName() + "-active1"; + String booleanKey2 = "@SpringBootConfiguration-" + mergedConfig.getTestClass().getName() + "-active2"; + AotTestAttributes aotAttributes = AotTestAttributes.getInstance(); + if (AotDetector.useGeneratedArtifacts()) { + assertThat(aotAttributes.getString(stringKey)) + .as("AOT String attribute must already be present during AOT run-time execution") + .isEqualTo("org.example.Main"); + assertThat(aotAttributes.getBoolean(booleanKey1)) + .as("AOT boolean attribute 1 must already be present during AOT run-time execution") + .isTrue(); + assertThat(aotAttributes.getBoolean(booleanKey2)) + .as("AOT boolean attribute 2 must already be present during AOT run-time execution") + .isTrue(); + } + else { + // Set AOT attributes during AOT build-time processing + aotAttributes.setAttribute(stringKey, "org.example.Main"); + aotAttributes.setAttribute(booleanKey1, "TrUe"); + aotAttributes.setAttribute(booleanKey2, true); + } + return mergedConfig; + } + } }