Skip to content

Commit faea891

Browse files
authored
[test optimization] [SDTEST-1630] Attempt to fix flaky tests implementation (#5429)
1 parent fac8988 commit faea891

File tree

32 files changed

+2300
-198
lines changed

32 files changed

+2300
-198
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Feature: Attempt to fix
2+
Scenario: Say attempt to fix
3+
When the greeter says attempt to fix
4+
Then I should have heard "attempt to fix"

integration-tests/ci-visibility/features-test-management/support/steps.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const assert = require('assert')
22
const { When, Then } = require('@cucumber/cucumber')
33

4+
let numAttempt = 0
5+
46
Then('I should have heard {string}', function (expectedResponse) {
57
if (this.whatIHeard === 'quarantine') {
68
assert.equal(this.whatIHeard, 'fail')
@@ -21,3 +23,16 @@ When('the greeter says disabled', function () {
2123
// expected to fail if not disabled
2224
this.whatIHeard = 'disabld'
2325
})
26+
27+
When('the greeter says attempt to fix', function () {
28+
// eslint-disable-next-line no-console
29+
console.log('I am running') // just to assert whether this is running
30+
// expected to fail
31+
if (process.env.SHOULD_ALWAYS_PASS) {
32+
this.whatIHeard = 'attempt to fix'
33+
} else if (process.env.SHOULD_FAIL_SOMETIMES) {
34+
this.whatIHeard = numAttempt++ % 2 === 0 ? 'attempt to fix' : 'attempt to fx'
35+
} else {
36+
this.whatIHeard = 'attempt to fx'
37+
}
38+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const { test, expect } = require('@playwright/test')
2+
3+
test.beforeEach(async ({ page }) => {
4+
await page.goto(process.env.PW_BASE_URL)
5+
})
6+
7+
test.describe('attempt to fix', () => {
8+
test('should attempt to fix failed test', async ({ page }) => {
9+
let textToAssert
10+
11+
if (process.env.SHOULD_ALWAYS_PASS) {
12+
textToAssert = 'Hello World'
13+
} else if (process.env.SHOULD_FAIL_SOMETIMES) {
14+
// can't use numAttempt++ because we're running in parallel
15+
if (Number(process.env.TEST_WORKER_INDEX) % 2 === 0) {
16+
throw new Error('Hello Warld')
17+
}
18+
textToAssert = 'Hello World'
19+
} else {
20+
textToAssert = 'Hello Warld'
21+
}
22+
23+
await expect(page.locator('.hello-world')).toHaveText([
24+
textToAssert
25+
])
26+
})
27+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const { expect } = require('chai')
2+
3+
let numAttempts = 0
4+
5+
describe('attempt to fix tests', () => {
6+
it('can attempt to fix a test', () => {
7+
// eslint-disable-next-line no-console
8+
console.log('I am running when attempt to fix') // to check if this is being run
9+
if (process.env.SHOULD_ALWAYS_PASS) {
10+
expect(1 + 2).to.equal(3)
11+
} else if (process.env.SHOULD_FAIL_SOMETIMES) {
12+
if (numAttempts++ % 2 === 0) {
13+
expect(1 + 2).to.equal(3)
14+
} else {
15+
expect(1 + 2).to.equal(4)
16+
}
17+
} else {
18+
expect(1 + 2).to.equal(4)
19+
}
20+
})
21+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { expect } = require('chai')
2+
3+
describe('attempt to fix tests 2', () => {
4+
it('can attempt to fix a test', () => {
5+
// eslint-disable-next-line no-console
6+
console.log('I am running when attempt to fix 2') // to check if this is being run
7+
expect(1 + 2).to.equal(3)
8+
})
9+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, test, expect } from 'vitest'
2+
3+
let numAttempt = 0
4+
5+
describe('attempt to fix tests', () => {
6+
test('can attempt to fix a test', () => {
7+
// eslint-disable-next-line no-console
8+
console.log('I am running') // to check if this is being run
9+
if (process.env.SHOULD_ALWAYS_PASS) {
10+
expect(1 + 2).to.equal(3)
11+
} else if (process.env.SHOULD_FAIL_SOMETIMES) {
12+
// We need the last attempt to fail for the exit code to be 1
13+
if (numAttempt++ % 2 === 1) {
14+
expect(1 + 2).to.equal(4)
15+
} else {
16+
expect(1 + 2).to.equal(3)
17+
}
18+
} else {
19+
expect(1 + 2).to.equal(4)
20+
}
21+
})
22+
})

integration-tests/cucumber/cucumber.spec.js

Lines changed: 227 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ const {
4949
TEST_MANAGEMENT_IS_DISABLED,
5050
DD_CAPABILITIES_TEST_IMPACT_ANALYSIS,
5151
DD_CAPABILITIES_EARLY_FLAKE_DETECTION,
52-
DD_CAPABILITIES_AUTO_TEST_RETRIES
52+
DD_CAPABILITIES_AUTO_TEST_RETRIES,
53+
TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX,
54+
TEST_HAS_FAILED_ALL_RETRIES,
55+
TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED
5356
} = require('../../packages/dd-trace/src/plugins/util/test')
5457
const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env')
5558

@@ -2031,6 +2034,229 @@ versions.forEach(version => {
20312034
})
20322035

20332036
context('test management', () => {
2037+
context('attempt to fix', () => {
2038+
beforeEach(() => {
2039+
receiver.setTestManagementTests({
2040+
cucumber: {
2041+
suites: {
2042+
'ci-visibility/features-test-management/attempt-to-fix.feature': {
2043+
tests: {
2044+
'Say attempt to fix': {
2045+
properties: {
2046+
attempt_to_fix: true
2047+
}
2048+
}
2049+
}
2050+
}
2051+
}
2052+
}
2053+
})
2054+
})
2055+
2056+
const getTestAssertions = ({
2057+
isAttemptToFix,
2058+
isQuarantined,
2059+
isDisabled,
2060+
shouldAlwaysPass,
2061+
shouldFailSometimes
2062+
}) =>
2063+
receiver
2064+
.gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => {
2065+
const events = payloads.flatMap(({ payload }) => payload.events)
2066+
const tests = events.filter(event => event.type === 'test').map(event => event.content)
2067+
const testSession = events.find(event => event.type === 'test_session_end').content
2068+
2069+
if (isAttemptToFix) {
2070+
assert.propertyVal(testSession.meta, TEST_MANAGEMENT_ENABLED, 'true')
2071+
} else {
2072+
assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED)
2073+
}
2074+
2075+
const retriedTests = tests.filter(
2076+
test => test.meta[TEST_NAME] === 'Say attempt to fix'
2077+
)
2078+
2079+
if (isAttemptToFix) {
2080+
// 3 retries + 1 initial run
2081+
assert.equal(retriedTests.length, 4)
2082+
} else {
2083+
assert.equal(retriedTests.length, 1)
2084+
}
2085+
2086+
for (let i = 0; i < retriedTests.length; i++) {
2087+
const isFirstAttempt = i === 0
2088+
const isLastAttempt = i === retriedTests.length - 1
2089+
const test = retriedTests[i]
2090+
2091+
assert.equal(
2092+
test.resource,
2093+
'ci-visibility/features-test-management/attempt-to-fix.feature.Say attempt to fix'
2094+
)
2095+
2096+
if (isDisabled) {
2097+
assert.propertyVal(test.meta, TEST_MANAGEMENT_IS_DISABLED, 'true')
2098+
} else if (isQuarantined) {
2099+
assert.propertyVal(test.meta, TEST_MANAGEMENT_IS_QUARANTINED, 'true')
2100+
} else {
2101+
assert.notProperty(test.meta, TEST_MANAGEMENT_IS_DISABLED)
2102+
assert.notProperty(test.meta, TEST_MANAGEMENT_IS_QUARANTINED)
2103+
}
2104+
2105+
if (isAttemptToFix) {
2106+
assert.propertyVal(test.meta, TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX, 'true')
2107+
if (!isFirstAttempt) {
2108+
assert.propertyVal(test.meta, TEST_IS_RETRY, 'true')
2109+
assert.propertyVal(test.meta, TEST_RETRY_REASON, 'attempt_to_fix')
2110+
}
2111+
if (isLastAttempt) {
2112+
if (shouldFailSometimes) {
2113+
assert.notProperty(test.meta, TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED)
2114+
assert.notProperty(test.meta, TEST_HAS_FAILED_ALL_RETRIES)
2115+
} else if (shouldAlwaysPass) {
2116+
assert.propertyVal(test.meta, TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'true')
2117+
} else {
2118+
assert.propertyVal(test.meta, TEST_HAS_FAILED_ALL_RETRIES, 'true')
2119+
}
2120+
}
2121+
} else {
2122+
assert.notProperty(test.meta, TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX)
2123+
assert.notProperty(test.meta, TEST_IS_RETRY)
2124+
assert.notProperty(test.meta, TEST_RETRY_REASON)
2125+
}
2126+
}
2127+
})
2128+
2129+
const runTest = (done, {
2130+
isAttemptToFix,
2131+
isQuarantined,
2132+
isDisabled,
2133+
extraEnvVars,
2134+
shouldAlwaysPass,
2135+
shouldFailSometimes
2136+
} = {}) => {
2137+
const testAssertions = getTestAssertions({
2138+
isAttemptToFix,
2139+
isQuarantined,
2140+
isDisabled,
2141+
shouldAlwaysPass,
2142+
shouldFailSometimes
2143+
})
2144+
let stdout = ''
2145+
2146+
childProcess = exec(
2147+
'./node_modules/.bin/cucumber-js ci-visibility/features-test-management/attempt-to-fix.feature',
2148+
{
2149+
cwd,
2150+
env: {
2151+
...getCiVisAgentlessConfig(receiver.port),
2152+
...extraEnvVars,
2153+
...(shouldAlwaysPass ? { SHOULD_ALWAYS_PASS: '1' } : {}),
2154+
...(shouldFailSometimes ? { SHOULD_FAIL_SOMETIMES: '1' } : {})
2155+
},
2156+
stdio: 'inherit'
2157+
}
2158+
)
2159+
2160+
childProcess.stdout.on('data', (data) => {
2161+
stdout += data.toString()
2162+
})
2163+
2164+
childProcess.on('exit', exitCode => {
2165+
testAssertions.then(() => {
2166+
assert.include(stdout, 'I am running')
2167+
if (isQuarantined || isDisabled || shouldAlwaysPass) {
2168+
assert.equal(exitCode, 0)
2169+
} else {
2170+
assert.equal(exitCode, 1)
2171+
}
2172+
done()
2173+
}).catch(done)
2174+
})
2175+
}
2176+
2177+
it('can attempt to fix and mark last attempt as failed if every attempt fails', (done) => {
2178+
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
2179+
2180+
runTest(done, { isAttemptToFix: true })
2181+
})
2182+
2183+
it('can attempt to fix and mark last attempt as passed if every attempt passes', (done) => {
2184+
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
2185+
2186+
runTest(done, { isAttemptToFix: true, shouldAlwaysPass: true })
2187+
})
2188+
2189+
it('can attempt to fix and not mark last attempt if attempts both pass and fail', (done) => {
2190+
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
2191+
2192+
runTest(done, { isAttemptToFix: true, shouldFailSometimes: true })
2193+
})
2194+
2195+
it('does not attempt to fix tests if test management is not enabled', (done) => {
2196+
receiver.setSettings({ test_management: { enabled: false, attempt_to_fix_retries: 3 } })
2197+
2198+
runTest(done)
2199+
})
2200+
2201+
it('does not enable attempt to fix tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => {
2202+
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
2203+
2204+
runTest(done, {
2205+
extraEnvVars: { DD_TEST_MANAGEMENT_ENABLED: '0' }
2206+
})
2207+
})
2208+
2209+
it('does not fail retry if a test is quarantined', (done) => {
2210+
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
2211+
receiver.setTestManagementTests({
2212+
cucumber: {
2213+
suites: {
2214+
'ci-visibility/features-test-management/attempt-to-fix.feature': {
2215+
tests: {
2216+
'Say attempt to fix': {
2217+
properties: {
2218+
attempt_to_fix: true,
2219+
quarantined: true
2220+
}
2221+
}
2222+
}
2223+
}
2224+
}
2225+
}
2226+
})
2227+
2228+
runTest(done, {
2229+
isAttemptToFix: true,
2230+
isQuarantined: true
2231+
})
2232+
})
2233+
2234+
it('does not fail retry if a test is disabled', (done) => {
2235+
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
2236+
receiver.setTestManagementTests({
2237+
cucumber: {
2238+
suites: {
2239+
'ci-visibility/features-test-management/attempt-to-fix.feature': {
2240+
tests: {
2241+
'Say attempt to fix': {
2242+
properties: {
2243+
attempt_to_fix: true,
2244+
disabled: true
2245+
}
2246+
}
2247+
}
2248+
}
2249+
}
2250+
}
2251+
})
2252+
2253+
runTest(done, {
2254+
isAttemptToFix: true,
2255+
isDisabled: true
2256+
})
2257+
})
2258+
})
2259+
20342260
context('disabled', () => {
20352261
beforeEach(() => {
20362262
receiver.setTestManagementTests({

0 commit comments

Comments
 (0)