UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

1,503 lines (1,331 loc) 62.1 kB
'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