dd-trace
Version:
Datadog APM tracing client for JavaScript
1,503 lines (1,331 loc) • 62.1 kB
JavaScript
'use strict'
// Capture real timers at module load time, before any test can install fake timers.
const realSetTimeout = setTimeout
const { performance } = require('node:perf_hooks')
const satisfies = require('../../../vendor/dist/semifies')
const shimmer = require('../../datadog-shimmer')
const {
parseAnnotations,
getTestSuitePath,
PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE,
getIsFaultyEarlyFlakeDetection,
DYNAMIC_NAME_RE,
getEfdRetryCount,
getMaxEfdRetryCount,
recordAttemptToFixExecution,
logAttemptToFixTestExecution,
logTestOptimizationSummary,
getTestOptimizationRequestResults,
} = require('../../dd-trace/src/plugins/util/test')
const log = require('../../dd-trace/src/log')
const {
getValueFromEnvSources,
} = require('../../dd-trace/src/config/helper')
const { DD_MAJOR } = require('../../../version')
const { addHook, channel } = require('./helpers/instrument')
const testStartCh = channel('ci:playwright:test:start')
const testFinishCh = channel('ci:playwright:test:finish')
const testSkipCh = channel('ci:playwright:test:skip')
const testSessionStartCh = channel('ci:playwright:session:start')
const testSessionFinishCh = channel('ci:playwright:session:finish')
const libraryConfigurationCh = channel('ci:playwright:library-configuration')
const knownTestsCh = channel('ci:playwright:known-tests')
const testManagementTestsCh = channel('ci:playwright:test-management-tests')
const modifiedFilesCh = channel('ci:playwright:modified-files')
const isModifiedCh = channel('ci:playwright:test:is-modified')
const testSuiteStartCh = channel('ci:playwright:test-suite:start')
const testSuiteFinishCh = channel('ci:playwright:test-suite:finish')
const workerReportCh = channel('ci:playwright:worker:report')
const testPageGotoCh = channel('ci:playwright:test:page-goto')
const testToCtx = new WeakMap()
const testSuiteToCtx = new Map()
const testSuiteToTestStatuses = new Map()
const testSuiteToErrors = new Map()
const testsToTestStatuses = new Map()
const RUM_FLUSH_WAIT_TIME = Number(getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')) || 500
let applyRepeatEachIndex = null
let startedSuites = []
// Browser-side callbacks live in a coverage-excluded file so coverage counters can't reach chromium.
const { detectRum, stopRumSession } = require('./playwright-browser-scripts')
const STATUS_TO_TEST_STATUS = {
passed: 'pass',
failed: 'fail',
timedOut: 'fail',
skipped: 'skip',
}
let remainingTestsByFile = {}
let isKnownTestsEnabled = false
let isEarlyFlakeDetectionEnabled = false
let earlyFlakeDetectionNumRetries = 0
let earlyFlakeDetectionSlowTestRetries = {}
let isEarlyFlakeDetectionFaulty = false
let earlyFlakeDetectionFaultyThreshold = 0
let isFlakyTestRetriesEnabled = false
let flakyTestRetriesCount = 0
let knownTests = {}
let isTestManagementTestsEnabled = false
let testManagementAttemptToFixRetries = 0
let testManagementTests = {}
let isImpactedTestsEnabled = false
let modifiedFiles = {}
let quarantinedButNotAttemptToFixFqns = new Set()
let testsReportedInGenerateSummary = new Set()
const newTestsWithDynamicNames = new Set()
const attemptToFixExecutions = new Map()
const loggedAttemptToFixTests = new Set()
const efdManagedTestKeys = new Set()
const efdRetryCountByTestKey = new Map()
const efdRetryCountRequestsByTestKey = new Map()
const efdRetryTestsById = new Map()
const efdScheduledOriginalTestKeys = new Set()
const efdStartedOriginalTestKeys = new Set()
const efdSlowAbortedTests = new Set()
let rootDir = ''
let sessionProjects = []
const MINIMUM_SUPPORTED_VERSION_RANGE_EFD = '>=1.38.0' // TODO: remove this once we drop support for v5
const EFD_RETRY_COUNT_REQUEST = 'ddEfdRetryCountRequest'
const EFD_RETRY_COUNT_RESPONSE = 'ddEfdRetryCountResponse'
function isValidKnownTests (receivedKnownTests) {
return !!receivedKnownTests.playwright
}
function getTestFullyQualifiedName (test) {
const fullname = getTestFullname(test)
return `${test._requireFile} ${fullname}`
}
/**
* @param {object} test
* @returns {string|undefined}
*/
function getTestProjectKey (test) {
const { _projectIndex, _projectId } = test
if (_projectIndex !== undefined) {
return `index:${_projectIndex}`
}
if (_projectId !== undefined) {
return `id:${_projectId}`
}
const projectSuite = getSuiteType(test, 'project')
const projectName = projectSuite?._fullProject?.project?.name ||
projectSuite?._fullProject?.name ||
projectSuite?.title
if (projectName) {
return `name:${projectName}`
}
}
/**
* @param {object} test
* @returns {number|undefined}
*/
function getTestEfdRepeatEachIndex (test) {
if (Object.hasOwn(test, '_ddEfdOriginalRepeatEachIndex')) {
return test._ddEfdOriginalRepeatEachIndex
}
return test.repeatEachIndex
}
/**
* @param {object} test
* @returns {string|undefined}
*/
function getTestRepeatEachKey (test) {
const repeatEachIndex = getTestEfdRepeatEachIndex(test)
if (repeatEachIndex !== undefined) {
return `repeat:${repeatEachIndex}`
}
}
/**
* @param {object} test
* @returns {string}
*/
function getTestEfdKey (test) {
const projectKey = getTestProjectKey(test)
const repeatEachKey = getTestRepeatEachKey(test)
const testFqn = getTestFullyQualifiedName(test)
return [projectKey, repeatEachKey, testFqn].filter(Boolean).join(' ')
}
function getConfiguredEfdRetryCount () {
if (!earlyFlakeDetectionSlowTestRetries || !Object.keys(earlyFlakeDetectionSlowTestRetries).length) {
return earlyFlakeDetectionNumRetries
}
return getMaxEfdRetryCount(earlyFlakeDetectionSlowTestRetries)
}
function markEfdManagedTest (test) {
test._ddIsEfdManagedTest = true
test._ddEfdSlowTestRetries = earlyFlakeDetectionSlowTestRetries
efdManagedTestKeys.add(getTestEfdKey(test))
}
function markEfdRetryTest (test, retryIndex, originalTest) {
test._ddIsEfdRetry = true
test._ddEfdRetryIndex = retryIndex
if (originalTest) {
test._ddEfdOriginalRepeatEachIndex = getTestEfdRepeatEachIndex(originalTest)
}
}
function registerEfdRetryTest (test) {
if (!test._ddIsEfdRetry) {
return
}
efdRetryTestsById.set(test.id, {
retryIndex: test._ddEfdRetryIndex,
testEfdKey: getTestEfdKey(test),
})
}
function getTestEfdSlowTestRetries (test) {
return test._ddEfdSlowTestRetries || earlyFlakeDetectionSlowTestRetries
}
function isTestEfdManaged (test) {
return !!test._ddIsEfdManagedTest || (
(test._ddIsNew || test._ddIsModified) &&
!test._ddIsAttemptToFix &&
isEarlyFlakeDetectionEnabled
)
}
function getFileSuiteRepeatEachIndex (fileSuite) {
const test = fileSuite.allTests()[0]
return test ? getTestEfdRepeatEachIndex(test) || 0 : 0
}
function getEfdRetryRepeatEachIndex (fileSuite, projectSuite, retryIndex, retryCount) {
const nativeRepeatEach = projectSuite._fullProject?.project?.repeatEach || 1
const originalRepeatEachIndex = getFileSuiteRepeatEachIndex(fileSuite)
return nativeRepeatEach + (originalRepeatEachIndex * retryCount) + retryIndex - 1
}
function getEfdRetryCountForTest (test) {
return efdRetryCountByTestKey.get(getTestEfdKey(test)) ?? getConfiguredEfdRetryCount()
}
function setEfdRetryCountForTest (test, retryCount) {
const testEfdKey = getTestEfdKey(test)
efdRetryCountByTestKey.set(testEfdKey, retryCount)
const requests = efdRetryCountRequestsByTestKey.get(testEfdKey)
if (requests) {
efdRetryCountRequestsByTestKey.delete(testEfdKey)
for (const resolveRequest of requests) {
resolveRequest(retryCount)
}
}
}
function sendEfdRetryCountToWorker (workerProcess, testId, retryIndex, retryCount) {
workerProcess.send({
type: EFD_RETRY_COUNT_RESPONSE,
testId,
isEfdRetry: retryIndex !== undefined,
retryIndex,
retryCount,
})
}
function sendEfdRetryCountToWorkerWhenAvailable (workerProcess, testId) {
const efdRetryTest = efdRetryTestsById.get(testId)
if (!efdRetryTest) {
sendEfdRetryCountToWorker(workerProcess, testId)
return
}
const { retryIndex, testEfdKey } = efdRetryTest
if (!testEfdKey || !efdManagedTestKeys.has(testEfdKey)) {
sendEfdRetryCountToWorker(workerProcess, testId)
return
}
const retryCount = efdRetryCountByTestKey.get(testEfdKey)
if (retryCount !== undefined) {
sendEfdRetryCountToWorker(workerProcess, testId, retryIndex, retryCount)
return
}
if (!efdStartedOriginalTestKeys.has(testEfdKey) && !efdScheduledOriginalTestKeys.has(testEfdKey)) {
sendEfdRetryCountToWorker(workerProcess, testId, retryIndex, 0)
return
}
if (!efdRetryCountRequestsByTestKey.has(testEfdKey)) {
efdRetryCountRequestsByTestKey.set(testEfdKey, [])
}
efdRetryCountRequestsByTestKey.get(testEfdKey).push((retryCount) => {
sendEfdRetryCountToWorker(workerProcess, testId, retryIndex, retryCount)
})
}
/**
* @param {object} test
* @returns {boolean}
*/
function shouldRequestEfdRetryCount (test) {
// The main process remains the source of truth. repeatEachIndex is only used as
// a cheap worker-side filter so first executions do not block on coordination.
return test._ddIsEfdRetry || test.repeatEachIndex > 0
}
function waitForEfdRetryCount (test) {
if (!process.send || !shouldRequestEfdRetryCount(test)) {
return Promise.resolve()
}
const testEfdKey = getTestEfdKey(test)
return new Promise(resolve => {
const messageHandler = (message) => {
if (message?.type === EFD_RETRY_COUNT_RESPONSE && message.testId === test.id) {
if (message.isEfdRetry) {
test._ddIsEfdRetry = true
test._ddEfdRetryIndex = message.retryIndex
test._ddEfdRetryCount = message.retryCount
efdRetryCountByTestKey.set(testEfdKey, message.retryCount)
}
process.removeListener('message', messageHandler)
resolve()
}
}
process.on('message', messageHandler)
process.send({
type: EFD_RETRY_COUNT_REQUEST,
testId: test.id,
})
})
}
function shouldSkipEfdRetry (test) {
if (!test._ddIsEfdRetry) {
return false
}
const retryCount = test._ddEfdRetryCount ?? efdRetryCountByTestKey.get(getTestEfdKey(test))
return retryCount !== undefined && test._ddEfdRetryIndex > retryCount
}
function getTestProperties (test) {
const testName = getTestFullname(test)
const testSuite = getTestSuitePath(test._requireFile, rootDir)
const { attempt_to_fix: attemptToFix, disabled, quarantined } =
testManagementTests?.playwright?.suites?.[testSuite]?.tests?.[testName]?.properties || {}
return { attemptToFix, disabled, quarantined }
}
function isNewTest (test) {
if (!isValidKnownTests(knownTests)) {
return false
}
const testSuite = getTestSuitePath(test._requireFile, rootDir)
const testsForSuite = knownTests.playwright[testSuite] || []
return !testsForSuite.includes(getTestFullname(test))
}
function getSuiteType (test, type) {
let suite = test.parent
while (suite && suite._type !== type) {
suite = suite.parent
}
return suite
}
// Copy of Suite#_deepClone but with a function to filter tests
function deepCloneSuite (suite, filterTest, tags = [], configureCopiedTest) {
const copy = suite._clone()
for (const entry of suite._entries) {
if (entry.constructor.name === 'Suite') {
copy._addSuite(deepCloneSuite(entry, filterTest, tags, configureCopiedTest))
} else {
if (filterTest(entry)) {
const copiedTest = entry._clone()
if (configureCopiedTest) {
configureCopiedTest(copiedTest, entry)
}
for (const tag of tags) {
const resolvedTag = typeof tag === 'function' ? tag(entry) : tag
if (resolvedTag) {
copiedTest[resolvedTag] = true
}
}
copy._addTest(copiedTest)
}
}
}
return copy
}
function getTestsBySuiteFromTestGroups (testGroups) {
return testGroups.reduce((acc, { requireFile, tests }) => {
if (acc[requireFile]) {
acc[requireFile].push(...tests)
} else {
// Copy the tests, otherwise we modify the original tests
acc[requireFile] = [...tests]
}
return acc
}, {})
}
function getTestsBySuiteFromTestsById (testsById) {
const testsByTestSuite = {}
for (const { test } of testsById.values()) {
const { _requireFile } = test
if (test._type === 'beforeAll' || test._type === 'afterAll') {
continue
}
if (testsByTestSuite[_requireFile]) {
testsByTestSuite[_requireFile].push(test)
} else {
testsByTestSuite[_requireFile] = [test]
}
}
return testsByTestSuite
}
function getPlaywrightConfig (playwrightRunner) {
try {
return playwrightRunner._configLoader.fullConfig()
} catch {
try {
return playwrightRunner._loader.fullConfig()
} catch {
return playwrightRunner._config || {}
}
}
}
function getRootDir (playwrightRunner, configArg) {
const config = configArg?.config || getPlaywrightConfig(playwrightRunner)
if (config.rootDir) {
return config.rootDir
}
if (playwrightRunner._configDir) {
return playwrightRunner._configDir
}
if (playwrightRunner._config) {
return playwrightRunner._config.config?.rootDir || process.cwd()
}
return process.cwd()
}
function getProjectsFromRunner (runner, configArg) {
const config = configArg?.projects ? configArg : getPlaywrightConfig(runner)
return config.projects?.map((project) => {
if (project.project) {
return project.project
}
return project
})
}
function getProjectsFromDispatcher (dispatcher) {
const newConfig = dispatcher._config?.config?.projects
if (newConfig) {
return newConfig
}
// old
return dispatcher._loader?.fullConfig()?.projects
}
function getBrowserNameFromProjects (projects, test) {
if (!projects || !test) {
return null
}
const { _projectIndex, _projectId: testProjectId } = test
if (_projectIndex !== undefined) {
return projects[_projectIndex]?.name
}
return projects.find(({ __projectId, _id, name }) => {
if (__projectId !== undefined) {
return __projectId === testProjectId
}
if (_id !== undefined) {
return _id === testProjectId
}
return name === testProjectId
})?.name
}
function formatTestHookError (error, hookType, isTimeout) {
let hookError = error
if (error) {
hookError.message = `Error in ${hookType} hook: ${error.message}`
}
if (!hookError && isTimeout) {
hookError = new Error(`${hookType} hook timed out`)
}
return hookError
}
function addErrorToTestSuite (testSuiteAbsolutePath, error) {
if (testSuiteToErrors.has(testSuiteAbsolutePath)) {
testSuiteToErrors.get(testSuiteAbsolutePath).push(error)
} else {
testSuiteToErrors.set(testSuiteAbsolutePath, [error])
}
}
function getTestSuiteError (testSuiteAbsolutePath) {
const errors = testSuiteToErrors.get(testSuiteAbsolutePath)
if (!errors) {
return null
}
if (errors.length === 1) {
return errors[0]
}
return new Error(`${errors.length} errors in this test suite:\n${errors.map(e => e.message).join('\n------\n')}`)
}
function getTestByTestId (dispatcher, testId) {
if (dispatcher._testById) {
return dispatcher._testById.get(testId)?.test
}
const allTests = dispatcher._allTests || dispatcher._ddAllTests
if (allTests) {
return allTests.find(({ id }) => id === testId)
}
}
function getChannelPromise (channelToPublishTo, params) {
return new Promise(resolve => {
channelToPublishTo.publish({ onDone: resolve, ...params })
})
}
// Inspired by https://github.com/microsoft/playwright/blob/2b77ed4d7aafa85a600caa0b0d101b72c8437eeb/packages/playwright/src/reporters/base.ts#L293
// We can't use test.outcome() directly because it's set on follow up handlers:
// our `testEndHandler` is called before the outcome is set.
function testWillRetry (test, testStatus) {
return testStatus === 'fail' && test.results.length <= test.retries
}
function getFinalStatus ({
isFinalExecution,
isDisabled,
isQuarantined,
isAtrRetry,
isEfdManagedTest,
isAttemptToFix,
hasFailedAllRetries,
hasFailedAttemptToFixRetries,
hasPassedAnyEfdAttempt,
testStatus,
}) {
if (!isFinalExecution) {
return
}
if (isDisabled || isQuarantined || testStatus === 'skip') {
return 'skip'
}
if (isAtrRetry) {
return hasFailedAllRetries ? 'fail' : 'pass'
}
if (isEfdManagedTest) {
return hasPassedAnyEfdAttempt ? 'pass' : 'fail'
}
if (isAttemptToFix) {
return hasFailedAttemptToFixRetries ? 'fail' : 'pass'
}
return testStatus
}
function getTestFullname (test) {
let parent = test.parent
const names = [test.title]
while (parent?._type === 'describe' || parent?._isDescribe) {
if (parent.title) {
names.unshift(parent.title)
}
parent = parent.parent
}
return names.join(' ')
}
function shouldFinishTestSuite (testSuiteAbsolutePath) {
const remainingTests = remainingTestsByFile[testSuiteAbsolutePath]
return !remainingTests.length || remainingTests.every(test => test.expectedStatus === 'skipped')
}
function testBeginHandler (test, browserName, shouldCreateTestSpan) {
const {
_requireFile: testSuiteAbsolutePath,
location: {
file: testSourceFileAbsolutePath,
line: testSourceLine,
},
_type,
} = test
if (_type === 'beforeAll' || _type === 'afterAll') {
return
}
if (shouldSkipEfdRetry(test)) {
test._ddShouldSkipEfdRetry = true
return
}
test._ddStartTime = performance.now()
if (isTestEfdManaged(test) && !test._ddIsEfdRetry) {
efdStartedOriginalTestKeys.add(getTestEfdKey(test))
}
// this means that a skipped test is being handled
if (!remainingTestsByFile[testSuiteAbsolutePath].length) {
return
}
const isNewTestSuite = !startedSuites.includes(testSuiteAbsolutePath)
if (isNewTestSuite) {
startedSuites.push(testSuiteAbsolutePath)
const testSuiteCtx = { testSuiteAbsolutePath, testSourceFileAbsolutePath }
testSuiteToCtx.set(testSuiteAbsolutePath, testSuiteCtx)
testSuiteStartCh.runStores(testSuiteCtx, () => {})
}
// We disable retries by default if attemptToFix is true
if (getTestProperties(test).attemptToFix) {
test.retries = 0
logAttemptToFixTestExecution(
getTestSuitePath(testSuiteAbsolutePath, rootDir),
getTestFullname(test),
loggedAttemptToFixTests
)
}
// this handles tests that do not go through the worker process (because they're skipped)
if (shouldCreateTestSpan) {
const testName = getTestFullname(test)
const testCtx = {
testName,
testSuiteAbsolutePath,
testSourceFileAbsolutePath,
testSourceLine,
browserName,
isDisabled: test._ddIsDisabled,
}
testToCtx.set(test, testCtx)
testStartCh.runStores(testCtx, () => {})
}
}
function finishTestSuiteIfDone (testSuiteAbsolutePath, projects) {
if (!shouldFinishTestSuite(testSuiteAbsolutePath)) {
return
}
const skippedTests = remainingTestsByFile[testSuiteAbsolutePath]
.filter(test => test.expectedStatus === 'skipped')
for (const test of skippedTests) {
const browserName = getBrowserNameFromProjects(projects, test)
testSkipCh.publish({
testName: getTestFullname(test),
testSuiteAbsolutePath,
testSourceFileAbsolutePath: test.location.file,
testSourceLine: test.location.line,
browserName,
isNew: test._ddIsNew,
isDisabled: test._ddIsDisabled,
isModified: test._ddIsModified,
isQuarantined: test._ddIsQuarantined,
})
}
remainingTestsByFile[testSuiteAbsolutePath] = []
const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath)
let testSuiteStatus = 'pass'
if (testStatuses?.includes('fail')) {
testSuiteStatus = 'fail'
} else if (testStatuses?.every(status => status === 'skip')) {
testSuiteStatus = 'skip'
}
const suiteError = getTestSuiteError(testSuiteAbsolutePath)
const testSuiteCtx = testSuiteToCtx.get(testSuiteAbsolutePath)
if (testSuiteCtx) {
testSuiteFinishCh.publish({ status: testSuiteStatus, error: suiteError, ...testSuiteCtx.currentStore })
}
}
function testEndHandler ({
test,
annotations,
testStatus,
error,
isTimeout,
shouldCreateTestSpan,
projects,
}) {
const {
_requireFile: testSuiteAbsolutePath,
results,
_type,
} = test
let annotationTags
if (annotations.length) {
annotationTags = parseAnnotations(annotations)
}
if (_type === 'beforeAll' || _type === 'afterAll') {
const hookError = formatTestHookError(error, _type, isTimeout)
if (hookError) {
addErrorToTestSuite(testSuiteAbsolutePath, hookError)
}
return
}
if (test._ddShouldSkipEfdRetry || shouldSkipEfdRetry(test)) {
test._ddShouldSkipEfdRetry = true
remainingTestsByFile[testSuiteAbsolutePath] = remainingTestsByFile[testSuiteAbsolutePath]
.filter(currentTest => currentTest !== test)
finishTestSuiteIfDone(testSuiteAbsolutePath, projects)
return
}
const isEfdManagedTest = isTestEfdManaged(test)
const testFqn = getTestFullyQualifiedName(test)
const testStatusKey = isEfdManagedTest ? getTestEfdKey(test) : testFqn
const testStatuses = testsToTestStatuses.get(testStatusKey) || []
if (testStatuses.length === 0) {
testsToTestStatuses.set(testStatusKey, [testStatus])
if (test._ddIsNew && DYNAMIC_NAME_RE.test(getTestFullname(test))) {
newTestsWithDynamicNames.add(`${getTestSuitePath(test._requireFile, rootDir)} › ${getTestFullname(test)}`)
}
} else {
testStatuses.push(testStatus)
}
const testEfdKey = getTestEfdKey(test)
if (isEfdManagedTest && !test._ddIsEfdRetry && !efdRetryCountByTestKey.has(testEfdKey)) {
const testResult = results.at(-1)
const duration = testResult?.duration > 0 ? testResult.duration : performance.now() - test._ddStartTime
const retryCount = getEfdRetryCount(duration, getTestEfdSlowTestRetries(test))
setEfdRetryCountForTest(test, retryCount)
if (retryCount === 0) {
efdSlowAbortedTests.add(testEfdKey)
}
}
const testProperties = getTestProperties(test)
if (testProperties.attemptToFix) {
test._ddHasFailedAttemptToFixRetries = false
test._ddHasFailedAllRetries = false
test._ddHasPassedAttemptToFixRetries = false
recordAttemptToFixExecution(attemptToFixExecutions, {
testSuite: getTestSuitePath(test._requireFile, rootDir),
testName: getTestFullname(test),
status: testStatus,
isDisabled: testProperties.disabled,
isQuarantined: testProperties.quarantined,
})
}
if (testStatuses.length === testManagementAttemptToFixRetries + 1 && testProperties.attemptToFix) {
if (testStatuses.includes('fail')) {
test._ddHasFailedAttemptToFixRetries = true
}
if (testStatuses.every(status => status === 'fail')) {
test._ddHasFailedAllRetries = true
} else if (testStatuses.every(status => status === 'pass')) {
test._ddHasPassedAttemptToFixRetries = true
}
}
// Check if all EFD retries failed
const efdRetryCount = getEfdRetryCountForTest(test)
if (efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 &&
(test._ddIsNew || test._ddIsModified) &&
isEarlyFlakeDetectionEnabled &&
testStatuses.every(status => status === 'fail')) {
test._ddHasFailedAllRetries = true
}
// ATR: set _ddHasFailedAllRetries when all auto test retries were exhausted and every attempt failed
if (isFlakyTestRetriesEnabled && !testProperties.attemptToFix && !test._ddIsEfdRetry &&
!(test._ddIsNew || test._ddIsModified) &&
flakyTestRetriesCount != null && flakyTestRetriesCount > 0 &&
testStatuses.length === flakyTestRetriesCount + 1 &&
testStatuses.every(status => status === 'fail')) {
test._ddHasFailedAllRetries = true
}
// this handles tests that do not go through the worker process (because they're skipped)
if (shouldCreateTestSpan) {
const testResult = results.at(-1)
const testCtx = testToCtx.get(test)
const isAtrRetry = testResult?.retry > 0 &&
isFlakyTestRetriesEnabled &&
!test._ddIsAttemptToFix &&
!test._ddIsEfdRetry
const finalStatus = getFinalStatus({
isFinalExecution: !testWillRetry(test, testStatus),
isDisabled: test._ddIsDisabled,
isQuarantined: test._ddIsQuarantined,
isAtrRetry,
isEfdManagedTest,
isAttemptToFix: test._ddIsAttemptToFix,
hasFailedAllRetries: test._ddHasFailedAllRetries,
hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
hasPassedAnyEfdAttempt: testStatuses.includes('pass'),
testStatus,
})
// if there is no testCtx, the skipped test will be created later
if (testCtx) {
testFinishCh.publish({
testStatus,
steps: testResult?.steps || [],
isRetry: testResult?.retry > 0,
error,
extraTags: annotationTags,
isNew: test._ddIsNew,
hasDynamicName: test._ddIsNew && DYNAMIC_NAME_RE.test(getTestFullname(test)),
isAttemptToFix: test._ddIsAttemptToFix,
isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
isQuarantined: test._ddIsQuarantined,
isEfdRetry: test._ddIsEfdRetry,
hasFailedAllRetries: test._ddHasFailedAllRetries,
hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
isAtrRetry,
isModified: test._ddIsModified,
finalStatus,
earlyFlakeAbortReason: efdSlowAbortedTests.has(testEfdKey) ? 'slow' : undefined,
...testCtx.currentStore,
})
}
}
if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
testSuiteToTestStatuses.get(testSuiteAbsolutePath).push(testStatus)
} else {
testSuiteToTestStatuses.set(testSuiteAbsolutePath, [testStatus])
}
if (error) {
addErrorToTestSuite(testSuiteAbsolutePath, error)
}
if (!testWillRetry(test, testStatus)) {
remainingTestsByFile[testSuiteAbsolutePath] = remainingTestsByFile[testSuiteAbsolutePath]
.filter(currentTest => currentTest !== test)
}
finishTestSuiteIfDone(testSuiteAbsolutePath, projects)
}
function dispatcherRunWrapper (run) {
return function (...args) {
remainingTestsByFile = getTestsBySuiteFromTestsById(this._testById)
return run.apply(this, args)
}
}
function deferEfdRetryGroups (testGroups) {
const groupsWithOriginalTests = []
const efdRetryOnlyGroups = []
for (const group of testGroups) {
const originalTests = []
const efdRetryTests = []
for (const test of group.tests) {
if (test._ddIsEfdRetry) {
efdRetryTests.push(test)
} else {
originalTests.push(test)
if (isTestEfdManaged(test)) {
efdScheduledOriginalTestKeys.add(getTestEfdKey(test))
}
}
}
if (efdRetryTests.length && originalTests.length) {
group.tests = [...originalTests, ...efdRetryTests]
}
if (originalTests.length) {
groupsWithOriginalTests.push(group)
} else {
efdRetryOnlyGroups.push(group)
}
}
return [...groupsWithOriginalTests, ...efdRetryOnlyGroups]
}
function dispatcherRunWrapperNew (run) {
return function (testGroups) {
// Filter out disabled tests from testGroups before they get scheduled,
// unless they have attemptToFix (in which case they should still run and be retried)
if (isTestManagementTestsEnabled) {
for (const group of testGroups) {
group.tests = group.tests.filter(test => !test._ddIsDisabled || test._ddIsAttemptToFix)
}
// Remove empty groups
testGroups = testGroups.filter(group => group.tests.length > 0)
}
if (isEarlyFlakeDetectionEnabled) {
testGroups = deferEfdRetryGroups(testGroups)
}
if (!this._allTests) {
// Removed in https://github.com/microsoft/playwright/commit/1e52c37b254a441cccf332520f60225a5acc14c7
// Not available from >=1.44.0
this._ddAllTests = testGroups.flatMap(g => g.tests)
}
remainingTestsByFile = getTestsBySuiteFromTestGroups(testGroups)
arguments[0] = testGroups
return run.apply(this, arguments)
}
}
function dispatcherHook (dispatcherExport) {
shimmer.wrap(dispatcherExport.Dispatcher.prototype, 'run', dispatcherRunWrapper)
shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function (...args) {
const dispatcher = this
const worker = createWorker.apply(this, args)
const projects = getProjectsFromDispatcher(dispatcher)
sessionProjects = projects
// for older versions of playwright, `shouldCreateTestSpan` should always be true,
// since the `_runTest` function wrapper is not available for older versions
worker.process.on('message', ({ method, params }) => {
if (method === 'testBegin') {
const { test } = dispatcher._testById.get(params.testId)
const browser = getBrowserNameFromProjects(projects, test)
testBeginHandler(test, browser, true)
} else if (method === 'testEnd') {
const { test } = dispatcher._testById.get(params.testId)
const { results } = test
const testResult = results.at(-1)
const isTimeout = testResult.status === 'timedOut'
testEndHandler(
{
test,
annotations: params.annotations,
testStatus: STATUS_TO_TEST_STATUS[testResult.status],
error: testResult.error,
isTimeout,
shouldCreateTestSpan: true,
projects,
}
)
}
})
return worker
})
return dispatcherExport
}
function dispatcherHookNew (dispatcherExport, runWrapper) {
shimmer.wrap(dispatcherExport.Dispatcher.prototype, 'run', runWrapper)
shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function (...args) {
const dispatcher = this
const worker = createWorker.apply(this, args)
const projects = getProjectsFromDispatcher(dispatcher)
sessionProjects = projects
worker.on('testBegin', ({ testId }) => {
const test = getTestByTestId(dispatcher, testId)
const browser = getBrowserNameFromProjects(projects, test)
const shouldCreateTestSpan = test.expectedStatus === 'skipped'
testBeginHandler(test, browser, shouldCreateTestSpan)
})
worker.on('testEnd', ({ testId, status, errors, annotations }) => {
const test = getTestByTestId(dispatcher, testId)
const isTimeout = status === 'timedOut'
const testStatus = STATUS_TO_TEST_STATUS[status]
const shouldCreateTestSpan = test.expectedStatus === 'skipped'
if (shouldCreateTestSpan && !testToCtx.has(test)) {
testBeginHandler(test, getBrowserNameFromProjects(projects, test), true)
}
testEndHandler(
{
test,
annotations,
testStatus,
error: errors && errors[0],
isTimeout,
shouldCreateTestSpan,
projects,
}
)
const testResult = test.results.at(-1)
const isAtrRetry = testResult?.retry > 0 &&
isFlakyTestRetriesEnabled &&
!test._ddIsAttemptToFix &&
!test._ddIsEfdRetry
// EFD retries (new or modified tests) are implemented as clones with retries=0,
// so testWillRetry always returns false for them. Instead, we track how many
// executions have been reported via testsToTestStatuses (updated by testEndHandler
// above) and mark the execution final once the count reaches the expected total.
// This mirrors how ATF finality is detected and centralizes the decision in the
// main process, so workers only need to act on the _ddIsFinalExecution flag.
const isEfdManagedTest = isTestEfdManaged(test)
let isFinalExecution
if (isEfdManagedTest) {
const efdTestStatuses = testsToTestStatuses.get(getTestEfdKey(test)) || []
isFinalExecution = efdTestStatuses.length === getEfdRetryCountForTest(test) + 1
} else if (test._ddIsAttemptToFix) {
isFinalExecution = !!(test._ddHasPassedAttemptToFixRetries || test._ddHasFailedAttemptToFixRetries)
} else {
isFinalExecution = !testWillRetry(test, testStatus)
}
// We want to send the ddProperties to the worker
worker.process.send({
type: 'ddProperties',
testId: test.id,
properties: {
_ddIsDisabled: test._ddIsDisabled,
_ddIsQuarantined: test._ddIsQuarantined,
_ddIsAttemptToFix: test._ddIsAttemptToFix,
_ddIsAttemptToFixRetry: test._ddIsAttemptToFixRetry,
_ddIsNew: test._ddIsNew,
_ddIsEfdRetry: test._ddIsEfdRetry,
_ddHasFailedAllRetries: test._ddHasFailedAllRetries,
_ddHasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
_ddHasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
_ddIsAtrRetry: isAtrRetry,
_ddIsModified: test._ddIsModified,
_ddIsFinalExecution: isFinalExecution,
_ddIsEfdManagedTest: isEfdManagedTest,
_ddEarlyFlakeAbortReason: efdSlowAbortedTests.has(getTestEfdKey(test)) ? 'slow' : undefined,
_ddHasPassedAnyEfdAttempt: (testsToTestStatuses.get(getTestEfdKey(test)) || []).includes('pass'),
},
})
})
return worker
})
return dispatcherExport
}
function runAllTestsWrapper (runAllTests, playwrightVersion) {
// Config parameter is only available from >=1.55.0
return async function (config) {
let onDone
rootDir = getRootDir(this, config)
const processArgv = process.argv.slice(2).join(' ')
const command = `playwright ${processArgv}`
testSessionStartCh.publish({ command, frameworkVersion: playwrightVersion, rootDir })
try {
const { err, libraryConfig } = await getChannelPromise(
libraryConfigurationCh,
{ frameworkVersion: playwrightVersion }
)
if (!err) {
isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {}
earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries
isImpactedTestsEnabled = libraryConfig.isImpactedTestsEnabled
}
} catch (e) {
isEarlyFlakeDetectionEnabled = false
isKnownTestsEnabled = false
isTestManagementTestsEnabled = false
isImpactedTestsEnabled = false
log.error('Playwright session start error', e)
}
const isTestOptimizationSupported = satisfies(playwrightVersion, MINIMUM_SUPPORTED_VERSION_RANGE_EFD)
const shouldGetKnownTests = isKnownTestsEnabled && isTestOptimizationSupported
const shouldGetTestManagementTests = isTestManagementTestsEnabled && isTestOptimizationSupported
const {
knownTestsResponse,
testManagementTestsResponse,
} = await getTestOptimizationRequestResults({
isKnownTestsEnabled: shouldGetKnownTests,
isTestManagementTestsEnabled: shouldGetTestManagementTests,
getKnownTests: () => getChannelPromise(knownTestsCh),
getTestManagementTests: () => getChannelPromise(testManagementTestsCh),
})
if (shouldGetKnownTests) {
try {
const { err, knownTests: receivedKnownTests } =
knownTestsResponse || await getChannelPromise(knownTestsCh)
if (err) {
isEarlyFlakeDetectionEnabled = false
isKnownTestsEnabled = false
} else {
knownTests = receivedKnownTests
}
if (!isValidKnownTests(receivedKnownTests)) {
isEarlyFlakeDetectionFaulty = true
isEarlyFlakeDetectionEnabled = false
isKnownTestsEnabled = false
}
} catch (err) {
isEarlyFlakeDetectionEnabled = false
isKnownTestsEnabled = false
log.error('Playwright known tests error', err)
}
}
if (shouldGetTestManagementTests) {
try {
const { err, testManagementTests: receivedTestManagementTests } =
testManagementTestsResponse || await getChannelPromise(testManagementTestsCh)
if (err) {
isTestManagementTestsEnabled = false
} else {
testManagementTests = receivedTestManagementTests
}
} catch (err) {
isTestManagementTestsEnabled = false
log.error('Playwright test management tests error', err)
}
}
if (isImpactedTestsEnabled && isTestOptimizationSupported) {
try {
const { err, modifiedFiles: receivedModifiedFiles } = await getChannelPromise(modifiedFilesCh)
if (err) {
isImpactedTestsEnabled = false
} else {
modifiedFiles = receivedModifiedFiles
}
} catch (err) {
isImpactedTestsEnabled = false
log.error('Playwright impacted tests error', err)
}
}
const projects = getProjectsFromRunner(this, config)
// ATR and `--retries` are now compatible with Test Management.
// Test Management tests have their retries set to 0 at the test level,
// preventing them from being retried by ATR or `--retries`.
const shouldSetATRRetries = isFlakyTestRetriesEnabled && flakyTestRetriesCount > 0
if (shouldSetATRRetries) {
for (const project of projects) {
if (project.retries === 0) { // Only if it hasn't been set by the user
project.retries = flakyTestRetriesCount
}
}
}
let runAllTestsReturn = await runAllTests.apply(this, arguments)
// Tests that have only skipped tests may reach this point
// Skipped tests may or may not go through `testBegin` or `testEnd`
// depending on the playwright configuration
for (const tests of Object.values(remainingTestsByFile)) {
// `tests` should normally be empty, but if it isn't,
// there were tests that did not go through `testBegin` or `testEnd`,
// because they were skipped
for (const test of tests) {
const alreadyReported = testsReportedInGenerateSummary.has(test)
const browser = getBrowserNameFromProjects(projects, test)
testBeginHandler(test, browser, !alreadyReported)
testEndHandler({
test,
annotations: [],
testStatus: 'skip',
error: null,
isTimeout: false,
shouldCreateTestSpan: !alreadyReported,
projects,
})
}
}
let preventedToFail = false
const sessionStatus = runAllTestsReturn.status || runAllTestsReturn
if (isTestManagementTestsEnabled && sessionStatus === 'failed') {
let totalFailedTestCount = 0
let totalPureQuarantinedFailedTestCount = 0
for (const [fqn, testStatuses] of testsToTestStatuses.entries()) {
// Only count as failed if the final status (after retries) is 'fail'
const lastStatus = testStatuses[testStatuses.length - 1]
if (lastStatus === 'fail') {
totalFailedTestCount += 1
if (quarantinedButNotAttemptToFixFqns.has(fqn)) {
totalPureQuarantinedFailedTestCount += 1
}
}
}
const totalIgnorableFailures = totalPureQuarantinedFailedTestCount
if (totalFailedTestCount > 0 && totalFailedTestCount === totalIgnorableFailures) {
runAllTestsReturn = 'passed'
preventedToFail = true
}
}
logTestOptimizationSummary({ attemptToFixExecutions, newTestsWithDynamicNames })
loggedAttemptToFixTests.clear()
const flushWait = new Promise(resolve => {
onDone = resolve
})
testSessionFinishCh.publish({
status: preventedToFail ? 'pass' : STATUS_TO_TEST_STATUS[sessionStatus],
isEarlyFlakeDetectionEnabled,
isEarlyFlakeDetectionFaulty,
isTestManagementTestsEnabled,
onDone,
})
await flushWait
startedSuites = []
remainingTestsByFile = {}
quarantinedButNotAttemptToFixFqns = new Set()
testsReportedInGenerateSummary = new Set()
efdManagedTestKeys.clear()
efdRetryCountByTestKey.clear()
efdRetryCountRequestsByTestKey.clear()
efdRetryTestsById.clear()
efdScheduledOriginalTestKeys.clear()
efdStartedOriginalTestKeys.clear()
efdSlowAbortedTests.clear()
// TODO: we can trick playwright into thinking the session passed by returning
// 'passed' here. We might be able to use this for both EFD and Test Management tests.
return runAllTestsReturn
}
}
function runnerHook (runnerExport, playwrightVersion) {
shimmer.wrap(
runnerExport.Runner.prototype,
'runAllTests',
runAllTests => runAllTestsWrapper(runAllTests, playwrightVersion)
)
}
function runnerHookNew (runnerExport, playwrightVersion) {
runnerExport = shimmer.wrap(runnerExport, 'runAllTestsWithConfig', function (originalGetter) {
const originalFunction = originalGetter.call(this)
return function () {
return runAllTestsWrapper(originalFunction, playwrightVersion)
}
})
return runnerExport
}
if (DD_MAJOR < 6) { // <1.38.0 is only supported up to version 5
addHook({
name: '@playwright/test',
file: 'lib/runner.js',
versions: ['>=1.18.0 <=1.30.0'],
}, runnerHook)
addHook({
name: '@playwright/test',
file: 'lib/dispatcher.js',
versions: ['>=1.18.0 <1.30.0'],
}, dispatcherHook)
addHook({
name: '@playwright/test',
file: 'lib/dispatcher.js',
versions: ['>=1.30.0 <1.31.0'],
}, (dispatcher) => dispatcherHookNew(dispatcher, dispatcherRunWrapper))
addHook({
name: '@playwright/test',
file: 'lib/runner/dispatcher.js',
versions: ['>=1.31.0 <1.38.0'],
}, (dispatcher) => dispatcherHookNew(dispatcher, dispatcherRunWrapperNew))
addHook({
name: '@playwright/test',
file: 'lib/runner/runner.js',
versions: ['>=1.31.0 <1.38.0'],
}, runnerHook)
}
addHook({
name: 'playwright',
file: 'lib/runner/runner.js',
versions: ['>=1.38.0'],
}, runnerHook)
addHook({
name: 'playwright',
file: 'lib/runner/testRunner.js',
versions: ['>=1.55.0'],
}, runnerHookNew)
addHook({
name: 'playwright',
file: 'lib/runner/dispatcher.js',
versions: ['>=1.38.0'],
}, (dispatcher) => dispatcherHookNew(dispatcher, dispatcherRunWrapperNew))
addHook({
name: 'playwright',
file: 'lib/common/suiteUtils.js',
versions: ['>=1.38.0'],
}, suiteUtilsPackage => {
// We grab `applyRepeatEachIndex` to use it later
// `applyRepeatEachIndex` needs to be applied to a cloned suite
applyRepeatEachIndex = suiteUtilsPackage.applyRepeatEachIndex
return suiteUtilsPackage
})
/**
* We could repeat the logic of `applyRepeatEachIndex` here, but it'd be more risky
* as playwright could change it at any time.
*
* `applyRepeatEachIndex` goes through all the tests in a suite and applies the "repeat" logic
* for a single repeat index.
*
* This means that the clone logic is cumbersome:
* - we grab the unique file suites that have new tests
* - we store its project suite
* - we clone each of these file suites for each repeat index
* - we execute `applyRepeatEachIndex` for each of these cloned file suites
* - we add the cloned file suites to the project suite
*/
function applyRetriesToTests (
fileSuitesWithTestsToRetry,
filterTest,
tagsToApply,
numRetries,
configureCopiedTest,
getRetryRepeatEachIndex
) {
for (const [fileSuite, projectSuite] of fileSuitesWithTestsToRetry.entries()) {
for (let repeatEachIndex = 1; repeatEachIndex <= numRetries; repeatEachIndex++) {
const copyFileSuite = deepCloneSuite(fileSuite, filterTest, tagsToApply, (copiedTest, originalTest) => {
if (configureCopiedTest) {
configureCopiedTest(copiedTest, originalTest, repeatEachIndex)
}
})
const retryRepeatEachIndex = getRetryRepeatEachIndex
? getRetryRepeatEachIndex(fileSuite, projectSuite, repeatEachIndex, numRetries)
: repeatEachIndex + 1
applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, retryRepeatEachIndex)
projectSuite._addSuite(copyFileSuite)
for (const copiedTest of copyFileSuite.allTests()) {
registerEfdRetryTest(copiedTest)
}
}
}
}
addHook({
name: 'playwright',
file: 'lib/runner/loadUtils.js',
versions: ['>=1.38.0'],
}, (loadUtilsPackage) => {
const oldCreateRootSuite = loadUtilsPackage.createRootSuite
async function newCreateRootSuite () {
if (!isKnownTestsEnabled && !isTestManagementTestsEnabled && !isImpactedTestsEnabled) {
return oldCreateRootSuite.apply(this, arguments)
}
const createRootSuiteReturnValue = await oldCreateRootSuite.apply(this, arguments)
// From v1.56.0 on, createRootSuite returns `{ rootSuite, topLevelProjects }`
const rootSuite = createRootSuiteReturnValue.rootSuite || createRootSuiteReturnValue
const allTests = rootSuite.allTests()
if (isTestManagementTestsEnabled) {
const fileSuitesWithManagedTestsToProjects = new Map()
for (const test of allTests) {
const testProperties = getTestProperties(test)
// Disabled tests are skipped unless they have attemptToFix
if (testProperties.disabled) {
test._ddIsDisabled = true
if (!testProperties.attemptToFix) {
test.expectedStatus = 'skipped'
// setting test.expectedStatus to 'skipped' does not work for every case,
// so we need to filter out disabled tests in dispatcherRunWrapperNew,
// so they don't get to the workers
continue
}
}
if (testProperties.quarantined) {
test._ddIsQuarantined = true
if (!testProperties.attemptToFix) {
// Do not skip quarantined tests, let them run and overwrite results post-run if they fail
const testFqn = getTestFullyQualifiedName(test)
quarantinedButNotAttemptToFixFqns.add(testFqn)
}
}
if (testProperties.attemptToFix) {
test._ddIsAttemptToFix = true
// Prevent ATR or `--retries` from retrying attemptToFix tests
test.retries = 0
const fileSuite = getSuiteType(test, 'file')
if (!fileSuitesWithManagedTestsToProjects.has(fileSuite)) {
fileSuitesWithManagedTestsToProjects.set(fileSuite, getSuiteType(test, 'project'))
}
}
}
applyRetriesToTests(
fileSuitesWithManagedTestsToProjects,
(test) => test._ddIsAttemptToFix,
[
(test) => test._ddIsQuarantined && '_ddIsQuarantined',
(test) => test._ddIsDisabled && '_ddIsDisabled',
'_ddIsAttemptToFix',
'_ddIsAttemptToFixRetry',
],
testManagementAttemptToFixRetries
)
}
if (isImpactedTestsEnabled) {
const impactedTests = allTests.filter(test => {
let isImpacted = false
isModifiedCh.publish({
filePath: test._requireFile,
modifiedFiles,
onDone: (isModified) => { isImpacted = isModified },
})
return isImpacted
})
const fileSuitesWithImpactedTestsToProjects = new Map()
for (const impactedTest of impactedTests) {
impactedTest._ddIsModified = true
if (isEarlyFlakeDetectionEnabled && impactedTest.expectedStatus !== 'skipped') {
markEfdManagedTest(impactedTest)
const fileSuite = getSuiteType(impactedTest, 'file')
if (!fileSuitesWithImpactedTestsToProjects.has(fileSuite)) {
fileSuitesWithImpactedTestsToProjects.set(fileSuite, getSuiteType(impactedTest, 'project'))
}
}
}
// If something change in the file, all tests in the file are impacted, hence the () => true filter
applyRetriesToTests(
fileSuitesWithImpactedTestsToProjects,
() => true,
[
'_ddIsModified',
'_ddIsEfdRetry',
(test) => (isKnownTestsEnabled && isNewTest(test) ? '_ddIsNew' : null),
],
getConfiguredEfdRetryCount(),
(copiedTest, originalTest, retryIndex) => {
markEfdRetryTest(copiedTest, retryIndex, originalTest)
markEfdManagedTest(copiedTest)
},
getEfdRetryRepeatEachIndex
)
}
if (isKnownTestsEnabled) {
const newTests = allTests.filter(isNewTest)
const isFaulty = getIsFaultyEarlyFlakeDetection(
allTests.map(test => getTestSuitePath(test._requireFile, rootDir)),
knownTests.playwright,
earlyFlakeDetectionFaultyThreshold
)
if (isFaulty) {
isEarlyFlakeDetectionEnabled = false
isKnownTestsEnabled = false
isEarlyFlakeDetectionFaulty = true
} else {
const fileSuitesWithNewTestsToProjects = new Map()
for (const newTest of newTests) {
newTest._ddIsNew = true
if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) {
// Prevent ATR or `--retries` from retrying new tests if EFD is enabled
newTest.retries = 0
markEfdManagedTest(newTest)
const fileSuite = getSuiteType(newTest, 'file')
if (!fileSuitesWithNewTestsToProjects.has(fileSuite)) {
fileSuitesWithNewTestsToProjects.set(fileSuite, getSuiteType(newTest, 'project'))
}
}
}
applyRetriesToTests(
fileSuitesWithNewTestsToProjects,
isNewTest,
['_ddIsNew', '_ddIsEfdRetry'],
getConfiguredEfdRetryCount(),
(copiedTest, originalTest, retryIndex) => {
markEfdRetryTest(copie