Skip to content

Commit

Permalink
Implement flaky tests retry for Spock
Browse files Browse the repository at this point in the history
  • Loading branch information
nikita-tkachenko-datadog committed Dec 13, 2023
1 parent 30789d4 commit bdf2753
Show file tree
Hide file tree
Showing 24 changed files with 1,851 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public String instrumentedType() {
public String[] helperClassNames() {
return new String[] {
packageName + ".JUnitPlatformUtils",
packageName + ".TestIdentifierFactory",
packageName + ".SpockUtils",
packageName + ".TestEventsHandlerHolder",
packageName + ".SpockTracingListener",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public ElementMatcher<TypeDescription> hierarchyMatcher() {
public String[] helperClassNames() {
return new String[] {
packageName + ".JUnitPlatformUtils",
packageName + ".TestIdentifierFactory",
packageName + ".SpockUtils",
packageName + ".TestEventsHandlerHolder",
};
Expand Down Expand Up @@ -96,7 +97,7 @@ public static void shouldBeSkipped(
}
}

TestIdentifier test = SpockUtils.toTestIdentifier(spockNode);
TestIdentifier test = SpockUtils.toTestIdentifier(spockNode, true);
if (test != null && TestEventsHandlerHolder.TEST_EVENTS_HANDLER.skip(test)) {
skipResult = Node.SkipResult.skip(InstrumentationBridge.ITR_SKIP_REASON);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import datadog.trace.api.civisibility.config.TestIdentifier;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -20,37 +19,17 @@

public class SpockUtils {

private static final MethodHandle GET_TEST_TAGS;
private static final MethodHandle GET_TEST_TAG_VALUE;
private static final datadog.trace.util.MethodHandles METHOD_HANDLES =
new datadog.trace.util.MethodHandles(ClassLoaderUtils.getDefaultClassLoader());

static {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
ClassLoader defaultClassLoader = ClassLoaderUtils.getDefaultClassLoader();
GET_TEST_TAGS = accessGetTestTags(lookup, defaultClassLoader);
GET_TEST_TAG_VALUE = accessGetTestTagValue(lookup, defaultClassLoader);
}
private static final MethodHandle GET_TEST_TAGS =
METHOD_HANDLES.method("org.spockframework.runtime.model.ITestTaggable", "getTestTags");

private static MethodHandle accessGetTestTags(
MethodHandles.Lookup lookup, ClassLoader classLoader) {
try {
Class<?> testTaggable =
classLoader.loadClass("org.spockframework.runtime.model.ITestTaggable");
Method method = testTaggable.getDeclaredMethod("getTestTags");
return lookup.unreflect(method);
} catch (Throwable throwable) {
return null;
}
}
private static final MethodHandle GET_TEST_TAG_VALUE =
METHOD_HANDLES.method("org.spockframework.runtime.model.TestTag", "getValue");

private static MethodHandle accessGetTestTagValue(
MethodHandles.Lookup lookup, ClassLoader classLoader) {
try {
Class<?> testTaggable = classLoader.loadClass("org.spockframework.runtime.model.TestTag");
Method method = testTaggable.getDeclaredMethod("getValue");
return lookup.unreflect(method);
} catch (Throwable throwable) {
return null;
}
static {
TestIdentifierFactory.register("spock", SpockUtils::toTestIdentifier);
}

/*
Expand Down Expand Up @@ -105,13 +84,20 @@ public static Method getTestMethod(MethodSource methodSource) {
return null;
}

public static TestIdentifier toTestIdentifier(SpockNode spockNode) {
TestSource testSource = spockNode.getSource().orElse(null);
if (testSource instanceof MethodSource) {
public static TestIdentifier toTestIdentifier(
TestDescriptor testDescriptor, boolean includeParameters) {
TestSource testSource = testDescriptor.getSource().orElse(null);
if (testSource instanceof MethodSource && testDescriptor instanceof SpockNode) {
SpockNode spockNode = (SpockNode) testDescriptor;
MethodSource methodSource = (MethodSource) testSource;
String testSuiteName = methodSource.getClassName();
String displayName = spockNode.getDisplayName();
String testParameters = JUnitPlatformUtils.getParameters(methodSource, displayName);
String testParameters;
if (includeParameters) {
testParameters = JUnitPlatformUtils.getParameters(methodSource, displayName);
} else {
testParameters = null;
}
return new TestIdentifier(testSuiteName, displayName, testParameters, null);

} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package datadog.trace.instrumentation.junit5.retry;

import static net.bytebuddy.matcher.ElementMatchers.isConstructor;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.api.Config;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.bytebuddy.asm.Advice;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestDescriptor;

/**
* Applies a patch to Spock's parameterized tests executor, needed to support retries for
* parameterized tests
*/
@AutoService(Instrumenter.class)
public class JUnit5SpockParameterizedRetryInstrumentation extends Instrumenter.CiVisibility
implements Instrumenter.ForSingleType {

public JUnit5SpockParameterizedRetryInstrumentation() {
super("ci-visibility", "junit-5", "junit-5-spock", "test-retry");
}

@Override
public boolean isApplicable(Set<TargetSystem> enabledSystems) {
return super.isApplicable(enabledSystems) && Config.get().isCiVisibilityFlakyRetryEnabled();
}

@Override
public String instrumentedType() {
return "org.spockframework.runtime.ParameterizedFeatureChildExecutor";
}

@Override
public String[] helperClassNames() {
return new String[] {
packageName + ".SpockParameterizedExecutionListener",
};
}

@Override
public void adviceTransformations(AdviceTransformation transformation) {
transformation.applyAdvice(
isConstructor(),
JUnit5SpockParameterizedRetryInstrumentation.class.getName()
+ "$SpockParameterizedRetryAdvice");
}

public static class SpockParameterizedRetryAdvice {

@SuppressFBWarnings(
value = "UC_USELESS_OBJECT",
justification = "executionListener is a field in the instrumented class")
@Advice.OnMethodExit
public static void afterConstructor(
@Advice.FieldValue(value = "executionListener", readOnly = false)
EngineExecutionListener executionListener,
@Advice.FieldValue("pending") Map<TestDescriptor, CompletableFuture<?>> pending) {
executionListener = new SpockParameterizedExecutionListener(executionListener, pending);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package datadog.trace.instrumentation.junit5.retry;

import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.reporting.ReportEntry;

public class SpockParameterizedExecutionListener implements EngineExecutionListener {
private final EngineExecutionListener delegate;
private final Map<TestDescriptor, CompletableFuture<?>> pending;

public SpockParameterizedExecutionListener(
EngineExecutionListener delegate, Map<TestDescriptor, CompletableFuture<?>> pending) {
this.delegate = delegate;
this.pending = pending;
}

@Override
public void dynamicTestRegistered(TestDescriptor testDescriptor) {
delegate.dynamicTestRegistered(testDescriptor);
if (RetryContext.RETRY_ATTEMPT_TEST_ID_SEGMENT.equals(
testDescriptor.getUniqueId().getLastSegment().getType())) {
// register generated retry descriptor
pending.put(testDescriptor, new CompletableFuture<>());
}
}

@Override
public void executionSkipped(TestDescriptor testDescriptor, String reason) {
delegate.executionSkipped(testDescriptor, reason);
}

@Override
public void executionStarted(TestDescriptor testDescriptor) {
delegate.executionStarted(testDescriptor);
}

@Override
public void executionFinished(
TestDescriptor testDescriptor, TestExecutionResult testExecutionResult) {
delegate.executionFinished(testDescriptor, testExecutionResult);
}

@Override
public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) {
delegate.reportingEntryPublished(testDescriptor, entry);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import datadog.trace.api.DisableTestTrace
import datadog.trace.api.civisibility.config.TestIdentifier
import datadog.trace.civisibility.CiVisibilityInstrumentationTest
import datadog.trace.instrumentation.junit5.TestEventsHandlerHolder
import org.example.TestFailedParameterizedSpock
import org.example.TestFailedSpock
import org.example.TestFailedThenSucceedParameterizedSpock
import org.example.TestFailedThenSucceedSpock
import org.example.TestParameterizedSpock
import org.example.TestSucceedSpock
import org.example.TestSucceedSpockUnskippable
Expand All @@ -19,6 +23,19 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass
class SpockTest extends CiVisibilityInstrumentationTest {

def "test #testcaseName"() {
setup:
runTests(tests)

expect:
assertSpansData(testcaseName, expectedTracesCount)

where:
testcaseName | tests | expectedTracesCount
"test-succeed" | [TestSucceedSpock] | 2
"test-succeed-parameterized" | [TestParameterizedSpock] | 3
}

def "test ITR #testcaseName"() {
setup:
givenSkippableTests(skippedTests)
runTests(tests)
Expand All @@ -28,8 +45,6 @@ class SpockTest extends CiVisibilityInstrumentationTest {

where:
testcaseName | tests | expectedTracesCount | skippedTests
"test-succeed" | [TestSucceedSpock] | 2 | []
"test-succeed-parameterized" | [TestParameterizedSpock] | 3 | []
"test-itr-skipping" | [TestSucceedSpock] | 2 | [new TestIdentifier("org.example.TestSucceedSpock", "test success", null, null)]
"test-itr-skipping-parameterized" | [TestParameterizedSpock] | 3 | [
new TestIdentifier("org.example.TestParameterizedSpock", "test add 1 and 2", '{"metadata":{"test_name":"test add 1 and 2"}}', null)
Expand All @@ -38,6 +53,28 @@ class SpockTest extends CiVisibilityInstrumentationTest {
"test-itr-unskippable-suite" | [TestSucceedSpockUnskippableSuite] | 2 | [new TestIdentifier("org.example.TestSucceedSpockUnskippableSuite", "test success", null, null)]
}

def "test flaky retries #testcaseName"() {
setup:
givenFlakyTests(retriedTests)

runTests(tests)

expect:
assertSpansData(testcaseName, expectedTracesCount)

where:
testcaseName | tests | expectedTracesCount | retriedTests
"test-failed" | [TestFailedSpock] | 2 | []
"test-retry-failed" | [TestFailedSpock] | 6 | [new TestIdentifier("org.example.TestFailedSpock", "test failed", null, null)]
"test-failed-then-succeed" | [TestFailedThenSucceedSpock] | 5 | [
new TestIdentifier("org.example.TestFailedThenSucceedSpock", "test failed then succeed", null, null)
]
"test-retry-parameterized" | [TestFailedParameterizedSpock] | 3 | [new TestIdentifier("org.example.TestFailedParameterizedSpock", "test add 4 and 4", null, null)]
"test-parameterized-failed-then-succeed" | [TestFailedThenSucceedParameterizedSpock] | 5 | [
new TestIdentifier("org.example.TestFailedThenSucceedParameterizedSpock", "test add 1 and 2", null, null)
]
}

private static void runTests(List<Class<?>> classes) {
TestEventsHandlerHolder.start()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.example

import spock.lang.Specification

class TestFailedParameterizedSpock extends Specification {

def "test add #a and #b"() {
expect:
a + b == c

where:
a | b | c
1 | 2 | 3
4 | 4 | 44
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.example

import spock.lang.Specification

class TestFailedSpock extends Specification {

def "test failed"() {
expect:
1 == 2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.example

import spock.lang.Specification

class TestFailedThenSucceedParameterizedSpock extends Specification {

public static int testExecutionCount = 0

def "test add #a and #b"() {
expect:
c == 3 && ++testExecutionCount > 2

where:
a | b | c
1 | 2 | 3
4 | 4 | 8
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.example

import spock.lang.Specification

class TestFailedThenSucceedSpock extends Specification {

public static int testExecutionCount = 0

def "test failed then succeed"() {
expect:
++testExecutionCount > 3
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ ]
Loading

0 comments on commit bdf2753

Please sign in to comment.