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 deleted file mode 100644 index ee9219aecc..0000000000 --- a/spring-test/src/main/java/org/springframework/test/context/aot/ProcessTestsAheadOfTimeCommand.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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/main/java/org/springframework/test/context/aot/TestAotProcessor.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestAotProcessor.java new file mode 100644 index 0000000000..b901cfc2a0 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestAotProcessor.java @@ -0,0 +1,156 @@ +/* + * 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.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Stream; + +import org.springframework.aot.generate.FileSystemGeneratedFiles; +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.generate.GeneratedFiles.Kind; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.nativex.FileNativeConfigurationWriter; +import org.springframework.util.Assert; +import org.springframework.util.FileSystemUtils; + +/** + * 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. + * + *

For internal use only. + * + * @author Sam Brannen + * @author Stephane Nicoll + * @author Andy Wilkinson + * @author Phillip Webb + * @since 6.0 + * @see TestClassScanner + * @see TestContextAotGenerator + * @see FileNativeConfigurationWriter + * @see org.springframework.boot.AotProcessor + */ +public class TestAotProcessor { + + private final Path[] classpathRoots; + + private final Path sourceOutput; + + private final Path resourceOutput; + + private final Path classOutput; + + private final String groupId; + + private final String artifactId; + + + /** + * Create a new processor for the specified test classpath roots and + * general settings. + * + * @param classpathRoots the classpath roots to scan for test classes + * @param sourceOutput the location of generated sources + * @param resourceOutput the location of generated resources + * @param classOutput the location of generated classes + * @param groupId the group ID of the application, used to locate + * {@code native-image.properties} + * @param artifactId the artifact ID of the application, used to locate + * {@code native-image.properties} + */ + public TestAotProcessor(Path[] classpathRoots, Path sourceOutput, Path resourceOutput, Path classOutput, + String groupId, String artifactId) { + + this.classpathRoots = classpathRoots; + this.sourceOutput = sourceOutput; + this.resourceOutput = resourceOutput; + this.classOutput = classOutput; + this.groupId = groupId; + this.artifactId = artifactId; + } + + + /** + * Trigger processing of the test classes in the configured classpath roots. + */ + public void process() { + deleteExistingOutput(); + performAotProcessing(); + } + + private void deleteExistingOutput() { + deleteExistingOutput(this.sourceOutput, this.resourceOutput, this.classOutput); + } + + private void deleteExistingOutput(Path... paths) { + for (Path path : paths) { + try { + FileSystemUtils.deleteRecursively(path); + } + catch (IOException ex) { + throw new UncheckedIOException("Failed to delete existing output in '%s'".formatted(path), ex); + } + } + } + + private void performAotProcessing() { + TestClassScanner scanner = new TestClassScanner(Set.of(this.classpathRoots)); + Stream> testClasses = scanner.scan(); + + GeneratedFiles generatedFiles = new FileSystemGeneratedFiles(this::getRoot); + TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles); + generator.processAheadOfTime(testClasses); + + writeHints(generator.getRuntimeHints()); + } + + private Path getRoot(Kind kind) { + return switch (kind) { + case SOURCE -> this.sourceOutput; + case RESOURCE -> this.resourceOutput; + case CLASS -> this.classOutput; + }; + } + + private void writeHints(RuntimeHints hints) { + FileNativeConfigurationWriter writer = + new FileNativeConfigurationWriter(this.resourceOutput, this.groupId, this.artifactId); + writer.write(hints); + } + + + public static void main(String[] args) { + int requiredArgs = 6; + Assert.isTrue(args.length >= requiredArgs, () -> + "Usage: %s " + .formatted(TestAotProcessor.class.getName())); + Path[] classpathRoots = Arrays.stream(args[0].split(File.pathSeparator)).map(Paths::get).toArray(Path[]::new); + Path sourceOutput = Paths.get(args[1]); + Path resourceOutput = Paths.get(args[2]); + Path classOutput = Paths.get(args[3]); + String groupId = args[4]; + String artifactId = args[5]; + new TestAotProcessor(classpathRoots, sourceOutput, resourceOutput, classOutput, groupId, artifactId).process(); + } + +} 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 d561f6c309..f2bec3dd6f 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 @@ -88,4 +88,13 @@ abstract class AbstractAotTests { } } + Path classpathRoot(Class clazz) { + try { + return Paths.get(clazz.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 deleted file mode 100644 index 66a5c6351b..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/aot/ProcessTestsAheadOfTimeCommandTests.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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(); - } - -} 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 new file mode 100644 index 0000000000..7c870699ab --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java @@ -0,0 +1,99 @@ +/* + * 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.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +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.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.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TestAotProcessor}. + * + * @author Sam Brannen + * @since 6.0 + */ +class TestAotProcessorTests extends AbstractAotTests { + + @Test + void process(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exception { + // 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( + BasicSpringJupiterSharedConfigTests.class, + BasicSpringJupiterTests.class, + BasicSpringJupiterTests.NestedTests.class, + BasicSpringTestNGTests.class, + BasicSpringVintageTests.class + ).forEach(testClass -> copy(testClass, classpathRoot)); + + Path[] classpathRoots = { classpathRoot }; + Path sourceOutput = tempDir.resolve("generated/sources"); + Path resourceOutput = tempDir.resolve("generated/resources"); + Path classOutput = tempDir.resolve("generated/classes"); + String groupId = "org.example"; + String artifactId = "app-tests"; + + TestAotProcessor processor = new TestAotProcessor(classpathRoots, sourceOutput, resourceOutput, classOutput, groupId, artifactId); + processor.process(); + + assertThat(findFiles(sourceOutput)).containsExactlyInAnyOrder( + expectedSourceFilesForBasicSpringTests); + + assertThat(findFiles(resourceOutput)).contains( + "META-INF/native-image/org.example/app-tests/reflect-config.json", + "META-INF/native-image/org.example/app-tests/resource-config.json", + "META-INF/native-image/org.example/app-tests/proxy-config.json"); + } + + private void copy(Class testClass, Path destination) { + String classFilename = ClassUtils.convertClassNameToResourcePath(testClass.getName()) + ".class"; + Path source = classpathRoot(testClass).resolve(classFilename); + Path target = destination.resolve(classFilename); + try { + Files.createDirectories(target.getParent()); + Files.copy(source, target); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static List findFiles(Path outputPath) throws IOException { + int prefixLength = 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(prefixLength)) + .toList(); + } + +}