From c1a6bfc701a7b0518a92b589792fe5f8bd257bb8 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 29 Jul 2022 21:41:06 +0300 Subject: [PATCH] Introduce initial support for processing test contexts ahead-of-time This commit introduces TestContextAotGenerator for processing Spring integration test classes and generating AOT artifacts. Specifically, this class performs the following. - bootstraps the TCF for a given test class - builds the MergedContextConfiguration for each test class and tracks all test classes that share the same MergedContextConfiguration - loads each test ApplicationContext without refreshing it - passes the test ApplicationContext to ApplicationContextAotGenerator to generate the AOT optimized ApplicationContextInitializer - The GenerationContext passed to ApplicationContextAotGenerator uses a feature name of the form "TestContext###_", where "###" is a 3-digit sequence ID left padded with zeros. This commit also includes tests using the TestCompiler to verify that each generated ApplicationContextInitializer can be used to populate a GenericApplicationContext as expected. See gh-28204 --- spring-test/spring-test.gradle | 1 + .../test/context/aot/TestClassScanner.java | 4 +- .../context/aot/TestContextAotException.java | 46 ++++ .../context/aot/TestContextAotGenerator.java | 206 ++++++++++++++++++ .../test/context/aot/AbstractAotTests.java | 80 +++++++ .../test/context/aot/AotSmokeTests.java | 57 +++++ .../context/aot/TestClassScannerTests.java | 31 +-- .../aot/TestContextAotGeneratorTests.java | 125 +++++++++++ .../BasicSpringJupiterSharedConfigTests.java | 55 +++++ .../basic/BasicSpringJupiterTests.java | 14 +- .../samples/basic/BasicSpringTestNGTests.java | 13 ++ .../basic/BasicSpringVintageTests.java | 13 ++ 12 files changed, 618 insertions(+), 27 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotException.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/aot/AotSmokeTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests.java diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index d9b166b705b..42839df64b1 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -45,6 +45,7 @@ dependencies { optional("io.projectreactor:reactor-test") optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + testImplementation(project(":spring-core-test")) testImplementation(project(":spring-context-support")) testImplementation(project(":spring-oxm")) testImplementation(testFixtures(project(":spring-beans"))) diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestClassScanner.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestClassScanner.java index 8a7208ea4a0..bcc14b86f00 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestClassScanner.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestClassScanner.java @@ -19,6 +19,7 @@ package org.springframework.test.context.aot; import java.lang.annotation.Annotation; import java.nio.file.Path; import java.util.Arrays; +import java.util.Comparator; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; @@ -156,7 +157,8 @@ class TestClassScanner { .map(this::getJavaClass) .flatMap(Optional::stream) .filter(this::isSpringTestClass) - .distinct(); + .distinct() + .sorted(Comparator.comparing(Class::getName)); } private Optional> getJavaClass(ClassSource classSource) { diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotException.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotException.java new file mode 100644 index 00000000000..ffd25d69be2 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotException.java @@ -0,0 +1,46 @@ +/* + * 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; + +/** + * Thrown if an error occurs during AOT build-time processing or AOT run-time + * execution in the Spring TestContext Framework. + * + * @author Sam Brannen + * @since 6.0 + */ +@SuppressWarnings("serial") +public class TestContextAotException extends RuntimeException { + + /** + * Create a new {@code TestContextAotException}. + * @param message the detail message + */ + public TestContextAotException(String message) { + super(message); + } + + /** + * Create a new {@code TestContextAotException}. + * @param message the detail message + * @param cause the root cause + */ + public TestContextAotException(String message, Throwable cause) { + super(message, cause); + } + +} 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 new file mode 100644 index 00000000000..b4c8626efa1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java @@ -0,0 +1,206 @@ +/* + * 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.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.javapoet.ClassName; +import org.springframework.test.context.BootstrapUtils; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.SmartContextLoader; +import org.springframework.test.context.TestContextBootstrapper; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * {@code TestContextAotGenerator} generates AOT artifacts for integration tests + * that depend on support from the Spring TestContext Framework. + * + * @author Sam Brannen + * @since 6.0 + * @see ApplicationContextAotGenerator + */ +class TestContextAotGenerator { + + private static final Log logger = LogFactory.getLog(TestClassScanner.class); + + private final ApplicationContextAotGenerator aotGenerator = new ApplicationContextAotGenerator(); + + private final AtomicInteger sequence = new AtomicInteger(); + + private final GeneratedFiles generatedFiles; + + private final RuntimeHints runtimeHints; + + + /** + * Create a new {@link TestContextAotGenerator} that uses the supplied + * {@link GeneratedFiles}. + * @param generatedFiles the {@code GeneratedFiles} to use + */ + public TestContextAotGenerator(GeneratedFiles generatedFiles) { + this(generatedFiles, new RuntimeHints()); + } + + /** + * Create a new {@link TestContextAotGenerator} that uses the supplied + * {@link GeneratedFiles} and {@link RuntimeHints}. + * @param generatedFiles the {@code GeneratedFiles} to use + * @param runtimeHints the {@code RuntimeHints} to use + */ + public TestContextAotGenerator(GeneratedFiles generatedFiles, RuntimeHints runtimeHints) { + this.generatedFiles = generatedFiles; + this.runtimeHints = runtimeHints; + } + + + /** + * Get the {@link RuntimeHints} gathered during {@linkplain #processAheadOfTime(Stream) + * AOT processing}. + */ + public final RuntimeHints getRuntimeHints() { + return this.runtimeHints; + } + + /** + * Process each of the supplied Spring integration test classes and generate + * AOT artifacts. + * @throws TestContextAotException if an error occurs during AOT processing + */ + public void processAheadOfTime(Stream> testClasses) throws TestContextAotException { + MultiValueMap> map = new LinkedMultiValueMap<>(); + testClasses.forEach(testClass -> map.add(buildMergedContextConfiguration(testClass), testClass)); + + map.forEach((mergedConfig, classes) -> { + // System.err.println(mergedConfig + " -> " + classes); + if (logger.isDebugEnabled()) { + logger.debug("Generating AOT artifacts for test classes [%s]" + .formatted(classes.stream().map(Class::getCanonicalName).toList())); + } + try { + // Use first test class discovered for a given unique MergedContextConfiguration. + Class testClass = classes.get(0); + DefaultGenerationContext generationContext = createGenerationContext(testClass); + ClassName className = processAheadOfTime(mergedConfig, generationContext); + // TODO Store ClassName in a map analogous to TestContextAotProcessor in Spring Native. + generationContext.writeGeneratedContent(); + } + catch (Exception ex) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to generate AOT artifacts for test classes [%s]" + .formatted(classes.stream().map(Class::getCanonicalName).toList()), ex); + } + } + }); + } + + /** + * Process the specified {@link MergedContextConfiguration} ahead-of-time + * using the specified {@link GenerationContext}. + *

Return the {@link ClassName} of the {@link ApplicationContextInitializer} + * to use to restore an optimized state of the test application context for + * the given {@code MergedContextConfiguration}. + * @param mergedConfig the {@code MergedContextConfiguration} to process + * @param generationContext the generation context to use + * @return the {@link ClassName} for the generated {@code ApplicationContextInitializer} + * @throws TestContextAotException if an error occurs during AOT processing + */ + ClassName processAheadOfTime(MergedContextConfiguration mergedConfig, + GenerationContext generationContext) throws TestContextAotException { + + GenericApplicationContext gac = loadContextForAotProcessing(mergedConfig); + try { + return this.aotGenerator.processAheadOfTime(gac, generationContext); + } + catch (Throwable ex) { + throw new TestContextAotException("Failed to process test class [%s] for AOT" + .formatted(mergedConfig.getTestClass().getCanonicalName()), ex); + } + } + + /** + * Load the {@code GenericApplicationContext} for the supplied merged context + * configuration for AOT processing. + *

Only supports {@link SmartContextLoader SmartContextLoaders} that + * create {@link GenericApplicationContext GenericApplicationContexts}. + * @throws TestContextAotException if an error occurs while loading the application + * context or if one of the prerequisites is not met + * @see SmartContextLoader#loadContextForAotProcessing(MergedContextConfiguration) + */ + private GenericApplicationContext loadContextForAotProcessing( + MergedContextConfiguration mergedConfig) throws TestContextAotException { + + Class testClass = mergedConfig.getTestClass(); + ContextLoader contextLoader = mergedConfig.getContextLoader(); + Assert.notNull(contextLoader, """ + Cannot load an ApplicationContext with a NULL 'contextLoader'. \ + Consider annotating test class [%s] with @ContextConfiguration or \ + @ContextHierarchy.""".formatted(testClass.getCanonicalName())); + + if (contextLoader instanceof SmartContextLoader smartContextLoader) { + try { + ApplicationContext context = smartContextLoader.loadContextForAotProcessing(mergedConfig); + if (context instanceof GenericApplicationContext gac) { + return gac; + } + } + catch (Exception ex) { + throw new TestContextAotException( + "Failed to load ApplicationContext for AOT processing for test class [%s]" + .formatted(testClass.getCanonicalName()), ex); + } + } + throw new TestContextAotException(""" + Cannot generate AOT artifacts for test class [%s]. The configured \ + ContextLoader [%s] must be a SmartContextLoader and must create a \ + GenericApplicationContext.""".formatted(testClass.getCanonicalName(), + contextLoader.getClass().getName())); + } + + MergedContextConfiguration buildMergedContextConfiguration(Class testClass) { + TestContextBootstrapper testContextBootstrapper = + BootstrapUtils.resolveTestContextBootstrapper(testClass); + return testContextBootstrapper.buildMergedContextConfiguration(); + } + + DefaultGenerationContext createGenerationContext(Class testClass) { + ClassNameGenerator classNameGenerator = new ClassNameGenerator(testClass); + DefaultGenerationContext generationContext = + new DefaultGenerationContext(classNameGenerator, this.generatedFiles, this.runtimeHints); + return generationContext.withName(nextTestContextId()); + } + + private String nextTestContextId() { + return "TestContext%03d_".formatted(this.sequence.incrementAndGet()); + } + +} 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 new file mode 100644 index 00000000000..89cdd4c8a7b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java @@ -0,0 +1,80 @@ +/* + * 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.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; +import java.util.stream.Stream; + +/** + * @author Sam Brannen + * @since 6.0 + */ +abstract class AbstractAotTests { + + static final String[] expectedSourceFilesForBasicSpringTests = { + // BasicSpringJupiterSharedConfigTests + "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/BasicTestConfiguration__TestContext001_BeanDefinitions.java", + // BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests. + // "org/springframework/context/event/DefaultEventListenerFactory__TestContext00?_BeanDefinitions.java", + // "org/springframework/context/event/EventListenerMethodProcessor__TestContext00?_BeanDefinitions.java", + // "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests__TestContext00?_ApplicationContextInitializer.java", + // "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests__TestContext00?_BeanFactoryRegistrations.java", + // "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext00?_BeanDefinitions.java", + // BasicSpringJupiterTests.NestedTests + "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/BasicTestConfiguration__TestContext002_BeanDefinitions.java", + // BasicSpringTestNGTests + "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/BasicTestConfiguration__TestContext003_BeanDefinitions.java", + // BasicSpringVintageTests + "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" + }; + + Stream> scan() { + return new TestClassScanner(classpathRoots()).scan(); + } + + Stream> scan(String... packageNames) { + return new TestClassScanner(classpathRoots()).scan(packageNames); + } + + Set classpathRoots() { + try { + return Set.of(Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI())); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotSmokeTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotSmokeTests.java new file mode 100644 index 00000000000..508a25a4e61 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotSmokeTests.java @@ -0,0 +1,57 @@ +/* + * 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.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedFiles.Kind; +import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.test.generator.compile.TestCompiler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Smoke tests for AOT support in the TestContext framework. + * + * @author Sam Brannen + * @since 6.0 + */ +class AotSmokeTests extends AbstractAotTests { + + @Test + // Using @CompileWithTargetClassAccess results in the following exception in classpathRoots(): + // java.lang.NullPointerException: Cannot invoke "java.net.URL.toURI()" because the return + // value of "java.security.CodeSource.getLocation()" is null + void scanClassPathThenGenerateSourceFilesAndCompileThem() { + Stream> testClasses = scan("org.springframework.test.context.aot.samples.basic"); + InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); + TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles); + + generator.processAheadOfTime(testClasses); + + List sourceFiles = generatedFiles.getGeneratedFiles(Kind.SOURCE).keySet().stream().toList(); + assertThat(sourceFiles).containsExactlyInAnyOrder(expectedSourceFilesForBasicSpringTests); + + TestCompiler.forSystem().withFiles(generatedFiles).compile(compiled -> { + // just make sure compilation completes without errors + }); + } + +} 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 69e4bc06a03..5a940fe5ca0 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 @@ -16,13 +16,9 @@ package org.springframework.test.context.aot; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Set; -import java.util.stream.Stream; - import org.junit.jupiter.api.Test; +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; import org.springframework.test.context.aot.samples.basic.BasicSpringVintageTests; @@ -35,12 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Sam Brannen * @since 6.0 */ -class TestClassScannerTests { +class TestClassScannerTests extends AbstractAotTests { @Test void scanBasicTestClasses() { assertThat(scan("org.springframework.test.context.aot.samples.basic")) .containsExactlyInAnyOrder( + BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class, BasicSpringVintageTests.class, @@ -51,7 +48,8 @@ class TestClassScannerTests { @Test void scanTestSuitesForJupiter() { assertThat(scan("org.springframework.test.context.aot.samples.suites.jupiter")) - .containsExactlyInAnyOrder(BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class); + .containsExactlyInAnyOrder(BasicSpringJupiterSharedConfigTests.class, + BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class); } @Test @@ -70,6 +68,7 @@ class TestClassScannerTests { void scanTestSuitesForAllTestEngines() { assertThat(scan("org.springframework.test.context.aot.samples.suites.all")) .containsExactlyInAnyOrder( + BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class, BasicSpringVintageTests.class, @@ -81,6 +80,7 @@ class TestClassScannerTests { void scanTestSuitesWithNestedSuites() { assertThat(scan("org.springframework.test.context.aot.samples.suites.nested")) .containsExactlyInAnyOrder( + BasicSpringJupiterSharedConfigTests.class, BasicSpringJupiterTests.class, BasicSpringJupiterTests.NestedTests.class, BasicSpringVintageTests.class @@ -92,21 +92,4 @@ class TestClassScannerTests { assertThat(scan()).hasSizeGreaterThan(400); } - private Stream> scan() { - return new TestClassScanner(classpathRoots()).scan(); - } - - private Stream> scan(String... packageNames) { - return new TestClassScanner(classpathRoots()).scan(packageNames); - } - - private Set classpathRoots() { - try { - return Set.of(Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI())); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - } 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 new file mode 100644 index 00000000000..1371daab153 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorTests.java @@ -0,0 +1,125 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.GeneratedFiles.Kind; +import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.test.generator.compile.CompileWithTargetClassAccess; +import org.springframework.aot.test.generator.compile.TestCompiler; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.javapoet.ClassName; +import org.springframework.test.context.MergedContextConfiguration; +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; +import org.springframework.test.context.aot.samples.basic.BasicSpringVintageTests; +import org.springframework.test.context.aot.samples.common.MessageService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TestContextAotGenerator}. + * + * @author Sam Brannen + * @since 6.0 + */ +@CompileWithTargetClassAccess +class TestContextAotGeneratorTests extends AbstractAotTests { + + /** + * @see AotSmokeTests#scanClassPathThenGenerateSourceFilesAndCompileThem() + */ + @Test + void generate() { + Stream> testClasses = Stream.of( + BasicSpringJupiterSharedConfigTests.class, + BasicSpringJupiterTests.class, + BasicSpringJupiterTests.NestedTests.class, + BasicSpringTestNGTests.class, + BasicSpringVintageTests.class); + + InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); + TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles); + + generator.processAheadOfTime(testClasses); + + List sourceFiles = generatedFiles.getGeneratedFiles(Kind.SOURCE).keySet().stream().toList(); + assertThat(sourceFiles).containsExactlyInAnyOrder(expectedSourceFilesForBasicSpringTests); + + TestCompiler.forSystem().withFiles(generatedFiles).compile(compiled -> { + // just make sure compilation completes without errors + }); + } + + @Test + // We cannot parameterize with the test classes, since @CompileWithTargetClassAccess + // cannot support @ParameterizedTest methods. + void generateApplicationContextInitializer() { + InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); + TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles); + Set> testClasses = Set.of( + BasicSpringTestNGTests.class, + BasicSpringVintageTests.class, + BasicSpringJupiterTests.class, + BasicSpringJupiterSharedConfigTests.class); + List classNames = new ArrayList<>(); + testClasses.forEach(testClass -> { + DefaultGenerationContext generationContext = generator.createGenerationContext(testClass); + MergedContextConfiguration mergedConfig = generator.buildMergedContextConfiguration(testClass); + ClassName className = generator.processAheadOfTime(mergedConfig, generationContext); + assertThat(className).isNotNull(); + classNames.add(className); + generationContext.writeGeneratedContent(); + }); + + compile(generatedFiles, classNames, context -> { + MessageService messageService = context.getBean(MessageService.class); + assertThat(messageService.generateMessage()).isEqualTo("Hello, AOT!"); + // TODO Support @TestPropertySource in AOT testing mode. + // assertThat(context.getEnvironment().getProperty("test.engine")) + // .as("@TestPropertySource").isNotNull(); + }); + } + + + @SuppressWarnings("unchecked") + private void compile(InMemoryGeneratedFiles generatedFiles, List classNames, + Consumer result) { + + TestCompiler.forSystem().withFiles(generatedFiles).compile(compiled -> { + classNames.forEach(className -> { + GenericApplicationContext gac = new GenericApplicationContext(); + ApplicationContextInitializer contextInitializer = + compiled.getInstance(ApplicationContextInitializer.class, className.reflectionName()); + contextInitializer.initialize(gac); + gac.refresh(); + result.accept(gac); + }); + }); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests.java new file mode 100644 index 00000000000..9c9320099d8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterSharedConfigTests.java @@ -0,0 +1,55 @@ +/* + * 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.test.context.TestPropertySource; +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}. + * + * @author Sam Brannen + * @since 6.0 + */ +@SpringJUnitConfig(BasicTestConfiguration.class) +@TestPropertySource(properties = "test.engine = jupiter") +public class BasicSpringJupiterSharedConfigTests { + + @Autowired + ApplicationContext context; + + @Autowired + MessageService messageService; + + @Value("${test.engine}") + String testEngine; + + @org.junit.jupiter.api.Test + void test() { + assertThat(messageService.generateMessage()).isEqualTo("Hello, AOT!"); + assertThat(testEngine).isEqualTo("jupiter"); + assertThat(context.getEnvironment().getProperty("test.engine")) + .as("@TestPropertySource").isEqualTo("jupiter"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests.java index e13a25afe3e..786a70797f7 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringJupiterTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.extension.Extension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.aot.samples.common.MessageService; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -36,11 +37,16 @@ import static org.assertj.core.api.Assertions.assertThat; // for repeated annotations. @ExtendWith(DummyExtension.class) @SpringJUnitConfig(BasicTestConfiguration.class) +@TestPropertySource(properties = "test.engine = jupiter") public class BasicSpringJupiterTests { @org.junit.jupiter.api.Test - void test(@Autowired MessageService messageService) { + void test(@Autowired ApplicationContext context, @Autowired MessageService messageService, + @Value("${test.engine}") String testEngine) { assertThat(messageService.generateMessage()).isEqualTo("Hello, AOT!"); + assertThat(testEngine).isEqualTo("jupiter"); + assertThat(context.getEnvironment().getProperty("test.engine")) + .as("@TestPropertySource").isEqualTo("jupiter"); } @Nested @@ -48,9 +54,13 @@ public class BasicSpringJupiterTests { public class NestedTests { @org.junit.jupiter.api.Test - void test(@Autowired MessageService messageService, @Value("${foo}") String foo) { + void test(@Autowired ApplicationContext context, @Autowired MessageService messageService, + @Value("${test.engine}") String testEngine, @Value("${foo}") String foo) { assertThat(messageService.generateMessage()).isEqualTo("Hello, AOT!"); assertThat(foo).isEqualTo("bar"); + assertThat(testEngine).isEqualTo("jupiter"); + assertThat(context.getEnvironment().getProperty("test.engine")) + .as("@TestPropertySource").isEqualTo("jupiter"); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests.java index bdcee045be2..d0163f82da1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests.java @@ -17,7 +17,10 @@ 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.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.aot.samples.common.MessageService; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; @@ -28,14 +31,24 @@ import static org.assertj.core.api.Assertions.assertThat; * @since 6.0 */ @ContextConfiguration(classes = BasicTestConfiguration.class) +@TestPropertySource(properties = "test.engine = testng") public class BasicSpringTestNGTests extends AbstractTestNGSpringContextTests { + @Autowired + ApplicationContext context; + @Autowired MessageService messageService; + @Value("${test.engine}") + String testEngine; + @org.testng.annotations.Test public void test() { assertThat(messageService.generateMessage()).isEqualTo("Hello, AOT!"); + assertThat(testEngine).isEqualTo("testng"); + assertThat(context.getEnvironment().getProperty("test.engine")) + .as("@TestPropertySource").isEqualTo("testng"); } } 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 6ec4ec6c61b..fe5244440c0 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 @@ -19,7 +19,10 @@ package org.springframework.test.context.aot.samples.basic; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.aot.samples.common.MessageService; import org.springframework.test.context.junit4.SpringRunner; @@ -31,14 +34,24 @@ import static org.assertj.core.api.Assertions.assertThat; */ @RunWith(SpringRunner.class) @ContextConfiguration(classes = BasicTestConfiguration.class) +@TestPropertySource(properties = "test.engine = vintage") public class BasicSpringVintageTests { + @Autowired + ApplicationContext context; + @Autowired MessageService messageService; + @Value("${test.engine}") + String testEngine; + @org.junit.Test public void test() { assertThat(messageService.generateMessage()).isEqualTo("Hello, AOT!"); + assertThat(testEngine).isEqualTo("vintage"); + assertThat(context.getEnvironment().getProperty("test.engine")) + .as("@TestPropertySource").isEqualTo("vintage"); } }