From 6446ac7195edbe37dee904c1d877e789ca3a744d Mon Sep 17 00:00:00 2001 From: Calvin Bayer Date: Mon, 19 Oct 2026 23:04:36 +0200 Subject: [PATCH 1/4] Apply automation exclusions to Spock features and spec selection --- .../JUnit5SpockSkipInstrumentation.java | 12 ++++++-- .../src/test/groovy/SpockTest.groovy | 29 +++++++++++++++++++ .../org/example/TestAutomationExcluded.groovy | 16 ++++++++++ docs/test-automation-exclusions.md | 6 ++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcluded.groovy diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockSkipInstrumentation.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockSkipInstrumentation.java index 023f0d53ac5..f2404bb3ee3 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockSkipInstrumentation.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockSkipInstrumentation.java @@ -124,7 +124,11 @@ public static void shouldBeSkipped( if (featureIdentifier == null) { return; } - SkipReason skipReason = testEventsHandler.skipReason(featureIdentifier); + SkipReason skipReason = + testEventsHandler.skipReason( + featureIdentifier, + SpockUtils.toTestSourceData(feature), + JUnitPlatformUtils.getTags(feature)); if (skipReason == null) { return; } @@ -149,7 +153,11 @@ public static void shouldBeSkipped( if (test == null) { return; } - SkipReason skipReason = testEventsHandler.skipReason(test); + SkipReason skipReason = + testEventsHandler.skipReason( + test, + SpockUtils.toTestSourceData(spockNode), + JUnitPlatformUtils.getTags(spockNode)); if (skipReason == null) { return; } diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy index 2f323a52507..9d746e37f4d 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy @@ -10,6 +10,7 @@ import datadog.trace.instrumentation.junit5.TestEventsHandlerHolder import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import org.example.TestFailedParameterizedSpock +import org.example.TestAutomationExcluded import org.example.TestFailedSpock import org.example.TestFailedThenSucceedParameterizedSpock import org.example.TestFailedThenSucceedSpock @@ -34,6 +35,34 @@ import org.spockframework.util.SpockReleaseInfo @DisableTestTrace(reason = "avoid self-tracing") class SpockTest extends CiVisibilityInstrumentationTest { + def "test automation exclusions preserve a blocking failure and execute once"() { + given: + TestAutomationExcluded.executions = 0 + def id = new TestFQN("org.example.TestAutomationExcluded", "protected_failure") + givenDisabledTests([id]) + givenQuarantinedTests([id]) + givenAttemptToFixTests([id]) + givenFlakyRetryEnabled(true) + givenFlakyTests([id]) + givenEarlyFlakinessDetectionEnabled(true) + givenKnownTests([]) + + when: + runTests([TestAutomationExcluded], false) + + then: + TestAutomationExcluded.executions == 1 + def testSpan = TEST_WRITER.toList().flatten().find { it.getTag("test.name") == "protected_failure" } + testSpan != null + testSpan.getTag("test.test_management.no_auto_retry") == true + testSpan.getTag("test.test_management.prevent_failure_suppression") == true + testSpan.getTag("test.test_management.is_test_disabled") == true + testSpan.getTag("test.test_management.is_quarantined") == true + testSpan.getTag("test.test_management.is_attempt_to_fix") == true + testSpan.getTag("test.test_management.attempt_to_fix_passed") == null + } + + def "test #testcaseName"() { runTests(tests) diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcluded.groovy b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcluded.groovy new file mode 100644 index 00000000000..fae037a8aec --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcluded.groovy @@ -0,0 +1,16 @@ +package org.example + +import datadog.trace.api.civisibility.NoAutoRetry +import datadog.trace.api.civisibility.PreventFailureSuppression +import spock.lang.Specification + +class TestAutomationExcluded extends Specification { + static int executions + + @NoAutoRetry + @PreventFailureSuppression + def "protected_failure"() { + expect: + ++executions < 0 + } +} diff --git a/docs/test-automation-exclusions.md b/docs/test-automation-exclusions.md index 634c571a6ef..eddef730731 100644 --- a/docs/test-automation-exclusions.md +++ b/docs/test-automation-exclusions.md @@ -41,3 +41,9 @@ also reference `TestAutomationExclusions` constants. Apply the public annotations to `org.testng.annotations.Test` methods, including data providers. TestNG groups can also reference `TestAutomationExclusions` constants. User-supplied retry analyzers retain their existing behavior. + +## Spock + +Apply the public annotations directly to a Spock feature method. They apply +to every data-driven iteration. Spec-level skip aggregation inspects the +feature declaration before deciding whether the whole spec can be skipped. From f6b686e021c6bec01a245bcf1c61fbdf259ce789 Mon Sep 17 00:00:00 2001 From: Calvin Bayer Date: Tue, 20 Oct 2026 10:47:43 +0200 Subject: [PATCH 2/4] Honor inherited Spock feature exclusion declarations --- .../instrumentation/junit5/SpockUtils.java | 18 ++++++----- .../src/test/groovy/SpockTest.groovy | 31 +++++++++++++++++++ .../TestAutomationExcludedInherited.groovy | 3 ++ 3 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedInherited.groovy diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java index 19796973732..59c1cb93e43 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java @@ -134,14 +134,16 @@ private static Method getTestMethod(MethodSource methodSource) { } try { - for (Method declaredMethod : testClass.getDeclaredMethods()) { - FeatureMetadata featureMetadata = declaredMethod.getAnnotation(FeatureMetadata.class); - if (featureMetadata == null) { - continue; - } - - if (methodName.equals(featureMetadata.name())) { - return declaredMethod; + for (Class current = testClass; current != null; current = current.getSuperclass()) { + for (Method declaredMethod : current.getDeclaredMethods()) { + FeatureMetadata featureMetadata = declaredMethod.getAnnotation(FeatureMetadata.class); + if (featureMetadata == null) { + continue; + } + + if (methodName.equals(featureMetadata.name())) { + return declaredMethod; + } } } diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy index 9d746e37f4d..2d0918c3359 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy @@ -11,6 +11,7 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import org.example.TestFailedParameterizedSpock import org.example.TestAutomationExcluded +import org.example.TestAutomationExcludedInherited import org.example.TestFailedSpock import org.example.TestFailedThenSucceedParameterizedSpock import org.example.TestFailedThenSucceedSpock @@ -63,6 +64,36 @@ class SpockTest extends CiVisibilityInstrumentationTest { } + def "test automation exclusions honor inherited feature declarations with attempt to fix #attemptToFix"() { + given: + TestAutomationExcluded.executions = 0 + def id = new TestFQN("org.example.TestAutomationExcludedInherited", "protected_failure") + givenDisabledTests([id]) + givenQuarantinedTests([id]) + givenAttemptToFixTests(attemptToFix ? [id] : []) + givenFlakyRetryEnabled(true) + givenEarlyFlakinessDetectionEnabled(true) + givenKnownTests([]) + + when: + runTests([TestAutomationExcludedInherited], false) + + then: + TestAutomationExcluded.executions == 1 + def spans = TEST_WRITER.toList().flatten().findAll { it.getTag("test.name") == "protected_failure" } + spans.size() == 1 + spans[0].getTag("test.suite") == "org.example.TestAutomationExcludedInherited" + spans[0].getTag("test.test_management.no_auto_retry") == true + spans[0].getTag("test.test_management.prevent_failure_suppression") == true + spans[0].getTag("test.test_management.is_test_disabled") == true + spans[0].getTag("test.test_management.is_quarantined") == true + spans[0].getTag("test.test_management.is_attempt_to_fix") == (attemptToFix ? true : null) + spans[0].getTag("test.test_management.attempt_to_fix_passed") == null + + where: + attemptToFix << [false, true] + } + def "test #testcaseName"() { runTests(tests) diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedInherited.groovy b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedInherited.groovy new file mode 100644 index 00000000000..38ad5c737f2 --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedInherited.groovy @@ -0,0 +1,3 @@ +package org.example + +class TestAutomationExcludedInherited extends TestAutomationExcluded {} From db50c6fdb6bdcb778ea4e2fc75ee24df5593474b Mon Sep 17 00:00:00 2001 From: Calvin Bayer Date: Tue, 20 Oct 2026 14:09:15 +0200 Subject: [PATCH 3/4] Resolve inherited Spock features by declaration identity --- .../instrumentation/junit5/SpockUtils.java | 31 +++++-------------- .../src/test/groovy/SpockTest.groovy | 28 +++++++++++++++++ ...TestAutomationExcludedCollisionBase.groovy | 12 +++++++ ...TestAutomationExcludedCollisionLeaf.groovy | 3 ++ ...stAutomationExcludedCollisionMiddle.groovy | 15 +++++++++ 5 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionBase.groovy create mode 100644 dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionLeaf.groovy create mode 100644 dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionMiddle.groovy diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java index 59c1cb93e43..475a4e2eb78 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java @@ -22,7 +22,6 @@ import org.spockframework.runtime.ParameterizedFeatureNode; import org.spockframework.runtime.SpockNode; import org.spockframework.runtime.model.FeatureInfo; -import org.spockframework.runtime.model.FeatureMetadata; import org.spockframework.runtime.model.SpecElementInfo; public class SpockUtils { @@ -106,7 +105,7 @@ public static TestSourceData toTestSourceData(TestDescriptor testDescriptor) { if (testSource instanceof MethodSource) { MethodSource methodSource = (MethodSource) testSource; Class testClass = methodSource.getJavaClass(); - Method testMethod = getTestMethod(methodSource); + Method testMethod = getTestMethod(testDescriptor); String testMethodName = methodSource.getMethodName(); return new TestSourceData(testClass, testMethod, testMethodName); } else { @@ -122,33 +121,17 @@ public static boolean shouldBeTraced(TestDescriptor testDescriptor) { || testDescriptor.getParent().map(c -> c instanceof ParameterizedFeatureNode).orElse(false); } - private static Method getTestMethod(MethodSource methodSource) { - String methodName = methodSource.getMethodName(); - if (methodName == null) { + private static Method getTestMethod(TestDescriptor testDescriptor) { + if (!(testDescriptor instanceof SpockNode)) { return null; } - - Class testClass = methodSource.getJavaClass(); - if (testClass == null) { - return null; - } - try { - for (Class current = testClass; current != null; current = current.getSuperclass()) { - for (Method declaredMethod : current.getDeclaredMethods()) { - FeatureMetadata featureMetadata = declaredMethod.getAnnotation(FeatureMetadata.class); - if (featureMetadata == null) { - continue; - } - - if (methodName.equals(featureMetadata.name())) { - return declaredMethod; - } - } + SpecElementInfo nodeInfo = ((SpockNode) testDescriptor).getNodeInfo(); + if (nodeInfo instanceof FeatureInfo) { + return ((FeatureInfo) nodeInfo).getFeatureMethod().getReflection(); } - } catch (Throwable e) { - LOGGER.warn("Could not get test method from method source", e); + LOGGER.warn("Could not get test method from Spock node", e); } return null; } diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy index 2d0918c3359..779712a6d1b 100644 --- a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/SpockTest.groovy @@ -12,6 +12,9 @@ import java.util.concurrent.CopyOnWriteArrayList import org.example.TestFailedParameterizedSpock import org.example.TestAutomationExcluded import org.example.TestAutomationExcludedInherited +import org.example.TestAutomationExcludedCollisionBase +import org.example.TestAutomationExcludedCollisionMiddle +import org.example.TestAutomationExcludedCollisionLeaf import org.example.TestFailedSpock import org.example.TestFailedThenSucceedParameterizedSpock import org.example.TestFailedThenSucceedSpock @@ -94,6 +97,31 @@ class SpockTest extends CiVisibilityInstrumentationTest { attemptToFix << [false, true] } + def "test automation exclusions distinguish inherited features with the same name when disabled #disabled"() { + given: + TestAutomationExcludedCollisionBase.normalExecutions = 0 + TestAutomationExcludedCollisionMiddle.protectedExecutions = 0 + def id = new TestFQN("org.example.TestAutomationExcludedCollisionLeaf", "same_named_feature") + givenDisabledTests(disabled ? [id] : []) + givenFlakyRetryEnabled(!disabled) + givenFlakyTests([id]) + + when: + runTests([TestAutomationExcludedCollisionLeaf], false) + + then: + disabled ? TestAutomationExcludedCollisionBase.normalExecutions == 0 : TestAutomationExcludedCollisionBase.normalExecutions > 1 + TestAutomationExcludedCollisionMiddle.protectedExecutions == 1 + def spans = TEST_WRITER.toList().flatten().findAll { it.getTag("test.name") == "same_named_feature" } + spans.every { it.getTag("test.suite") == "org.example.TestAutomationExcludedCollisionLeaf" } + spans.count { it.getTag("test.test_management.no_auto_retry") == true } == 1 + spans.count { it.getTag("test.test_management.prevent_failure_suppression") == true } == 1 + disabled ? spans.any { it.getTag("test.status")?.toString() == "skip" } : spans.any { it.getTag("test.is_retry") == true } + + where: + disabled << [false, true] + } + def "test #testcaseName"() { runTests(tests) diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionBase.groovy b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionBase.groovy new file mode 100644 index 00000000000..e74e39ec750 --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionBase.groovy @@ -0,0 +1,12 @@ +package org.example + +import spock.lang.Specification + +class TestAutomationExcludedCollisionBase extends Specification { + static int normalExecutions + + def "same_named_feature"() { + expect: + ++normalExecutions < 0 + } +} diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionLeaf.groovy b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionLeaf.groovy new file mode 100644 index 00000000000..6a8b6e926fe --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionLeaf.groovy @@ -0,0 +1,3 @@ +package org.example + +class TestAutomationExcludedCollisionLeaf extends TestAutomationExcludedCollisionMiddle {} diff --git a/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionMiddle.groovy b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionMiddle.groovy new file mode 100644 index 00000000000..acecd6c8807 --- /dev/null +++ b/dd-java-agent/instrumentation/junit/junit-5/junit-5-spock-2.0/src/test/groovy/org/example/TestAutomationExcludedCollisionMiddle.groovy @@ -0,0 +1,15 @@ +package org.example + +import datadog.trace.api.civisibility.NoAutoRetry +import datadog.trace.api.civisibility.PreventFailureSuppression + +class TestAutomationExcludedCollisionMiddle extends TestAutomationExcludedCollisionBase { + static int protectedExecutions + + @NoAutoRetry + @PreventFailureSuppression + def "same_named_feature"() { + expect: + ++protectedExecutions < 0 + } +} From 144197a2599ff6c03de8dc106fa32b6b5debdfa8 Mon Sep 17 00:00:00 2001 From: Calvin Bayer Date: Tue, 20 Oct 2026 14:13:32 +0200 Subject: [PATCH 4/4] Use declaring source files for inherited test methods --- .../trace/civisibility/domain/TestImpl.java | 7 +- .../civisibility/test/ExecutionStrategy.java | 10 +- .../domain/InheritedTestSourceTest.java | 131 ++++++++++++++++++ 3 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/domain/InheritedTestSourceTest.java diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java index e8f4aaf9f69..7cc327adb3e 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java @@ -180,13 +180,14 @@ private void populateSourceDataTags( SourcePathResolver sourcePathResolver, LinesResolver linesResolver, Codeowners codeowners) { - if (testClass == null) { + Class sourceClass = testMethod != null ? testMethod.getDeclaringClass() : testClass; + if (sourceClass == null) { return; } - Collection sourcePaths = sourcePathResolver.getSourcePaths(testClass); + Collection sourcePaths = sourcePathResolver.getSourcePaths(sourceClass); if (sourcePaths.size() != 1) { - log.debug("Could not populate source path for {}", testClass); + log.debug("Could not populate source path for {}", sourceClass); return; } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/test/ExecutionStrategy.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/test/ExecutionStrategy.java index f3fef8d1513..7d6f7ad0090 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/test/ExecutionStrategy.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/test/ExecutionStrategy.java @@ -207,18 +207,20 @@ public boolean isEFDLimitReached() { } public boolean isModified(@Nonnull TestSourceData testSourceData) { - Class testClass = testSourceData.getTestClass(); - if (testClass == null) { + Method testMethod = testSourceData.getTestMethod(); + Class sourceClass = + testMethod != null ? testMethod.getDeclaringClass() : testSourceData.getTestClass(); + if (sourceClass == null) { return false; } try { - Collection sourcePaths = sourcePathResolver.getSourcePaths(testClass); + Collection sourcePaths = sourcePathResolver.getSourcePaths(sourceClass); if (sourcePaths.size() != 1) { return false; } String sourcePath = sourcePaths.iterator().next(); - LinesResolver.Lines lines = getLines(testSourceData.getTestMethod()); + LinesResolver.Lines lines = getLines(testMethod); return executionSettings .getPullRequestDiff() .contains(sourcePath, lines.getStartLineNumber(), lines.getEndLineNumber()); diff --git a/dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/domain/InheritedTestSourceTest.java b/dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/domain/InheritedTestSourceTest.java new file mode 100644 index 00000000000..4d6d534741c --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/domain/InheritedTestSourceTest.java @@ -0,0 +1,131 @@ +package datadog.trace.civisibility.domain; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.RETURNS_SELF; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import datadog.trace.api.Config; +import datadog.trace.api.DDTraceId; +import datadog.trace.api.civisibility.config.TestSourceData; +import datadog.trace.api.civisibility.coverage.CoveragePerTestBridge; +import datadog.trace.api.civisibility.coverage.NoOpCoverageStore; +import datadog.trace.api.civisibility.telemetry.NoOpMetricCollector; +import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.civisibility.codeowners.NoCodeowners; +import datadog.trace.civisibility.config.ConfigurationErrors; +import datadog.trace.civisibility.config.ExecutionSettings; +import datadog.trace.civisibility.decorator.TestDecorator; +import datadog.trace.civisibility.diff.LineDiff; +import datadog.trace.civisibility.source.LinesResolver; +import datadog.trace.civisibility.source.SourcePathResolver; +import datadog.trace.civisibility.test.ExecutionResults; +import datadog.trace.civisibility.test.ExecutionStrategy; +import java.lang.reflect.Method; +import java.util.BitSet; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class InheritedTestSourceTest { + private static final String PARENT_SOURCE = "src/test/java/ParentSpec.java"; + private static final String LEAF_SOURCE = "src/test/java/LeafSpec.java"; + + static class ParentSpec { + public void feature() {} + } + + static class LeafSpec extends ParentSpec {} + + @Test + void reportsParentSourceAndLinesWhilePreservingConcreteSuiteIdentity() throws Exception { + Method method = ParentSpec.class.getDeclaredMethod("feature"); + SourcePathResolver sourcePaths = sourcePaths(); + LinesResolver lines = mock(LinesResolver.class); + when(lines.getMethodLines(method)).thenReturn(new LinesResolver.Lines(10, 15)); + AgentSpan span = mock(AgentSpan.class); + AgentTracer.SpanBuilder builder = mock(AgentTracer.SpanBuilder.class, RETURNS_SELF); + when(builder.start()).thenReturn(span); + AgentTracer.TracerAPI tracer = mock(AgentTracer.TracerAPI.class); + when(tracer.buildSpan(anyString(), any())).thenReturn(builder); + AgentSpanContext context = mock(AgentSpanContext.class); + when(context.getTraceId()).thenReturn(DDTraceId.ONE); + Config config = mock(Config.class); + when(config.isCiVisibilitySourceDataEnabled()).thenReturn(true); + TestDecorator decorator = mock(TestDecorator.class); + when(decorator.component()).thenReturn("spock"); + AgentTracer.TracerAPI original = AgentTracer.get(); + AgentTracer.forceRegister(tracer); + try { + new TestImpl( + context, + 1L, + "module", + LeafSpec.class.getName(), + "feature", + null, + null, + false, + null, + LeafSpec.class, + method, + InstrumentationType.BUILD, + TestFrameworkInstrumentation.SPOCK, + config, + NoOpMetricCollector.INSTANCE, + decorator, + sourcePaths, + lines, + NoCodeowners.INSTANCE, + new NoOpCoverageStore.Factory(), + new ExecutionResults(), + ConfigurationErrors.NONE, + emptyList(), + ignored -> {}); + + verify(span).setTag(Tags.TEST_SUITE, LeafSpec.class.getName()); + verify(span).setTag(Tags.TEST_SOURCE_FILE, PARENT_SOURCE); + verify(span).setTag(Tags.TEST_SOURCE_START, 10); + verify(span).setTag(Tags.TEST_SOURCE_END, 15); + } finally { + CoveragePerTestBridge.removeThreadLocalCoverageProbes(); + AgentTracer.forceRegister(original); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void checksModifiedLinesInTheDeclaringSource(boolean parentModified) throws Exception { + Method method = ParentSpec.class.getDeclaredMethod("feature"); + BitSet changedLines = new BitSet(); + changedLines.set(12); + ExecutionSettings settings = mock(ExecutionSettings.class); + when(settings.getPullRequestDiff()) + .thenReturn( + new LineDiff(singletonMap(parentModified ? PARENT_SOURCE : LEAF_SOURCE, changedLines))); + LinesResolver lines = mock(LinesResolver.class); + when(lines.getMethodLines(method)).thenReturn(new LinesResolver.Lines(10, 15)); + ExecutionStrategy strategy = + new ExecutionStrategy(mock(Config.class), settings, sourcePaths(), lines); + + assertEquals( + parentModified, strategy.isModified(new TestSourceData(LeafSpec.class, method, "feature"))); + } + + private static SourcePathResolver sourcePaths() { + SourcePathResolver resolver = mock(SourcePathResolver.class); + when(resolver.getSourcePaths(ParentSpec.class)).thenReturn(singletonList(PARENT_SOURCE)); + when(resolver.getSourcePaths(LeafSpec.class)).thenReturn(singletonList(LEAF_SOURCE)); + return resolver; + } +}