Skip to content

Commit a29522b

Browse files
Implement early exit for retry mechanisms (#10604)
feat: implement early exit policy chore: update test fixtures removing additional retries chore: missed final status updates fix: only tag "test.has_failed_all_retries" if test retried > 0 chore: lint Co-authored-by: daniel.mohedano <daniel.mohedano@datadoghq.com>
1 parent d77d632 commit a29522b

91 files changed

Lines changed: 461 additions & 6709 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ public void onTestFinish(
269269
if (outcome.lastExecution()) {
270270
test.setTag(Tags.TEST_FINAL_STATUS, outcome.finalStatus());
271271

272-
if (outcome.failedAllRetries()) {
272+
if (retryReason != null && outcome.failedAllRetries()) {
273273
test.setTag(Tags.TEST_HAS_FAILED_ALL_RETRIES, true);
274274
}
275275

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/execution/RunNTimes.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,37 @@
44
import datadog.trace.api.civisibility.execution.TestStatus;
55
import datadog.trace.api.civisibility.telemetry.tag.RetryReason;
66
import datadog.trace.civisibility.config.ExecutionsByDuration;
7+
import datadog.trace.civisibility.execution.exit.EarlyExitPolicy;
78
import java.util.List;
89

9-
/** Runs a test case N times (N depends on test duration) regardless of success or failure. */
10+
/**
11+
* Runs a test case N times (N depends on test duration) regardless of success or failure. The
12+
* execution can also be terminated early if its ExitPolicy evaluates to {@code true}.
13+
*/
1014
public class RunNTimes implements TestExecutionPolicy {
1115

1216
private final boolean suppressFailures;
1317
private final List<ExecutionsByDuration> executionsByDuration;
1418
private int executions;
1519
private int maxExecutions;
1620
private int successfulExecutionsSeen;
21+
private int failedExecutionsSeen;
1722
private final RetryReason retryReason;
1823
private TestStatus lastStatus;
24+
private final EarlyExitPolicy exitPolicy;
1925

2026
public RunNTimes(
2127
List<ExecutionsByDuration> executionsByDuration,
2228
boolean suppressFailures,
23-
RetryReason retryReason) {
29+
RetryReason retryReason,
30+
EarlyExitPolicy exitPolicy) {
2431
this.suppressFailures = suppressFailures;
2532
this.executionsByDuration = executionsByDuration;
2633
this.executions = 0;
2734
this.maxExecutions = getExecutions(0);
2835
this.successfulExecutionsSeen = 0;
2936
this.retryReason = retryReason;
37+
this.exitPolicy = exitPolicy;
3038
}
3139

3240
@Override
@@ -35,20 +43,21 @@ public ExecutionOutcome registerExecution(TestStatus status, long durationMillis
3543
++executions;
3644
if (status != TestStatus.fail) {
3745
++successfulExecutionsSeen;
46+
} else {
47+
++failedExecutionsSeen;
3848
}
3949
int maxExecutionsForGivenDuration = getExecutions(durationMillis);
4050
maxExecutions = Math.min(maxExecutions, maxExecutionsForGivenDuration);
4151

4252
boolean lastExecution = !retriesLeft();
4353
boolean retry = executions > 1; // first execution is not a retry
4454
boolean failureSuppressed = status == TestStatus.fail && suppressFailures();
45-
boolean succeededAllRetries = lastExecution && successfulExecutionsSeen == executions;
46-
55+
boolean succeededAllExecutions = successfulExecutionsSeen == executions;
4756
TestStatus finalStatus = null;
4857
if (lastExecution) {
4958
// final status will only be "pass" if all retries pass (or the failures were suppressed)
5059
// also, the `suppressFailures()` call works because its value cannot change between retries
51-
if (succeededAllRetries || suppressFailures()) {
60+
if (succeededAllExecutions || suppressFailures()) {
5261
finalStatus = TestStatus.pass;
5362
} else {
5463
finalStatus = TestStatus.fail;
@@ -58,15 +67,17 @@ public ExecutionOutcome registerExecution(TestStatus status, long durationMillis
5867
return new ExecutionOutcomeImpl(
5968
failureSuppressed,
6069
lastExecution,
61-
lastExecution && successfulExecutionsSeen == 0,
62-
succeededAllRetries,
70+
lastExecution && failedExecutionsSeen == executions,
71+
lastExecution && succeededAllExecutions,
6372
retry ? retryReason : null,
6473
finalStatus);
6574
}
6675

6776
private boolean retriesLeft() {
6877
// skipped tests (either by the framework or DD) should not be retried
69-
return lastStatus != TestStatus.skip && executions < maxExecutions;
78+
return lastStatus != TestStatus.skip
79+
&& executions < maxExecutions
80+
&& !exitPolicy.evaluate(failedExecutionsSeen != 0, successfulExecutionsSeen != 0);
7081
}
7182

7283
@Override
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package datadog.trace.civisibility.execution.exit;
2+
3+
public interface EarlyExitPolicy {
4+
5+
/**
6+
* @return {@code true} if the policy indicates that the test should not be retried anymore.
7+
*/
8+
boolean evaluate(boolean hasFailedExecutions, boolean hasPassedExecutions);
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package datadog.trace.civisibility.execution.exit;
2+
3+
/** Policy to avoid retries once a test fails once. Used for Attempt to Fix. */
4+
public class ExitOnFailure implements EarlyExitPolicy {
5+
public static final EarlyExitPolicy INSTANCE = new ExitOnFailure();
6+
7+
private ExitOnFailure() {}
8+
9+
@Override
10+
public boolean evaluate(boolean hasFailedExecutions, boolean hasPassedExecutions) {
11+
return hasFailedExecutions;
12+
}
13+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package datadog.trace.civisibility.execution.exit;
2+
3+
/**
4+
* Policy to avoid retries once a test flakes (has at least one failed and passed execution). Used
5+
* for Early Flake Detection.
6+
*/
7+
public class ExitOnFlake implements EarlyExitPolicy {
8+
public static final EarlyExitPolicy INSTANCE = new ExitOnFlake();
9+
10+
private ExitOnFlake() {}
11+
12+
@Override
13+
public boolean evaluate(boolean hasFailedExecutions, boolean hasPassedExecutions) {
14+
return hasFailedExecutions && hasPassedExecutions;
15+
}
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package datadog.trace.civisibility.execution.exit;
2+
3+
/** Policy that ignores early exit on retries. Currently only used in testing. */
4+
public class NoExit implements EarlyExitPolicy {
5+
public static final EarlyExitPolicy INSTANCE = new NoExit();
6+
7+
private NoExit() {}
8+
9+
@Override
10+
public boolean evaluate(boolean hasFailedExecutions, boolean hasPassedExecutions) {
11+
return false;
12+
}
13+
}

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/test/ExecutionStrategy.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import datadog.trace.civisibility.execution.RetryUntilSuccessful;
1717
import datadog.trace.civisibility.execution.RunNTimes;
1818
import datadog.trace.civisibility.execution.RunOnceIgnoreOutcome;
19+
import datadog.trace.civisibility.execution.exit.ExitOnFailure;
20+
import datadog.trace.civisibility.execution.exit.ExitOnFlake;
1921
import datadog.trace.civisibility.source.LinesResolver;
2022
import datadog.trace.civisibility.source.SourcePathResolver;
2123
import java.lang.reflect.Method;
@@ -129,7 +131,8 @@ public TestExecutionPolicy executionPolicy(
129131
return new RunNTimes(
130132
executionSettings.getTestManagementSettings().getAttemptToFixExecutions(),
131133
isQuarantined(test) || isDisabled(test),
132-
RetryReason.attemptToFix);
134+
RetryReason.attemptToFix,
135+
ExitOnFailure.INSTANCE);
133136
}
134137

135138
if (isEFDApplicable(test, testSource, testTags)) {
@@ -139,7 +142,8 @@ public TestExecutionPolicy executionPolicy(
139142
return new RunNTimes(
140143
executionSettings.getEarlyFlakeDetectionSettings().getExecutionsByDuration(),
141144
isQuarantined(test),
142-
RetryReason.efd);
145+
RetryReason.efd,
146+
ExitOnFlake.INSTANCE);
143147
}
144148

145149
if (isAutoRetryApplicable(test)) {

dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/execution/RunNTimesTest.groovy

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package datadog.trace.civisibility.execution
33
import datadog.trace.api.civisibility.execution.TestStatus
44
import datadog.trace.api.civisibility.telemetry.tag.RetryReason
55
import datadog.trace.civisibility.config.ExecutionsByDuration
6+
import datadog.trace.civisibility.execution.exit.ExitOnFailure
7+
import datadog.trace.civisibility.execution.exit.ExitOnFlake
8+
import datadog.trace.civisibility.execution.exit.NoExit
69
import spock.lang.Specification
710

811
class RunNTimesTest extends Specification {
912

1013
def "test run N times"() {
1114
setup:
12-
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], false, RetryReason.efd)
15+
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], false, RetryReason.efd, NoExit.INSTANCE)
1316

1417
when:
1518
def outcome = executionPolicy.registerExecution(TestStatus.fail, 0)
@@ -47,7 +50,7 @@ class RunNTimesTest extends Specification {
4750

4851
def "test failed all retries"() {
4952
setup:
50-
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], false, RetryReason.efd)
53+
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], false, RetryReason.efd, NoExit.INSTANCE)
5154

5255
when:
5356
def outcome = executionPolicy.registerExecution(TestStatus.fail, 0)
@@ -85,7 +88,7 @@ class RunNTimesTest extends Specification {
8588

8689
def "test succeeded all retries"() {
8790
setup:
88-
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], false, RetryReason.efd)
91+
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], false, RetryReason.efd, NoExit.INSTANCE)
8992

9093
when:
9194
def outcome = executionPolicy.registerExecution(TestStatus.pass, 0)
@@ -123,7 +126,7 @@ class RunNTimesTest extends Specification {
123126

124127
def "test suppress failures"() {
125128
setup:
126-
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], true, RetryReason.attemptToFix)
129+
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], true, RetryReason.attemptToFix, NoExit.INSTANCE)
127130

128131
when:
129132
def outcome = executionPolicy.registerExecution(TestStatus.fail, 0)
@@ -159,17 +162,48 @@ class RunNTimesTest extends Specification {
159162
outcome3.finalStatus() == TestStatus.pass
160163
}
161164

165+
def "test exits on flake"(){
166+
setup:
167+
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], false, RetryReason.efd, ExitOnFlake.INSTANCE)
168+
169+
when:
170+
def outcome = executionPolicy.registerExecution(TestStatus.fail, 0)
171+
172+
then:
173+
!outcome.lastExecution()
174+
outcome.finalStatus() == null
175+
176+
when:
177+
def outcome2 = executionPolicy.registerExecution(TestStatus.pass, 0)
178+
179+
then:
180+
outcome2.lastExecution()
181+
outcome2.finalStatus() == TestStatus.fail
182+
}
183+
184+
def "test exits on failure"() {
185+
setup:
186+
def executionPolicy = new RunNTimes([new ExecutionsByDuration(Long.MAX_VALUE, 3)], false, RetryReason.attemptToFix, ExitOnFailure.INSTANCE)
187+
188+
when:
189+
def outcome = executionPolicy.registerExecution(TestStatus.fail, 0)
190+
191+
then:
192+
outcome.lastExecution()
193+
outcome.finalStatus() == TestStatus.fail
194+
}
195+
162196
def "test adaptive retry count"() {
163197
when:
164-
def executionPolicy = new RunNTimes([new ExecutionsByDuration(100, 3), new ExecutionsByDuration(Long.MAX_VALUE, 1)], true, RetryReason.efd)
198+
def executionPolicy = new RunNTimes([new ExecutionsByDuration(100, 3), new ExecutionsByDuration(Long.MAX_VALUE, 1)], true, RetryReason.efd, NoExit.INSTANCE)
165199

166200
then:
167201
!executionPolicy.registerExecution(TestStatus.fail, 0).lastExecution()
168202
!executionPolicy.registerExecution(TestStatus.fail, 0).lastExecution()
169203
executionPolicy.registerExecution(TestStatus.fail, 0).lastExecution()
170204

171205
when:
172-
executionPolicy = new RunNTimes([new ExecutionsByDuration(100, 3), new ExecutionsByDuration(Long.MAX_VALUE, 1)], true, RetryReason.efd)
206+
executionPolicy = new RunNTimes([new ExecutionsByDuration(100, 3), new ExecutionsByDuration(Long.MAX_VALUE, 1)], true, RetryReason.efd, NoExit.INSTANCE)
173207

174208
then:
175209
!executionPolicy.registerExecution(TestStatus.fail, 0).lastExecution()

dd-java-agent/instrumentation/junit/junit-4/junit-4-cucumber-5.4/src/test/resources/test-attempt-to-fix-disabled-failed/coverages.ftl

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,4 @@
55
"span_id" : ${content_span_id},
66
"test_session_id" : ${content_test_session_id},
77
"test_suite_id" : ${content_test_suite_id}
8-
}, {
9-
"files" : [ {
10-
"filename" : "org/example/cucumber/calculator/basic_arithmetic_failed.feature"
11-
} ],
12-
"span_id" : ${content_span_id_2},
13-
"test_session_id" : ${content_test_session_id},
14-
"test_suite_id" : ${content_test_suite_id}
15-
}, {
16-
"files" : [ {
17-
"filename" : "org/example/cucumber/calculator/basic_arithmetic_failed.feature"
18-
} ],
19-
"span_id" : ${content_span_id_3},
20-
"test_session_id" : ${content_test_session_id},
21-
"test_suite_id" : ${content_test_suite_id}
22-
}, {
23-
"files" : [ {
24-
"filename" : "org/example/cucumber/calculator/basic_arithmetic_failed.feature"
25-
} ],
26-
"span_id" : ${content_span_id_4},
27-
"test_session_id" : ${content_test_session_id},
28-
"test_suite_id" : ${content_test_suite_id}
29-
}, {
30-
"files" : [ {
31-
"filename" : "org/example/cucumber/calculator/basic_arithmetic_failed.feature"
32-
} ],
33-
"span_id" : ${content_span_id_5},
34-
"test_session_id" : ${content_test_session_id},
35-
"test_suite_id" : ${content_test_suite_id}
368
} ]

0 commit comments

Comments
 (0)