dd-trace
Version:
Datadog APM tracing client for JavaScript
1,273 lines (1,135 loc) • 79 kB
JavaScript
'use strict'
// Capture real timers at module load time, before any test can install fake timers.
const realSetTimeout = setTimeout
const path = require('path')
const shimmer = require('../../datadog-shimmer')
const log = require('../../dd-trace/src/log')
const {
getCoveredFilenamesFromCoverage,
JEST_WORKER_TRACE_PAYLOAD_CODE,
JEST_WORKER_COVERAGE_PAYLOAD_CODE,
JEST_WORKER_TELEMETRY_PAYLOAD_CODE,
JEST_WORKER_QUARANTINE_PAYLOAD_CODE,
getTestLineStart,
getTestSuitePath,
getTestParametersString,
getIsFaultyEarlyFlakeDetection,
JEST_WORKER_LOGS_PAYLOAD_CODE,
getTestEndLine,
isModifiedTest,
DYNAMIC_NAME_RE,
collectDynamicNamesFromTraces,
} = require('../../dd-trace/src/plugins/util/test')
const {
SEED_SUFFIX_RE,
getFormattedJestTestParameters,
getJestTestName,
getJestSuitesToRun,
getEfdRetryCount,
} = require('../../datadog-plugin-jest/src/util')
const { addHook, channel } = require('./helpers/instrument')
const testSessionStartCh = channel('ci:jest:session:start')
const testSessionFinishCh = channel('ci:jest:session:finish')
const codeCoverageReportCh = channel('ci:jest:coverage-report')
const testSessionConfigurationCh = channel('ci:jest:session:configuration')
const testSuiteStartCh = channel('ci:jest:test-suite:start')
const testSuiteFinishCh = channel('ci:jest:test-suite:finish')
const testSuiteErrorCh = channel('ci:jest:test-suite:error')
const workerReportTraceCh = channel('ci:jest:worker-report:trace')
const workerReportCoverageCh = channel('ci:jest:worker-report:coverage')
const workerReportLogsCh = channel('ci:jest:worker-report:logs')
const workerReportTelemetryCh = channel('ci:jest:worker-report:telemetry')
const testSuiteCodeCoverageCh = channel('ci:jest:test-suite:code-coverage')
const testStartCh = channel('ci:jest:test:start')
const testSkippedCh = channel('ci:jest:test:skip')
const testFinishCh = channel('ci:jest:test:finish')
const testErrCh = channel('ci:jest:test:err')
const testFnCh = channel('ci:jest:test:fn')
const testSuiteHookFnCh = channel('ci:jest:test-suite:hook:fn')
const skippableSuitesCh = channel('ci:jest:test-suite:skippable')
const libraryConfigurationCh = channel('ci:jest:library-configuration')
const knownTestsCh = channel('ci:jest:known-tests')
const testManagementTestsCh = channel('ci:jest:test-management-tests')
const modifiedFilesCh = channel('ci:jest:modified-files')
const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
// Message sent by jest's main process to workers to run a test suite (=test file)
// https://github.com/jestjs/jest/blob/1d682f21c7a35da4d3ab3a1436a357b980ebd0fa/packages/jest-worker/src/types.ts#L37
const CHILD_MESSAGE_CALL = 1
// Maximum time we'll wait for the tracer to flush
const FLUSH_TIMEOUT = 10_000
// https://github.com/jestjs/jest/blob/41f842a46bb2691f828c3a5f27fc1d6290495b82/packages/jest-circus/src/types.ts#L9C8-L9C54
const RETRY_TIMES = Symbol.for('RETRY_TIMES')
let skippableSuites = []
let knownTests = {}
let isCodeCoverageEnabled = false
let isCodeCoverageEnabledBecauseOfUs = false
let isSuitesSkippingEnabled = false
let isKeepingCoverageConfiguration = false
let isUserCodeCoverageEnabled = false
let isSuitesSkipped = false
let numSkippedSuites = 0
let hasUnskippableSuites = false
let hasForcedToRunSuites = false
let isEarlyFlakeDetectionEnabled = false
let earlyFlakeDetectionNumRetries = 0
let earlyFlakeDetectionSlowTestRetries = {}
let earlyFlakeDetectionFaultyThreshold = 30
let isEarlyFlakeDetectionFaulty = false
let hasFilteredSkippableSuites = false
let isKnownTestsEnabled = false
let isTestManagementTestsEnabled = false
let testManagementTests = {}
let testManagementAttemptToFixRetries = 0
let isImpactedTestsEnabled = false
let modifiedFiles = {}
const testContexts = new WeakMap()
const originalTestFns = new WeakMap()
const originalHookFns = new WeakMap()
const retriedTestsToNumAttempts = new Map()
const newTestsTestStatuses = new Map()
const attemptToFixRetriedTestsStatuses = new Map()
const wrappedWorkerChannels = new WeakMap()
// New tests whose names contain likely dynamic data (timestamps, UUIDs, etc.)
// Populated in-process for runInBand, and via worker-report:trace for parallel mode.
const newTestsWithDynamicNames = new Set()
const testSuiteMockedFiles = new Map()
const testsToBeRetried = new Set()
// Per-test: how many EFD retries were determined after the first execution.
const efdDeterminedRetries = new Map()
// Tests whose first run exceeded the 5-min threshold — tagged "slow".
const efdSlowAbortedTests = new Set()
// Tests added as EFD new-test candidates (not ATF, not impacted).
const efdNewTestCandidates = new Set()
// Tests that are genuinely new (not in known tests list).
const newTests = new Set()
const testSuiteAbsolutePathsWithFastCheck = new Set()
const testSuiteJestObjects = new Map()
const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200
const ATR_RETRY_SUPPRESSION_FLAG = '_ddDisableAtrRetry'
const atrSuppressedErrors = new Map()
// Track quarantined tests whose errors were suppressed, keyed by "suite › testName"
const quarantinedFailingTests = new Set()
/**
* Sends suppressed quarantine test names from a worker process to the main process.
* Supports both child_process (process.send) and worker_threads (parentPort.postMessage).
* Returns true if the data was sent (worker mode), false if in main process (runInBand).
*
* @param {string[]} testNames
* @returns {boolean}
*/
function sendQuarantineInfoToMainProcess (testNames) {
const payload = [JEST_WORKER_QUARANTINE_PAYLOAD_CODE, JSON.stringify(testNames)]
if (process.send) {
process.send(payload)
return true
}
try {
const { isMainThread, parentPort } = require('node:worker_threads')
if (!isMainThread && parentPort) {
parentPort.postMessage(payload)
return true
}
} catch {
// Not in a worker context
}
return false
}
// based on https://github.com/facebook/jest/blob/main/packages/jest-circus/src/formatNodeAssertErrors.ts#L41
function formatJestError (errors) {
let error
if (Array.isArray(errors)) {
const [originalError, asyncError] = errors
if (originalError === null || !originalError.stack) {
error = asyncError
error.message = originalError
} else {
error = originalError
}
} else {
error = errors
}
return error
}
function getTestEnvironmentOptions (config) {
if (config.projectConfig && config.projectConfig.testEnvironmentOptions) { // newer versions
return config.projectConfig.testEnvironmentOptions
}
if (config.testEnvironmentOptions) {
return config.testEnvironmentOptions
}
return {}
}
const MAX_IGNORED_TEST_NAMES = 10
function getTestStats (testStatuses) {
return testStatuses.reduce((acc, testStatus) => {
acc[testStatus]++
return acc
}, { pass: 0, fail: 0 })
}
/**
* @param {string[]} efdNames
* @param {string[]} quarantineNames
* @param {number} totalCount
*/
/**
* Renders a truncated bullet list from an array of items.
*
* @param {Array<{ text: string, suffix?: string }>} items
* @returns {string}
*/
function formatList (items) {
const shown = items.slice(0, MAX_IGNORED_TEST_NAMES)
const more = items.length - shown.length
const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
return shown.map(({ text, suffix }) => ` • ${text}${suffix ? ` (${suffix})` : ''}`).join('\n') + moreSuffix
}
/**
* Logs a single "Datadog Test Optimization" summary at session end,
* combining all relevant sections (ignored failures, dynamic names).
*
* @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
*/
function logSessionSummary (ignoredFailures) {
const sections = []
if (ignoredFailures) {
const items = []
for (const n of ignoredFailures.efdNames) {
items.push({ text: n, suffix: 'Early Flake Detection' })
}
for (const n of ignoredFailures.quarantineNames) {
items.push({ text: n, suffix: 'Quarantine' })
}
sections.push(
`${ignoredFailures.totalCount} test failure(s) were ignored. Exit code set to 0.\n\n` +
formatList(items)
)
}
if (newTestsWithDynamicNames.size > 0) {
const items = [...newTestsWithDynamicNames].map(name => ({ text: name }))
sections.push(
`${newTestsWithDynamicNames.size} test(s) detected as new but their names contain ` +
'dynamic data (timestamps, UUIDs, etc.).\n' +
'Tests with changing names are always treated as new on every run, ' +
'causing unnecessary Early Flake Detection retries and preventing correct new test detection.\n' +
'Consider using stable, deterministic test names.\n\n' +
formatList(items)
)
newTestsWithDynamicNames.clear()
}
if (sections.length === 0) return
const line = '-'.repeat(50)
// eslint-disable-next-line no-console -- Intentional user-facing session summary
console.warn(`\n${line}\nDatadog Test Optimization\n${line}\n${sections.join('\n\n')}\n`)
}
function getWrappedEnvironment (BaseEnvironment, jestVersion) {
return class DatadogEnvironment extends BaseEnvironment {
constructor (config, context) {
super(config, context)
const rootDir = config.globalConfig ? config.globalConfig.rootDir : config.rootDir
this.rootDir = rootDir
this.testSuite = getTestSuitePath(context.testPath, rootDir)
this.nameToParams = {}
this.global._ddtrace = global._ddtrace
this.hasSnapshotTests = undefined
this.testSuiteAbsolutePath = context.testPath
this.displayName = config.projectConfig?.displayName?.name || config.displayName
this.testEnvironmentOptions = getTestEnvironmentOptions(config)
const repositoryRoot = this.testEnvironmentOptions._ddRepositoryRoot
// TODO: could we grab testPath from `this.getVmContext().expect.getState()` instead?
// so we don't rely on context being passed (some custom test environment do not pass it)
if (repositoryRoot) {
this.testSourceFile = getTestSuitePath(context.testPath, repositoryRoot)
this.repositoryRoot = repositoryRoot
}
this.isEarlyFlakeDetectionEnabled = this.testEnvironmentOptions._ddIsEarlyFlakeDetectionEnabled
this.isFlakyTestRetriesEnabled = this.testEnvironmentOptions._ddIsFlakyTestRetriesEnabled
this.flakyTestRetriesCount = this.testEnvironmentOptions._ddFlakyTestRetriesCount
this.isDiEnabled = this.testEnvironmentOptions._ddIsDiEnabled
this.isKnownTestsEnabled = this.testEnvironmentOptions._ddIsKnownTestsEnabled
this.isTestManagementTestsEnabled = this.testEnvironmentOptions._ddIsTestManagementTestsEnabled
this.isImpactedTestsEnabled = this.testEnvironmentOptions._ddIsImpactedTestsEnabled
if (this.isKnownTestsEnabled) {
earlyFlakeDetectionSlowTestRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionSlowTestRetries ?? {}
try {
this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
if (!Array.isArray(this.knownTestsForThisSuite)) {
log.warn('this.knownTestsForThisSuite is not an array so new test and Early Flake detection is disabled.')
this.isEarlyFlakeDetectionEnabled = false
this.isKnownTestsEnabled = false
}
} catch {
// If there has been an error parsing the tests, we'll disable Early Flake Deteciton
this.isEarlyFlakeDetectionEnabled = false
this.isKnownTestsEnabled = false
}
}
if (this.isFlakyTestRetriesEnabled) {
const currentNumRetries = this.global[RETRY_TIMES]
if (!currentNumRetries) {
this.global[RETRY_TIMES] = this.flakyTestRetriesCount
}
}
if (this.isTestManagementTestsEnabled) {
try {
const hasTestManagementTests = !!testManagementTests?.jest
testManagementAttemptToFixRetries = this.testEnvironmentOptions._ddTestManagementAttemptToFixRetries
this.testManagementTestsForThisSuite = hasTestManagementTests
? this.getTestManagementTestsForSuite(testManagementTests?.jest?.suites?.[this.testSuite]?.tests)
: this.getTestManagementTestsForSuite(this.testEnvironmentOptions._ddTestManagementTests)
} catch (e) {
log.error('Error parsing test management tests', e)
this.isTestManagementTestsEnabled = false
}
}
if (this.isImpactedTestsEnabled) {
try {
const hasImpactedTests = Object.keys(modifiedFiles).length > 0
this.modifiedFiles = hasImpactedTests ? modifiedFiles : this.testEnvironmentOptions._ddModifiedFiles
} catch (e) {
log.error('Error parsing impacted tests', e)
this.isImpactedTestsEnabled = false
}
}
}
/**
* Jest snapshot counter issue during test retries
*
* Problem:
* - Jest tracks snapshot calls using an internal counter per test name
* - Each `toMatchSnapshot()` call increments this counter
* - When a test is retried, it keeps the same name but the counter continues from where it left off
*
* Example Issue:
* Original test run creates: `exports["test can do multiple snapshots 1"] = "hello"`
* Retried test expects: `exports["test can do multiple snapshots 2"] = "hello"`
*
* This mismatch causes snapshot tests to fail on retry because Jest is looking
* for the wrong snapshot number. The solution is to reset the snapshot state.
*/
resetSnapshotState () {
try {
const expectGlobal = this.getVmContext().expect
const { snapshotState: { _counters: counters } } = expectGlobal.getState()
if (counters) {
counters.clear()
}
} catch (e) {
log.warn('Error resetting snapshot state', e)
}
}
/**
* Jest mock state issue during test retries
*
* Problem:
* - Jest tracks mock function calls using internal state (call count, call arguments, etc.)
* - When a test is retried, the mock state is not automatically reset
* - This causes assertions like `toHaveBeenCalledTimes(1)` to fail because the call count
* accumulates across retries
*
* The solution is to clear all mocks before each retry attempt.
*/
resetMockState () {
try {
const jestObject = testSuiteJestObjects.get(this.testSuiteAbsolutePath)
if (jestObject?.clearAllMocks) {
jestObject.clearAllMocks()
}
} catch (e) {
log.warn('Error resetting mock state', e)
}
}
// This function returns an array if the known tests are valid and null otherwise.
getKnownTestsForSuite (suiteKnownTests) {
// `suiteKnownTests` is `this.testEnvironmentOptions._ddKnownTests`,
// which is only set if jest is configured to run in parallel.
if (suiteKnownTests) {
return suiteKnownTests
}
// Global variable `knownTests` is set only in the main process.
// If jest is configured to run serially, the tests run in the same process, so `knownTests` is set.
// The assumption is that if the key `jest` is defined in the dictionary, the response is valid.
if (knownTests?.jest) {
return knownTests.jest[this.testSuite] || []
}
return null
}
getTestManagementTestsForSuite (testManagementTests) {
if (this.testManagementTestsForThisSuite) {
return this.testManagementTestsForThisSuite
}
if (!testManagementTests) {
return {
attemptToFix: [],
disabled: [],
quarantined: [],
}
}
let testManagementTestsForSuite = testManagementTests
// If jest is using workers, test management tests are serialized to json.
// If jest runs in band, they are not.
if (typeof testManagementTestsForSuite === 'string') {
testManagementTestsForSuite = JSON.parse(testManagementTestsForSuite)
}
const result = {
attemptToFix: [],
disabled: [],
quarantined: [],
}
for (const [testName, { properties }] of Object.entries(testManagementTestsForSuite)) {
if (properties?.attempt_to_fix) {
result.attemptToFix.push(testName)
}
if (properties?.disabled) {
result.disabled.push(testName)
}
if (properties?.quarantined) {
result.quarantined.push(testName)
}
}
return result
}
// Generic function to handle test retries
retryTest ({
jestEvent,
retryCount,
retryType,
}) {
const { testName, fn, timeout } = jestEvent
for (let retryIndex = 0; retryIndex < retryCount; retryIndex++) {
if (this.global.test) {
this.global.test(testName, fn, timeout)
} else {
log.error('%s could not retry test because global.test is undefined', retryType)
}
}
}
getShouldStripSeedFromTestName () {
return testSuiteAbsolutePathsWithFastCheck.has(this.testSuiteAbsolutePath)
}
// At the `add_test` event we don't have the test object yet, so we can't use it
getTestNameFromAddTestEvent (event, state) {
const describeSuffix = getJestTestName(state.currentDescribeBlock, this.getShouldStripSeedFromTestName())
return describeSuffix ? `${describeSuffix} ${event.testName}` : event.testName
}
async handleTestEvent (event, state) {
if (super.handleTestEvent) {
await super.handleTestEvent(event, state)
}
const setNameToParams = (name, params) => { this.nameToParams[name] = [...params] }
if (event.name === 'setup' && this.global.test) {
shimmer.wrap(this.global.test, 'each', each => function () {
const testParameters = getFormattedJestTestParameters(arguments)
const eachBind = each.apply(this, arguments)
return function () {
const [testName] = arguments
setNameToParams(testName, testParameters)
return eachBind.apply(this, arguments)
}
})
}
if (event.name === 'test_start') {
const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
if (testsToBeRetried.has(testName)) {
// This is needed because we're retrying tests with the same name
this.resetSnapshotState()
this.resetMockState()
}
let isNewTest = false
let numEfdRetry = null
let numOfAttemptsToFixRetries = null
const testParameters = getTestParametersString(this.nameToParams, event.test.name)
let isAttemptToFix = false
let isDisabled = false
let isQuarantined = false
if (this.isTestManagementTestsEnabled) {
isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(testName)
isDisabled = this.testManagementTestsForThisSuite?.disabled?.includes(testName)
isQuarantined = this.testManagementTestsForThisSuite?.quarantined?.includes(testName)
if (isAttemptToFix) {
numOfAttemptsToFixRetries = retriedTestsToNumAttempts.get(testName)
retriedTestsToNumAttempts.set(testName, numOfAttemptsToFixRetries + 1)
} else if (isDisabled) {
event.test.mode = 'skip'
}
}
let isModified = false
if (this.isImpactedTestsEnabled) {
const testStartLine = getTestLineStart(event.test.asyncError, this.testSuite)
const testEndLine = getTestEndLine(event.test.fn, testStartLine)
isModified = isModifiedTest(
this.testSourceFile,
testStartLine,
testEndLine,
this.modifiedFiles,
'jest'
)
}
if (this.isKnownTestsEnabled) {
isNewTest = newTests.has(testName)
}
const willRunEfd = this.isEarlyFlakeDetectionEnabled && (isNewTest || isModified)
event.test[ATR_RETRY_SUPPRESSION_FLAG] = Boolean(isAttemptToFix || willRunEfd)
if (!isAttemptToFix && willRunEfd) {
numEfdRetry = retriedTestsToNumAttempts.get(testName)
retriedTestsToNumAttempts.set(testName, numEfdRetry + 1)
}
const isJestRetry = event.test?.invocations > 1
const hasDynamicName = isNewTest && DYNAMIC_NAME_RE.test(testName)
const ctx = {
name: testName,
suite: this.testSuite,
testSourceFile: this.testSourceFile,
displayName: this.displayName,
testParameters,
frameworkVersion: jestVersion,
isNew: isNewTest,
isEfdRetry: numEfdRetry > 0,
isAttemptToFix,
isAttemptToFixRetry: numOfAttemptsToFixRetries > 0,
isJestRetry,
isDisabled,
isQuarantined,
isModified,
hasDynamicName,
testSuiteAbsolutePath: this.testSuiteAbsolutePath,
}
testContexts.set(event.test, ctx)
testStartCh.runStores(ctx, () => {
let p = event.test.parent
const hooks = []
while (p != null) {
hooks.push(...p.hooks)
p = p.parent
}
for (const hook of hooks) {
let hookFn = hook.fn
if (originalHookFns.has(hook)) {
hookFn = originalHookFns.get(hook)
} else {
originalHookFns.set(hook, hookFn)
}
const newHookFn = shimmer.wrapFunction(hookFn, hookFn => function () {
return testFnCh.runStores(ctx, () => hookFn.apply(this, arguments))
})
hook.fn = newHookFn
}
const originalFn = event.test.fn
originalTestFns.set(event.test, originalFn)
const newFn = shimmer.wrapFunction(event.test.fn, testFn => function () {
return testFnCh.runStores(ctx, () => testFn.apply(this, arguments))
})
event.test.fn = newFn
})
}
if (event.name === 'hook_start' && (event.hook.type === 'beforeAll' || event.hook.type === 'afterAll')) {
const ctx = { testSuiteAbsolutePath: this.testSuiteAbsolutePath }
let hookFn = event.hook.fn
if (originalHookFns.has(event.hook)) {
hookFn = originalHookFns.get(event.hook)
} else {
originalHookFns.set(event.hook, hookFn)
}
event.hook.fn = shimmer.wrapFunction(hookFn, hookFn => function () {
return testSuiteHookFnCh.runStores(ctx, () => hookFn.apply(this, arguments))
})
}
if (event.name === 'add_test') {
if (event.failing) {
return
}
const testFullName = this.getTestNameFromAddTestEvent(event, state)
const isSkipped = event.mode === 'todo' || event.mode === 'skip'
const isAttemptToFix = this.isTestManagementTestsEnabled &&
this.testManagementTestsForThisSuite?.attemptToFix?.includes(testFullName)
if (
isAttemptToFix &&
!isSkipped &&
!retriedTestsToNumAttempts.has(testFullName)
) {
retriedTestsToNumAttempts.set(testFullName, 0)
testsToBeRetried.add(testFullName)
this.retryTest({
jestEvent: event,
retryCount: testManagementAttemptToFixRetries,
retryType: 'Test Management (Attempt to Fix)',
})
}
if (!isAttemptToFix && this.isImpactedTestsEnabled) {
const testStartLine = getTestLineStart(event.asyncError, this.testSuite)
const testEndLine = getTestEndLine(event.fn, testStartLine)
const isModified = isModifiedTest(
this.testSourceFile,
testStartLine,
testEndLine,
this.modifiedFiles,
'jest'
)
if (isModified && !retriedTestsToNumAttempts.has(testFullName) && this.isEarlyFlakeDetectionEnabled) {
retriedTestsToNumAttempts.set(testFullName, 0)
testsToBeRetried.add(testFullName)
this.retryTest({
jestEvent: event,
retryCount: earlyFlakeDetectionNumRetries,
retryType: 'Impacted tests',
})
}
}
if (!isAttemptToFix && this.isKnownTestsEnabled) {
const isNew = !this.knownTestsForThisSuite.includes(testFullName)
if (isNew && !isSkipped) {
newTests.add(testFullName)
}
if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(testFullName)) {
if (DYNAMIC_NAME_RE.test(testFullName)) {
// Populated directly for runInBand; for parallel workers the main process
// collects these from the TEST_HAS_DYNAMIC_NAME span tag via worker-report:trace.
newTestsWithDynamicNames.add(`${this.testSuite} › ${testFullName}`)
}
retriedTestsToNumAttempts.set(testFullName, 0)
if (this.isEarlyFlakeDetectionEnabled) {
testsToBeRetried.add(testFullName)
efdNewTestCandidates.add(testFullName)
// Cloning is deferred to test_done after the first execution,
// when we know the duration and can choose the right retry count.
}
}
}
}
if (event.name === 'test_done') {
const originalError = event.test?.errors?.[0]
let status = 'pass'
if (event.test.errors && event.test.errors.length) {
status = 'fail'
}
// restore in case it is retried
event.test.fn = originalTestFns.get(event.test)
// If ATR retry is being suppressed for this test (due to EFD or Attempt to Fix taking precedence)
// and the test has errors for this attempt, store the errors temporarily and clear them
// so Jest won't treat this attempt as failed (the real status will be reported after retries).
if (event.test?.[ATR_RETRY_SUPPRESSION_FLAG] && event.test.errors?.length) {
atrSuppressedErrors.set(event.test, event.test.errors)
event.test.errors = []
}
let attemptToFixPassed = false
let attemptToFixFailed = false
let failedAllTests = false
let isAttemptToFix = false
const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
if (this.isTestManagementTestsEnabled) {
isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(testName)
if (isAttemptToFix) {
if (attemptToFixRetriedTestsStatuses.has(testName)) {
attemptToFixRetriedTestsStatuses.get(testName).push(status)
} else {
attemptToFixRetriedTestsStatuses.set(testName, [status])
}
const testStatuses = attemptToFixRetriedTestsStatuses.get(testName)
// Check if this is the last attempt to fix.
// If it is, we'll set the failedAllTests flag to true if all the tests failed
// If all tests passed, we'll set the attemptToFixPassed flag to true
if (testStatuses.length === testManagementAttemptToFixRetries + 1) {
if (testStatuses.includes('fail')) {
attemptToFixFailed = true
}
if (testStatuses.every(status => status === 'fail')) {
failedAllTests = true
} else if (testStatuses.every(status => status === 'pass')) {
attemptToFixPassed = true
}
}
}
}
// EFD dynamic cloning: on first execution of a new EFD candidate,
// determine the retry count from the test's duration.
if (
this.isEarlyFlakeDetectionEnabled &&
this.isKnownTestsEnabled &&
efdNewTestCandidates.has(testName) &&
event.test.invocations === 1 &&
!efdDeterminedRetries.has(testName)
) {
const durationMs = event.test.duration ?? 0
const retryCount = getEfdRetryCount(durationMs, earlyFlakeDetectionSlowTestRetries)
efdDeterminedRetries.set(testName, retryCount)
if (retryCount > 0) {
// Temporarily adjust jest-circus state so that retry tests are registered
// into the correct describe block and bypass the "tests have started" guard.
//
// Problem 1 (jest-circus ≤24): currentDescribeBlock points to ROOT during
// execution, and ROOT's tests loop already finished before children ran.
//
// Problem 2 (jest-circus ≥27): `hasStarted = true` causes `test()` to throw
// "Cannot add a test after tests have started running".
//
// Fix: temporarily point currentDescribeBlock to the test's parent (so retries
// land in the still-iterating children array) and set hasStarted = false (so the
// guard is bypassed). Both are restored immediately after scheduling the retries.
const originalDescribeBlock = state.currentDescribeBlock
const originalHasStarted = state.hasStarted
state.currentDescribeBlock = event.test.parent ?? originalDescribeBlock
state.hasStarted = false
this.retryTest({
jestEvent: {
testName: event.test.name,
fn: event.test.fn,
timeout: event.test.timeout,
},
retryCount,
retryType: 'Early flake detection',
})
state.currentDescribeBlock = originalDescribeBlock
state.hasStarted = originalHasStarted
} else {
efdSlowAbortedTests.add(testName)
}
}
let isEfdRetry = false
// We'll store the test statuses of the retries
if (this.isKnownTestsEnabled) {
const isNewTest = newTests.has(testName)
if (isNewTest) {
if (newTestsTestStatuses.has(testName)) {
newTestsTestStatuses.get(testName).push(status)
isEfdRetry = true
} else {
newTestsTestStatuses.set(testName, [status])
}
const testStatuses = newTestsTestStatuses.get(testName)
// Check if this is the last EFD retry.
// If it is, we'll set the failedAllTests flag to true if all the tests failed
const efdRetryCount = efdDeterminedRetries.get(testName) ?? 0
if (efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 &&
testStatuses.every(status => status === 'fail')) {
failedAllTests = true
}
}
}
// ATR: set failedAllTests when all auto test retries were exhausted and every attempt failed
if (this.isFlakyTestRetriesEnabled && !isAttemptToFix && !isEfdRetry) {
const maxRetries = Number(this.global[RETRY_TIMES]) || 0
if (event.test?.invocations === maxRetries + 1 && status === 'fail') {
failedAllTests = true
}
}
const promises = {}
const numRetries = this.global[RETRY_TIMES]
const numTestExecutions = event.test?.invocations
const willBeRetriedByFailedTestReplay = numRetries > 0 && numTestExecutions - 1 < numRetries
const mightHitBreakpoint = this.isDiEnabled && numTestExecutions >= 2
// For quarantined tests, suppress errors so Jest doesn't count them as failures.
// This prevents --bail from stopping the test run on quarantined test failures.
// The actual status ('fail') is already captured above for dd-trace reporting.
// Only suppress on the final execution — not when ATR/EFD/ATF will retry the test.
if (!event.test?.[ATR_RETRY_SUPPRESSION_FLAG] && !willBeRetriedByFailedTestReplay) {
const quarantineCtx = testContexts.get(event.test)
if (quarantineCtx?.isQuarantined && event.test.errors?.length) {
quarantinedFailingTests.add(`${quarantineCtx.suite} › ${quarantineCtx.name}`)
event.test.errors = []
}
}
const ctx = testContexts.get(event.test)
if (!ctx) {
log.warn('"ci:jest:test_done": no context found for test "%s"', testName)
return
}
const finalStatus = this.getFinalStatus(testName,
status,
!!ctx.isNew,
!!ctx.isModified,
isEfdRetry,
isAttemptToFix,
numTestExecutions)
if (status === 'fail') {
const shouldSetProbe = this.isDiEnabled && willBeRetriedByFailedTestReplay && numTestExecutions === 1
testErrCh.publish({
...ctx.currentStore,
error: formatJestError(originalError),
shouldSetProbe,
promises,
})
}
// After finishing it might take a bit for the snapshot to be handled.
// This means that tests retried with DI are BREAKPOINT_HIT_GRACE_PERIOD_MS slower at least.
if (status === 'fail' && mightHitBreakpoint) {
await new Promise(resolve => {
realSetTimeout(() => {
resolve()
}, BREAKPOINT_HIT_GRACE_PERIOD_MS)
})
}
let isAtrRetry = false
if (this.isFlakyTestRetriesEnabled && event.test?.invocations > 1 && !isAttemptToFix && !isEfdRetry) {
isAtrRetry = true
}
testFinishCh.publish({
...ctx.currentStore,
status,
testStartLine: getTestLineStart(event.test.asyncError, this.testSuite),
attemptToFixPassed,
failedAllTests,
attemptToFixFailed,
isAtrRetry,
finalStatus,
earlyFlakeAbortReason: efdSlowAbortedTests.has(testName) ? 'slow' : undefined,
})
if (promises.isProbeReady) {
await promises.isProbeReady
}
}
if (event.name === 'run_finish') {
for (const [test, errors] of atrSuppressedErrors) {
// Do not restore errors for quarantined tests — they should stay suppressed
// so Jest doesn't see the failure (prevents --bail from stopping the run).
const ctx = testContexts.get(test)
if (ctx?.isQuarantined) {
const testName = getJestTestName(test, this.getShouldStripSeedFromTestName())
quarantinedFailingTests.add(`${ctx.suite} › ${testName}`)
} else {
test.errors = errors
}
}
atrSuppressedErrors.clear()
// In parallel mode, send suppressed quarantine info to the main process
// so it can include them in the session summary.
// In runInBand mode, keep the set — it will be consumed by the session-level code directly.
if (quarantinedFailingTests.size > 0 && sendQuarantineInfoToMainProcess([...quarantinedFailingTests])) {
quarantinedFailingTests.clear()
}
efdDeterminedRetries.clear()
efdSlowAbortedTests.clear()
efdNewTestCandidates.clear()
newTests.clear()
retriedTestsToNumAttempts.clear()
attemptToFixRetriedTestsStatuses.clear()
testsToBeRetried.clear()
}
if (event.name === 'test_skip' || event.name === 'test_todo') {
const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
testSkippedCh.publish({
test: {
name: testName,
suite: this.testSuite,
testSourceFile: this.testSourceFile,
displayName: this.displayName,
frameworkVersion: jestVersion,
testStartLine: getTestLineStart(event.test.asyncError, this.testSuite),
},
isDisabled: this.testManagementTestsForThisSuite?.disabled?.includes(testName),
})
}
}
getEfdResult ({ testName, isNewTest, isModifiedTest, isEfdRetry, numberOfExecutedRetries }) {
const isEfdEnabled = this.isEarlyFlakeDetectionEnabled
const isEfdActive = isEfdEnabled && (isNewTest || isModifiedTest)
const retryCount = efdDeterminedRetries.get(testName) ?? 0
const isSlowAbort = efdSlowAbortedTests.has(testName)
const isLastEfdRetry = (isEfdRetry && numberOfExecutedRetries >= (retryCount + 1)) || isSlowAbort
const isFinalEfdTestExecution = isEfdActive && isLastEfdRetry
let finalStatus
if (isEfdActive && isFinalEfdTestExecution) {
// For EFD: The framework reports 'pass' if ANY attempt passed (flaky but not failing)
const testStatuses = newTestsTestStatuses.get(testName)
finalStatus = testStatuses && testStatuses.includes('pass') ? 'pass' : 'fail'
}
return { isEfdEnabled, isEfdActive, isFinalEfdTestExecution, finalStatus }
}
getAtrResult ({ status, isEfdRetry, isAttemptToFix, numberOfTestInvocations }) {
const isAtrEnabled =
this.isFlakyTestRetriesEnabled &&
!isEfdRetry &&
!isAttemptToFix &&
Number.isFinite(this.global[RETRY_TIMES])
const isLastAtrRetry =
status === 'pass' || numberOfTestInvocations >= (Number(this.global[RETRY_TIMES]) + 1)
const isFinalAtrTestExecution = isAtrEnabled && isLastAtrRetry
// For ATR: The last execution's status is what the framework reports
return { isAtrEnabled, isFinalAtrTestExecution, finalStatus: status }
}
getAttemptToFixResult ({ testName, isAttemptToFix, numberOfExecutedRetries }) {
const isAttemptToFixEnabled =
this.isTestManagementTestsEnabled &&
isAttemptToFix &&
Number.isFinite(testManagementAttemptToFixRetries)
const isFinalAttemptToFixExecution = isAttemptToFixEnabled &&
numberOfExecutedRetries >= (testManagementAttemptToFixRetries + 1)
let finalStatus
if (isAttemptToFixEnabled && isFinalAttemptToFixExecution) {
// For Attempt to Fix: 'pass' only if ALL attempts passed, 'fail' if ANY failed
const testStatuses = attemptToFixRetriedTestsStatuses.get(testName)
finalStatus = testStatuses && testStatuses.every(status => status === 'pass') ? 'pass' : 'fail'
}
return { isAttemptToFixEnabled, isFinalAttemptToFixExecution, finalStatus }
}
getFinalStatus (testName, status, isNewTest, isModifiedTest, isEfdRetry, isAttemptToFix, numberOfTestInvocations) {
const numberOfExecutedRetries = retriedTestsToNumAttempts.get(testName) ?? 0
const efdResult = this.getEfdResult({
testName,
isNewTest,
isModifiedTest,
isEfdRetry,
numberOfExecutedRetries,
})
const atrResult = this.getAtrResult({ status, isEfdRetry, isAttemptToFix, numberOfTestInvocations })
const attemptToFixResult = this.getAttemptToFixResult({
testName,
isAttemptToFix,
numberOfExecutedRetries,
})
// When no retry features are active, every test execution is final
const noRetryFeaturesActive =
!efdResult.isEfdActive &&
!atrResult.isAtrEnabled &&
!attemptToFixResult.isAttemptToFixEnabled
const isFinalTestExecution = noRetryFeaturesActive ||
efdResult.isFinalEfdTestExecution ||
atrResult.isFinalAtrTestExecution ||
attemptToFixResult.isFinalAttemptToFixExecution
if (!isFinalTestExecution) {
return
}
// If the test is quarantined, regardless of its actual execution result,
// the final status of its last execution should be reported as 'skip'.
if (this.isTestManagementTestsEnabled &&
this.testManagementTestsForThisSuite?.quarantined?.includes(testName)) {
return 'skip'
}
return efdResult.finalStatus || attemptToFixResult.finalStatus || atrResult.finalStatus
}
teardown () {
if (this._globalProxy?.propertyToValue) {
for (const [key] of this._globalProxy.propertyToValue) {
if (typeof key === 'string' && key.startsWith('_dd')) {
this._globalProxy.propertyToValue.delete(key)
}
}
}
return super.teardown()
}
}
}
function getTestEnvironment (pkg, jestVersion) {
if (pkg.default) {
const wrappedTestEnvironment = getWrappedEnvironment(pkg.default, jestVersion)
return new Proxy(pkg, {
get (target, prop) {
if (prop === 'default') {
return wrappedTestEnvironment
}
if (prop === 'TestEnvironment') {
return wrappedTestEnvironment
}
return target[prop]
},
})
}
return getWrappedEnvironment(pkg, jestVersion)
}
function applySuiteSkipping (originalTests, rootDir, frameworkVersion) {
const jestSuitesToRun = getJestSuitesToRun(skippableSuites, originalTests, rootDir || process.cwd())
hasFilteredSkippableSuites = true
log.debug('%d out of %d suites are going to run.', jestSuitesToRun.suitesToRun.length, originalTests.length)
hasUnskippableSuites = jestSuitesToRun.hasUnskippableSuites
hasForcedToRunSuites = jestSuitesToRun.hasForcedToRunSuites
isSuitesSkipped = jestSuitesToRun.suitesToRun.length !== originalTests.length
numSkippedSuites = jestSuitesToRun.skippedSuites.length
itrSkippedSuitesCh.publish({ skippedSuites: jestSuitesToRun.skippedSuites, frameworkVersion })
return jestSuitesToRun.suitesToRun
}
addHook({
name: 'jest-environment-node',
versions: ['>=24.8.0'],
}, getTestEnvironment)
addHook({
name: 'jest-environment-jsdom',
versions: ['>=24.8.0'],
}, getTestEnvironment)
addHook({
name: '@happy-dom/jest-environment',
versions: ['>=10.0.0'],
}, getTestEnvironment)
function getWrappedScheduleTests (scheduleTests, frameworkVersion) {
// `scheduleTests` is an async function
return function (tests) {
if (!isSuitesSkippingEnabled || hasFilteredSkippableSuites) {
return scheduleTests.apply(this, arguments)
}
const [test] = tests
const rootDir = test?.context?.config?.rootDir
arguments[0] = applySuiteSkipping(tests, rootDir, frameworkVersion)
return scheduleTests.apply(this, arguments)
}
}
function searchSourceWrapper (searchSourcePackage, frameworkVersion) {
const SearchSource = searchSourcePackage.default ?? searchSourcePackage
shimmer.wrap(SearchSource.prototype, 'getTestPaths', getTestPaths => async function () {
const testPaths = await getTestPaths.apply(this, arguments)
const [{ rootDir, shard }] = arguments
if (isKnownTestsEnabled) {
const projectSuites = testPaths.tests.map(test => getTestSuitePath(test.path, test.context.config.rootDir))
// If the `jest` key does not exist in the known tests response, we consider the Early Flake detection faulty.
const isFaulty = !knownTests?.jest ||
getIsFaultyEarlyFlakeDetection(projectSuites, knownTests.jest, earlyFlakeDetectionFaultyThreshold)
if (isFaulty) {
log.error('Early flake detection is disabled because the number of new suites is too high.')
isEarlyFlakeDetectionEnabled = false
isKnownTestsEnabled = false
const testEnvironmentOptions = testPaths.tests[0]?.context?.config?.testEnvironmentOptions
// Project config is shared among all tests, so we can modify it here
if (testEnvironmentOptions) {
testEnvironmentOptions._ddIsEarlyFlakeDetectionEnabled = false
testEnvironmentOptions._ddIsKnownTestsEnabled = false
}
isEarlyFlakeDetectionFaulty = true
}
}
if (shard?.shardCount > 1 || !isSuitesSkippingEnabled || !skippableSuites.length) {
// If the user is using jest sharding, we want to apply the filtering of tests in the shard process.
// The reason for this is the following:
// The tests for different shards are likely being run in different CI jobs so
// the requests to the skippable endpoint might be done at different times and their responses might be different.
// If the skippable endpoint is returning different suites and we filter the list of tests here,
// the base list of tests that is used for sharding might be different,
// causing the shards to potentially run the same suite.
return testPaths
}
const { tests } = testPaths
const suitesToRun = applySuiteSkipping(tests, rootDir, frameworkVersion)
return { ...testPaths, tests: suitesToRun }
})
return searchSourcePackage
}
function getCliWrapper (isNewJestVersion) {
return function cliWrapper (cli, jestVersion) {
if (isNewJestVersion) {
cli = shimmer.wrap(
cli,
'SearchSource',
searchSource => searchSourceWrapper(searchSource, jestVersion),
{ replaceGetter: true }
)
}
return shimmer.wrap(cli, 'runCLI', runCLI => async function () {
let onDone
const configurationPromise = new Promise((resolve) => {
onDone = resolve
})
if (!libraryConfigurationCh.hasSubscribers) {
return runCLI.apply(this, arguments)
}
libraryConfigurationCh.publish({ onDone, frameworkVersion: jestVersion })
try {
const { err, libraryConfig } = await configurationPromise
if (!err) {
isCodeCoverageEnabled = libraryConfig.isCodeCoverageEnabled
isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
isKeepingCoverageConfiguration =
libraryConfig.isKeepingCoverageConfiguration ?? isKeepingCoverageConfiguration
isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {}
earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries
isImpactedTestsEnabled = libraryConfig.isImpactedTestsEnabled
}
} catch (err) {
log.error('Jest library configuration error', err)
}
if (isKnownTestsEnabled) {
const knownTestsPromise = new Promise((resolve) => {
onDone = resolve
})
knownTestsCh.publish({ onDone })
try {
const { err, knownTests: receivedKnownTests } = await knownTestsPromise
if (err) {
// We disable EFD if there has been an error in the known tests request
isEarlyFlakeDetectionEnabled = false
isKnownTestsEnabled = false
} else {
knownTests = receivedKnownTests
}
} catch (err) {
log.error('Jest known tests error', err)
}
}
if (isSuitesSkippingEnabled) {
const skippableSuitesPromise = new Promise((resolve) => {
onDone = resolve
})
skippableSuitesCh.publish({ onDone })
try {
const { err, skippableSuites: receivedSkippableSuites } = await skippableSuitesPromise
if (!err) {
skippableSuites = receivedSkippableSuites
}
} catch (err) {
log.error('Jest test-suite skippable error', err)
}
}
if (isTestManagementTestsEnabled) {
const testManagementTestsPromise = new Promise((resolve) => {
onDone = resolve
})
testManagementTestsCh.publish({ onDone })
try {
const { err, testManagementTests: receivedTestManagementTests } = await testManagementTestsPromise
if (err) {
isTestManagementTestsEnabled = false
testManagementTests = {}
} else {
testManagementTests = receivedTestManagementTests || {}
}
} catch (err) {
log.error('Jest test management tests error', err)
isTestManagementTestsEnabled = false
}
}
if (isImpactedTestsEnabled) {
const impactedTestsPromise = new Promise((resolve) => {
onDone = resolve
})
modifiedFilesCh.publish({ onDone })
try {
const { err, modifiedFiles: receivedModifiedFiles } = await impactedTestsPromise
if (!err) {
modifiedFiles = receivedModifiedFiles
}
} catch (err) {
log.error('Jest impacted tests error', err)
}
}
const processArgv = process.argv.slice(2).join(' ')
testSessionStartCh.publish({ command: `jest ${processArgv}`, frameworkVersion: jestVersion })
const result = await runCLI.apply(this, arguments)
const {
results: {
coverageMap,
numFailedTestSuites,
numFailedTests,
numRuntimeErrorTestSuites = 0,
numTotalTests,
numTotalTestSuites,
runExecError,
wasInterrupted,
},
} = result
const hasSuiteLevelFailures = numRuntimeErrorTestSuites > 0
const hasRunLevelFailure = runExecError != null || wasInterrupted === true
const mustNotFlipSuccess = hasSuiteLevelFailures || hasRunLevelFailure
let testCodeCoverageLinesTotal
if (isUserCodeCoverageEnabled) {
try {
const { pct, total } = coverageMap.getCoverageSummary().lines
testCodeCoverageLinesTotal = total === 0 ? 0 : pct
} catch {
// ignore errors
}
}
/**
* If Early Flake Detection (EFD) is enabled the logic is as follows:
* - If all attempts for a test are failing, the test has failed and we will let the test process fail.
* - If just a single attempt passes, we will prevent the test process from failing.
* The rationale behind is the following: you may still be able to block your CI pipeline by gating
* on flakiness (the