From 87de555b418124ff5b666f56fb19e61d6fcd6573 Mon Sep 17 00:00:00 2001 From: Jonathan Knight Date: Wed, 30 Jul 2025 11:59:32 +0300 Subject: [PATCH 1/3] Allow the test classes directory to be configured for the NativeTestMojo --- ...aApplicationWithTestsFunctionalTest.groovy | 25 ++ .../buildtools/maven/NativeTestMojo.java | 8 +- .../build.gradle | 66 ++++ .../gradle.properties | 3 + .../pom.xml | 340 ++++++++++++++++++ .../settings.gradle | 48 +++ .../assembly/test-jar-with-dependencies.xml | 71 ++++ .../java/org/graalvm/demo/Application.java | 11 + .../java/org/graalvm/demo/Calculator.java | 18 + .../java/org/graalvm/demo/CalculatorTest.java | 38 ++ 10 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 samples/java-application-with-tests-in-src-main/build.gradle create mode 100644 samples/java-application-with-tests-in-src-main/gradle.properties create mode 100644 samples/java-application-with-tests-in-src-main/pom.xml create mode 100644 samples/java-application-with-tests-in-src-main/settings.gradle create mode 100644 samples/java-application-with-tests-in-src-main/src/assembly/test-jar-with-dependencies.xml create mode 100644 samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/Application.java create mode 100644 samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/Calculator.java create mode 100644 samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/CalculatorTest.java diff --git a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithTestsFunctionalTest.groovy b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithTestsFunctionalTest.groovy index 887c16f98..81d6ac6a6 100644 --- a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithTestsFunctionalTest.groovy +++ b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithTestsFunctionalTest.groovy @@ -73,6 +73,31 @@ class JavaApplicationWithTestsFunctionalTest extends AbstractGraalVMMavenFunctio """.trim() } + def "can run tests in src/main/java in a native image with the Maven plugin"() { + withSample("java-application-with-tests-in-src-main") + + when: + mvn '-Pnative', '-DquickBuild', 'test' + + then: + buildSucceeded + outputContains "[junit-platform-native] Running in 'test listener' mode" + outputContains """ +[ 3 containers found ] +[ 0 containers skipped ] +[ 3 containers started ] +[ 0 containers aborted ] +[ 3 containers successful ] +[ 0 containers failed ] +[ 6 tests found ] +[ 0 tests skipped ] +[ 6 tests started ] +[ 0 tests aborted ] +[ 6 tests successful ] +[ 0 tests failed ] +""".trim() + } + def "can run tests in a native image with the Maven plugin using shading"() { withSample("java-application-with-tests") diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java index 8579abb8a..335062aee 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java @@ -64,6 +64,7 @@ import org.graalvm.buildtools.utils.JUnitUtils; import org.graalvm.buildtools.utils.NativeImageConfigurationUtils; +import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -99,10 +100,13 @@ public class NativeTestMojo extends AbstractNativeImageMojo { @Parameter(property = "skipNativeTests", defaultValue = "false") private boolean skipNativeTests; + @Parameter(property = "testClassesDirectory", defaultValue = "${project.build.testOutputDirectory}") + private File testClassesDirectory; + @Override protected void populateApplicationClasspath() throws MojoExecutionException { super.populateApplicationClasspath(); - imageClasspath.add(Paths.get(project.getBuild().getTestOutputDirectory())); + imageClasspath.add(testClassesDirectory.toPath()); project.getBuild() .getTestResources() .stream() @@ -217,7 +221,7 @@ private void applyPluginProperties(Xpp3Dom pluginProperty, Map v } private boolean hasTests() { - Path testOutputPath = Paths.get(project.getBuild().getTestOutputDirectory()); + Path testOutputPath = testClassesDirectory.toPath(); if (Files.exists(testOutputPath) && Files.isDirectory(testOutputPath)) { try (Stream testClasses = Files.walk(testOutputPath)) { return testClasses.anyMatch(p -> p.getFileName().toString().endsWith(".class")); diff --git a/samples/java-application-with-tests-in-src-main/build.gradle b/samples/java-application-with-tests-in-src-main/build.gradle new file mode 100644 index 000000000..2975d4266 --- /dev/null +++ b/samples/java-application-with-tests-in-src-main/build.gradle @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +plugins { + id 'application' + id 'org.graalvm.buildtools.native' +} + +repositories { + mavenCentral() +} + +application { + mainClass.set('org.graalvm.demo.Application') +} + +def junitVersion = providers.gradleProperty('junit.jupiter.version') + .get() + +dependencies { + testImplementation(platform("org.junit:junit-bom:${junitVersion}")) + testImplementation('org.junit.jupiter:junit-jupiter') + testRuntimeOnly('org.junit.platform:junit-platform-launcher') +} + +test { + useJUnitPlatform() +} diff --git a/samples/java-application-with-tests-in-src-main/gradle.properties b/samples/java-application-with-tests-in-src-main/gradle.properties new file mode 100644 index 000000000..edd8bcc8e --- /dev/null +++ b/samples/java-application-with-tests-in-src-main/gradle.properties @@ -0,0 +1,3 @@ +native.gradle.plugin.version = 0.11.1-SNAPSHOT +junit.jupiter.version = 5.13.0 +junit.platform.version = 1.13.0 diff --git a/samples/java-application-with-tests-in-src-main/pom.xml b/samples/java-application-with-tests-in-src-main/pom.xml new file mode 100644 index 000000000..d8fa59270 --- /dev/null +++ b/samples/java-application-with-tests-in-src-main/pom.xml @@ -0,0 +1,340 @@ + + + + + 4.0.0 + + org.graalvm.buildtools.examples + maven + 1.0.0-SNAPSHOT + + + 1.8 + UTF-8 + 5.13.0 + 0.11.1-SNAPSHOT + 0.11.1-SNAPSHOT + example-app + org.graalvm.demo.Application + + + + + org.apache.commons + commons-lang3 + 3.12.0 + provided + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + + + + native + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + true + + + + test-native + + test + + test + + + build-native + + compile-no-fork + + package + + + + false + ${imageName} + false + ${project.build.outputDirectory} + + + + + + + shaded + + + + org.graalvm.buildtools + junit-platform-native + ${junit.platform.native.version} + test + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + true + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.4.1 + + + src/assembly/test-jar-with-dependencies.xml + + + + + make-test-jar + package + + single + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + true + + + test-native + + test + + integration-test + + + ${project.build.directory}/${project.artifactId}-${project.version}-tests.jar + + + + + build-native + + compile-no-fork + + package + + + ${project.build.directory}/${project.artifactId}-${project.version}-shaded.jar + + + + + + false + false + ${imageName} + false + + + + + + + + test-variables + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + test-value + + + test-value + + + + + + + + test-runtime-args + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + true + + + test-native + + test + + test + + + + + --exact-reachability-metadata + + + -XX:MissingRegistrationReportingMode=Warn + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + 1.8 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.2 + + + + true + ${mainClass} + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + java + + java + + + ${mainClass} + + + + native + + exec + + + ${project.build.directory}/${imageName} + ${project.build.directory} + + + + + + + + diff --git a/samples/java-application-with-tests-in-src-main/settings.gradle b/samples/java-application-with-tests-in-src-main/settings.gradle new file mode 100644 index 000000000..d53987b2a --- /dev/null +++ b/samples/java-application-with-tests-in-src-main/settings.gradle @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pluginManagement { + plugins { + id 'org.graalvm.buildtools.native' version getProperty('native.gradle.plugin.version') + } +} + +rootProject.name = 'java-application' diff --git a/samples/java-application-with-tests-in-src-main/src/assembly/test-jar-with-dependencies.xml b/samples/java-application-with-tests-in-src-main/src/assembly/test-jar-with-dependencies.xml new file mode 100644 index 000000000..319f48403 --- /dev/null +++ b/samples/java-application-with-tests-in-src-main/src/assembly/test-jar-with-dependencies.xml @@ -0,0 +1,71 @@ + + + + + + tests + + jar + + + + ${project.build.directory}/test-classes + / + + + ${project.build.outputDirectory} + / + + + false + + + / + true + true + test + + + + diff --git a/samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/Application.java b/samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/Application.java new file mode 100644 index 000000000..27820c4b5 --- /dev/null +++ b/samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/Application.java @@ -0,0 +1,11 @@ +package org.graalvm.demo; + +public class Application { + static String getMessage() { + return "Hello, native!"; + } + + public static void main(String[] args) { + System.out.println(getMessage()); + } +} diff --git a/samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/Calculator.java b/samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/Calculator.java new file mode 100644 index 000000000..b1facd323 --- /dev/null +++ b/samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/Calculator.java @@ -0,0 +1,18 @@ +package org.graalvm.demo; + +public class Calculator { + + public Calculator() { + if (System.getenv("TEST_ENV") != null) { + System.out.println("TEST_ENV = " + System.getenv("TEST_ENV")); + } + if (System.getProperty("test-property") != null) { + System.out.println("test-property = " + System.getProperty("test-property")); + } + } + + public int add(int a, int b) { + return a + b; + } + +} diff --git a/samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/CalculatorTest.java b/samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/CalculatorTest.java new file mode 100644 index 000000000..842385862 --- /dev/null +++ b/samples/java-application-with-tests-in-src-main/src/main/java/org/graalvm/demo/CalculatorTest.java @@ -0,0 +1,38 @@ +package org.graalvm.demo; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class CalculatorTest { + + @Test + @DisplayName("1 + 1 = 2") + void addsTwoNumbers() { + Calculator calculator = new Calculator(); + assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2"); + } + + @Test + @DisplayName("1 + 2 = 3") + void addsTwoNumbers2() { + Calculator calculator = new Calculator(); + assertEquals(3, calculator.add(1, 2), "1 + 2 should equal 3"); + } + + @ParameterizedTest(name = "{0} + {1} = {2}") + @CsvSource({ + "0, 1, 1", + "1, 2, 3", + "49, 51, 100", + "1, 100, 101" + }) + void add(int first, int second, int expectedResult) { + Calculator calculator = new Calculator(); + assertEquals(expectedResult, calculator.add(first, second), + () -> first + " + " + second + " should equal " + expectedResult); + } +} From a28f722d8e5a5974b72a5c0734f91367ffc37040 Mon Sep 17 00:00:00 2001 From: Jonathan Knight Date: Thu, 21 Aug 2025 12:22:53 +0300 Subject: [PATCH 2/3] Refactoring to be able to correctly obtain configuration from either Surefire or Failsafe depending on the goal being executed. --- .../buildtools/maven/IntegrationTest.groovy | 1 + ...aApplicationWithTestsFunctionalTest.groovy | 28 ++++ .../buildtools/maven/NativeExtension.java | 10 +- .../maven/NativeIntegrationTestMojo.java | 58 +++++++ .../buildtools/maven/NativeTestMojo.java | 145 +++++++++++++++--- samples/integration-test/pom.xml | 2 +- .../pom.xml | 5 +- samples/java-application-with-tests/pom.xml | 32 +++- .../java/org/graalvm/demo/CalculatorIT.java | 17 ++ 9 files changed, 268 insertions(+), 30 deletions(-) create mode 100644 native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeIntegrationTestMojo.java create mode 100644 samples/java-application-with-tests/src/test/java/org/graalvm/demo/CalculatorIT.java diff --git a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/IntegrationTest.groovy b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/IntegrationTest.groovy index e2116c309..641905346 100644 --- a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/IntegrationTest.groovy +++ b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/IntegrationTest.groovy @@ -11,6 +11,7 @@ class IntegrationTest extends AbstractGraalVMMavenFunctionalTest { buildSucceeded file("target/failsafe-reports").exists() file("target/failsafe-reports/org.graalvm.demo.CalculatorTestIT.txt").readLines().any(line -> line.contains("Tests run: 6, Failures: 0, Errors: 0, Skipped: 0")) + outputContains "Using configuration from maven-failsafe-plugin" outputContains "[junit-platform-native] Running in 'test listener' mode" outputContains """ [ 3 containers found ] diff --git a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithTestsFunctionalTest.groovy b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithTestsFunctionalTest.groovy index 81d6ac6a6..dfe6d7841 100644 --- a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithTestsFunctionalTest.groovy +++ b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithTestsFunctionalTest.groovy @@ -56,6 +56,7 @@ class JavaApplicationWithTestsFunctionalTest extends AbstractGraalVMMavenFunctio then: buildSucceeded + outputContains "Using configuration from maven-surefire-plugin" outputContains "[junit-platform-native] Running in 'test listener' mode" outputContains """ [ 3 containers found ] @@ -73,6 +74,32 @@ class JavaApplicationWithTestsFunctionalTest extends AbstractGraalVMMavenFunctio """.trim() } + def "can run integration tests in a native image with the Maven plugin"() { + withSample("java-application-with-tests") + + when: + mvn '-Pnative', '-DquickBuild', 'verify' + + then: + buildSucceeded + outputContains "Using configuration from maven-failsafe-plugin" + outputContains "[junit-platform-native] Running in 'test listener' mode" + outputContains """ +[ 2 containers found ] +[ 0 containers skipped ] +[ 2 containers started ] +[ 0 containers aborted ] +[ 2 containers successful ] +[ 0 containers failed ] +[ 1 tests found ] +[ 0 tests skipped ] +[ 1 tests started ] +[ 0 tests aborted ] +[ 1 tests successful ] +[ 0 tests failed ] +""".trim() + } + def "can run tests in src/main/java in a native image with the Maven plugin"() { withSample("java-application-with-tests-in-src-main") @@ -81,6 +108,7 @@ class JavaApplicationWithTestsFunctionalTest extends AbstractGraalVMMavenFunctio then: buildSucceeded + outputContains "Using configuration from maven-surefire-plugin" outputContains "[junit-platform-native] Running in 'test listener' mode" outputContains """ [ 3 containers found ] diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeExtension.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeExtension.java index 2fb97e41e..255e1fd9f 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeExtension.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeExtension.java @@ -92,8 +92,8 @@ public void enableLogging(Logger logger) { */ enum Context {main, test} - static String testIdsDirectory(String baseDir) { - return baseDir + File.separator + "test-ids"; + static String testIdsDirectory(String baseDir, String pluginName) { + return baseDir + File.separator + pluginName + "-test-ids"; } static String buildAgentArgument(String baseDir, Context context, List agentOptions) { @@ -124,7 +124,6 @@ public void afterProjectsRead(MavenSession session) { Build build = project.getBuild(); withPlugin(build, "native-maven-plugin", nativePlugin -> { String target = build.getDirectory(); - String testIdsDir = testIdsDirectory(target); Xpp3Dom configurationRoot = (Xpp3Dom) nativePlugin.getConfiguration(); AgentConfiguration agent; @@ -138,6 +137,7 @@ public void afterProjectsRead(MavenSession session) { List plugins = List.of("maven-surefire-plugin", "maven-failsafe-plugin"); for (String pluginName : plugins) { withPlugin(build, pluginName, plugin -> { + String testIdsDir = testIdsDirectory(target, plugin.getArtifactId()); configureJunitListener(plugin, testIdsDir); if (agent.isEnabled()) { List agentOptions = agent.getAgentCommandLine(); @@ -217,8 +217,8 @@ private static void configureAgentForPlugin(Plugin plugin, String agentArgument) }); } - private static void configureJunitListener(Plugin surefirePlugin, String testIdsDir) { - updatePluginConfiguration(surefirePlugin, (exec, configuration) -> { + private static void configureJunitListener(Plugin plugin, String testIdsDir) { + updatePluginConfiguration(plugin, (exec, configuration) -> { Xpp3Dom systemProperties = findOrAppend(configuration, "systemProperties"); Xpp3Dom junitTracking = findOrAppend(systemProperties, JUNIT_PLATFORM_LISTENERS_UID_TRACKING_ENABLED); Xpp3Dom testIdsProperty = findOrAppend(systemProperties, JUNIT_PLATFORM_LISTENERS_UID_TRACKING_OUTPUT_DIR); diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeIntegrationTestMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeIntegrationTestMojo.java new file mode 100644 index 000000000..e7ffd4ae3 --- /dev/null +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeIntegrationTestMojo.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.graalvm.buildtools.maven; + +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; + +/** + * This goal builds and runs native integration tests. + * + * @author Jonathan Knight + */ +@Mojo(name = NativeIntegrationTestMojo.INTEGRATION_TEST_GOAL, + defaultPhase = LifecyclePhase.INTEGRATION_TEST, threadSafe = true, + requiresDependencyResolution = ResolutionScope.TEST, + requiresDependencyCollection = ResolutionScope.TEST) +public class NativeIntegrationTestMojo extends NativeTestMojo { +} diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java index 335062aee..c2668abe6 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java @@ -44,6 +44,7 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.model.FileSet; import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginExecution; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; @@ -64,7 +65,6 @@ import org.graalvm.buildtools.utils.JUnitUtils; import org.graalvm.buildtools.utils.NativeImageConfigurationUtils; -import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -89,24 +89,39 @@ * * @author Sebastien Deleuze */ -@Mojo(name = "test", defaultPhase = LifecyclePhase.TEST, threadSafe = true, +@Mojo(name = NativeTestMojo.TEST_GOAL, defaultPhase = LifecyclePhase.TEST, threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST, requiresDependencyCollection = ResolutionScope.TEST) public class NativeTestMojo extends AbstractNativeImageMojo { + /** + * The test goal for this plugin. + */ + public static final String TEST_GOAL = "test"; + + /** + * The integration-test goal for this plugin. + */ + public static final String INTEGRATION_TEST_GOAL = "integration-test"; + @Parameter(property = "skipTests", defaultValue = "false") private boolean skipTests; @Parameter(property = "skipNativeTests", defaultValue = "false") private boolean skipNativeTests; - @Parameter(property = "testClassesDirectory", defaultValue = "${project.build.testOutputDirectory}") - private File testClassesDirectory; + /** + * The location of the test classes. + *

+ * This field will be set at execution time from either the Surefire or + * Failsafe plugin configurations. + */ + private String testClassesDirectory; @Override protected void populateApplicationClasspath() throws MojoExecutionException { super.populateApplicationClasspath(); - imageClasspath.add(testClassesDirectory.toPath()); + imageClasspath.add(Path.of(testClassesDirectory)); project.getBuild() .getTestResources() .stream() @@ -150,6 +165,9 @@ public void execute() throws MojoExecutionException { logger.info("Skipping native-image tests (parameter 'skipTests' or 'skipNativeTests' is true)."); return; } + + configureEnvironment(); + if (!hasTests()) { logger.info("Skipped native-image tests since there are no test classes."); return; @@ -163,7 +181,6 @@ public void execute() throws MojoExecutionException { logger.info("Initializing project: " + project.getName()); logger.info("===================="); - configureEnvironment(); buildArgs.add("--features=org.graalvm.junit.platform.JUnitPlatformFeature"); /* in version 5.12.0 JUnit added initialize-at-build-time properties files which we need to exclude */ @@ -172,8 +189,7 @@ public void execute() throws MojoExecutionException { if (systemProperties == null) { systemProperties = new HashMap<>(); } - systemProperties.put("junit.platform.listeners.uid.tracking.output.dir", - NativeExtension.testIdsDirectory(outputDirectory.getAbsolutePath())); + if (runtimeArgs == null) { runtimeArgs = new ArrayList<>(); } @@ -185,25 +201,112 @@ public void execute() throws MojoExecutionException { runNativeTests(outputDirectory.toPath().resolve(NATIVE_TESTS_EXE)); } - private void configureEnvironment() { - List plugins = new ArrayList<>(); + protected void configureEnvironment() { + // set the default test classes location + testClassesDirectory = project.getBuild().getTestOutputDirectory(); Plugin surefire = project.getPlugin("org.apache.maven.plugins:maven-surefire-plugin"); + Plugin failsafe = project.getPlugin("org.apache.maven.plugins:maven-failsafe-plugin"); + String currentGoal = mojoExecution.getGoal(); + boolean testPluginProcessed = false; + if (surefire != null) { - plugins.add(surefire); + testPluginProcessed = processTestPluginConfig(surefire, currentGoal); } - Plugin failsafe = project.getPlugin("org.apache.maven.plugins:maven-failsafe-plugin"); - if (failsafe != null) { - plugins.add(failsafe); + if (!testPluginProcessed && failsafe != null) { + // Surefire was not configured with an execution for the current goal, so try Failsafe + testPluginProcessed = processTestPluginConfig(failsafe, currentGoal); + } + + if (!testPluginProcessed) { + // neither Surefire nor Failsafe has an execution for the current goal, + // so use the configuration for whichever plugin's default goal matches + // the current goal + if (mojoExecution.getGoal().equals(TEST_GOAL) && surefire != null) { + // the current goal is "test" which is Surefire's default goal + processTestPluginConfig(surefire, null); + getLog().info("Using configuration from " + surefire.getArtifactId()); + } else if (mojoExecution.getGoal().equals(INTEGRATION_TEST_GOAL) && failsafe != null) { + // the current goal is "integration-test" which is Failsafe's default goal + processTestPluginConfig(failsafe, null); + getLog().info("Using configuration from " + failsafe.getArtifactId()); + } } + } + + /** + * Process the configuration from the Surefire or Failsafe plugins. + *

+ * This method will check to see whether the plugin has an execution + * matching the goa for this plugin. If it does the plugin's configuration + * and the configuration for the matching execution will be processed. + * If the plugin has no executions, then just its global configuration + * will be processed. + * + * @param plugin the Surefire or Failsafe plugin + * @param currentGoal the current goal being executed + */ + private boolean processTestPluginConfig(Plugin plugin, String currentGoal) + { + List executions = plugin.getExecutions(); + boolean found = false; + if (!executions.isEmpty()) { + for (PluginExecution execution : executions) { + if (execution.getGoals().contains(currentGoal)) { + processTestPlugin(plugin, execution); + found = true; + break; + } + } + } + return found; + } + + /** + * Process the configuration from the Surefire or Failsafe plugins. + * + * @param plugin the Surefire or Failsafe plugin configuration + * @param execution the plugin execution to use for configuration + */ + private void processTestPlugin(Plugin plugin, PluginExecution execution) { + processTestPluginConfig(plugin.getConfiguration()); + if (execution != null) { + processTestPluginConfig(execution.getConfiguration()); + } + systemProperties.put("junit.platform.listeners.uid.tracking.output.dir", + NativeExtension.testIdsDirectory(outputDirectory.getAbsolutePath(), plugin.getArtifactId())); + getLog().info("Using configuration from " + plugin.getArtifactId() + + (execution != null ? ", execution id " + execution.getId() : "")); + } + + /** + * Process the configuration from the Surefire or Failsafe plugins. + * + * @param configuration the Surefire or Failsafe plugin configuration + */ + private void processTestPluginConfig(Object configuration) { + if (configuration instanceof Xpp3Dom) { + Xpp3Dom dom = (Xpp3Dom) configuration; + applyPluginProperties(dom.getChild("environmentVariables"), environment); + applyPluginProperties(dom.getChild("systemPropertyVariables"), systemProperties); + setTestClassesDirectory(dom.getChild("testClassesDirectory")); + } + } - for (Plugin plugin : plugins) { - Object configuration = plugin.getConfiguration(); - if (configuration instanceof Xpp3Dom) { - Xpp3Dom dom = (Xpp3Dom) configuration; - applyPluginProperties(dom.getChild("environmentVariables"), environment); - applyPluginProperties(dom.getChild("systemPropertyVariables"), systemProperties); + /** + * Set the test classes directory from the testClassesDirectory configuration {@link Xpp3Dom}. + *

+ * The {@link #testClassesDirectory} field is only set if the dom is not {@code null} + * and it contains is non-blank value. + * + * @param dom the testClassesDirectory configuration {@link Xpp3Dom}. + */ + protected void setTestClassesDirectory(Xpp3Dom dom) { + if (dom != null) { + String value = dom.getValue(); + if (value != null && !value.isBlank()) { + testClassesDirectory = value; } } } @@ -221,7 +324,7 @@ private void applyPluginProperties(Xpp3Dom pluginProperty, Map v } private boolean hasTests() { - Path testOutputPath = testClassesDirectory.toPath(); + Path testOutputPath = Path.of(testClassesDirectory); if (Files.exists(testOutputPath) && Files.isDirectory(testOutputPath)) { try (Stream testClasses = Files.walk(testOutputPath)) { return testClasses.anyMatch(p -> p.getFileName().toString().endsWith(".class")); diff --git a/samples/integration-test/pom.xml b/samples/integration-test/pom.xml index 2f1b6f7d2..37d91af9b 100644 --- a/samples/integration-test/pom.xml +++ b/samples/integration-test/pom.xml @@ -92,7 +92,7 @@ test-native - test + integration-test verify diff --git a/samples/java-application-with-tests-in-src-main/pom.xml b/samples/java-application-with-tests-in-src-main/pom.xml index d8fa59270..f636f248b 100644 --- a/samples/java-application-with-tests-in-src-main/pom.xml +++ b/samples/java-application-with-tests-in-src-main/pom.xml @@ -69,7 +69,6 @@ org.junit.jupiter junit-jupiter ${junit.jupiter.version} - test @@ -82,6 +81,9 @@ org.apache.maven.plugins maven-surefire-plugin 3.0.0-M5 + + ${project.build.outputDirectory} + @@ -110,7 +112,6 @@ false ${imageName} false - ${project.build.outputDirectory} diff --git a/samples/java-application-with-tests/pom.xml b/samples/java-application-with-tests/pom.xml index 906f75c69..e3065f828 100644 --- a/samples/java-application-with-tests/pom.xml +++ b/samples/java-application-with-tests/pom.xml @@ -98,6 +98,13 @@ test + + integration-test-native + + integration-test + + integration-test + build-native @@ -280,7 +287,30 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.5.3 + + + **/*Test.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.1.2 + + + **/*IT.java + + + + + + integration-test + verify + + + diff --git a/samples/java-application-with-tests/src/test/java/org/graalvm/demo/CalculatorIT.java b/samples/java-application-with-tests/src/test/java/org/graalvm/demo/CalculatorIT.java new file mode 100644 index 000000000..c506e8a29 --- /dev/null +++ b/samples/java-application-with-tests/src/test/java/org/graalvm/demo/CalculatorIT.java @@ -0,0 +1,17 @@ +package org.graalvm.demo; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CalculatorIT { + + @Test + @DisplayName("1 + 1 = 2") + void addsTwoNumbers() { + Calculator calculator = new Calculator(); + assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2"); + } + +} From 841966cff6eddd1c9dad01a1cdb6d52ed9d156ba Mon Sep 17 00:00:00 2001 From: Jonathan Knight Date: Thu, 21 Aug 2025 16:27:26 +0300 Subject: [PATCH 3/3] Clean up unused code paths due to the fact that a Maven plugin will always have a PluginExecution --- .../buildtools/maven/NativeTestMojo.java | 42 ++++--------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java index c2668abe6..4743960a5 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java @@ -216,22 +216,7 @@ protected void configureEnvironment() { if (!testPluginProcessed && failsafe != null) { // Surefire was not configured with an execution for the current goal, so try Failsafe - testPluginProcessed = processTestPluginConfig(failsafe, currentGoal); - } - - if (!testPluginProcessed) { - // neither Surefire nor Failsafe has an execution for the current goal, - // so use the configuration for whichever plugin's default goal matches - // the current goal - if (mojoExecution.getGoal().equals(TEST_GOAL) && surefire != null) { - // the current goal is "test" which is Surefire's default goal - processTestPluginConfig(surefire, null); - getLog().info("Using configuration from " + surefire.getArtifactId()); - } else if (mojoExecution.getGoal().equals(INTEGRATION_TEST_GOAL) && failsafe != null) { - // the current goal is "integration-test" which is Failsafe's default goal - processTestPluginConfig(failsafe, null); - getLog().info("Using configuration from " + failsafe.getArtifactId()); - } + processTestPluginConfig(failsafe, currentGoal); } } @@ -239,7 +224,7 @@ protected void configureEnvironment() { * Process the configuration from the Surefire or Failsafe plugins. *

* This method will check to see whether the plugin has an execution - * matching the goa for this plugin. If it does the plugin's configuration + * matching the goal for this plugin. If it does the plugin's configuration * and the configuration for the matching execution will be processed. * If the plugin has no executions, then just its global configuration * will be processed. @@ -270,28 +255,18 @@ private boolean processTestPluginConfig(Plugin plugin, String currentGoal) * @param execution the plugin execution to use for configuration */ private void processTestPlugin(Plugin plugin, PluginExecution execution) { - processTestPluginConfig(plugin.getConfiguration()); - if (execution != null) { - processTestPluginConfig(execution.getConfiguration()); - } - systemProperties.put("junit.platform.listeners.uid.tracking.output.dir", - NativeExtension.testIdsDirectory(outputDirectory.getAbsolutePath(), plugin.getArtifactId())); - getLog().info("Using configuration from " + plugin.getArtifactId() - + (execution != null ? ", execution id " + execution.getId() : "")); - } - - /** - * Process the configuration from the Surefire or Failsafe plugins. - * - * @param configuration the Surefire or Failsafe plugin configuration - */ - private void processTestPluginConfig(Object configuration) { + Object configuration = execution.getConfiguration(); if (configuration instanceof Xpp3Dom) { Xpp3Dom dom = (Xpp3Dom) configuration; + getLog().debug("Using plugin configuration\n" + dom); applyPluginProperties(dom.getChild("environmentVariables"), environment); applyPluginProperties(dom.getChild("systemPropertyVariables"), systemProperties); setTestClassesDirectory(dom.getChild("testClassesDirectory")); } + systemProperties.put("junit.platform.listeners.uid.tracking.output.dir", + NativeExtension.testIdsDirectory(outputDirectory.getAbsolutePath(), plugin.getArtifactId())); + getLog().info("Using configuration from " + plugin.getArtifactId() + + ", execution id \"" + execution.getId() + '"'); } /** @@ -306,6 +281,7 @@ protected void setTestClassesDirectory(Xpp3Dom dom) { if (dom != null) { String value = dom.getValue(); if (value != null && !value.isBlank()) { + getLog().debug("Setting test classes directory to " + value); testClassesDirectory = value; } }