UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

1,334 lines (1,204 loc) 125 kB
'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