diff --git a/dd-java-agent/instrumentation/maven/maven-3.2.1/src/main/java/datadog/trace/instrumentation/maven3/MavenExecutionListener.java b/dd-java-agent/instrumentation/maven/maven-3.2.1/src/main/java/datadog/trace/instrumentation/maven3/MavenExecutionListener.java
index acbe78f7946..227c5dc3b10 100644
--- a/dd-java-agent/instrumentation/maven/maven-3.2.1/src/main/java/datadog/trace/instrumentation/maven3/MavenExecutionListener.java
+++ b/dd-java-agent/instrumentation/maven/maven-3.2.1/src/main/java/datadog/trace/instrumentation/maven3/MavenExecutionListener.java
@@ -145,6 +145,22 @@ public void mojoSucceeded(ExecutionEvent event) {
String moduleName = MavenUtils.getUniqueModuleName(project, mojoExecution);
if (MavenUtils.isTestExecution(mojoExecution)) {
+ // Surefire/Failsafe complete successfully when explicitly configured to skip tests;
+ // Maven does not send mojoSkipped for these executions. Read the resolved plugin
+ // configuration, since a POM can override the command-line skip properties.
+ if (Boolean.parseBoolean(
+ MavenUtils.getConfigurationValue(session, mojoExecution, "skipTests"))
+ || Boolean.parseBoolean(MavenUtils.getConfigurationValue(session, mojoExecution, "skip"))
+ || (("maven-surefire-plugin".equals(mojoExecution.getArtifactId())
+ || "maven-failsafe-plugin".equals(mojoExecution.getArtifactId()))
+ && Boolean.parseBoolean(
+ MavenUtils.getConfigurationValue(session, mojoExecution, "skipExec")))
+ || ("maven-failsafe-plugin".equals(mojoExecution.getArtifactId())
+ && Boolean.parseBoolean(
+ MavenUtils.getConfigurationValue(session, mojoExecution, "skipITs")))) {
+ buildEventsHandler.onTestModuleSkip(
+ request, moduleName, "Tests were skipped by Maven configuration");
+ }
buildEventsHandler.onTestModuleFinish(request, moduleName);
} else {
buildEventsHandler.onBuildTaskFinish(request, moduleName);
diff --git a/dd-java-agent/instrumentation/maven/maven-3.2.1/src/test/groovy/MavenInstrumentationTest.groovy b/dd-java-agent/instrumentation/maven/maven-3.2.1/src/test/groovy/MavenInstrumentationTest.groovy
index 960d2926937..303855fdf64 100644
--- a/dd-java-agent/instrumentation/maven/maven-3.2.1/src/test/groovy/MavenInstrumentationTest.groovy
+++ b/dd-java-agent/instrumentation/maven/maven-3.2.1/src/test/groovy/MavenInstrumentationTest.groovy
@@ -7,6 +7,7 @@ import spock.lang.TempDir
import java.nio.file.Path
import java.nio.file.Paths
+import java.util.concurrent.TimeUnit
import static org.junit.jupiter.api.Assertions.assertEquals
@@ -57,6 +58,116 @@ class MavenInstrumentationTest extends CiVisibilityInstrumentationTest {
"test_maven_build_with_no_fork_generates_spans" | ["-B", "clean", "test"] | 0
}
+ def "Maven test status follows resolved skip configuration: #skipArgument, POM skipTests=#pomSkipTests"() {
+ given:
+ if (pomSkipTests != null) {
+ def pom = projectFolder.resolve("pom.xml").toFile()
+ pom.text = pom.text.replace("maven-surefire-plugin",
+ "maven-surefire-plugin${pomSkipTests}")
+ }
+
+ when:
+ def exitCode = executeMaven(["-B", "clean", "test", skipArgument])
+
+ then:
+ exitCode == 0
+ spanFilter.waitForSpan({ span -> span.spanType == "test_session_end" }, TimeUnit.SECONDS.toMillis(20))
+ def spans = TEST_WRITER.toList().flatten()
+ def modules = spans.findAll { it.spanType == "test_module_end" }
+ def sessions = spans.findAll { it.spanType == "test_session_end" }
+ modules.size() == 1
+ sessions.size() == 1
+ modules.every { it.getTag("test.status").toString() == expectedStatus }
+ sessions.every { it.getTag("test.status").toString() == expectedStatus }
+ expectedStatus != "skip" || modules.every { it.getTag("test.skip_reason") == "Tests were skipped by Maven configuration" }
+ expectedStatus != "skip" || !spans.any { it.spanType == "test" || it.spanType == "test_suite_end" }
+
+ where:
+ testcaseName | skipArgument | pomSkipTests | expectedStatus
+ "test_maven_build_with_tests_generates_spans" | "-DskipTests" | null | "skip"
+ "test_maven_build_with_tests_generates_spans" | "-Dmaven.test.skip=true" | null | "skip"
+ "test_maven_build_with_tests_generates_spans" | "-DskipTests=false" | null | "pass"
+ "test_maven_build_with_tests_generates_spans" | "-DskipTests" | "false" | "pass"
+ "test_maven_build_with_tests_generates_spans" | "-DskipTests=false" | "true" | "skip"
+ }
+
+ def "Failsafe status follows skipITs: CLI=#cliSkipITs, POM=#pomSkipITs, unit tests skipped=#skipUnitTests"() {
+ given:
+ def pom = projectFolder.resolve("pom.xml").toFile()
+ // An unrelated skipITs element must not mark Surefire tests as skipped.
+ pom.text = pom.text.replace("maven-surefire-plugin",
+ "maven-surefire-plugin${skipUnitTests}true")
+ if (pomSkipITs != null) {
+ pom.text = pom.text.replace("maven-failsafe-plugin",
+ "maven-failsafe-plugin${pomSkipITs}")
+ }
+
+ when:
+ def exitCode = executeMaven(["-B", "clean", "verify", "-DskipITs=${cliSkipITs}".toString()])
+
+ then:
+ exitCode == 0
+ spanFilter.waitForSpan({ span -> span.spanType == "test_session_end" }, TimeUnit.SECONDS.toMillis(20))
+ def spans = TEST_WRITER.toList().flatten()
+ def modules = spans.findAll { it.spanType == "test_module_end" }
+ def sessions = spans.findAll { it.spanType == "test_session_end" }
+ modules.size() == 2
+ sessions.size() == 1
+ def unitModule = modules.find { it.getTag("test.execution").toString().startsWith("maven-surefire-plugin:") }
+ def integrationModule = modules.find { it.getTag("test.execution").toString().startsWith("maven-failsafe-plugin:") }
+ unitModule.getTag("test.status").toString() == (skipUnitTests ? "skip" : "pass")
+ integrationModule.getTag("test.status").toString() == expectedIntegrationStatus
+ expectedIntegrationStatus != "skip" || integrationModule.getTag("test.skip_reason") == "Tests were skipped by Maven configuration"
+ sessions[0].getTag("test.status").toString() == (skipUnitTests && expectedIntegrationStatus == "skip" ? "skip" : "pass")
+ projectFolder.resolve("target/surefire-reports/TEST-org.example.TestSucceed.xml").toFile().exists() == !skipUnitTests
+ projectFolder.resolve("target/failsafe-reports/TEST-org.example.ITSucceed.xml").toFile().exists() == (expectedIntegrationStatus == "pass")
+
+ where:
+ cliSkipITs | pomSkipITs | skipUnitTests | expectedIntegrationStatus
+ true | null | false | "skip"
+ true | null | true | "skip"
+ false | null | false | "pass"
+ true | "false" | false | "pass"
+ false | "true" | false | "skip"
+ testcaseName = "test_maven_build_with_unit_and_integration_tests_generates_spans"
+ }
+
+ def "Surefire and Failsafe status follows skipExec: CLI=#cliSkipExec, POM=#pomSkipExec"() {
+ given:
+ if (pomSkipExec != null) {
+ def pom = projectFolder.resolve("pom.xml").toFile()
+ ["maven-surefire-plugin", "maven-failsafe-plugin"].each { plugin ->
+ pom.text = pom.text.replace("${plugin}",
+ "${plugin}${pomSkipExec}")
+ }
+ }
+
+ when:
+ def exitCode = executeMaven(["-B", "clean", "verify", "-Dmaven.test.skip.exec=${cliSkipExec}".toString()])
+
+ then:
+ exitCode == 0
+ spanFilter.waitForSpan({ span -> span.spanType == "test_session_end" }, TimeUnit.SECONDS.toMillis(20))
+ def spans = TEST_WRITER.toList().flatten()
+ def modules = spans.findAll { it.spanType == "test_module_end" }
+ def sessions = spans.findAll { it.spanType == "test_session_end" }
+ modules.size() == 2
+ sessions.size() == 1
+ modules.every { it.getTag("test.status").toString() == expectedStatus }
+ sessions[0].getTag("test.status").toString() == expectedStatus
+ expectedStatus != "skip" || modules.every { it.getTag("test.skip_reason") == "Tests were skipped by Maven configuration" }
+ projectFolder.resolve("target/surefire-reports/TEST-org.example.TestSucceed.xml").toFile().exists() == (expectedStatus == "pass")
+ projectFolder.resolve("target/failsafe-reports/TEST-org.example.ITSucceed.xml").toFile().exists() == (expectedStatus == "pass")
+
+ where:
+ cliSkipExec | pomSkipExec | expectedStatus
+ true | null | "skip"
+ false | null | "pass"
+ true | "false" | "pass"
+ false | "true" | "skip"
+ testcaseName = "test_maven_build_with_unit_and_integration_tests_generates_spans"
+ }
+
private void givenMavenProjectFiles(String projectFilesSources) {
def projectResourcesUri = this.getClass().getClassLoader().getResource(projectFilesSources).toURI()
def projectResourcesPath = Paths.get(projectResourcesUri)