dd-trace
Version:
Datadog APM tracing client for JavaScript
1,334 lines (1,204 loc) • 125 kB
JavaScript
'use strict'
// Capture real timers at module load time, before any test can install fake timers.
const realSetTimeout = setTimeout
const { readFileSync } = require('node:fs')
const { builtinModules, createRequire } = require('node:module')
const path = require('path')
const satisfies = require('../../../vendor/dist/semifies')
const { DD_MAJOR } = require('../../../version')
const shimmer = require('../../datadog-shimmer')
const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
const log = require('../../dd-trace/src/log')
const {
getCoveredFilesFromCoverage,
getExecutableFilesFromCoverage,
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,
recordAttemptToFixExecution,
logAttemptToFixTestExecution,
logTestOptimizationSummary,
getEfdRetryCount,
getTestCoverageLinesPercentage,
applySkippedCoverageToCoverage,
getTestOptimizationRequestResults,
} = require('../../dd-trace/src/plugins/util/test')
const {
getFormattedJestTestParameters,
getJestTestName,
getRawJestTestName,
getJestSuitesToRun,
removeSeedSuffixFromTestName,
} = require('../../datadog-plugin-jest/src/util')
const {
addCoverageBackfillUntestedFiles,
getCoverageBackfillFiles,
} = require('./jest/coverage-backfill')
const {
getChannelPromise,
publishWithCompletion,
} = require('./helpers/channel')
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
const JEST_SESSION_STATE = Symbol.for('dd-trace:jest:session')
const JEST_BAIL_REPORTER_PATH = require.resolve('./jest/bail-reporter')
const DD_JEST_HANDLE_TEST_EVENT_WRAPPED = Symbol('dd-trace:jest:handle-test-event-wrapped')
const DD_JEST_HANDLE_TEST_EVENT_DATADOG = Symbol('dd-trace:jest:handle-test-event-datadog')
const DD_JEST_CONCURRENT_TEST_ORIGINAL = Symbol('dd-trace:jest:concurrent-test-original')
const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID')
const jestSessionState = globalThis[JEST_SESSION_STATE] || (globalThis[JEST_SESSION_STATE] = {})
// 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 skippableSuitesCoverage = {}
let skippedSuitesCoverage = {}
let knownTests = {}
let isCodeCoverageEnabled = false
let isCoverageReportUploadEnabled = false
let isItrEnabled = false
let isSuitesSkippingEnabled = 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 = {}
let repositoryRoot
let lastCoverageMap
let lastCoverageMapRootDir
let coverageBackfillContexts
let coverageBackfillFiles
let coverageReporterClass
let coverageReporterRequire
let activeTestSuiteAbsolutePath
let isConsoleErrorWrapped = false
const testContexts = new WeakMap()
const originalTestFns = new WeakMap()
const originalHookFns = new WeakMap()
const concurrentHookContextQueues = new WeakMap()
const concurrentHookFns = 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 loggedAttemptToFixTests = 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 testSuiteJestObjects = new Map()
const testSuiteDatadogEnvironments = new Map()
const wrappedJestGlobals = new WeakSet()
const wrappedJestObjects = new WeakSet()
const wrappedWorkerInitializers = new WeakSet()
const publishedRuntimeReferenceErrors = new WeakMap()
const wrappedCoverageReporters = new WeakSet()
const coverageReporterRequires = new WeakMap()
const handledJestEvents = new WeakSet()
/**
* @typedef {object} ConcurrentTestOptions
* @property {(...args: unknown[]) => unknown} [concurrentTest]
* @property {unknown} [concurrentTestThisArg]
* @property {boolean} [isAttemptToFixRetry]
* @property {boolean} [isEfdRetry]
* @property {boolean} [isModified]
* @property {(...args: unknown[]) => unknown} [serialTest]
* @property {unknown} [serialTestThisArg]
* @property {(...args: unknown[]) => unknown} [sourceTestFn]
* @property {string} [testParameters]
*/
const ATR_RETRY_SUPPRESSION_FLAG = '_ddDisableAtrRetry'
const MINIMUM_JEST_VERSION = DD_MAJOR >= 6 ? '>=28.0.0' : '>=24.8.0'
const MINIMUM_JEST_VERSION_BEFORE_30 = DD_MAJOR >= 6 ? '>=28.0.0 <30.0.0' : '>=24.8.0 <30.0.0'
const MINIMUM_JEST_WORKER_VERSION_BEFORE_30 = DD_MAJOR >= 6 ? '>=28.0.0 <30.0.0' : '>=24.9.0 <30.0.0'
const MINIMUM_JEST_CONFIG_ASYNC_VERSION = DD_MAJOR >= 6 ? '>=28.0.0' : '>=25.1.0'
const MINIMUM_JEST_TEST_SCHEDULER_VERSION = DD_MAJOR >= 6 ? '>=28.0.0' : '>=27.0.0'
const MINIMUM_JEST_COVERAGE_BACKFILL_VERSION = '>=28.0.0'
const atrSuppressedErrors = new Map()
let hasWarnedDeprecatedJestVersion = false
let isJestCoverageBackfillSupported = false
let hasFinishedTestSession = false
let jestEachBind
// Track quarantined tests whose errors were suppressed, keyed by "suite › testName"
const quarantinedFailingTests = new Set()
function getJestRepositoryRoot (readConfigsResult) {
const configuredRepositoryRoot = readConfigsResult.configs
?.find(config => config.testEnvironmentOptions?._ddRepositoryRoot)
?.testEnvironmentOptions._ddRepositoryRoot
return configuredRepositoryRoot || process.cwd()
}
/**
* 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 warnDeprecatedJestVersion (frameworkVersion) {
if (DD_MAJOR >= 6 || hasWarnedDeprecatedJestVersion || !frameworkVersion ||
!satisfies(frameworkVersion, '<28.0.0')) {
return
}
hasWarnedDeprecatedJestVersion = true
// eslint-disable-next-line no-console
console.warn(
'dd-trace support for Jest<28.0.0 is deprecated and will be removed in dd-trace v6. ' +
'Please upgrade Jest to >=28.0.0.'
)
}
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 })
}
/**
* Formats the ignored-failure section for the Test Optimization summary.
*
* @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
* @returns {string}
*/
function formatIgnoredFailuresSummary (ignoredFailures) {
if (!ignoredFailures) return ''
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' })
}
if (items.length === 0 || ignoredFailures.totalCount <= 0) return ''
const shown = items.slice(0, MAX_IGNORED_TEST_NAMES)
const more = items.length - shown.length
const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
const formattedItems = shown
.map(({ text, suffix }) => ` • ${text}${suffix ? ` (${suffix})` : ''}`)
.join('\n') + moreSuffix
return `${ignoredFailures.totalCount} test failure(s) were ignored. Exit code set to 0.\n\n${formattedItems}`
}
/**
* Logs a single "Datadog Test Optimization" summary at session end.
*
* @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
*/
function logSessionSummary (ignoredFailures, attemptToFixExecutions) {
logTestOptimizationSummary({
attemptToFixExecutions,
extraSections: [formatIgnoredFailuresSummary(ignoredFailures)],
newTestsWithDynamicNames,
})
loggedAttemptToFixTests.clear()
}
function getTestStatusFromJestResult (status) {
if (status === 'failed') return 'fail'
if (status === 'passed') return 'pass'
}
function getAttemptToFixExecutionsFromJestResults (result) {
const executions = new Map()
const rootDir = result.globalConfig?.rootDir || process.cwd()
for (const { testResults, testFilePath } of result.results.testResults) {
const testSuite = getTestSuitePath(testFilePath, rootDir)
const testManagementTestsForSuite = testManagementTests
?.jest
?.suites
?.[testSuite]
?.tests
if (!testManagementTestsForSuite) continue
for (const { fullName, status } of testResults) {
const testName = removeSeedSuffixFromTestName(fullName)
const testStatus = getTestStatusFromJestResult(status)
if (!testStatus) continue
const testManagementTest = testManagementTestsForSuite[testName]?.properties
if (!testManagementTest?.attempt_to_fix) continue
recordAttemptToFixExecution(executions, {
testSuite,
testName,
status: testStatus,
isDisabled: testManagementTest.disabled,
isQuarantined: testManagementTest.quarantined,
})
}
}
return executions
}
function wrapConsoleErrorForJestReferenceErrors () {
if (isConsoleErrorWrapped) return
isConsoleErrorWrapped = true
// eslint-disable-next-line no-console
const originalConsoleError = console.error
// eslint-disable-next-line no-console
console.error = function () {
const [message] = arguments
if (
typeof message === 'string' &&
message.includes('Jest environment has been torn down') &&
activeTestSuiteAbsolutePath
) {
publishRuntimeReferenceError({ _testPath: activeTestSuiteAbsolutePath }, message)
}
return originalConsoleError.apply(this, arguments)
}
}
function isDatadogJestEventHandled (event) {
return event && typeof event === 'object' && handledJestEvents.has(event)
}
function markDatadogJestEventHandled (event) {
if (event && typeof event === 'object') {
handledJestEvents.add(event)
}
}
/**
* Gets the original Jest concurrent registration function behind a Datadog wrapper.
*
* @param {(...args: unknown[]) => unknown} concurrentTest
* @returns {(...args: unknown[]) => unknown}
*/
function getOriginalConcurrentTest (concurrentTest) {
return concurrentTest[DD_JEST_CONCURRENT_TEST_ORIGINAL] || concurrentTest
}
/**
* Marks a Datadog concurrent registration wrapper with its original Jest function.
*
* @param {(...args: unknown[]) => unknown} wrappedConcurrentTest
* @param {(...args: unknown[]) => unknown} originalConcurrentTest
* @returns {void}
*/
function setOriginalConcurrentTest (wrappedConcurrentTest, originalConcurrentTest) {
Object.defineProperty(wrappedConcurrentTest, DD_JEST_CONCURRENT_TEST_ORIGINAL, {
configurable: true,
value: originalConcurrentTest,
})
}
/**
* Wraps a custom Jest environment handler so Datadog still observes events even
* when the custom environment does not call `super.handleTestEvent`.
*
* @param {(event: object, state: object) => Promise<void>|void} handleTestEvent
* @param {(event: object, state: object) => Promise<void>|void} datadogHandleTestEvent
* @returns {(event: object, state: object) => Promise<void>|void}
*/
function getWrappedCustomHandleTestEvent (handleTestEvent, datadogHandleTestEvent) {
if (
!handleTestEvent ||
handleTestEvent === datadogHandleTestEvent ||
handleTestEvent[DD_JEST_HANDLE_TEST_EVENT_WRAPPED]
) {
return handleTestEvent || datadogHandleTestEvent
}
const wrappedHandleTestEvent = function (event, state) {
const result = handleTestEvent.call(this, event, state)
const runDatadogHandler = value => {
if (isDatadogJestEventHandled(event)) return value
const datadogResult = datadogHandleTestEvent.call(this, event, state)
if (typeof datadogResult?.then === 'function') {
return datadogResult.then(() => value)
}
return value
}
if (event.name === 'add_test') {
runDatadogHandler(result)
return result
}
if (typeof result?.then === 'function') {
return result.then(runDatadogHandler)
}
return runDatadogHandler(result)
}
wrappedHandleTestEvent[DD_JEST_HANDLE_TEST_EVENT_WRAPPED] = true
return wrappedHandleTestEvent
}
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.nameToParams = {}
this.global._ddtrace = global._ddtrace
this.hasSnapshotTests = undefined
this.testSuiteAbsolutePath = context.testPath
activeTestSuiteAbsolutePath = this.testSuiteAbsolutePath
testSuiteDatadogEnvironments.set(this.testSuiteAbsolutePath, this)
wrapConsoleErrorForJestReferenceErrors()
this.globalConfig = config.globalConfig
this.displayName = config.projectConfig?.displayName?.name || config.displayName
this.testEnvironmentOptions = getTestEnvironmentOptions(config)
const repositoryRoot = this.testEnvironmentOptions._ddRepositoryRoot
this.testSuite = getTestSuitePath(context.testPath, rootDir)
// 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
this.hasConcurrentTests = false
this.concurrentTestContexts = new Map()
this.concurrentTestStates = new WeakMap()
this.concurrentTestSourceFns = new WeakMap()
this.wrappedConcurrentTestFunctions = new WeakSet()
this.jestEachBind = undefined
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
}
}
this[DD_JEST_HANDLE_TEST_EVENT_DATADOG] = DatadogEnvironment.prototype.handleTestEvent
this.wrapCustomHandleTestEvent(DatadogEnvironment.prototype.handleTestEvent)
}
/**
* Rechecks custom `handleTestEvent` implementations after subclass instance fields
* and constructors have run.
*
* @returns {Promise<void>|void}
*/
setup () {
this.wrapCustomHandleTestEvent(DatadogEnvironment.prototype.handleTestEvent)
if (super.setup) {
const result = super.setup()
if (typeof result?.then === 'function') {
return result.then(() => {
this.wrapCustomHandleTestEvent(DatadogEnvironment.prototype.handleTestEvent)
})
}
this.wrapCustomHandleTestEvent(DatadogEnvironment.prototype.handleTestEvent)
return result
}
}
/**
* Wraps Jest's concurrent test registration methods so eager concurrent bodies
* in older Jest versions execute inside their test span context.
*
* @param {object} state
* @returns {void}
*/
wrapConcurrentTest (state) {
this.concurrentTestState = state
this.wrapConcurrentTestGlobals(this.global.test, state)
}
/**
* Wraps one Jest `test` function object's concurrent registration methods.
*
* @param {Function|undefined} test
* @param {object} state
* @returns {void}
*/
wrapConcurrentTestGlobals (test, state) {
if (!state) return
const concurrentTest = test?.concurrent
if (typeof concurrentTest !== 'function') return
this.wrapConcurrentTestFunction(test, 'concurrent', state, test, test)
this.wrapConcurrentTestFunction(test.concurrent, 'only', state, test.only, test)
this.wrapConcurrentTestFunction(test.concurrent, 'failing', state, test.failing, test)
this.wrapConcurrentTestFunction(test.concurrent.only, 'failing', state, test.only?.failing, test.only)
}
/**
* Wraps one concurrent test function variant.
*
* @param {object} target
* @param {string} methodName
* @param {object} state
* @param {Function|undefined} serialTest
* @param {unknown} serialTestThisArg
* @returns {void}
*/
wrapConcurrentTestFunction (target, methodName, state, serialTest, serialTestThisArg) {
let concurrentTest = target?.[methodName]
if (typeof concurrentTest !== 'function') return
if (this.wrappedConcurrentTestFunctions.has(concurrentTest)) return
const originalConcurrentTest = getOriginalConcurrentTest(concurrentTest)
if (originalConcurrentTest !== concurrentTest) {
target[methodName] = originalConcurrentTest
concurrentTest = originalConcurrentTest
}
if (typeof concurrentTest !== 'function') return
const environment = this
shimmer.wrap(target, methodName, concurrentTest => {
return function wrappedConcurrentTest (testName, testFn, ...args) {
if (typeof testFn !== 'function') {
return concurrentTest.apply(this, arguments)
}
environment.hasConcurrentTests = true
const asyncError = environment.isImpactedTestsEnabled
? new Error('Datadog concurrent test registration')
: undefined
const wrappedTestFn = environment.createConcurrentTestFn(testName, testFn, asyncError, state, {
concurrentTest,
concurrentTestThisArg: this,
serialTest,
serialTestThisArg,
sourceTestFn: environment.concurrentTestSourceFns.get(testFn),
})
return concurrentTest.call(this, testName, wrappedTestFn, ...args)
}
})
const wrappedConcurrentTest = target[methodName]
setOriginalConcurrentTest(wrappedConcurrentTest, concurrentTest)
this.wrappedConcurrentTestFunctions.add(wrappedConcurrentTest)
this.bindConcurrentEach(wrappedConcurrentTest, methodName === 'failing')
}
/**
* Rebuilds a concurrent `.each` helper so each generated row calls the wrapped test function.
*
* @param {Function} concurrentTest
* @param {boolean} needsEachError
* @returns {void}
*/
bindConcurrentEach (concurrentTest, needsEachError) {
if (typeof concurrentTest?.each !== 'function') return
const bind = this.getJestEachBind()
if (typeof bind !== 'function') return
const environment = this
concurrentTest.each = function wrappedConcurrentEach (...eachArgs) {
const testParameters = getFormattedJestTestParameters(eachArgs)
return function (...testArgs) {
let parameterIndex = 0
const sourceTestFn = testArgs[1]
const concurrentEachTest = function (testName, testFn, ...callArgs) {
const parameters = testParameters?.[parameterIndex++]
if (parameters !== undefined) {
environment.appendNameToParams(testName, parameters)
}
if (typeof testFn === 'function' && typeof sourceTestFn === 'function') {
environment.concurrentTestSourceFns.set(testFn, sourceTestFn)
}
return concurrentTest.call(this, testName, testFn, ...callArgs)
}
const eachBind = bind(concurrentEachTest, false, needsEachError).apply(this, eachArgs)
return eachBind.apply(this, testArgs)
}
}
}
/**
* Creates a Jest test function wrapper and stores its concurrent execution state.
*
* @param {string} testName
* @param {(...args: unknown[]) => unknown} testFn
* @param {Error|undefined} asyncError
* @param {object} state
* @param {ConcurrentTestOptions|undefined} options
* @returns {(...args: unknown[]) => unknown}
*/
createConcurrentTestFn (testName, testFn, asyncError, state, options) {
const concurrentTestState = {
concurrentTest: options?.concurrentTest,
concurrentTestThisArg: options?.concurrentTestThisArg,
ctx: this.createConcurrentTestContext(testName, testFn, asyncError, state, options),
numExecutions: 0,
serialTest: options?.serialTest,
serialTestThisArg: options?.serialTestThisArg,
state,
testFn,
}
concurrentTestState.ctx.concurrentTestState = concurrentTestState
const environment = this
const wrappedTestFn = shimmer.wrapFunction(testFn, testFn => function (...args) {
return environment.runConcurrentTestFn(concurrentTestState, testFn, this, args)
})
this.concurrentTestStates.set(wrappedTestFn, concurrentTestState)
return wrappedTestFn
}
/**
* Gets Jest's table-driven test binder from the user's Jest installation.
*
* @returns {Function|undefined}
*/
getJestEachBind () {
if (this.jestEachBind !== undefined) return this.jestEachBind
if (typeof jestEachBind === 'function') {
this.jestEachBind = jestEachBind
return this.jestEachBind
}
let jestEach
try {
jestEach = createRequire(path.join(this.rootDir, 'package.json'))('jest-each')
} catch {
try {
jestEach = createRequire(path.join(process.cwd(), 'package.json'))('jest-each')
} catch {
jestEach = undefined
}
}
this.jestEachBind = jestEach?.bind
return this.jestEachBind
}
/**
* Creates the context used to start and finish a concurrent test span.
*
* @param {string} testName
* @param {(...args: unknown[]) => unknown} testFn
* @param {Error|undefined} asyncError
* @param {object} state
* @param {ConcurrentTestOptions|undefined} options
* @returns {object}
*/
createConcurrentTestContext (testName, testFn, asyncError, state, options) {
const testFullName = this.getTestNameFromAddTestEvent({ testName }, state)
const isNewTest = this.isKnownTestsEnabled && !this.knownTestsForThisSuite?.includes(testFullName)
const isAttemptToFix = this.isTestManagementTestsEnabled &&
this.testManagementTestsForThisSuite?.attemptToFix?.includes(testFullName)
const sourceTestFn = options?.sourceTestFn || testFn
const ctx = {
name: testFullName,
suite: this.testSuite,
testSourceFile: this.testSourceFile,
displayName: this.displayName,
testParameters: options?.testParameters || getTestParametersString(this.nameToParams, testName),
frameworkVersion: jestVersion,
isNew: isNewTest,
isEfdRetry: options?.isEfdRetry === true,
isAttemptToFix,
isAttemptToFixRetry: options?.isAttemptToFixRetry === true,
isJestRetry: false,
isDisabled: this.testManagementTestsForThisSuite?.disabled?.includes(testFullName),
isQuarantined: this.testManagementTestsForThisSuite?.quarantined?.includes(testFullName),
isModified: options?.isModified ?? this.isTestModified(asyncError, sourceTestFn),
hasDynamicName: isNewTest && DYNAMIC_NAME_RE.test(testFullName),
testSuiteAbsolutePath: this.testSuiteAbsolutePath,
}
let contexts = this.concurrentTestContexts.get(testFullName)
if (contexts) {
contexts.push(ctx)
} else {
contexts = [ctx]
this.concurrentTestContexts.set(testFullName, contexts)
}
return ctx
}
/**
* Checks if a test overlaps modified lines for impacted-test detection.
*
* @param {Error|undefined} asyncError
* @param {Function|undefined} testFn
* @returns {boolean}
*/
isTestModified (asyncError, testFn) {
if (!this.isImpactedTestsEnabled || !asyncError || typeof testFn !== 'function') return false
const testStartLine = getTestLineStart(asyncError, this.testSuite)
const testEndLine = getTestEndLine(testFn, testStartLine)
return isModifiedTest(
this.testSourceFile,
testStartLine,
testEndLine,
this.modifiedFiles,
'jest'
)
}
/**
* Gets the next registered concurrent test context for a full Jest test name.
*
* @param {string} testName
* @returns {object|undefined}
*/
getNextConcurrentTestContext (testName) {
const contexts = this.concurrentTestContexts.get(testName)
if (!contexts?.length) return
const ctx = contexts.shift()
if (contexts.length === 0) {
this.concurrentTestContexts.delete(testName)
}
return ctx
}
/**
* Removes one registered concurrent test context from the name fallback queue.
*
* @param {string} testName
* @param {object} ctx
* @returns {void}
*/
removeConcurrentTestContext (testName, ctx) {
const contexts = this.concurrentTestContexts.get(testName)
if (!contexts?.length) return
const contextIndex = contexts.indexOf(ctx)
if (contextIndex === -1) return
contexts.splice(contextIndex, 1)
if (contexts.length === 0) {
this.concurrentTestContexts.delete(testName)
}
}
/**
* Records formatted parameters for one table-driven Jest test declaration.
*
* @param {string} name
* @param {unknown[]} params
* @returns {void}
*/
setNameToParams (name, params) {
this.nameToParams[name] = [...params]
}
/**
* Appends formatted parameters for one table-driven Jest test row.
*
* @param {string} name
* @param {unknown[]|object} params
* @returns {void}
*/
appendNameToParams (name, params) {
if (this.nameToParams[name]) {
this.nameToParams[name].push(params)
} else {
this.nameToParams[name] = [params]
}
}
/**
* Runs a concurrent test function inside its test span context.
*
* @param {{ ctx: object, numExecutions: number }} concurrentTestState
* @param {(...args: unknown[]) => unknown} testFn
* @param {object} thisArg
* @param {unknown[]} args
* @returns {unknown|void}
*/
runConcurrentTestFn (concurrentTestState, testFn, thisArg, args) {
const ctx = concurrentTestState.ctx
if (ctx.isDisabled && !ctx.isAttemptToFix) {
const done = args[0]
if (typeof done === 'function') {
done()
}
return
}
const runTest = () => testFn.apply(thisArg, args)
if (!ctx.isAttemptToFixRetry && !ctx.isEfdRetry) {
ctx.isJestRetry = concurrentTestState.numExecutions > 0
}
concurrentTestState.numExecutions++
if (ctx.currentStore) {
return testFnCh.runStores(ctx, runTest)
}
let result
testStartCh.runStores(ctx, () => {
result = testFnCh.runStores(ctx, runTest)
})
return result
}
/**
* Ensures Datadog handles Jest circus events even when a custom environment overrides
* `handleTestEvent` without calling `super.handleTestEvent`.
*
* @param {(event: object, state: object) => Promise<void>|void} datadogHandleTestEvent
* @returns {void}
*/
wrapCustomHandleTestEvent (datadogHandleTestEvent) {
const descriptor = Object.getOwnPropertyDescriptor(this, 'handleTestEvent')
let handleTestEvent = getWrappedCustomHandleTestEvent(this.handleTestEvent, datadogHandleTestEvent)
Object.defineProperty(this, 'handleTestEvent', {
configurable: true,
enumerable: descriptor?.enumerable ?? false,
get () {
return handleTestEvent
},
set (value) {
handleTestEvent = getWrappedCustomHandleTestEvent(value, datadogHandleTestEvent)
},
})
}
/**
* 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 {
if (this.moduleMocker?.clearAllMocks) {
this.moduleMocker.clearAllMocks()
return
}
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,
forceSerial,
retryCount,
retryType,
}) {
const { testName, fn, timeout } = jestEvent
const isAttemptToFixRetry = retryType === 'Test Management (Attempt to Fix)'
const isEfdRetry = retryType === 'Impacted tests' || retryType === 'Early flake detection'
for (let retryIndex = 0; retryIndex < retryCount; retryIndex++) {
const concurrentTestState = this.concurrentTestStates.get(fn)
if (concurrentTestState) {
const test = forceSerial
? concurrentTestState.serialTest
: (
concurrentTestState.concurrentTest ||
getOriginalConcurrentTest(this.global.test?.concurrent)
)
if (typeof test !== 'function') {
log.error('%s could not retry concurrent test because test is undefined', retryType)
continue
}
const retryFn = this.createConcurrentTestFn(
testName,
concurrentTestState.testFn,
undefined,
concurrentTestState.state,
{
concurrentTest: concurrentTestState.concurrentTest,
concurrentTestThisArg: concurrentTestState.concurrentTestThisArg,
isAttemptToFixRetry,
isEfdRetry,
isModified: concurrentTestState.ctx.isModified,
serialTest: concurrentTestState.serialTest,
serialTestThisArg: concurrentTestState.serialTestThisArg,
testParameters: concurrentTestState.ctx.testParameters,
}
)
const thisArg = forceSerial
? concurrentTestState.serialTestThisArg
: concurrentTestState.concurrentTestThisArg
test.call(thisArg, testName, retryFn, timeout)
} else if (this.global.test) {
this.global.test(testName, fn, timeout)
} else {
log.error('%s could not retry test because global.test is undefined', retryType)
}
}
}
// At the `add_test` event we don't have the test object yet, so we can't use it
getTestNameFromAddTestEvent (event, state) {
const describeSuffix = getRawJestTestName(state.currentDescribeBlock)
const testName = describeSuffix ? `${describeSuffix} ${event.testName}` : event.testName
return removeSeedSuffixFromTestName(testName)
}
async handleTestEvent (event, state) {
if (isDatadogJestEventHandled(event)) return
markDatadogJestEventHandled(event)
if (super.handleTestEvent) {
await super.handleTestEvent(event, state)
}
if (event.name === 'setup') {
this.wrapConcurrentTest(state)
}
if (event.name === 'setup' && this.global.test) {
const environment = this
shimmer.wrap(this.global.test, 'each', each => function (...args) {
const testParameters = getFormattedJestTestParameters(args)
const eachBind = each.apply(this, args)
return function (...args) {
const [testName] = args
environment.setNameToParams(testName, testParameters)
return eachBind.apply(this, args)
}
})
}
if (event.name === 'test_start') {
const testName = getJestTestName(event.test)
const concurrentTestState = this.concurrentTestStates.get(event.test.fn)
let concurrentCtx = concurrentTestState?.ctx
if (concurrentCtx) {
this.removeConcurrentTestContext(testName, concurrentCtx)
} else {
concurrentCtx = this.getNextConcurrentTestContext(testName)
}
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 = concurrentCtx?.testParameters ||
concurrentTestState?.ctx?.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 = this.isTestModified(event.test.asyncError, event.test.fn)
if (concurrentCtx?.isModified || concurrentTestState?.ctx?.isModified) {
isModified = true
}
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 = concurrentCtx || concurrentTestState?.ctx || {}
ctx.name = testName
ctx.suite = this.testSuite
ctx.testSourceFile = this.testSourceFile
ctx.displayName = this.displayName
ctx.testParameters = testParameters
ctx.frameworkVersion = jestVersion
ctx.isNew = isNewTest
ctx.isEfdRetry = ctx.isEfdRetry || numEfdRetry > 0
ctx.isAttemptToFix = isAttemptToFix
ctx.isAttemptToFixRetry = ctx.isAttemptToFixRetry || numOfAttemptsToFixRetries > 0
ctx.isJestRetry = isJestRetry
ctx.isDisabled = isDisabled
ctx.isQuarantined = isQuarantined
ctx.isModified = isModified
ctx.hasDynamicName = hasDynamicName
ctx.testSuiteAbsolutePath = this.testSuiteAbsolutePath
if (concurrentTestState) {
ctx.concurrentTestState = concurrentTestState
concurrentTestState.ctx = ctx
}
testContexts.set(event.test, ctx)
if (isAttemptToFix) {
logAttemptToFixTestExecution(this.testSuite, testName, loggedAttemptToFixTests)
}
const wrapTestAndHooks = () => {
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 (...args) {
return testFnCh.runStores(ctx, () => hookFn.apply(this, args))
})
hook.fn = newHookFn
}
const originalFn = event.test.fn
originalTestFns.set(event.test, originalFn)
const newFn = shimmer.wrapFunction(event.test.fn, testFn => function (...args) {
return testFnCh.runStores(ctx, () => testFn.apply(this, args))
})
event.test.fn = newFn
}
const isConcurrentTest = event.test.concurrent || ctx.concurrentTestState
if (isConcurrentTest) {
if (!ctx.currentStore && event.test.errors?.length) {
testStartCh.runStores(ctx, () => {})
}
} else if (ctx.currentStore) {
wrapTestAndHooks()
} else {
testStartCh.runStores(ctx, wrapTestAndHooks)
}
}
if (event.name === 'hook_start' && (event.hook.type === 'beforeEach' || event.hook.type === 'afterEach')) {
const ctx = testContexts.get(event.test) || this.concurrentTestStates.get(event.test?.fn)?.ctx
if (ctx?.concurrentTestState && (!ctx.isDisabled || ctx.isAttemptToFix)) {
let hookFn = event.hook.fn
if (originalHookFns.has(event.hook)) {
hookFn = originalHookFns.get(event.hook)
} else {
originalHookFns.set(event.hook, hookFn)
}
let hookContexts = concurrentHookContextQueues.get(event.hook)
if (!hookContexts) {
hookContexts = []
concurrentHookContextQueues.set(event.hook, hookContexts)
}
hookContexts.push(ctx)
if (!ctx.currentStore) {
testStartCh.runStores(ctx, () => {})
}
let concurrentHookFn = concurrentHookFns.get(event.hook)
if (!concurrentHookFn) {
concurrentHookFn = shimmer.wrapFunction(hookFn, hookFn => function (...args) {
const hookContexts = concurrentHookContextQueues.get(event.hook)
const [hookCtx] = hookContexts || []
if (hookCtx) {
hookContexts.shift()
return testFnCh.runStores(hookCtx, () => hookFn.apply(this, args))
}
return hookFn.apply(this, args)
})
concurrentHookFns.set(event.hook, concurrentHookFn)
}
event.hook.fn = concurrentHookFn
}
}
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 (...args) {
return testSuiteHookFnCh.runStores(ctx, () => hookFn.apply(this, args))
})
}
if (event.name === 'add_test') {
if (event.concurrent) {
this.hasConcurrentTests = true
}
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 concurrentTestState = this.concurrentTestStates.get(event.fn)
const isModified = concurrentTestState?.ctx?.isModified ||
this.isTestModified(event.asyncError, event.fn)
if (isModified && !retriedTestsToNumAttempts.has(testFullName) && this.isEarlyFlakeDetec