Skip to content

Commit 90b7c96

Browse files
[test optimization] Fix retry logic for flaky test management in playwright (#6757)
1 parent a377e2a commit 90b7c96

File tree

3 files changed

+167
-111
lines changed

3 files changed

+167
-111
lines changed

integration-tests/ci-visibility/playwright-tests-test-management/attempt-to-fix-test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,8 @@ test.describe('attempt to fix', () => {
2626
textToAssert
2727
])
2828
})
29+
30+
test('should attempt to fix passed test', async () => {
31+
expect(true).toBe(true)
32+
})
2933
})

integration-tests/playwright/playwright.spec.js

Lines changed: 129 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,7 @@ versions.forEach((version) => {
10841084
})
10851085

10861086
contextNewVersions('test management', () => {
1087+
const ATTEMPT_TO_FIX_NUM_RETRIES = 3
10871088
context('attempt to fix', () => {
10881089
beforeEach(() => {
10891090
receiver.setTestManagementTests({
@@ -1095,6 +1096,11 @@ versions.forEach((version) => {
10951096
properties: {
10961097
attempt_to_fix: true
10971098
}
1099+
},
1100+
'attempt to fix should attempt to fix passed test': {
1101+
properties: {
1102+
attempt_to_fix: true
1103+
}
10981104
}
10991105
}
11001106
}
@@ -1123,27 +1129,30 @@ versions.forEach((version) => {
11231129
}
11241130

11251131
const attemptedToFixTests = tests.filter(
1126-
test => test.meta[TEST_NAME] === 'attempt to fix should attempt to fix failed test'
1132+
test => test.meta[TEST_NAME].startsWith('attempt to fix should attempt to fix')
11271133
)
11281134

1129-
if (isAttemptingToFix) {
1130-
assert.equal(attemptedToFixTests.length, 4)
1131-
} else {
1132-
assert.equal(attemptedToFixTests.length, 1)
1133-
}
1134-
11351135
if (isDisabled) {
1136-
const numDisabledTests = attemptedToFixTests.filter(test =>
1136+
assert.equal(attemptedToFixTests.length, 2)
1137+
assert.isTrue(attemptedToFixTests.every(test =>
11371138
test.meta[TEST_MANAGEMENT_IS_DISABLED] === 'true'
1138-
).length
1139-
assert.equal(numDisabledTests, attemptedToFixTests.length)
1139+
))
1140+
// if the test is disabled, there will be no retries
1141+
return
1142+
}
1143+
1144+
if (isAttemptingToFix) {
1145+
assert.equal(attemptedToFixTests.length, 2 * (ATTEMPT_TO_FIX_NUM_RETRIES + 1))
1146+
} else {
1147+
assert.equal(attemptedToFixTests.length, 2)
11401148
}
11411149

11421150
if (isQuarantined) {
11431151
const numQuarantinedTests = attemptedToFixTests.filter(test =>
11441152
test.meta[TEST_MANAGEMENT_IS_QUARANTINED] === 'true'
11451153
).length
1146-
assert.equal(numQuarantinedTests, attemptedToFixTests.length)
1154+
// quarantined tests still run and are retried
1155+
assert.equal(numQuarantinedTests, 2 * (ATTEMPT_TO_FIX_NUM_RETRIES + 1))
11471156
}
11481157

11491158
// Retried tests are in randomly order, so we just count number of tests
@@ -1169,31 +1178,34 @@ versions.forEach((version) => {
11691178
test.meta[TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED] === 'false'
11701179
).length
11711180

1181+
// One of the tests is passing always
11721182
if (isAttemptingToFix) {
1173-
assert.equal(countAttemptToFixTests, attemptedToFixTests.length)
1174-
assert.equal(countRetriedAttemptToFixTests, attemptedToFixTests.length - 1)
1183+
assert.equal(countAttemptToFixTests, 2 * (ATTEMPT_TO_FIX_NUM_RETRIES + 1))
1184+
assert.equal(countRetriedAttemptToFixTests, 2 * ATTEMPT_TO_FIX_NUM_RETRIES)
11751185
if (shouldAlwaysPass) {
11761186
assert.equal(testsMarkedAsFailedAllRetries, 0)
11771187
assert.equal(testsMarkedAsFailed, 0)
1178-
assert.equal(testsMarkedAsPassedAllRetries, 1)
1188+
assert.equal(testsMarkedAsPassedAllRetries, 2)
11791189
} else if (shouldFailSometimes) {
1190+
// one test failed sometimes, the other always passed
11801191
assert.equal(testsMarkedAsFailedAllRetries, 0)
11811192
assert.equal(testsMarkedAsFailed, 1)
1182-
assert.equal(testsMarkedAsPassedAllRetries, 0)
1183-
} else { // always fail
1193+
assert.equal(testsMarkedAsPassedAllRetries, 1)
1194+
} else {
1195+
// one test failed always, the other always passed
11841196
assert.equal(testsMarkedAsFailedAllRetries, 1)
11851197
assert.equal(testsMarkedAsFailed, 1)
1186-
assert.equal(testsMarkedAsPassedAllRetries, 0)
1198+
assert.equal(testsMarkedAsPassedAllRetries, 1)
11871199
}
11881200
} else {
11891201
assert.equal(countAttemptToFixTests, 0)
11901202
assert.equal(countRetriedAttemptToFixTests, 0)
11911203
assert.equal(testsMarkedAsFailedAllRetries, 0)
11921204
assert.equal(testsMarkedAsPassedAllRetries, 0)
11931205
}
1194-
})
1206+
}, 25000)
11951207

1196-
const runAttemptToFixTest = (done, {
1208+
const runAttemptToFixTest = async ({
11971209
isAttemptingToFix,
11981210
isQuarantined,
11991211
extraEnvVars,
@@ -1225,51 +1237,63 @@ versions.forEach((version) => {
12251237
}
12261238
)
12271239

1228-
childProcess.on('exit', (exitCode) => {
1229-
testAssertionsPromise.then(() => {
1230-
if (isQuarantined || isDisabled || shouldAlwaysPass) {
1231-
// even though a test fails, the exit code is 0 because the test is quarantined
1232-
assert.equal(exitCode, 0)
1233-
} else {
1234-
assert.equal(exitCode, 1)
1235-
}
1236-
done()
1237-
}).catch(done)
1238-
})
1240+
const [[exitCode]] = await Promise.all([
1241+
once(childProcess, 'exit'),
1242+
testAssertionsPromise
1243+
])
1244+
1245+
if (isQuarantined || isDisabled || shouldAlwaysPass) {
1246+
// even though a test fails, the exit code is 0 because the test is quarantined
1247+
assert.equal(exitCode, 0)
1248+
} else {
1249+
assert.equal(exitCode, 1)
1250+
}
12391251
}
12401252

1241-
it('can attempt to fix and mark last attempt as failed if every attempt fails', (done) => {
1242-
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
1253+
it('can attempt to fix and mark last attempt as failed if every attempt fails', async () => {
1254+
receiver.setSettings({
1255+
test_management: { enabled: true, attempt_to_fix_retries: ATTEMPT_TO_FIX_NUM_RETRIES }
1256+
})
12431257

1244-
runAttemptToFixTest(done, { isAttemptingToFix: true })
1258+
await runAttemptToFixTest({ isAttemptingToFix: true })
12451259
})
12461260

1247-
it('can attempt to fix and mark last attempt as passed if every attempt passes', (done) => {
1248-
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
1261+
it('can attempt to fix and mark last attempt as passed if every attempt passes', async () => {
1262+
receiver.setSettings({
1263+
test_management: { enabled: true, attempt_to_fix_retries: ATTEMPT_TO_FIX_NUM_RETRIES }
1264+
})
12491265

1250-
runAttemptToFixTest(done, { isAttemptingToFix: true, shouldAlwaysPass: true })
1266+
await runAttemptToFixTest({ isAttemptingToFix: true, shouldAlwaysPass: true })
12511267
})
12521268

1253-
it('can attempt to fix and not mark last attempt if attempts both pass and fail', (done) => {
1254-
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
1269+
it('can attempt to fix and not mark last attempt if attempts both pass and fail', async () => {
1270+
receiver.setSettings({
1271+
test_management: { enabled: true, attempt_to_fix_retries: ATTEMPT_TO_FIX_NUM_RETRIES }
1272+
})
12551273

1256-
runAttemptToFixTest(done, { isAttemptingToFix: true, shouldFailSometimes: true })
1274+
await runAttemptToFixTest({ isAttemptingToFix: true, shouldFailSometimes: true })
12571275
})
12581276

1259-
it('does not attempt to fix tests if test management is not enabled', (done) => {
1260-
receiver.setSettings({ test_management: { enabled: false, attempt_to_fix_retries: 3 } })
1277+
it('does not attempt to fix tests if test management is not enabled', async () => {
1278+
receiver.setSettings({
1279+
test_management: { enabled: false, attempt_to_fix_retries: ATTEMPT_TO_FIX_NUM_RETRIES }
1280+
})
12611281

1262-
runAttemptToFixTest(done)
1282+
await runAttemptToFixTest()
12631283
})
12641284

1265-
it('does not enable attempt to fix tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => {
1266-
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
1285+
it('does not enable attempt to fix tests if DD_TEST_MANAGEMENT_ENABLED is set to false', async () => {
1286+
receiver.setSettings({
1287+
test_management: { enabled: true, attempt_to_fix_retries: ATTEMPT_TO_FIX_NUM_RETRIES }
1288+
})
12671289

1268-
runAttemptToFixTest(done, { extraEnvVars: { DD_TEST_MANAGEMENT_ENABLED: '0' } })
1290+
await runAttemptToFixTest({ extraEnvVars: { DD_TEST_MANAGEMENT_ENABLED: '0' } })
12691291
})
12701292

1271-
it('does not fail retry if a test is quarantined', (done) => {
1272-
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
1293+
it('does not fail retry if a test is quarantined', async () => {
1294+
receiver.setSettings({
1295+
test_management: { enabled: true, attempt_to_fix_retries: ATTEMPT_TO_FIX_NUM_RETRIES }
1296+
})
12731297
receiver.setTestManagementTests({
12741298
playwright: {
12751299
suites: {
@@ -1280,18 +1304,26 @@ versions.forEach((version) => {
12801304
attempt_to_fix: true,
12811305
quarantined: true
12821306
}
1307+
},
1308+
'attempt to fix should attempt to fix passed test': {
1309+
properties: {
1310+
attempt_to_fix: true,
1311+
quarantined: true
1312+
}
12831313
}
12841314
}
12851315
}
12861316
}
12871317
}
12881318
})
12891319

1290-
runAttemptToFixTest(done, { isAttemptingToFix: true, isQuarantined: true })
1320+
await runAttemptToFixTest({ isAttemptingToFix: true, isQuarantined: true })
12911321
})
12921322

1293-
it('does not fail retry if a test is disabled', (done) => {
1294-
receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } })
1323+
it('does not fail retry if a test is disabled', async () => {
1324+
receiver.setSettings({
1325+
test_management: { enabled: true, attempt_to_fix_retries: ATTEMPT_TO_FIX_NUM_RETRIES }
1326+
})
12951327
receiver.setTestManagementTests({
12961328
playwright: {
12971329
suites: {
@@ -1302,14 +1334,20 @@ versions.forEach((version) => {
13021334
attempt_to_fix: true,
13031335
disabled: true
13041336
}
1337+
},
1338+
'attempt to fix should attempt to fix passed test': {
1339+
properties: {
1340+
attempt_to_fix: true,
1341+
disabled: true
1342+
}
13051343
}
13061344
}
13071345
}
13081346
}
13091347
}
13101348
})
13111349

1312-
runAttemptToFixTest(done, { isAttemptingToFix: true, isDisabled: true })
1350+
await runAttemptToFixTest({ isAttemptingToFix: true, isDisabled: true })
13131351
})
13141352
})
13151353

@@ -1353,9 +1391,9 @@ versions.forEach((version) => {
13531391
assert.equal(skippedTest.meta[TEST_STATUS], 'fail')
13541392
assert.notProperty(skippedTest.meta, TEST_MANAGEMENT_IS_DISABLED)
13551393
}
1356-
})
1394+
}, 25000)
13571395

1358-
const runDisableTest = (done, isDisabling, extraEnvVars) => {
1396+
const runDisableTest = async (isDisabling, extraEnvVars) => {
13591397
const testAssertionsPromise = getTestAssertions(isDisabling)
13601398

13611399
childProcess = exec(
@@ -1372,34 +1410,34 @@ versions.forEach((version) => {
13721410
}
13731411
)
13741412

1375-
childProcess.on('exit', (exitCode) => {
1376-
testAssertionsPromise.then(() => {
1377-
if (isDisabling) {
1378-
assert.equal(exitCode, 0)
1379-
} else {
1380-
assert.equal(exitCode, 1)
1381-
}
1382-
done()
1383-
}).catch(done)
1384-
})
1413+
const [[exitCode]] = await Promise.all([
1414+
once(childProcess, 'exit'),
1415+
testAssertionsPromise
1416+
])
1417+
1418+
if (isDisabling) {
1419+
assert.equal(exitCode, 0)
1420+
} else {
1421+
assert.equal(exitCode, 1)
1422+
}
13851423
}
13861424

1387-
it('can disable tests', (done) => {
1425+
it('can disable tests', async () => {
13881426
receiver.setSettings({ test_management: { enabled: true } })
13891427

1390-
runDisableTest(done, true)
1428+
await runDisableTest(true)
13911429
})
13921430

1393-
it('fails if disable is not enabled', (done) => {
1431+
it('fails if disable is not enabled', async () => {
13941432
receiver.setSettings({ test_management: { enabled: false } })
13951433

1396-
runDisableTest(done, false)
1434+
await runDisableTest(false)
13971435
})
13981436

1399-
it('does not enable disable tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => {
1437+
it('does not enable disable tests if DD_TEST_MANAGEMENT_ENABLED is set to false', async () => {
14001438
receiver.setSettings({ test_management: { enabled: true } })
14011439

1402-
runDisableTest(done, false, { DD_TEST_MANAGEMENT_ENABLED: '0' })
1440+
await runDisableTest(false, { DD_TEST_MANAGEMENT_ENABLED: '0' })
14031441
})
14041442
})
14051443

@@ -1438,9 +1476,9 @@ versions.forEach((version) => {
14381476
assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED)
14391477
assert.notProperty(failedTest.meta, TEST_MANAGEMENT_IS_QUARANTINED)
14401478
}
1441-
})
1479+
}, 25000)
14421480

1443-
const runQuarantineTest = (done, isQuarantining, extraEnvVars) => {
1481+
const runQuarantineTest = async (isQuarantining, extraEnvVars) => {
14441482
const testAssertionsPromise = getTestAssertions(isQuarantining)
14451483

14461484
childProcess = exec(
@@ -1457,34 +1495,34 @@ versions.forEach((version) => {
14571495
}
14581496
)
14591497

1460-
childProcess.on('exit', (exitCode) => {
1461-
testAssertionsPromise.then(() => {
1462-
if (isQuarantining) {
1463-
assert.equal(exitCode, 0)
1464-
} else {
1465-
assert.equal(exitCode, 1)
1466-
}
1467-
done()
1468-
}).catch(done)
1469-
})
1498+
const [[exitCode]] = await Promise.all([
1499+
once(childProcess, 'exit'),
1500+
testAssertionsPromise
1501+
])
1502+
1503+
if (isQuarantining) {
1504+
assert.equal(exitCode, 0)
1505+
} else {
1506+
assert.equal(exitCode, 1)
1507+
}
14701508
}
14711509

1472-
it('can quarantine tests', (done) => {
1510+
it('can quarantine tests', async () => {
14731511
receiver.setSettings({ test_management: { enabled: true } })
14741512

1475-
runQuarantineTest(done, true)
1513+
await runQuarantineTest(true)
14761514
})
14771515

1478-
it('fails if quarantine is not enabled', (done) => {
1516+
it('fails if quarantine is not enabled', async () => {
14791517
receiver.setSettings({ test_management: { enabled: false } })
14801518

1481-
runQuarantineTest(done, false)
1519+
await runQuarantineTest(false)
14821520
})
14831521

1484-
it('does not enable quarantine tests if DD_TEST_MANAGEMENT_ENABLED is set to false', (done) => {
1522+
it('does not enable quarantine tests if DD_TEST_MANAGEMENT_ENABLED is set to false', async () => {
14851523
receiver.setSettings({ test_management: { enabled: true } })
14861524

1487-
runQuarantineTest(done, false, { DD_TEST_MANAGEMENT_ENABLED: '0' })
1525+
await runQuarantineTest(false, { DD_TEST_MANAGEMENT_ENABLED: '0' })
14881526
})
14891527
})
14901528

@@ -1502,8 +1540,10 @@ versions.forEach((version) => {
15021540
const testSession = events.find(event => event.type === 'test_session_end').content
15031541
assert.notProperty(testSession.meta, TEST_MANAGEMENT_ENABLED)
15041542
const tests = events.filter(event => event.type === 'test').map(event => event.content)
1505-
// it is not retried
1506-
assert.equal(tests.length, 1)
1543+
// they are not retried
1544+
assert.equal(tests.length, 2)
1545+
const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true')
1546+
assert.equal(retriedTests.length, 0)
15071547
})
15081548

15091549
childProcess = exec(

0 commit comments

Comments
 (0)