diff --git a/build.gradle b/build.gradle index 44f18a088bd..935c25a6f50 100644 --- a/build.gradle +++ b/build.gradle @@ -93,6 +93,7 @@ configure(allprojects) { project -> dependency "com.github.librepdf:openpdf:1.3.29" dependency "com.rometools:rome:1.18.0" dependency "commons-io:commons-io:2.11.0" + dependency "info.picocli:picocli:4.6.3" dependency "io.vavr:vavr:0.10.4" dependency "net.sf.jopt-simple:jopt-simple:5.0.4" dependencySet(group: 'org.apache.activemq', version: '5.16.2') { diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index 42839df64b1..b992cd021df 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -14,6 +14,7 @@ dependencies { optional(project(":spring-webflux")) optional(project(":spring-webmvc")) optional(project(":spring-websocket")) + optional('info.picocli:picocli') optional("jakarta.activation:jakarta.activation-api") optional("jakarta.el:jakarta.el-api") optional("jakarta.inject:jakarta.inject-api") diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/ProcessTestsAheadOfTimeCommand.java b/spring-test/src/main/java/org/springframework/test/context/aot/ProcessTestsAheadOfTimeCommand.java new file mode 100644 index 00000000000..ee9219aecce --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/ProcessTestsAheadOfTimeCommand.java @@ -0,0 +1,91 @@ +/* + * 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.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.stream.Stream; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +import org.springframework.aot.generate.FileSystemGeneratedFiles; +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.nativex.FileNativeConfigurationWriter; +import org.springframework.aot.nativex.NativeConfigurationWriter; + +/** + * Command-line application that scans the provided classpath roots for Spring + * integration test classes and then generates AOT artifacts for those test + * classes in the provided output directories. + * + * @author Sam Brannen + * @since 6.0 + * @see TestClassScanner + * @see TestContextAotGenerator + * @see FileNativeConfigurationWriter + */ +@Command(mixinStandardHelpOptions = true, description = "Process test classes ahead of time") +public class ProcessTestsAheadOfTimeCommand implements Callable { + + @Parameters(index = "0", arity = "1..*", description = "Classpath roots for compiled test classes.") + private Path[] testClasspathRoots; + + @Option(names = {"--packages"}, required = false, description = "Test packages to scan. This is optional any only intended for testing purposes.") + private String[] packagesToScan = new String[0]; + + @Option(names = {"--sources-out"}, required = true, description = "Output path for the generated sources.") + private Path sourcesOutputPath; + + @Option(names = {"--resources-out"}, required = true, description = "Output path for the generated resources.") + private Path resourcesOutputPath; + + + @Override + public Integer call() throws Exception { + TestClassScanner testClassScanner = new TestClassScanner(Set.of(this.testClasspathRoots)); + Stream> testClasses = testClassScanner.scan(this.packagesToScan); + + // TODO Determine if we need to support CLASS output path. + Path tempDir = Files.createTempDirectory("classes"); + GeneratedFiles generatedFiles = new FileSystemGeneratedFiles(kind -> switch(kind) { + case SOURCE -> this.sourcesOutputPath; + case RESOURCE -> this.resourcesOutputPath; + case CLASS -> tempDir; + }); + TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles); + generator.processAheadOfTime(testClasses); + + NativeConfigurationWriter writer = new FileNativeConfigurationWriter(this.resourcesOutputPath); + writer.write(generator.getRuntimeHints()); + + return 0; + } + + static int execute(String[] args) throws Exception { + return new CommandLine(new ProcessTestsAheadOfTimeCommand()).execute(args); + } + + public static void main(String[] args) throws Exception { + System.exit(execute(args)); + } + +} 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 1a2160f8b8f..d561f6c3098 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 @@ -72,7 +72,16 @@ abstract class AbstractAotTests { Set classpathRoots() { try { - return Set.of(Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI())); + return Set.of(classpathRoot()); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + Path classpathRoot() { + try { + return 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/ProcessTestsAheadOfTimeCommandTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/ProcessTestsAheadOfTimeCommandTests.java new file mode 100644 index 00000000000..66a5c6351bc --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/ProcessTestsAheadOfTimeCommandTests.java @@ -0,0 +1,71 @@ +/* + * 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.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.CleanupMode; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ProcessTestsAheadOfTimeCommand}. + * + * @author Sam Brannen + * @since 6.0 + */ +class ProcessTestsAheadOfTimeCommandTests extends AbstractAotTests { + + @Test + void execute(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exception { + Path sourcesOutputPath = tempDir.resolve("src/test/java").toAbsolutePath(); + Path resourcesOutputPath = tempDir.resolve("src/test/resources").toAbsolutePath(); + String testPackage = "org.springframework.test.context.aot.samples.basic"; + String[] args = { + "--sources-out=" + sourcesOutputPath, + "--resources-out=" + resourcesOutputPath, + "--packages=" + testPackage, + classpathRoot().toString() + }; + int exitCode = ProcessTestsAheadOfTimeCommand.execute(args); + assertThat(exitCode).as("exit code").isZero(); + + assertThat(findFiles(sourcesOutputPath)).containsExactlyInAnyOrder( + expectedSourceFilesForBasicSpringTests); + + assertThat(findFiles(resourcesOutputPath)).contains( + "META-INF/native-image/reflect-config.json", + "META-INF/native-image/resource-config.json", + "META-INF/native-image/proxy-config.json"); + } + + private static List findFiles(Path outputPath) throws IOException { + int lengthOfOutputPath = outputPath.toFile().getAbsolutePath().length() + 1; + return Files.find(outputPath, Integer.MAX_VALUE, + (path, attributes) -> attributes.isRegularFile()) + .map(Path::toAbsolutePath) + .map(Path::toString) + .map(path -> path.substring(lengthOfOutputPath)) + .toList(); + } + +}