From 4c99796f72f0f7464ba73903d67f97bdfd5f303f Mon Sep 17 00:00:00 2001 From: henriquejsza Date: Thu, 23 Jul 2026 03:11:28 -0300 Subject: [PATCH] Allow Maven run and start goals to use test classes Closes gh-36115 Signed-off-by: henriquejsza --- .../boot/maven/RunIntegrationTests.java | 18 +++++ .../boot/maven/StartStopIntegrationTests.java | 13 +++- .../src/intTest/projects/start-stop/pom.xml | 2 + .../java/org/test/TestSampleApplication.java | 31 +++++++++ .../boot/maven/AbstractRunMojo.java | 20 +++++- .../springframework/boot/maven/RunMojo.java | 15 +++- .../springframework/boot/maven/StartMojo.java | 15 +++- .../boot/maven/TestClasspath.java | 68 +++++++++++++++++++ .../boot/maven/TestRunMojo.java | 15 +--- .../boot/maven/TestClasspathTests.java | 53 +++++++++++++++ 10 files changed, 229 insertions(+), 21 deletions(-) create mode 100644 build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop/src/test/java/org/test/TestSampleApplication.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestClasspath.java create mode 100644 build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/TestClasspathTests.java diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java index bf9e6a214e37..3bfe730d8ed1 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.maven; import java.io.File; +import java.io.IOException; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.condition.DisabledOnOs; @@ -101,6 +102,19 @@ void whenUseTestClasspathIsEnabledTheApplicationHasTestDependenciesOnItsClasspat .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); } + @TestTemplate + void whenUseTestClasspathIsAllTheApplicationRunsWithTestClassesAndDependencies(MavenBuild mavenBuild) { + mavenBuild.project("test-run") + .goals("spring-boot:run", "-X") + .systemProperty("spring-boot.run.useTestClasspath", "ALL") + .execute((project) -> assertThat(buildLog(project)) + .contains("Main class name = org.test.TestSampleApplication") + .contains("1. " + canonicalPathOf(project, "target/test-classes")) + .contains("2. " + canonicalPathOf(project, "target/classes")) + .containsPattern("3\\. .*spring-core") + .containsPattern("4\\. .*commons-logging")); + } + @TestTemplate void whenAWorkingDirectoryIsConfiguredTheApplicationIsRunFromThatDirectory(MavenBuild mavenBuild) { mavenBuild.project("run-working-directory") @@ -163,4 +177,8 @@ private String buildLog(File project) { return contentOf(new File(project, "target/build.log")); } + private String canonicalPathOf(File project, String path) throws IOException { + return new File(project, path).getCanonicalPath(); + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java index 5e6b8efe1b04..c93c8fc44002 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java @@ -36,7 +36,18 @@ class StartStopIntegrationTests { void startStopWaitsForApplicationToBeReadyAndThenRequestsShutdown(MavenBuild mavenBuild) { mavenBuild.project("start-stop") .goals("verify") - .execute((project) -> assertThat(buildLog(project)).contains("isReady: true") + .execute((project) -> assertThat(buildLog(project)).doesNotContain("Running application from test classes") + .contains("isReady: true") + .contains("Shutdown requested")); + } + + @TestTemplate + void startStopWithAllTestClasspathRunsApplicationFromTestClasses(MavenBuild mavenBuild) { + mavenBuild.project("start-stop") + .goals("verify") + .systemProperty("use-test-classpath", "ALL") + .execute((project) -> assertThat(buildLog(project)).contains("Running application from test classes") + .contains("isReady: true") .contains("Shutdown requested")); } diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop/pom.xml index 3c533ce7669c..9795e613e950 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop/pom.xml +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop/pom.xml @@ -9,6 +9,7 @@ UTF-8 @java.version@ @java.version@ + false @@ -51,6 +52,7 @@ ${jmx.port} + ${use-test-classpath} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop/src/test/java/org/test/TestSampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop/src/test/java/org/test/TestSampleApplication.java new file mode 100644 index 000000000000..73996bd5d774 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop/src/test/java/org/test/TestSampleApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present 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.test; + +/** + * Test application used to verify that the start goal can use test classes. + * + * @author Henrique (henriquejsza) + */ +public class TestSampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Running application from test classes"); + SampleApplication.main(args); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index 08feb075567f..56212adafce5 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -51,6 +51,7 @@ * @author Daniel Young * @author Dmytro Nosan * @author Moritz Halbritter + * @author Henrique (henriquejsza) * @since 1.3.0 * @see RunMojo * @see StartMojo @@ -205,6 +206,14 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { @SuppressWarnings("NullAway.Init") private File classesDirectory; + /** + * Directory containing the test classes and resource files that should be used to run + * the application. + */ + @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) + @SuppressWarnings("NullAway.Init") + private File testClassesDirectory; + /** * Skip the execution. * @@ -241,11 +250,20 @@ private String determineMainClass() throws MojoExecutionException { * @since 3.1.0 */ protected List getClassesDirectories() { - return List.of(this.classesDirectory); + List classesDirectories = new ArrayList<>(); + if (isUseTestClasses()) { + classesDirectories.add(this.testClassesDirectory); + } + classesDirectories.add(this.classesDirectory); + return classesDirectories; } protected abstract boolean isUseTestClasspath(); + protected boolean isUseTestClasses() { + return false; + } + private void run(String startClassName) throws MojoExecutionException, MojoFailureException { List args = new ArrayList<>(); addAgents(args); diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java index 8992ed3d55f6..1cbb08840fc4 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java @@ -55,11 +55,15 @@ public class RunMojo extends AbstractRunMojo { private boolean optimizedLaunch; /** - * Flag to include the test classpath when running. + * Test classpath to use when running. Valid values are {@code OFF} (no test + * classpath), {@code DEPENDENCIES} (test dependencies only), and {@code ALL} (test + * dependencies, classes, and resources). For backwards compatibility, {@code false} + * is equivalent to {@code OFF} and {@code true} is equivalent to + * {@code DEPENDENCIES}. * @since 1.3.0 */ @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") - private boolean useTestClasspath; + private String useTestClasspath = "false"; @Inject public RunMojo(ToolchainManager toolchainManager) { @@ -86,7 +90,12 @@ protected void run(JavaProcessExecutor processExecutor, File workingDirectory, L @Override protected boolean isUseTestClasspath() { - return this.useTestClasspath; + return TestClasspath.of(this.useTestClasspath).isUseTestDependencies(); + } + + @Override + protected boolean isUseTestClasses() { + return TestClasspath.of(this.useTestClasspath).isUseTestClasses(); } private static final class RunProcessKiller implements Runnable { diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java index 728e663546c0..d64475d3350e 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java @@ -90,10 +90,14 @@ public class StartMojo extends AbstractRunMojo { private final Object lock = new Object(); /** - * Flag to include the test classpath when running. + * Test classpath to use when running. Valid values are {@code OFF} (no test + * classpath), {@code DEPENDENCIES} (test dependencies only), and {@code ALL} (test + * dependencies, classes, and resources). For backwards compatibility, {@code false} + * is equivalent to {@code OFF} and {@code true} is equivalent to + * {@code DEPENDENCIES}. */ @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") - private boolean useTestClasspath; + private String useTestClasspath = "false"; @Inject public StartMojo(ToolchainManager toolchainManager) { @@ -206,7 +210,12 @@ public T execute(long wait, int maxAttempts, Callable<@Nullable T> callback) @Override protected boolean isUseTestClasspath() { - return this.useTestClasspath; + return TestClasspath.of(this.useTestClasspath).isUseTestDependencies(); + } + + @Override + protected boolean isUseTestClasses() { + return TestClasspath.of(this.useTestClasspath).isUseTestClasses(); } private class CreateJmxConnector implements Callable<@Nullable JMXConnector> { diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestClasspath.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestClasspath.java new file mode 100644 index 000000000000..a17f0c3b9d53 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestClasspath.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present 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.boot.maven; + +import java.util.Locale; + +/** + * Use of the test classpath when running an application. + * + * @author Henrique (henriquejsza) + */ +enum TestClasspath { + + OFF(false, false), + + DEPENDENCIES(true, false), + + ALL(true, true); + + private final boolean useTestDependencies; + + private final boolean useTestClasses; + + TestClasspath(boolean useTestDependencies, boolean useTestClasses) { + this.useTestDependencies = useTestDependencies; + this.useTestClasses = useTestClasses; + } + + boolean isUseTestDependencies() { + return this.useTestDependencies; + } + + boolean isUseTestClasses() { + return this.useTestClasses; + } + + static TestClasspath of(String value) { + if ("true".equalsIgnoreCase(value)) { + return DEPENDENCIES; + } + if ("false".equalsIgnoreCase(value)) { + return OFF; + } + try { + return valueOf(value.toUpperCase(Locale.ROOT)); + } + catch (IllegalArgumentException ex) { + String message = "Unsupported test classpath '%s'. Valid values are OFF, DEPENDENCIES, ALL, true, and false" + .formatted(value); + throw new IllegalArgumentException(message, ex); + } + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java index 4be0c6185e6a..0a3aa69564ea 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java @@ -17,7 +17,6 @@ package org.springframework.boot.maven; import java.io.File; -import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -57,24 +56,14 @@ public class TestRunMojo extends AbstractRunMojo { @Parameter(property = "spring-boot.test-run.optimizedLaunch", defaultValue = "true") private boolean optimizedLaunch; - /** - * Directory containing the test classes and resource files that should be used to run - * the application. - */ - @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) - @SuppressWarnings("NullAway.Init") - private File testClassesDirectory; - @Inject public TestRunMojo(ToolchainManager toolchainManager) { super(toolchainManager); } @Override - protected List getClassesDirectories() { - ArrayList classesDirectories = new ArrayList<>(super.getClassesDirectories()); - classesDirectories.add(0, this.testClassesDirectory); - return classesDirectories; + protected boolean isUseTestClasses() { + return true; } @Override diff --git a/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/TestClasspathTests.java b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/TestClasspathTests.java new file mode 100644 index 000000000000..be29fae1c9fe --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/TestClasspathTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-present 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.boot.maven; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link TestClasspath}. + * + * @author Henrique (henriquejsza) + */ +class TestClasspathTests { + + @ParameterizedTest + @CsvSource({ "false, OFF", "off, OFF", "true, DEPENDENCIES", "dependencies, DEPENDENCIES", "all, ALL" }) + void mapsValueToTestClasspath(String value, TestClasspath expected) { + assertThat(TestClasspath.of(value)).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ "OFF, false, false", "DEPENDENCIES, true, false", "ALL, true, true" }) + void exposesIncludedParts(TestClasspath testClasspath, boolean useTestDependencies, boolean useTestClasses) { + assertThat(testClasspath.isUseTestDependencies()).isEqualTo(useTestDependencies); + assertThat(testClasspath.isUseTestClasses()).isEqualTo(useTestClasses); + } + + @ParameterizedTest + @CsvSource({ "unknown", "yes" }) + void rejectsUnsupportedValue(String value) { + assertThatIllegalArgumentException().isThrownBy(() -> TestClasspath.of(value)) + .withMessage("Unsupported test classpath '%s'. Valid values are OFF, DEPENDENCIES, ALL, true, and false", + value); + } + +}