diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java new file mode 100644 index 00000000000..6a6d4e68137 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java @@ -0,0 +1,112 @@ +/* + * 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.cache; + +import java.util.Collections; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.core.style.ToStringCreator; +import org.springframework.lang.Nullable; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.MergedContextConfiguration; + +/** + * {@link MergedContextConfiguration} implementation based on an AOT-generated + * {@link ApplicationContextInitializer} that is used to load an AOT-optimized + * {@link org.springframework.context.ApplicationContext ApplicationContext}. + * + *

An {@code ApplicationContext} should not be loaded using the metadata in + * this {@code AotMergedContextConfiguration}. Rather the metadata from the + * {@linkplain #getOriginal() original} {@code MergedContextConfiguration} must + * be used. + * + * @author Sam Brannen + * @since 6.0 + */ +class AotMergedContextConfiguration extends MergedContextConfiguration { + + private static final long serialVersionUID = 1963364911008547843L; + + private final Class> contextInitializerClass; + + private final MergedContextConfiguration original; + + + AotMergedContextConfiguration(Class testClass, + Class> contextInitializerClass, + MergedContextConfiguration original, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + + super(testClass, null, null, Collections.singleton(contextInitializerClass), null, + original.getContextLoader(), cacheAwareContextLoaderDelegate, original.getParent()); + this.contextInitializerClass = contextInitializerClass; + this.original = original; + } + + + /** + * Get the original {@link MergedContextConfiguration} that this + * {@code AotMergedContextConfiguration} was created for. + */ + MergedContextConfiguration getOriginal() { + return this.original; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || other.getClass() != getClass()) { + return false; + } + AotMergedContextConfiguration that = (AotMergedContextConfiguration) other; + if (!this.contextInitializerClass.equals(that.contextInitializerClass)) { + return false; + } + if (!nullSafeClassName(getContextLoader()).equals(nullSafeClassName(that.getContextLoader()))) { + return false; + } + if (getParent() == null) { + if (that.getParent() != null) { + return false; + } + } + else if (!getParent().equals(that.getParent())) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = this.contextInitializerClass.hashCode(); + result = 31 * result + nullSafeClassName(getContextLoader()).hashCode(); + result = 31 * result + (getParent() != null ? getParent().hashCode() : 0); + return result; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("testClass", getTestClass().getName()) + .append("contextInitializerClass", this.contextInitializerClass.getName()) + .append("original", this.original) + .toString(); + } + +} 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 a2919db9341..4f9f6784f29 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 @@ -86,18 +86,19 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext @Override public boolean isContextLoaded(MergedContextConfiguration mergedContextConfiguration) { synchronized (this.contextCache) { - return this.contextCache.contains(mergedContextConfiguration); + return this.contextCache.contains(replaceIfNecessary(mergedContextConfiguration)); } } @Override public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) { + mergedContextConfiguration = replaceIfNecessary(mergedContextConfiguration); synchronized (this.contextCache) { ApplicationContext context = this.contextCache.get(mergedContextConfiguration); if (context == null) { try { - if (runningInAotMode(mergedContextConfiguration.getTestClass())) { - context = loadContextInAotMode(mergedContextConfiguration); + if (mergedContextConfiguration instanceof AotMergedContextConfiguration aotMergedConfig) { + context = loadContextInAotMode(aotMergedConfig); } else { context = loadContextInternal(mergedContextConfiguration); @@ -129,7 +130,7 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext @Override public void closeContext(MergedContextConfiguration mergedContextConfiguration, @Nullable HierarchyMode hierarchyMode) { synchronized (this.contextCache) { - this.contextCache.remove(mergedContextConfiguration, hierarchyMode); + this.contextCache.remove(replaceIfNecessary(mergedContextConfiguration), hierarchyMode); } } @@ -163,23 +164,23 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext } } - protected ApplicationContext loadContextInAotMode(MergedContextConfiguration mergedConfig) throws Exception { - Class testClass = mergedConfig.getTestClass(); + protected ApplicationContext loadContextInAotMode(AotMergedContextConfiguration aotMergedConfig) throws Exception { + Class testClass = aotMergedConfig.getTestClass(); ApplicationContextInitializer contextInitializer = this.aotTestContextInitializers.getContextInitializer(testClass); Assert.state(contextInitializer != null, () -> "Failed to load AOT ApplicationContextInitializer for test class [%s]" .formatted(testClass.getName())); - ContextLoader contextLoader = getContextLoader(mergedConfig); - logger.info(LogMessage.format("Loading ApplicationContext in AOT mode for %s", mergedConfig)); + ContextLoader contextLoader = getContextLoader(aotMergedConfig); + logger.info(LogMessage.format("Loading ApplicationContext in AOT mode for %s", aotMergedConfig.getOriginal())); if (!((contextLoader instanceof AotContextLoader aotContextLoader) && - (aotContextLoader.loadContextForAotRuntime(mergedConfig, contextInitializer) + (aotContextLoader.loadContextForAotRuntime(aotMergedConfig.getOriginal(), contextInitializer) instanceof GenericApplicationContext gac))) { throw new TestContextAotException(""" Cannot load ApplicationContext for AOT runtime for %s. The configured \ ContextLoader [%s] must be an AotContextLoader and must create a \ GenericApplicationContext.""" - .formatted(mergedConfig, contextLoader.getClass().getName())); + .formatted(aotMergedConfig.getOriginal(), contextLoader.getClass().getName())); } gac.registerShutdownHook(); return gac; @@ -195,10 +196,24 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext } /** - * Determine if we are running in AOT mode for the supplied test class. + * If the test class associated with the supplied {@link MergedContextConfiguration} + * has an AOT-optimized {@link ApplicationContext}, this method will create an + * {@link AotMergedContextConfiguration} to replace the provided {@code MergedContextConfiguration}. + *

Otherwise, this method simply returns the supplied {@code MergedContextConfiguration} + * unmodified. + *

This allows for transparent {@link org.springframework.test.context.cache.ContextCache ContextCache} + * support for AOT-optimized application contexts. */ - private boolean runningInAotMode(Class testClass) { - return this.aotTestContextInitializers.isSupportedTestClass(testClass); + @SuppressWarnings("unchecked") + private MergedContextConfiguration replaceIfNecessary(MergedContextConfiguration mergedConfig) { + Class testClass = mergedConfig.getTestClass(); + if (this.aotTestContextInitializers.isSupportedTestClass(testClass)) { + Class> contextInitializerClass = + (Class>) + this.aotTestContextInitializers.getContextInitializer(testClass).getClass(); + return new AotMergedContextConfiguration(testClass, contextInitializerClass, mergedConfig, this); + } + return mergedConfig; } } 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 5c83a336c46..20c67d1dfce 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 @@ -33,31 +33,38 @@ abstract class AbstractAotTests { // Global "org/springframework/test/context/aot/AotTestContextInitializers__Generated.java", "org/springframework/test/context/aot/AotTestAttributes__Generated.java", - // BasicSpringJupiterSharedConfigTests + // BasicSpringJupiterImportedConfigTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext001_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext001_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext001_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java", - // BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests. - // BasicSpringJupiterTests.NestedTests + // BasicSpringJupiterSharedConfigTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext002_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests__TestContext002_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java", - // BasicSpringTestNGTests + // BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests. + // BasicSpringJupiterTests.NestedTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext003_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests_NestedTests__TestContext003_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java", - // BasicSpringVintageTests + // BasicSpringTestNGTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_BeanFactoryRegistrations.java", - "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java" + "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java", + // BasicSpringVintageTests + "org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java", + "org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext005_BeanDefinitions.java" }; Stream> scan() { diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java index b5c45ce7ffc..89701d6969d 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java @@ -39,6 +39,7 @@ import org.springframework.aot.generate.InMemoryGeneratedFiles; import org.springframework.aot.test.generate.CompilerFiles; import org.springframework.core.test.tools.CompileWithForkedClassLoader; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterImportedConfigTests; import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests; import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests; import org.springframework.test.context.aot.samples.basic.BasicSpringTestNGTests; @@ -96,9 +97,13 @@ class AotIntegrationTests extends AbstractAotTests { // .printFiles(System.out) .compile(compiled -> // AOT RUN-TIME: EXECUTION - runTestsInAotMode(5, List.of( + runTestsInAotMode(6, List.of( BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class, // NestedTests get executed automatically + // Run @Import tests AFTER the tests with otherwise identical config + // in order to ensure that the other test classes are not accidentally + // using the config for the @Import tests. + BasicSpringJupiterImportedConfigTests.class, BasicSpringTestNGTests.class, BasicSpringVintageTests.class))); } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java index 376bbc62062..fdce41c3f34 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDir; +import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterImportedConfigTests; import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests; import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests; import org.springframework.test.context.aot.samples.basic.BasicSpringTestNGTests; @@ -49,6 +50,7 @@ class TestAotProcessorTests extends AbstractAotTests { // Limit the scope of this test by creating a new classpath root on the fly. Path classpathRoot = Files.createDirectories(tempDir.resolve("build/classes")); Stream.of( + BasicSpringJupiterImportedConfigTests.class, BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class, diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestClassScannerTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestClassScannerTests.java index 5a940fe5ca0..59a188056f8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestClassScannerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestClassScannerTests.java @@ -18,6 +18,7 @@ package org.springframework.test.context.aot; import org.junit.jupiter.api.Test; +import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterImportedConfigTests; import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterSharedConfigTests; import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests; import org.springframework.test.context.aot.samples.basic.BasicSpringTestNGTests; @@ -37,6 +38,7 @@ class TestClassScannerTests extends AbstractAotTests { void scanBasicTestClasses() { assertThat(scan("org.springframework.test.context.aot.samples.basic")) .containsExactlyInAnyOrder( + BasicSpringJupiterImportedConfigTests.class, BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class, @@ -48,8 +50,9 @@ class TestClassScannerTests extends AbstractAotTests { @Test void scanTestSuitesForJupiter() { assertThat(scan("org.springframework.test.context.aot.samples.suites.jupiter")) - .containsExactlyInAnyOrder(BasicSpringJupiterSharedConfigTests.class, - BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class); + .containsExactlyInAnyOrder(BasicSpringJupiterImportedConfigTests.class, + BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class, + BasicSpringJupiterTests.NestedTests.class); } @Test @@ -68,6 +71,7 @@ class TestClassScannerTests extends AbstractAotTests { void scanTestSuitesForAllTestEngines() { assertThat(scan("org.springframework.test.context.aot.samples.suites.all")) .containsExactlyInAnyOrder( + BasicSpringJupiterImportedConfigTests.class, BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class, @@ -80,6 +84,7 @@ class TestClassScannerTests extends AbstractAotTests { void scanTestSuitesWithNestedSuites() { assertThat(scan("org.springframework.test.context.aot.samples.suites.nested")) .containsExactlyInAnyOrder( + BasicSpringJupiterImportedConfigTests.class, BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class, 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 24e00b9c835..2ce0b3de509 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 @@ -193,7 +193,8 @@ class TestContextAotGeneratorTests extends AbstractAotTests { // ContextCustomizerFactory Stream.of( "org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactory", - "org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory" + "org.springframework.test.context.web.socket.MockServerContainerContextCustomizerFactory", + "org.springframework.test.context.aot.samples.basic.ImportsContextCustomizerFactory" ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS)); Stream.of( diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests.java new file mode 100644 index 00000000000..cc917a7610b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests.java @@ -0,0 +1,75 @@ +/* + * 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.samples.basic; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterImportedConfigTests.ImportedConfig; +import org.springframework.test.context.aot.samples.common.MessageService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Uses configuration identical to {@link BasicSpringJupiterTests} and + * {@link BasicSpringJupiterImportedConfigTests} EXCEPT that this class is + * annotated with {@link Import @Import} to register an additional bean. + * + * @author Sam Brannen + * @since 6.0 + */ +@SpringJUnitConfig(BasicTestConfiguration.class) +@Import(ImportedConfig.class) +@TestPropertySource(properties = "test.engine = jupiter") +public class BasicSpringJupiterImportedConfigTests { + + @Autowired + ApplicationContext context; + + @Autowired + MessageService messageService; + + @Autowired + String enigma; + + @Value("${test.engine}") + String testEngine; + + @org.junit.jupiter.api.Test + void test() { + assertThat(messageService.generateMessage()).isEqualTo("Hello, AOT!"); + assertThat(enigma).isEqualTo("imported!"); + assertThat(testEngine).isEqualTo("jupiter"); + assertThat(context.getEnvironment().getProperty("test.engine")) + .as("@TestPropertySource").isEqualTo("jupiter"); + } + + @Configuration(proxyBeanMethods = false) + static class ImportedConfig { + + @Bean + String enigma() { + return "imported!"; + } + } + +} 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 new file mode 100644 index 00000000000..059a42cee02 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/ImportsContextCustomizerFactory.java @@ -0,0 +1,74 @@ +/* + * 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.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; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.MergedContextConfiguration; + +/** + * Emulates {@code ImportsContextCustomizerFactory} from Spring Boot's testing support. + * + * @author Sam Brannen + * @since 6.0 + */ +class ImportsContextCustomizerFactory implements ContextCustomizerFactory { + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + + if (AotDetector.useGeneratedArtifacts()) { + return null; + } + if (testClass.getName().startsWith("org.springframework.test.context.aot.samples") && + testClass.isAnnotationPresent(Import.class)) { + return new ImportsContextCustomizer(testClass); + } + return null; + } + + /** + * Emulates {@code ImportsContextCustomizer} from Spring Boot's testing support. + */ + private static class ImportsContextCustomizer implements ContextCustomizer { + + private final Class testClass; + + ImportsContextCustomizer(Class testClass) { + this.testClass = testClass; + } + + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + AnnotatedBeanDefinitionReader annotatedBeanDefinitionReader = + new AnnotatedBeanDefinitionReader((GenericApplicationContext) context); + Arrays.stream(this.testClass.getAnnotation(Import.class).value()) + .forEach(annotatedBeanDefinitionReader::register); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/AotMergedContextConfigurationTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/AotMergedContextConfigurationTests.java new file mode 100644 index 00000000000..2f91668473e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/cache/AotMergedContextConfigurationTests.java @@ -0,0 +1,79 @@ +/* + * 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.cache; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.support.DelegatingSmartContextLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AotMergedContextConfiguration}. + * + * @author Sam Brannen + * @since 6.0 + */ +class AotMergedContextConfigurationTests { + + private final CacheAwareContextLoaderDelegate delegate = + new DefaultCacheAwareContextLoaderDelegate(mock(ContextCache.class)); + + private final ContextLoader contextLoader = new DelegatingSmartContextLoader(); + + private final MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(), null, null, + Set.of(DemoApplicationContextInitializer.class), null, contextLoader); + + private final AotMergedContextConfiguration aotMergedConfig1 = new AotMergedContextConfiguration(getClass(), + DemoApplicationContextInitializer.class, mergedConfig, delegate); + + private final AotMergedContextConfiguration aotMergedConfig2 = new AotMergedContextConfiguration(getClass(), + DemoApplicationContextInitializer.class, mergedConfig, delegate); + + + @Test + void testEquals() { + assertThat(aotMergedConfig1).isEqualTo(aotMergedConfig1); + assertThat(aotMergedConfig1).isEqualTo(aotMergedConfig2); + + assertThat(mergedConfig).isNotEqualTo(aotMergedConfig1); + assertThat(aotMergedConfig1).isNotEqualTo(mergedConfig); + } + + @Test + void testHashCode() { + assertThat(aotMergedConfig1).hasSameHashCodeAs(aotMergedConfig2); + + assertThat(aotMergedConfig1).doesNotHaveSameHashCodeAs(mergedConfig); + } + + + static class DemoApplicationContextInitializer implements ApplicationContextInitializer { + @Override + public void initialize(GenericApplicationContext applicationContext) { + } + } + +} diff --git a/spring-test/src/test/resources/META-INF/spring.factories b/spring-test/src/test/resources/META-INF/spring.factories index 8b6ce9b4187..b26a9de824b 100644 --- a/spring-test/src/test/resources/META-INF/spring.factories +++ b/spring-test/src/test/resources/META-INF/spring.factories @@ -1,2 +1,6 @@ -# Test configuration file containing a non-existent default TestExecutionListener. +# Test configuration file containing a non-existent default TestExecutionListener and a demo ContextCustomizerFactory. + org.springframework.test.context.TestExecutionListener = org.example.FooListener + +org.springframework.test.context.ContextCustomizerFactory =\ + org.springframework.test.context.aot.samples.basic.ImportsContextCustomizerFactory