Add a test-run goal to the Maven plugin
Closes gh-35202
This commit is contained in:
@@ -43,7 +43,7 @@ You can restore it at any time by configuring your project:
|
||||
include::../maven/running/hot-refresh-pom.xml[tags=hot-refresh]
|
||||
----
|
||||
|
||||
When `addResources` is enabled, any `src/main/resources` directory will be added to the application classpath when you run the application and any duplicate found in `target/classes` will be removed.
|
||||
When `addResources` is enabled, any `src/main/resources` directory will be added to the application classpath when you run the application and any duplicate found in the classes output will be removed.
|
||||
This allows hot refreshing of resources which can be very useful when developing web applications.
|
||||
For example, you can work on HTML, CSS or JavaScript files and see your changes immediately without recompiling your application.
|
||||
It is also a helpful way of allowing your front end developers to work without needing to download and install a Java IDE.
|
||||
@@ -53,14 +53,14 @@ NOTE: A side effect of using this feature is that filtering of resources at buil
|
||||
In order to be consistent with the `repackage` goal, the `run` goal builds the classpath in such a way that any dependency that is excluded in the plugin's configuration gets excluded from the classpath as well.
|
||||
For more details, see <<packaging.examples.exclude-dependency,the dedicated example>>.
|
||||
|
||||
Sometimes it is useful to include test dependencies when running the application.
|
||||
For example, if you want to run your application in a test mode that uses stub classes.
|
||||
If you wish to do this, you can set the `useTestClasspath` parameter to true.
|
||||
|
||||
NOTE: This is only applied when you run an application: the `repackage` goal will not add test dependencies to the resulting JAR/WAR.
|
||||
Sometimes it is useful to run a test variant of your application.
|
||||
For example, if you want to {spring-boot-reference}/#features.testing.testcontainers.at-development-time[use Testcontainers at development time] or make use of some test stubs.
|
||||
Use the `test-run` goal with many of the same features and configuration options as `run` for this purpose.
|
||||
|
||||
include::goals/run.adoc[leveloffset=+1]
|
||||
|
||||
include::goals/test-run.adoc[leveloffset=+1]
|
||||
|
||||
|
||||
|
||||
[[run.examples]]
|
||||
@@ -70,7 +70,7 @@ include::goals/run.adoc[leveloffset=+1]
|
||||
|
||||
[[run.examples.debug]]
|
||||
=== Debug the Application
|
||||
The `run` goal runs your application in a forked process.
|
||||
The `run` and `test-run` goals run your application in a forked process.
|
||||
If you need to debug it, you should add the necessary JVM arguments to enable remote debugging.
|
||||
The following configuration suspend the process until a debugger has joined on port 5005:
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.TestTemplate;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.contentOf;
|
||||
|
||||
/**
|
||||
* Integration tests for the Maven plugin's {@code test-run} goal.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@ExtendWith(MavenBuildExtension.class)
|
||||
class TestRunIntegrationTests {
|
||||
|
||||
@TestTemplate
|
||||
void whenTheTestRunGoalIsExecutedTheApplicationIsRunWithTestAndMainClassesAndTestClasspath(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("test-run")
|
||||
.goals("spring-boot:test-run", "-X")
|
||||
.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\\. .*spring-jcl"));
|
||||
}
|
||||
|
||||
private String canonicalPathOf(File project, String path) throws IOException {
|
||||
return new File(project, path).getCanonicalPath();
|
||||
}
|
||||
|
||||
private String buildLog(File project) {
|
||||
return contentOf(new File(project, "target/build.log"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.boot.maven.it</groupId>
|
||||
<artifactId>test-run</artifactId>
|
||||
<version>0.0.1.BUILD-SNAPSHOT</version>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>@java.version@</maven.compiler.source>
|
||||
<maven.compiler.target>@java.version@</maven.compiler.target>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>@project.groupId@</groupId>
|
||||
<artifactId>@project.artifactId@</artifactId>
|
||||
<version>@project.version@</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
<version>@spring-framework.version@</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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;
|
||||
|
||||
public class SampleApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("I haz been run");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.management.ManagementFactory;
|
||||
|
||||
public class TestSampleApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Main class name = " + TestSampleApplication.class.getName());
|
||||
int i = 1;
|
||||
for (String entry : ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) {
|
||||
System.out.println(i++ + ". " + entry);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -173,20 +173,13 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo {
|
||||
private String[] directories;
|
||||
|
||||
/**
|
||||
* Directory containing the classes and resource files that should be packaged into
|
||||
* the archive.
|
||||
* Directory containing the classes and resource files that should be used to run the
|
||||
* application.
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
|
||||
private File classesDirectory;
|
||||
|
||||
/**
|
||||
* Flag to include the test classpath when running.
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false")
|
||||
private Boolean useTestClasspath;
|
||||
|
||||
/**
|
||||
* Skip the execution.
|
||||
* @since 1.3.2
|
||||
@@ -200,11 +193,29 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo {
|
||||
getLog().debug("skipping run as per configuration.");
|
||||
return;
|
||||
}
|
||||
String startClass = (this.mainClass != null) ? this.mainClass
|
||||
: SpringBootApplicationClassFinder.findSingleClass(this.classesDirectory);
|
||||
run(startClass);
|
||||
run(determineMainClass());
|
||||
}
|
||||
|
||||
private String determineMainClass() throws MojoExecutionException {
|
||||
if (this.mainClass != null) {
|
||||
return this.mainClass;
|
||||
}
|
||||
return SpringBootApplicationClassFinder.findSingleClass(getClassesDirectories());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directories that contain the application's classes and resources. When
|
||||
* the application's main class has not been configured, each directory is searched in
|
||||
* turn for an appropriate main class.
|
||||
* @return the directories that contain the application's classes and resources
|
||||
* @since 3.1.0
|
||||
*/
|
||||
protected List<File> getClassesDirectories() {
|
||||
return List.of(this.classesDirectory);
|
||||
}
|
||||
|
||||
protected abstract boolean isUseTestClasspath();
|
||||
|
||||
private void run(String startClassName) throws MojoExecutionException, MojoFailureException {
|
||||
List<String> args = new ArrayList<>();
|
||||
addAgents(args);
|
||||
@@ -361,17 +372,21 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo {
|
||||
for (Resource resource : this.project.getResources()) {
|
||||
File directory = new File(resource.getDirectory());
|
||||
urls.add(directory.toURI().toURL());
|
||||
FileUtils.removeDuplicatesFromOutputDirectory(this.classesDirectory, directory);
|
||||
for (File classesDirectory : getClassesDirectories()) {
|
||||
FileUtils.removeDuplicatesFromOutputDirectory(classesDirectory, directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addProjectClasses(List<URL> urls) throws MalformedURLException {
|
||||
urls.add(this.classesDirectory.toURI().toURL());
|
||||
for (File classesDirectory : getClassesDirectories()) {
|
||||
urls.add(classesDirectory.toURI().toURL());
|
||||
}
|
||||
}
|
||||
|
||||
private void addDependencies(List<URL> urls) throws MalformedURLException, MojoExecutionException {
|
||||
Set<Artifact> artifacts = (this.useTestClasspath) ? filterDependencies(this.project.getArtifacts())
|
||||
Set<Artifact> artifacts = (isUseTestClasspath()) ? filterDependencies(this.project.getArtifacts())
|
||||
: filterDependencies(this.project.getArtifacts(), new ExcludeTestScopeArtifactFilter());
|
||||
for (Artifact artifact : artifacts) {
|
||||
if (artifact.getFile() != null) {
|
||||
|
||||
@@ -51,6 +51,13 @@ public class RunMojo extends AbstractRunMojo {
|
||||
@Parameter(property = "spring-boot.run.optimizedLaunch", defaultValue = "true")
|
||||
private boolean optimizedLaunch;
|
||||
|
||||
/**
|
||||
* Flag to include the test classpath when running.
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false")
|
||||
private Boolean useTestClasspath;
|
||||
|
||||
@Override
|
||||
protected RunArguments resolveJvmArguments() {
|
||||
RunArguments jvmArguments = super.resolveJvmArguments();
|
||||
@@ -69,6 +76,11 @@ public class RunMojo extends AbstractRunMojo {
|
||||
.run(workingDirectory, args, environmentVariables);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isUseTestClasspath() {
|
||||
return this.useTestClasspath;
|
||||
}
|
||||
|
||||
private static final class RunProcessKiller implements Runnable {
|
||||
|
||||
private final RunProcess runProcess;
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.boot.maven;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.maven.plugin.MojoExecutionException;
|
||||
|
||||
@@ -34,11 +35,17 @@ abstract class SpringBootApplicationClassFinder {
|
||||
private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
|
||||
|
||||
static String findSingleClass(File classesDirectory) throws MojoExecutionException {
|
||||
return findSingleClass(List.of(classesDirectory));
|
||||
}
|
||||
|
||||
static String findSingleClass(List<File> classesDirectories) throws MojoExecutionException {
|
||||
try {
|
||||
String mainClass = MainClassFinder.findSingleMainClass(classesDirectory,
|
||||
SPRING_BOOT_APPLICATION_CLASS_NAME);
|
||||
if (mainClass != null) {
|
||||
return mainClass;
|
||||
for (File classesDirectory : classesDirectories) {
|
||||
String mainClass = MainClassFinder.findSingleMainClass(classesDirectory,
|
||||
SPRING_BOOT_APPLICATION_CLASS_NAME);
|
||||
if (mainClass != null) {
|
||||
return mainClass;
|
||||
}
|
||||
}
|
||||
throw new MojoExecutionException("Unable to find a suitable main class, please add a 'mainClass' property");
|
||||
}
|
||||
|
||||
@@ -85,6 +85,12 @@ public class StartMojo extends AbstractRunMojo {
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
/**
|
||||
* Flag to include the test classpath when running.
|
||||
*/
|
||||
@Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false")
|
||||
private Boolean useTestClasspath;
|
||||
|
||||
@Override
|
||||
protected void run(JavaProcessExecutor processExecutor, File workingDirectory, List<String> args,
|
||||
Map<String, String> environmentVariables) throws MojoExecutionException, MojoFailureException {
|
||||
@@ -188,6 +194,11 @@ public class StartMojo extends AbstractRunMojo {
|
||||
"Spring application did not start before the configured timeout (" + (wait * maxAttempts) + "ms");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isUseTestClasspath() {
|
||||
return this.useTestClasspath;
|
||||
}
|
||||
|
||||
private class CreateJmxConnector implements Callable<JMXConnector> {
|
||||
|
||||
private final int port;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.maven.plugin.MojoExecutionException;
|
||||
import org.apache.maven.plugin.MojoFailureException;
|
||||
import org.apache.maven.plugins.annotations.Execute;
|
||||
import org.apache.maven.plugins.annotations.LifecyclePhase;
|
||||
import org.apache.maven.plugins.annotations.Mojo;
|
||||
import org.apache.maven.plugins.annotations.Parameter;
|
||||
import org.apache.maven.plugins.annotations.ResolutionScope;
|
||||
|
||||
import org.springframework.boot.loader.tools.RunProcess;
|
||||
|
||||
/**
|
||||
* Run an application in place using the test runtime classpath. The main class that will
|
||||
* be used to launch the application is determined as follows:
|
||||
*
|
||||
* <ol>
|
||||
* <li>The configured main class, if any</li>
|
||||
* <li>The main class found in the test classes directory, if any</li>
|
||||
* <li>The main class found in the classes directory, if any</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dmytro Nosan
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.1.0
|
||||
*/
|
||||
@Mojo(name = "test-run", requiresProject = true, defaultPhase = LifecyclePhase.VALIDATE,
|
||||
requiresDependencyResolution = ResolutionScope.TEST)
|
||||
@Execute(phase = LifecyclePhase.TEST_COMPILE)
|
||||
public class TestRunMojo extends AbstractRunMojo {
|
||||
|
||||
/**
|
||||
* Whether the JVM's launch should be optimized.
|
||||
*/
|
||||
@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)
|
||||
private File testClassesDirectory;
|
||||
|
||||
@Override
|
||||
protected List<File> getClassesDirectories() {
|
||||
ArrayList<File> classesDirectories = new ArrayList<>(super.getClassesDirectories());
|
||||
classesDirectories.add(0, this.testClassesDirectory);
|
||||
return classesDirectories;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isUseTestClasspath() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RunArguments resolveJvmArguments() {
|
||||
RunArguments jvmArguments = super.resolveJvmArguments();
|
||||
if (this.optimizedLaunch) {
|
||||
jvmArguments.getArgs().addFirst("-XX:TieredStopAtLevel=1");
|
||||
}
|
||||
return jvmArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(JavaProcessExecutor processExecutor, File workingDirectory, List<String> args,
|
||||
Map<String, String> environmentVariables) throws MojoExecutionException, MojoFailureException {
|
||||
processExecutor
|
||||
.withRunProcessCustomizer(
|
||||
(runProcess) -> Runtime.getRuntime().addShutdownHook(new Thread(new RunProcessKiller(runProcess))))
|
||||
.run(workingDirectory, args, environmentVariables);
|
||||
}
|
||||
|
||||
private static final class RunProcessKiller implements Runnable {
|
||||
|
||||
private final RunProcess runProcess;
|
||||
|
||||
private RunProcessKiller(RunProcess runProcess) {
|
||||
this.runProcess = runProcess;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.runProcess.kill();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user