UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

1,371 lines (1,212 loc) 51.1 kB
'use strict' const { addHook, channel } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') const log = require('../../dd-trace/src/log') const path = require('path') const { getCoveredFilenamesFromCoverage, JEST_WORKER_TRACE_PAYLOAD_CODE, JEST_WORKER_COVERAGE_PAYLOAD_CODE, getTestLineStart, getTestSuitePath, getTestParametersString, addEfdStringToTestName, removeEfdStringFromTestName, getIsFaultyEarlyFlakeDetection, JEST_WORKER_LOGS_PAYLOAD_CODE, addAttemptToFixStringToTestName, removeAttemptToFixStringFromTestName, getTestEndLine, isModifiedTest } = require('../../dd-trace/src/plugins/util/test') const { getFormattedJestTestParameters, getJestTestName, getJestSuitesToRun } = require('../../datadog-plugin-jest/src/util') const testSessionStartCh = channel('ci:jest:session:start') const testSessionFinishCh = channel('ci:jest:session:finish') const testSessionConfigurationCh = channel('ci:jest:session:configuration') const testSuiteStartCh = channel('ci:jest:test-suite:start') const testSuiteFinishCh = channel('ci:jest:test-suite:finish') 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 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 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 impactedTestsCh = channel('ci:jest:modified-tests') 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 isSuitesSkippingEnabled = false let isUserCodeCoverageEnabled = false let isSuitesSkipped = false let numSkippedSuites = 0 let hasUnskippableSuites = false let hasForcedToRunSuites = false let isEarlyFlakeDetectionEnabled = false let earlyFlakeDetectionNumRetries = 0 let earlyFlakeDetectionFaultyThreshold = 30 let isEarlyFlakeDetectionFaulty = false let hasFilteredSkippableSuites = false let isKnownTestsEnabled = false let isTestManagementTestsEnabled = false let testManagementTests = {} let testManagementAttemptToFixRetries = 0 let isImpactedTestsEnabled = false let modifiedTests = {} 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 wrappedWorkers = new WeakSet() const testSuiteMockedFiles = new Map() const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200 // 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 {} } function getTestStats (testStatuses) { return testStatuses.reduce((acc, testStatus) => { acc[testStatus]++ return acc }, { pass: 0, fail: 0 }) } 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 this.testEnvironmentOptions = getTestEnvironmentOptions(config) const repositoryRoot = this.testEnvironmentOptions._ddRepositoryRoot 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) { try { const hasKnownTests = !!knownTests?.jest earlyFlakeDetectionNumRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionNumRetries this.knownTestsForThisSuite = hasKnownTests ? (knownTests?.jest?.[this.testSuite] || []) : this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests) } 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(modifiedTests).length > 0 this.modifiedTestsForThisSuite = hasImpactedTests ? this.getModifiedTestForThisSuite(modifiedTests) : this.getModifiedTestForThisSuite(this.testEnvironmentOptions._ddModifiedTests) } catch (e) { log.error('Error parsing impacted tests', e) this.isImpactedTestsEnabled = false } } } getHasSnapshotTests () { if (this.hasSnapshotTests !== undefined) { return this.hasSnapshotTests } let hasSnapshotTests = true try { const { _snapshotData } = this.getVmContext().expect.getState().snapshotState hasSnapshotTests = Object.keys(_snapshotData).length > 0 } catch { // if we can't be sure, we'll err on the side of caution and assume it has snapshots } this.hasSnapshotTests = hasSnapshotTests return hasSnapshotTests } // Function that receives a list of known tests for a test service and // returns the ones that belong to the current suite getKnownTestsForSuite (knownTests) { if (this.knownTestsForThisSuite) { return this.knownTestsForThisSuite } let knownTestsForSuite = knownTests // If jest is using workers, known tests are serialized to json. // If jest runs in band, they are not. if (typeof knownTestsForSuite === 'string') { knownTestsForSuite = JSON.parse(knownTestsForSuite) } return knownTestsForSuite } 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: [] } Object.entries(testManagementTestsForSuite).forEach(([testName, { properties }]) => { 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 } getModifiedTestForThisSuite (modifiedTests) { if (this.modifiedTestsForThisSuite) { return this.modifiedTestsForThisSuite } let modifiedTestsForThisSuite = modifiedTests // If jest is using workers, modified tests are serialized to json. // If jest runs in band, they are not. if (typeof modifiedTestsForThisSuite === 'string') { modifiedTestsForThisSuite = JSON.parse(modifiedTestsForThisSuite) } return modifiedTestsForThisSuite } // Generic function to handle test retries retryTest (testName, retryCount, addRetryStringToTestName, retryType, event) { // Retrying snapshots has proven to be problematic, so we'll skip them for now // We'll still detect new tests, but we won't retry them. // TODO: do not bail out of retrying tests for the whole test suite if (this.getHasSnapshotTests()) { log.warn('%s is disabled for suites with snapshots', retryType) return } for (let retryIndex = 0; retryIndex < retryCount; retryIndex++) { if (this.global.test) { this.global.test(addRetryStringToTestName(testName, retryIndex), event.fn, event.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 = getJestTestName(state.currentDescribeBlock) const fullTestName = describeSuffix ? `${describeSuffix} ${event.testName}` : event.testName return removeAttemptToFixStringFromTestName(removeEfdStringFromTestName(fullTestName)) } 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') { let isNewTest = false let numEfdRetry = null let numOfAttemptsToFixRetries = null const testParameters = getTestParametersString(this.nameToParams, event.test.name) // Async resource for this test is created here // It is used later on by the test_done handler const testName = getJestTestName(event.test) const originalTestName = removeEfdStringFromTestName(removeAttemptToFixStringFromTestName(testName)) let isAttemptToFix = false let isDisabled = false let isQuarantined = false if (this.isTestManagementTestsEnabled) { isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(originalTestName) isDisabled = this.testManagementTestsForThisSuite?.disabled?.includes(originalTestName) isQuarantined = this.testManagementTestsForThisSuite?.quarantined?.includes(originalTestName) if (isAttemptToFix) { numOfAttemptsToFixRetries = retriedTestsToNumAttempts.get(originalTestName) retriedTestsToNumAttempts.set(originalTestName, 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.modifiedTestsForThisSuite, 'jest' ) } if (this.isKnownTestsEnabled) { isNewTest = retriedTestsToNumAttempts.has(originalTestName) } if (this.isEarlyFlakeDetectionEnabled && (isNewTest || isModified)) { numEfdRetry = retriedTestsToNumAttempts.get(originalTestName) retriedTestsToNumAttempts.set(originalTestName, numEfdRetry + 1) } const isJestRetry = event.test?.invocations > 1 const ctx = { name: originalTestName, 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 } testContexts.set(event.test, ctx) testStartCh.runStores(ctx, () => { for (const hook of event.test.parent.hooks) { let hookFn = hook.fn if (originalHookFns.has(hook)) { hookFn = originalHookFns.get(hook) } else { originalHookFns.set(hook, hookFn) } // The rule has a bug, see https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2164 // eslint-disable-next-line unicorn/consistent-function-scoping const wrapperHook = function () { return testFnCh.runStores(ctx, () => hookFn.apply(this, arguments)) } // If we don't do this, the timeout will not be triggered Object.defineProperty(wrapperHook, 'length', { value: hookFn.length }) hook.fn = wrapperHook } const originalFn = event.test.fn originalTestFns.set(event.test, originalFn) const wrapper = function () { return testFnCh.runStores(ctx, () => originalFn.apply(this, arguments)) } // If we don't do this, the timeout will be not be triggered Object.defineProperty(wrapper, 'length', { value: originalFn.length }) event.test.fn = wrapper }) } if (event.name === 'add_test') { const originalTestName = this.getTestNameFromAddTestEvent(event, state) if (event.failing) { return } const isSkipped = event.mode === 'todo' || event.mode === 'skip' if (this.isTestManagementTestsEnabled) { const isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(originalTestName) if (isAttemptToFix && !isSkipped && !retriedTestsToNumAttempts.has(originalTestName)) { retriedTestsToNumAttempts.set(originalTestName, 0) this.retryTest( event.testName, testManagementAttemptToFixRetries, addAttemptToFixStringToTestName, 'Test Management (Attempt to Fix)', event ) } } if (this.isImpactedTestsEnabled) { const testStartLine = getTestLineStart(event.asyncError, this.testSuite) const testEndLine = getTestEndLine(event.fn, testStartLine) const isModified = isModifiedTest( this.testSourceFile, testStartLine, testEndLine, this.modifiedTestsForThisSuite, 'jest' ) if (isModified && !retriedTestsToNumAttempts.has(originalTestName) && this.isEarlyFlakeDetectionEnabled) { retriedTestsToNumAttempts.set(originalTestName, 0) this.retryTest( event.testName, earlyFlakeDetectionNumRetries, addEfdStringToTestName, 'Early flake detection', event ) } } if (this.isKnownTestsEnabled) { const isNew = !this.knownTestsForThisSuite?.includes(originalTestName) if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(originalTestName)) { retriedTestsToNumAttempts.set(originalTestName, 0) if (this.isEarlyFlakeDetectionEnabled) { this.retryTest( event.testName, earlyFlakeDetectionNumRetries, addEfdStringToTestName, 'Early flake detection', event ) } } } } if (event.name === 'test_done') { 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) let attemptToFixPassed = false let attemptToFixFailed = false let failedAllTests = false let isAttemptToFix = false if (this.isTestManagementTestsEnabled) { const testName = getJestTestName(event.test) const originalTestName = removeAttemptToFixStringFromTestName(testName) isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(originalTestName) if (isAttemptToFix) { if (attemptToFixRetriedTestsStatuses.has(originalTestName)) { attemptToFixRetriedTestsStatuses.get(originalTestName).push(status) } else { attemptToFixRetriedTestsStatuses.set(originalTestName, [status]) } const testStatuses = attemptToFixRetriedTestsStatuses.get(originalTestName) // 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 } } } } let isEfdRetry = false // We'll store the test statuses of the retries if (this.isKnownTestsEnabled) { const testName = getJestTestName(event.test) const originalTestName = removeEfdStringFromTestName(testName) const isNewTest = retriedTestsToNumAttempts.has(originalTestName) if (isNewTest) { if (newTestsTestStatuses.has(originalTestName)) { newTestsTestStatuses.get(originalTestName).push(status) isEfdRetry = true } else { newTestsTestStatuses.set(originalTestName, [status]) } } } const promises = {} const numRetries = this.global[RETRY_TIMES] const numTestExecutions = event.test?.invocations const willBeRetried = numRetries > 0 && numTestExecutions - 1 < numRetries const mightHitBreakpoint = this.isDiEnabled && numTestExecutions >= 2 const ctx = testContexts.get(event.test) if (status === 'fail') { const shouldSetProbe = this.isDiEnabled && willBeRetried && numTestExecutions === 1 testErrCh.publish({ ...ctx.currentStore, error: formatJestError(event.test.errors[0]), 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 => { setTimeout(() => { 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 }) if (promises.isProbeReady) { await promises.isProbeReady } } if (event.name === 'test_skip' || event.name === 'test_todo') { testSkippedCh.publish({ test: { name: getJestTestName(event.test), suite: this.testSuite, testSourceFile: this.testSourceFile, displayName: this.displayName, frameworkVersion: jestVersion, testStartLine: getTestLineStart(event.test.asyncError, this.testSuite) }, isDisabled: this.testManagementTestsForThisSuite?.disabled?.includes(getJestTestName(event.test)) }) } } 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) pkg.default = wrappedTestEnvironment pkg.TestEnvironment = wrappedTestEnvironment return pkg } return getWrappedEnvironment(pkg, jestVersion) } function applySuiteSkipping (originalTests, rootDir, frameworkVersion) { const jestSuitesToRun = getJestSuitesToRun(skippableSuites, originalTests, rootDir || process.cwd()) hasFilteredSkippableSuites = true log.debug( () => `${jestSuitesToRun.suitesToRun.length} out of ${originalTests.length} suites are going to run.` ) 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) 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)) const isFaulty = 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 isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries 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) { testManagementTests = receivedTestManagementTests } } catch (err) { log.error('Jest test management tests error', err) } } if (isImpactedTestsEnabled) { const impactedTestsPromise = new Promise((resolve) => { onDone = resolve }) impactedTestsCh.publish({ onDone }) try { const { err, modifiedTests: receivedModifiedTests } = await impactedTestsPromise if (!err) { modifiedTests = receivedModifiedTests } } 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: { success, coverageMap, numFailedTestSuites, numFailedTests, numTotalTests, numTotalTestSuites } } = result let testCodeCoverageLinesTotal if (isUserCodeCoverageEnabled) { try { const { pct, total } = coverageMap.getCoverageSummary().lines testCodeCoverageLinesTotal = total === 0 ? 0 : pct } catch { // ignore errors } } let status, error if (success) { status = numTotalTests === 0 && numTotalTestSuites === 0 ? 'skip' : 'pass' } else { status = 'fail' error = new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`) } let timeoutId // Pass the resolve callback to defer it to DC listener const flushPromise = new Promise((resolve) => { onDone = () => { clearTimeout(timeoutId) resolve() } }) const timeoutPromise = new Promise((resolve) => { timeoutId = setTimeout(() => { resolve('timeout') }, FLUSH_TIMEOUT).unref() }) testSessionFinishCh.publish({ status, isSuitesSkipped, isSuitesSkippingEnabled, isCodeCoverageEnabled, testCodeCoverageLinesTotal, numSkippedSuites, hasUnskippableSuites, hasForcedToRunSuites, error, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, isTestManagementTestsEnabled, onDone }) const waitingResult = await Promise.race([flushPromise, timeoutPromise]) if (waitingResult === 'timeout') { log.error('Timeout waiting for the tracer to flush') } numSkippedSuites = 0 /** * 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 test will be considered flaky), but you may choose to unblock the pipeline too. */ if (isEarlyFlakeDetectionEnabled) { let numFailedTestsToIgnore = 0 for (const testStatuses of newTestsTestStatuses.values()) { const { pass, fail } = getTestStats(testStatuses) if (pass > 0) { // as long as one passes, we'll consider the test passed numFailedTestsToIgnore += fail } } // If every test that failed was an EFD retry, we'll consider the suite passed if (numFailedTestsToIgnore !== 0 && result.results.numFailedTests === numFailedTestsToIgnore) { result.results.success = true } } if (isTestManagementTestsEnabled) { const failedTests = result .results .testResults.flatMap(({ testResults, testFilePath: testSuiteAbsolutePath }) => ( testResults.map(({ fullName: testName, status }) => ( { testName, testSuiteAbsolutePath, status } )) )) .filter(({ status }) => status === 'failed') let numFailedQuarantinedTests = 0 let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0 for (const { testName, testSuiteAbsolutePath } of failedTests) { const testSuite = getTestSuitePath(testSuiteAbsolutePath, result.globalConfig.rootDir) const originalName = removeAttemptToFixStringFromTestName(testName) const testManagementTest = testManagementTests ?.jest ?.suites ?.[testSuite] ?.tests ?.[originalName] ?.properties // This uses `attempt_to_fix` because this is always the main process and it's not formatted in camelCase if (testManagementTest?.attempt_to_fix && (testManagementTest?.quarantined || testManagementTest?.disabled)) { numFailedQuarantinedOrDisabledAttemptedToFixTests++ } else if (testManagementTest?.quarantined) { numFailedQuarantinedTests++ } } // If every test that failed was quarantined, we'll consider the suite passed // Note that if a test is attempted to fix, // it's considered quarantined both if it's disabled and if it's quarantined // (it'll run but its status is ignored) if ( (numFailedQuarantinedOrDisabledAttemptedToFixTests !== 0 || numFailedQuarantinedTests !== 0) && result.results.numFailedTests === numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests ) { result.results.success = true } } return result }, { replaceGetter: true }) } } function coverageReporterWrapper (coverageReporter) { const CoverageReporter = coverageReporter.default ?? coverageReporter /** * If ITR is active, we're running fewer tests, so of course the total code coverage is reduced. * This calculation adds no value, so we'll skip it, as long as the user has not manually opted in to code coverage, * in which case we'll leave it. */ // `_addUntestedFiles` is an async function shimmer.wrap(CoverageReporter.prototype, '_addUntestedFiles', addUntestedFiles => function () { // If the user has added coverage manually, they're willing to pay the price of this execution, so // we will not skip it. if (isSuitesSkippingEnabled && !isUserCodeCoverageEnabled) { return Promise.resolve() } return addUntestedFiles.apply(this, arguments) }) return coverageReporter } addHook({ name: '@jest/core', file: 'build/TestScheduler.js', versions: ['>=27.0.0'] }, (testSchedulerPackage, frameworkVersion) => { const oldCreateTestScheduler = testSchedulerPackage.createTestScheduler const newCreateTestScheduler = async function () { if (!isSuitesSkippingEnabled || hasFilteredSkippableSuites) { return oldCreateTestScheduler.apply(this, arguments) } // If suite skipping is enabled and has not filtered skippable suites yet, we'll attempt to do it const scheduler = await oldCreateTestScheduler.apply(this, arguments) shimmer.wrap(scheduler, 'scheduleTests', scheduleTests => getWrappedScheduleTests(scheduleTests, frameworkVersion)) return scheduler } testSchedulerPackage.createTestScheduler = newCreateTestScheduler return testSchedulerPackage }) addHook({ name: '@jest/core', file: 'build/TestScheduler.js', versions: ['>=24.8.0 <27.0.0'] }, (testSchedulerPackage, frameworkVersion) => { shimmer.wrap( testSchedulerPackage.default.prototype, 'scheduleTests', scheduleTests => getWrappedScheduleTests(scheduleTests, frameworkVersion) ) return testSchedulerPackage }) addHook({ name: '@jest/test-sequencer', versions: ['>=28'] }, (sequencerPackage, frameworkVersion) => { shimmer.wrap(sequencerPackage.default.prototype, 'shard', shard => function () { const shardedTests = shard.apply(this, arguments) if (!shardedTests.length || !isSuitesSkippingEnabled || !skippableSuites.length) { return shardedTests } const [test] = shardedTests const rootDir = test?.context?.config?.rootDir return applySuiteSkipping(shardedTests, rootDir, frameworkVersion) }) return sequencerPackage }) addHook({ name: '@jest/reporters', file: 'build/coverage_reporter.js', versions: ['>=24.8.0 <26.6.2'] }, coverageReporterWrapper) addHook({ name: '@jest/reporters', file: 'build/CoverageReporter.js', versions: ['>=26.6.2'] }, coverageReporterWrapper) addHook({ name: '@jest/reporters', versions: ['>=30.0.0'] }, (reporters) => { return shimmer.wrap(reporters, 'CoverageReporter', coverageReporterWrapper, { replaceGetter: true }) }) addHook({ name: '@jest/core', file: 'build/cli/index.js', versions: ['>=24.8.0 <30.0.0'] }, getCliWrapper(false)) addHook({ name: '@jest/core', versions: ['>=30.0.0'] }, getCliWrapper(true)) function jestAdapterWrapper (jestAdapter, jestVersion) { const adapter = jestAdapter.default ?? jestAdapter const newAdapter = shimmer.wrapFunction(adapter, adapter => function () { const environment = arguments[2] if (!environment || !environment.testEnvironmentOptions) { return adapter.apply(this, arguments) } testSuiteStartCh.publish({ testSuite: environment.testSuite, testEnvironmentOptions: environment.testEnvironmentOptions, testSourceFile: environment.testSourceFile, displayName: environment.displayName, frameworkVersion: jestVersion }) return adapter.apply(this, arguments).then(suiteResults => { const { numFailingTests, skipped, failureMessage: errorMessage } = suiteResults let status = 'pass' if (skipped) { status = 'skipped' } else if (numFailingTests !== 0) { status = 'fail' } /** * Child processes do not each request ITR configuration, so the jest's parent process * needs to pass them the configuration. This is done via _ddTestCodeCoverageEnabled, which * controls whether coverage is reported. */ if (environment.testEnvironmentOptions?._ddTestCodeCoverageEnabled) { const root = environment.repositoryRoot || environment.rootDir const getFilesWithPath = (files) => files.map(file => getTestSuitePath(file, root)) const coverageFiles = getFilesWithPath(getCoveredFilenamesFromCoverage(environment.global.__coverage__)) const mockedFiles = getFilesWithPath(testSuiteMockedFiles.get(environment.testSuiteAbsolutePath) || []) testSuiteCodeCoverageCh.publish({ coverageFiles, testSuite: environment.testSourceFile, mockedFiles }) } testSuiteFinishCh.publish({ status, errorMessage }) return suiteResults }).catch(error => { testSuiteFinishCh.publish({ status: 'fail', error }) throw error }) }) if (jestAdapter.default) { jestAdapter.default = newAdapter } else { jestAdapter = newAdapter } return jestAdapter } addHook({ name: 'jest-circus', file: 'build/runner.js', versions: ['>=30.0.0'] }, jestAdapterWrapper) addHook({ name: 'jest-circus', file: 'build/legacy-code-todo-rewrite/jestAdapter.js', versions: ['>=24.8.0'] }, jestAdapterWrapper) function configureTestEnvironment (readConfigsResult) { const { configs } = readConfigsResult testSessionConfigurationCh.publish(configs.map(config => config.testEnvironmentOptions)) // We can't directly use isCodeCoverageEnabled when reporting coverage in `jestAdapterWrapper` // because `jestAdapterWrapper` runs in a different process. We have to go through `testEnvironmentOptions` configs.forEach(config => { config.testEnvironmentOptions._ddTestCodeCoverageEnabled = isCodeCoverageEnabled }) isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage if (readConfigsResult.globalConfig.forceExit) { log.warn("Jest's '--forceExit' flag has been passed. This may cause loss of data.") } if (isCodeCoverageEnabled) { const globalConfig = { ...readConfigsResult.globalConfig, collectCoverage: true } readConfigsResult.globalConfig = globalConfig } if (isSuitesSkippingEnabled) { // If suite skipping is enabled, the code coverage results are not going to be relevant, // so we do not show them. // Also, we might skip every test, so we need to pass `passWithNoTests` const globalConfig = { ...readConfigsResult.globalConfig, coverageReporters: ['none'], passWithNoTests: true } readConfigsResult.globalConfig = globalConfig } return readConfigsResult } function jestConfigAsyncWrapper (jestConfig) { return shimmer.wrap(jestConfig, 'readConfigs', readConfigs => async function () { const readConfigsResult = await readConfigs.apply(this, arguments) configureTestEnvironment(readConfigsResult) return readConfigsResult }) } function jestConfigSyncWrapper (jestConfig) { return shimmer.wrap(jestConfig, 'readConfigs', readConfigs => function () { const readConfigsResult = readConfigs.apply(this, arguments) configureTestEnvironment(readConfigsResult) return readConfigsResult }) } addHook({ name: '@jest/transform', versions: ['>=24.8.0'], file: 'build/ScriptTransformer.js' }, transformPackage => { const originalCreateScriptTransformer = transformPackage.createScriptTransformer // `createScriptTransformer` is an async function transformPackage.createScriptTransformer = function (config) { const { testEnvironmentOptions, ...restOfConfig } = config const { _ddTestModuleId, _ddTestSessionId, _ddTestCommand, _ddTestSessionName, _ddForcedToRun, _ddUnskippable, _ddItrCorrelationId, _ddKnownTests, _ddIsEarlyFlakeDetectionEnabled, _ddEarlyFlakeDetectionNumRetries, _ddRepositoryRoot, _ddIsFlakyTestRetriesEnabled, _ddFlakyTestRetriesCount, _ddIsDiEnabled, _ddIsKnownTestsEnabled, _ddIsTestManagementTestsEnabled, _ddTestManagementTests, _ddTestManagementAttemptToFixRetries, _ddModifiedTests, ...restOfTestEnvironmentOptions } = testEnvironmentOptions restOfConfig.testEnvironmentOptions = restOfTestEnvironmentOptions arguments[0] = restOfConfig return originalCreateScriptTransformer.apply(this, arguments) } return transformPackage }) /** * Hook to remove the test paths (test suite) that are part of `skippableSuites` */ addHook({ name: '@jest/core', versions: ['>=24.8.0 <30.0.0'], file: 'build/SearchSource.js' }, searchSourceWrapper) // from 25.1.0 on, readConfigs becomes async addHook({ name: 'jest-config', versions: ['>=25.1.0'] }, jestConfigAsyncWrapper) addHook({ name: 'jest-config', versions: ['24.8.0 - 24.9.0'] }, jestConfigSyncWrapper) const LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE = new Set([ 'selenium-webdriver', 'selenium-webdriver/chrome', 'selenium-webdriver/edge', 'selenium-webdriver/safari', 'selenium-webdriver/firefox', 'selenium-webdriver/ie', 'selenium-webdriver/chromium', 'winston' ]) function shouldBypassJestRequireEngine (moduleName) { return ( LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.has(moduleName) ) } addHook({ name: 'jest-runtime', versions: ['>=24.8.0'] }, (runtimePackage) => { const Runtime = runtimePackage.default ?? runtimePackage shimmer.wrap(Runtime.prototype, '_createJestObjectFor', _createJestObjectFor => function (from) { const result = _createJestObjectFor.apply(this, arguments) const suiteFilePath = this._testPath shimmer.wrap(result, 'mock', mock => function (moduleName) { if (suiteFilePath) { const existingMockedFiles = testSuiteMockedFiles.get(suiteFilePath) || [] const suiteDir = path.dirname(suiteFilePath) const mockPath = path.resolve(suiteDir, moduleName) existingMockedFiles.push(mockPath) testSuiteMockedFiles.set(suiteFilePath, existingMockedFiles) } return mock.apply(this, arguments) }) return result }) shimmer.wrap(Runtime.prototype, 'requireModuleOrMock', requireModuleOrMock => function (from, moduleName) { // TODO: do this for every library that we instrument if (shouldBypassJestRequireEngine(moduleName)) { // To bypass jest's own require engine return this._requireCoreModule(moduleName) } return requireModuleOrMock.apply(this, arguments) }) return runtimePackage }) function onMessageWrapper (onMessage) { return function () { const [code, data] = arguments[0] if (code === JEST_WORKER_TRACE_PAYLOAD_CODE) { // datadog trace payload workerReportTraceCh.publish(data) return } if (code === JEST_WORKER_COVERAGE_PAYLOAD_CODE) { // datadog coverage payload workerReportCoverageCh.publish(data) return } if (code === JEST_WORKER_LOGS_PAYLOAD_CODE) { // datadog logs payload workerReportLogsCh.publish(data) return } return onMessage.apply(this, arguments) } } function sendWrapper (send) { return function (request) { if (!isKnownTestsEnabled && !isTestManagementTestsEnabled && !isImpactedTestsEnabled) { return send.apply(this, arguments) } const [type] = request // https://github.com/jestjs/jest/blob/1d682f21c7a35da4d3ab3a1436a357b980ebd0fa/packages/jest-worker/src/workers/ChildProcessWorker.ts#L424 if (type === CHILD_MESSAGE_CALL) { // This is the message that the main process sends to the worker to run a test suite (=test file). // In here we modify the config.testEnvironmentOptions to include the known tests for the suite. // This way the suite only knows about the tests that are part of it. const args = request.at(-1) if (args.length > 1) { return send.apply(this, arguments) } if (!args[0]?.config) { return send.apply(this, arguments) } const [{ globalConfig, config, path: testSuiteAbsolutePath }] = args const testSuite = getTestSuitePath(testSuiteAbsolutePath, globalConfig.rootDir || process.cwd()) const suiteKnownTests = knownTests?.jest?.[testSuite] || [] const suiteTestManagementTests = testManagementTests?.jest?.suites?.[testSuite]?.tests || {} const suiteModifiedTests = Object.keys(modifiedTests).length > 0 ? modifiedTests : {} args[0].config = { ...config, testEnvironmentOptions: { ...config.testEnvironmentOptions, _ddKnownTests: suiteKnownTests, _ddTestManagementTests: suiteTestManagementTests, _ddModifiedTests: suiteModifiedTests } } } return send.apply(this, arguments) } } function enqueueWrapper (enqueue) { return function () { shimmer.wrap(arguments[0], 'onStart', onStart => function (worker) { if (worker && !wrappedWorkers.has(worker)) { shimmer.wrap(worker._child, 'send', sendWrapper) shimmer.wrap(worker, '_onMessag