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; + } + } }