Skip to content

Commit 43efd4d

Browse files
[test optimization] Fix retry logic in impacted tests for playwright (#6755)
1 parent 5adf85f commit 43efd4d

3 files changed

Lines changed: 94 additions & 73 deletions

File tree

integration-tests/ci-visibility/playwright-tests-impacted-tests/impacted-test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ test.describe('impacted test', () => {
1313
])
1414
})
1515
})
16+
17+
test.describe('impacted test 2', () => {
18+
test('should be impacted 2', async ({ page }) => {
19+
await expect(page.locator('.hello-world')).toHaveText([
20+
'Hello World'
21+
])
22+
})
23+
})

integration-tests/playwright/playwright.spec.js

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,7 +1740,8 @@ versions.forEach((version) => {
17401740
beforeEach(() => {
17411741
receiver.setKnownTests({
17421742
playwright: {
1743-
'ci-visibility/playwright-tests-impacted-tests/impacted-test.js': ['impacted test should be impacted']
1743+
'ci-visibility/playwright-tests-impacted-tests/impacted-test.js':
1744+
['impacted test should be impacted', 'impacted test 2 should be impacted 2']
17441745
}
17451746
})
17461747
})
@@ -1762,6 +1763,13 @@ versions.forEach((version) => {
17621763
'Hello Worldd'
17631764
])
17641765
})
1766+
})
1767+
test.describe('impacted test 2', () => {
1768+
test('should be impacted 2', async ({ page }) => {
1769+
await expect(page.locator('.hello-world')).toHaveText([
1770+
'Hello World'
1771+
])
1772+
})
17651773
})`
17661774
)
17671775
execSync('git add ci-visibility/playwright-tests-impacted-tests/impacted-test.js', { cwd, stdio: 'ignore' })
@@ -1790,18 +1798,18 @@ versions.forEach((version) => {
17901798

17911799
assert.includeMembers(resourceNames,
17921800
[
1793-
'impacted-test.js.impacted test should be impacted'
1801+
'impacted-test.js.impacted test should be impacted',
1802+
'impacted-test.js.impacted test 2 should be impacted 2'
17941803
]
17951804
)
17961805

17971806
const impactedTests = tests.filter(test =>
1798-
test.meta[TEST_SOURCE_FILE] === 'ci-visibility/playwright-tests-impacted-tests/impacted-test.js' &&
1799-
test.meta[TEST_NAME] === 'impacted test should be impacted')
1807+
test.meta[TEST_SOURCE_FILE] === 'ci-visibility/playwright-tests-impacted-tests/impacted-test.js')
18001808

18011809
if (isEfd) {
1802-
assert.equal(impactedTests.length, NUM_RETRIES_EFD + 1) // Retries + original test
1810+
assert.equal(impactedTests.length, (NUM_RETRIES_EFD + 1) * 2) // Retries + original test
18031811
} else {
1804-
assert.equal(impactedTests.length, 1)
1812+
assert.equal(impactedTests.length, 2)
18051813
}
18061814

18071815
for (const impactedTest of impactedTests) {
@@ -1819,10 +1827,9 @@ versions.forEach((version) => {
18191827

18201828
if (isEfd) {
18211829
const retriedTests = tests.filter(
1822-
test => test.meta[TEST_IS_RETRY] === 'true' &&
1823-
test.meta[TEST_NAME] === 'impacted test should be impacted'
1830+
test => test.meta[TEST_IS_RETRY] === 'true'
18241831
)
1825-
assert.equal(retriedTests.length, NUM_RETRIES_EFD)
1832+
assert.equal(retriedTests.length, NUM_RETRIES_EFD * 2)
18261833
let retriedTestNew = 0
18271834
let retriedTestsWithReason = 0
18281835
retriedTests.forEach(test => {
@@ -1833,13 +1840,12 @@ versions.forEach((version) => {
18331840
retriedTestsWithReason++
18341841
}
18351842
})
1836-
assert.equal(retriedTestNew, isNew ? NUM_RETRIES_EFD : 0)
1837-
assert.equal(retriedTestsWithReason, NUM_RETRIES_EFD)
1843+
assert.equal(retriedTestNew, isNew ? NUM_RETRIES_EFD * 2 : 0)
1844+
assert.equal(retriedTestsWithReason, NUM_RETRIES_EFD * 2)
18381845
}
18391846
}, 25000)
18401847

1841-
const runImpactedTest = (
1842-
done,
1848+
const runImpactedTest = async (
18431849
{ isModified, isEfd = false, isNew = false },
18441850
extraEnvVars = {}
18451851
) => {
@@ -1860,37 +1866,38 @@ versions.forEach((version) => {
18601866
}
18611867
)
18621868

1863-
childProcess.on('exit', () => {
1864-
testAssertionsPromise.then(done).catch(done)
1865-
})
1869+
await Promise.all([
1870+
once(childProcess, 'exit'),
1871+
testAssertionsPromise
1872+
])
18661873
}
18671874

18681875
context('test is not new', () => {
1869-
it('should be detected as impacted', (done) => {
1876+
it('should be detected as impacted', async () => {
18701877
receiver.setSettings({ impacted_tests_enabled: true })
18711878

1872-
runImpactedTest(done, { isModified: true })
1879+
await runImpactedTest({ isModified: true })
18731880
})
18741881

1875-
it('should not be detected as impacted if disabled', (done) => {
1882+
it('should not be detected as impacted if disabled', async () => {
18761883
receiver.setSettings({ impacted_tests_enabled: false })
18771884

1878-
runImpactedTest(done, { isModified: false })
1885+
await runImpactedTest({ isModified: false })
18791886
})
18801887

18811888
it('should not be detected as impacted if DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED is false',
1882-
(done) => {
1889+
async () => {
18831890
receiver.setSettings({ impacted_tests_enabled: true })
18841891

1885-
runImpactedTest(done,
1892+
await runImpactedTest(
18861893
{ isModified: false },
18871894
{ DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED: '0' }
18881895
)
18891896
})
18901897
})
18911898

18921899
context('test is new', () => {
1893-
it('should be retried and marked both as new and modified', (done) => {
1900+
it('should be retried and marked both as new and modified', async () => {
18941901
receiver.setKnownTests({
18951902
playwright: {}
18961903
})
@@ -1904,8 +1911,7 @@ versions.forEach((version) => {
19041911
},
19051912
known_tests_enabled: true
19061913
})
1907-
runImpactedTest(
1908-
done,
1914+
await runImpactedTest(
19091915
{ isModified: true, isEfd: true, isNew: true }
19101916
)
19111917
})

packages/datadog-instrumentations/src/playwright.js

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,10 @@ function deepCloneSuite (suite, filterTest, tags = []) {
113113
if (filterTest(entry)) {
114114
const copiedTest = entry._clone()
115115
tags.forEach(tag => {
116-
if (tag) {
117-
copiedTest[tag] = true
116+
const resolvedTag = typeof tag === 'function' ? tag(entry) : tag
117+
118+
if (resolvedTag) {
119+
copiedTest[resolvedTag] = true
118120
}
119121
})
120122
copy._addTest(copiedTest)
@@ -764,6 +766,30 @@ addHook({
764766
return suiteUtilsPackage
765767
})
766768

769+
/**
770+
* We could repeat the logic of `applyRepeatEachIndex` here, but it'd be more risky
771+
* as playwright could change it at any time.
772+
*
773+
* `applyRepeatEachIndex` goes through all the tests in a suite and applies the "repeat" logic
774+
* for a single repeat index.
775+
*
776+
* This means that the clone logic is cumbersome:
777+
* - we grab the unique file suites that have new tests
778+
* - we store its project suite
779+
* - we clone each of these file suites for each repeat index
780+
* - we execute `applyRepeatEachIndex` for each of these cloned file suites
781+
* - we add the cloned file suites to the project suite
782+
*/
783+
function applyRetriesToTests (fileSuitesWithTestsToRetry, filterTest, tagsToApply) {
784+
for (const [fileSuite, projectSuite] of fileSuitesWithTestsToRetry.entries()) {
785+
for (let repeatEachIndex = 1; repeatEachIndex <= earlyFlakeDetectionNumRetries; repeatEachIndex++) {
786+
const copyFileSuite = deepCloneSuite(fileSuite, filterTest, tagsToApply)
787+
applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
788+
projectSuite._addSuite(copyFileSuite)
789+
}
790+
}
791+
}
792+
767793
addHook({
768794
name: 'playwright',
769795
file: 'lib/runner/loadUtils.js',
@@ -819,31 +845,36 @@ addHook({
819845
}
820846

821847
if (isImpactedTestsEnabled) {
822-
await Promise.all(allTests.map(async (test) => {
823-
const { isModified } = await getChannelPromise(isModifiedCh, {
848+
const impactedTests = allTests.filter(test => {
849+
let isImpacted = false
850+
isModifiedCh.publish({
824851
filePath: test._requireFile,
825-
modifiedFiles
852+
modifiedFiles,
853+
onDone: (isModified) => { isImpacted = isModified }
826854
})
827-
if (isModified) {
828-
test._ddIsModified = true
829-
}
830-
if (isEarlyFlakeDetectionEnabled && test.expectedStatus !== 'skipped') {
831-
const isNew = isKnownTestsEnabled && isNewTest(test)
832-
const fileSuite = getSuiteType(test, 'file')
833-
const projectSuite = getSuiteType(test, 'project')
834-
// If something change in the file, all tests in the file are impacted
835-
const isModifiedTest = () => isModified
836-
for (let repeatEachIndex = 1; repeatEachIndex <= earlyFlakeDetectionNumRetries; repeatEachIndex++) {
837-
const copyFileSuite = deepCloneSuite(fileSuite, isModifiedTest, [
838-
isNew && '_ddIsNew',
839-
'_ddIsModified',
840-
'_ddIsEfdRetry'
841-
])
842-
applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
843-
projectSuite._addSuite(copyFileSuite)
855+
return isImpacted
856+
})
857+
858+
const fileSuitesWithImpactedTestsToProjects = new Map()
859+
impactedTests.forEach(impactedTest => {
860+
impactedTest._ddIsModified = true
861+
if (isEarlyFlakeDetectionEnabled && impactedTest.expectedStatus !== 'skipped') {
862+
const fileSuite = getSuiteType(impactedTest, 'file')
863+
if (!fileSuitesWithImpactedTestsToProjects.has(fileSuite)) {
864+
fileSuitesWithImpactedTestsToProjects.set(fileSuite, getSuiteType(impactedTest, 'project'))
844865
}
845866
}
846-
}))
867+
})
868+
// If something change in the file, all tests in the file are impacted, hence the () => true filter
869+
applyRetriesToTests(
870+
fileSuitesWithImpactedTestsToProjects,
871+
() => true,
872+
[
873+
'_ddIsModified',
874+
'_ddIsEfdRetry',
875+
(test) => (isKnownTestsEnabled && isNewTest(test) ? '_ddIsNew' : null)
876+
]
877+
)
847878
}
848879

849880
if (isKnownTestsEnabled) {
@@ -860,21 +891,6 @@ addHook({
860891
isKnownTestsEnabled = false
861892
isEarlyFlakeDetectionFaulty = true
862893
} else {
863-
/**
864-
* We could repeat the logic of `applyRepeatEachIndex` here, but it'd be more risky
865-
* as playwright could change it at any time.
866-
*
867-
* `applyRepeatEachIndex` goes through all the tests in a suite and applies the "repeat" logic
868-
* for a single repeat index.
869-
*
870-
* This means that the clone logic is cumbersome:
871-
* - we grab the unique file suites that have new tests
872-
* - we store its project suite
873-
* - we clone each of these file suites for each repeat index
874-
* - we execute `applyRepeatEachIndex` for each of these cloned file suites
875-
* - we add the cloned file suites to the project suite
876-
*/
877-
878894
const fileSuitesWithNewTestsToProjects = new Map()
879895
newTests.forEach(newTest => {
880896
newTest._ddIsNew = true
@@ -886,16 +902,7 @@ addHook({
886902
}
887903
})
888904

889-
for (const [fileSuite, projectSuite] of fileSuitesWithNewTestsToProjects.entries()) {
890-
for (let repeatEachIndex = 1; repeatEachIndex <= earlyFlakeDetectionNumRetries; repeatEachIndex++) {
891-
const copyFileSuite = deepCloneSuite(fileSuite, isNewTest, [
892-
'_ddIsNew',
893-
'_ddIsEfdRetry'
894-
])
895-
applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
896-
projectSuite._addSuite(copyFileSuite)
897-
}
898-
}
905+
applyRetriesToTests(fileSuitesWithNewTestsToProjects, isNewTest, ['_ddIsNew', '_ddIsEfdRetry'])
899906
}
900907
}
901908

0 commit comments

Comments
 (0)