Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> sourcePaths = sourcePathResolver.getSourcePaths(testClass);
Collection<String> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> sourcePaths = sourcePathResolver.getSourcePaths(testClass);
Collection<String> 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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Comment thread
calvinbayer marked this conversation as resolved.
JUnitPlatformUtils.getTags(feature));
if (skipReason == null) {
return;
}
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -122,31 +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 (Method declaredMethod : testClass.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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ 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.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
Expand All @@ -34,6 +39,89 @@ 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 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 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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.example

class TestAutomationExcludedCollisionLeaf extends TestAutomationExcludedCollisionMiddle {}
Loading
Loading