dd-trace 6.0.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-3rdparty.csv +1 -2
- package/index.d.ts +28 -1
- package/initialize.mjs +4 -2
- package/package.json +34 -30
- package/packages/datadog-instrumentations/src/ai.js +45 -0
- package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
- package/packages/datadog-instrumentations/src/child_process.js +3 -3
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
- package/packages/datadog-instrumentations/src/cucumber.js +102 -43
- package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
- package/packages/datadog-instrumentations/src/fastify.js +27 -8
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +5 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +19 -5
- package/packages/datadog-instrumentations/src/jest.js +713 -63
- package/packages/datadog-instrumentations/src/mercurius.js +11 -0
- package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
- package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
- package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
- package/packages/datadog-instrumentations/src/mongodb.js +74 -0
- package/packages/datadog-instrumentations/src/mongoose.js +4 -5
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +15 -0
- package/packages/datadog-instrumentations/src/playwright.js +13 -9
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +26 -1
- package/packages/datadog-instrumentations/src/vitest-util.js +2 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +103 -34
- package/packages/datadog-plugin-ai/src/index.js +1 -1
- package/packages/datadog-plugin-ai/src/tracing.js +66 -3
- package/packages/datadog-plugin-ai/src/utils.js +17 -4
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
- package/packages/datadog-plugin-child_process/src/index.js +13 -2
- package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
- package/packages/datadog-plugin-cucumber/src/index.js +14 -23
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
- package/packages/datadog-plugin-cypress/src/index.js +47 -2
- package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
- package/packages/datadog-plugin-graphql/src/execute.js +25 -25
- package/packages/datadog-plugin-graphql/src/index.js +23 -1
- package/packages/datadog-plugin-graphql/src/request.js +104 -0
- package/packages/datadog-plugin-graphql/src/utils.js +186 -2
- package/packages/datadog-plugin-graphql/src/validate.js +25 -2
- package/packages/datadog-plugin-jest/src/index.js +30 -14
- package/packages/datadog-plugin-mocha/src/index.js +39 -14
- package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
- package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
- package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
- package/packages/datadog-plugin-playwright/src/index.js +3 -2
- package/packages/datadog-plugin-vitest/src/index.js +43 -9
- package/packages/dd-trace/src/aiguard/index.js +9 -14
- package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
- package/packages/dd-trace/src/appsec/graphql.js +9 -6
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
- package/packages/dd-trace/src/appsec/lambda.js +8 -8
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +37 -0
- package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
- package/packages/dd-trace/src/datastreams/writer.js +14 -2
- package/packages/dd-trace/src/encode/0.4.js +23 -2
- package/packages/dd-trace/src/encode/0.5.js +12 -1
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +11 -1
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +20 -2
- package/packages/dd-trace/src/exporters/common/writer.js +17 -1
- package/packages/dd-trace/src/guardrails/index.js +3 -1
- package/packages/dd-trace/src/guardrails/telemetry.js +40 -3
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
- package/packages/dd-trace/src/msgpack/chunk.js +33 -1
- package/packages/dd-trace/src/msgpack/index.js +6 -1
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugin_manager.js +16 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
- package/packages/dd-trace/src/plugins/index.js +5 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
- package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +6 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +8 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +5 -6
|
@@ -88,6 +88,9 @@ const CHILD_MESSAGE_CALL = 1
|
|
|
88
88
|
const FLUSH_TIMEOUT = 10_000
|
|
89
89
|
const JEST_SESSION_STATE = Symbol.for('dd-trace:jest:session')
|
|
90
90
|
const JEST_BAIL_REPORTER_PATH = require.resolve('./jest/bail-reporter')
|
|
91
|
+
const DD_JEST_HANDLE_TEST_EVENT_WRAPPED = Symbol('dd-trace:jest:handle-test-event-wrapped')
|
|
92
|
+
const DD_JEST_HANDLE_TEST_EVENT_DATADOG = Symbol('dd-trace:jest:handle-test-event-datadog')
|
|
93
|
+
const DD_JEST_CONCURRENT_TEST_ORIGINAL = Symbol('dd-trace:jest:concurrent-test-original')
|
|
91
94
|
const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID')
|
|
92
95
|
const jestSessionState = globalThis[JEST_SESSION_STATE] || (globalThis[JEST_SESSION_STATE] = {})
|
|
93
96
|
|
|
@@ -132,6 +135,8 @@ let isConsoleErrorWrapped = false
|
|
|
132
135
|
const testContexts = new WeakMap()
|
|
133
136
|
const originalTestFns = new WeakMap()
|
|
134
137
|
const originalHookFns = new WeakMap()
|
|
138
|
+
const concurrentHookContextQueues = new WeakMap()
|
|
139
|
+
const concurrentHookFns = new WeakMap()
|
|
135
140
|
const retriedTestsToNumAttempts = new Map()
|
|
136
141
|
const newTestsTestStatuses = new Map()
|
|
137
142
|
const attemptToFixRetriedTestsStatuses = new Map()
|
|
@@ -151,14 +156,28 @@ const efdNewTestCandidates = new Set()
|
|
|
151
156
|
// Tests that are genuinely new (not in known tests list).
|
|
152
157
|
const newTests = new Set()
|
|
153
158
|
const testSuiteJestObjects = new Map()
|
|
159
|
+
const testSuiteDatadogEnvironments = new Map()
|
|
154
160
|
const wrappedJestGlobals = new WeakSet()
|
|
155
161
|
const wrappedJestObjects = new WeakSet()
|
|
156
162
|
const wrappedWorkerInitializers = new WeakSet()
|
|
157
163
|
const publishedRuntimeReferenceErrors = new WeakMap()
|
|
158
164
|
const wrappedCoverageReporters = new WeakSet()
|
|
159
165
|
const coverageReporterRequires = new WeakMap()
|
|
166
|
+
const handledJestEvents = new WeakSet()
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @typedef {object} ConcurrentTestOptions
|
|
170
|
+
* @property {(...args: unknown[]) => unknown} [concurrentTest]
|
|
171
|
+
* @property {unknown} [concurrentTestThisArg]
|
|
172
|
+
* @property {boolean} [isAttemptToFixRetry]
|
|
173
|
+
* @property {boolean} [isEfdRetry]
|
|
174
|
+
* @property {boolean} [isModified]
|
|
175
|
+
* @property {(...args: unknown[]) => unknown} [serialTest]
|
|
176
|
+
* @property {unknown} [serialTestThisArg]
|
|
177
|
+
* @property {(...args: unknown[]) => unknown} [sourceTestFn]
|
|
178
|
+
* @property {string} [testParameters]
|
|
179
|
+
*/
|
|
160
180
|
|
|
161
|
-
const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200
|
|
162
181
|
const ATR_RETRY_SUPPRESSION_FLAG = '_ddDisableAtrRetry'
|
|
163
182
|
const MINIMUM_JEST_VERSION = DD_MAJOR >= 6 ? '>=28.0.0' : '>=24.8.0'
|
|
164
183
|
const MINIMUM_JEST_VERSION_BEFORE_30 = DD_MAJOR >= 6 ? '>=28.0.0 <30.0.0' : '>=24.8.0 <30.0.0'
|
|
@@ -170,6 +189,7 @@ const atrSuppressedErrors = new Map()
|
|
|
170
189
|
let hasWarnedDeprecatedJestVersion = false
|
|
171
190
|
let isJestCoverageBackfillSupported = false
|
|
172
191
|
let hasFinishedTestSession = false
|
|
192
|
+
let jestEachBind
|
|
173
193
|
|
|
174
194
|
// Track quarantined tests whose errors were suppressed, keyed by "suite › testName"
|
|
175
195
|
const quarantinedFailingTests = new Set()
|
|
@@ -364,6 +384,83 @@ function wrapConsoleErrorForJestReferenceErrors () {
|
|
|
364
384
|
}
|
|
365
385
|
}
|
|
366
386
|
|
|
387
|
+
function isDatadogJestEventHandled (event) {
|
|
388
|
+
return event && typeof event === 'object' && handledJestEvents.has(event)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function markDatadogJestEventHandled (event) {
|
|
392
|
+
if (event && typeof event === 'object') {
|
|
393
|
+
handledJestEvents.add(event)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Gets the original Jest concurrent registration function behind a Datadog wrapper.
|
|
399
|
+
*
|
|
400
|
+
* @param {(...args: unknown[]) => unknown} concurrentTest
|
|
401
|
+
* @returns {(...args: unknown[]) => unknown}
|
|
402
|
+
*/
|
|
403
|
+
function getOriginalConcurrentTest (concurrentTest) {
|
|
404
|
+
return concurrentTest[DD_JEST_CONCURRENT_TEST_ORIGINAL] || concurrentTest
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Marks a Datadog concurrent registration wrapper with its original Jest function.
|
|
409
|
+
*
|
|
410
|
+
* @param {(...args: unknown[]) => unknown} wrappedConcurrentTest
|
|
411
|
+
* @param {(...args: unknown[]) => unknown} originalConcurrentTest
|
|
412
|
+
* @returns {void}
|
|
413
|
+
*/
|
|
414
|
+
function setOriginalConcurrentTest (wrappedConcurrentTest, originalConcurrentTest) {
|
|
415
|
+
Object.defineProperty(wrappedConcurrentTest, DD_JEST_CONCURRENT_TEST_ORIGINAL, {
|
|
416
|
+
configurable: true,
|
|
417
|
+
value: originalConcurrentTest,
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Wraps a custom Jest environment handler so Datadog still observes events even
|
|
423
|
+
* when the custom environment does not call `super.handleTestEvent`.
|
|
424
|
+
*
|
|
425
|
+
* @param {(event: object, state: object) => Promise<void>|void} handleTestEvent
|
|
426
|
+
* @param {(event: object, state: object) => Promise<void>|void} datadogHandleTestEvent
|
|
427
|
+
* @returns {(event: object, state: object) => Promise<void>|void}
|
|
428
|
+
*/
|
|
429
|
+
function getWrappedCustomHandleTestEvent (handleTestEvent, datadogHandleTestEvent) {
|
|
430
|
+
if (
|
|
431
|
+
!handleTestEvent ||
|
|
432
|
+
handleTestEvent === datadogHandleTestEvent ||
|
|
433
|
+
handleTestEvent[DD_JEST_HANDLE_TEST_EVENT_WRAPPED]
|
|
434
|
+
) {
|
|
435
|
+
return handleTestEvent || datadogHandleTestEvent
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const wrappedHandleTestEvent = function (event, state) {
|
|
439
|
+
const result = handleTestEvent.call(this, event, state)
|
|
440
|
+
const runDatadogHandler = value => {
|
|
441
|
+
if (isDatadogJestEventHandled(event)) return value
|
|
442
|
+
|
|
443
|
+
const datadogResult = datadogHandleTestEvent.call(this, event, state)
|
|
444
|
+
if (typeof datadogResult?.then === 'function') {
|
|
445
|
+
return datadogResult.then(() => value)
|
|
446
|
+
}
|
|
447
|
+
return value
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (event.name === 'add_test') {
|
|
451
|
+
runDatadogHandler(result)
|
|
452
|
+
return result
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (typeof result?.then === 'function') {
|
|
456
|
+
return result.then(runDatadogHandler)
|
|
457
|
+
}
|
|
458
|
+
return runDatadogHandler(result)
|
|
459
|
+
}
|
|
460
|
+
wrappedHandleTestEvent[DD_JEST_HANDLE_TEST_EVENT_WRAPPED] = true
|
|
461
|
+
return wrappedHandleTestEvent
|
|
462
|
+
}
|
|
463
|
+
|
|
367
464
|
function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
368
465
|
return class DatadogEnvironment extends BaseEnvironment {
|
|
369
466
|
constructor (config, context) {
|
|
@@ -375,6 +472,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
375
472
|
this.hasSnapshotTests = undefined
|
|
376
473
|
this.testSuiteAbsolutePath = context.testPath
|
|
377
474
|
activeTestSuiteAbsolutePath = this.testSuiteAbsolutePath
|
|
475
|
+
testSuiteDatadogEnvironments.set(this.testSuiteAbsolutePath, this)
|
|
378
476
|
wrapConsoleErrorForJestReferenceErrors()
|
|
379
477
|
this.globalConfig = config.globalConfig
|
|
380
478
|
|
|
@@ -398,6 +496,12 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
398
496
|
this.isKnownTestsEnabled = this.testEnvironmentOptions._ddIsKnownTestsEnabled
|
|
399
497
|
this.isTestManagementTestsEnabled = this.testEnvironmentOptions._ddIsTestManagementTestsEnabled
|
|
400
498
|
this.isImpactedTestsEnabled = this.testEnvironmentOptions._ddIsImpactedTestsEnabled
|
|
499
|
+
this.hasConcurrentTests = false
|
|
500
|
+
this.concurrentTestContexts = new Map()
|
|
501
|
+
this.concurrentTestStates = new WeakMap()
|
|
502
|
+
this.concurrentTestSourceFns = new WeakMap()
|
|
503
|
+
this.wrappedConcurrentTestFunctions = new WeakSet()
|
|
504
|
+
this.jestEachBind = undefined
|
|
401
505
|
|
|
402
506
|
if (this.isKnownTestsEnabled) {
|
|
403
507
|
earlyFlakeDetectionSlowTestRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionSlowTestRetries ?? {}
|
|
@@ -445,6 +549,390 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
445
549
|
this.isImpactedTestsEnabled = false
|
|
446
550
|
}
|
|
447
551
|
}
|
|
552
|
+
|
|
553
|
+
this[DD_JEST_HANDLE_TEST_EVENT_DATADOG] = DatadogEnvironment.prototype.handleTestEvent
|
|
554
|
+
this.wrapCustomHandleTestEvent(DatadogEnvironment.prototype.handleTestEvent)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Rechecks custom `handleTestEvent` implementations after subclass instance fields
|
|
559
|
+
* and constructors have run.
|
|
560
|
+
*
|
|
561
|
+
* @returns {Promise<void>|void}
|
|
562
|
+
*/
|
|
563
|
+
setup () {
|
|
564
|
+
this.wrapCustomHandleTestEvent(DatadogEnvironment.prototype.handleTestEvent)
|
|
565
|
+
|
|
566
|
+
if (super.setup) {
|
|
567
|
+
const result = super.setup()
|
|
568
|
+
if (typeof result?.then === 'function') {
|
|
569
|
+
return result.then(() => {
|
|
570
|
+
this.wrapCustomHandleTestEvent(DatadogEnvironment.prototype.handleTestEvent)
|
|
571
|
+
})
|
|
572
|
+
}
|
|
573
|
+
this.wrapCustomHandleTestEvent(DatadogEnvironment.prototype.handleTestEvent)
|
|
574
|
+
return result
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Wraps Jest's concurrent test registration methods so eager concurrent bodies
|
|
580
|
+
* in older Jest versions execute inside their test span context.
|
|
581
|
+
*
|
|
582
|
+
* @param {object} state
|
|
583
|
+
* @returns {void}
|
|
584
|
+
*/
|
|
585
|
+
wrapConcurrentTest (state) {
|
|
586
|
+
this.concurrentTestState = state
|
|
587
|
+
this.wrapConcurrentTestGlobals(this.global.test, state)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Wraps one Jest `test` function object's concurrent registration methods.
|
|
592
|
+
*
|
|
593
|
+
* @param {Function|undefined} test
|
|
594
|
+
* @param {object} state
|
|
595
|
+
* @returns {void}
|
|
596
|
+
*/
|
|
597
|
+
wrapConcurrentTestGlobals (test, state) {
|
|
598
|
+
if (!state) return
|
|
599
|
+
|
|
600
|
+
const concurrentTest = test?.concurrent
|
|
601
|
+
if (typeof concurrentTest !== 'function') return
|
|
602
|
+
|
|
603
|
+
this.wrapConcurrentTestFunction(test, 'concurrent', state, test, test)
|
|
604
|
+
this.wrapConcurrentTestFunction(test.concurrent, 'only', state, test.only, test)
|
|
605
|
+
this.wrapConcurrentTestFunction(test.concurrent, 'failing', state, test.failing, test)
|
|
606
|
+
this.wrapConcurrentTestFunction(test.concurrent.only, 'failing', state, test.only?.failing, test.only)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Wraps one concurrent test function variant.
|
|
611
|
+
*
|
|
612
|
+
* @param {object} target
|
|
613
|
+
* @param {string} methodName
|
|
614
|
+
* @param {object} state
|
|
615
|
+
* @param {Function|undefined} serialTest
|
|
616
|
+
* @param {unknown} serialTestThisArg
|
|
617
|
+
* @returns {void}
|
|
618
|
+
*/
|
|
619
|
+
wrapConcurrentTestFunction (target, methodName, state, serialTest, serialTestThisArg) {
|
|
620
|
+
let concurrentTest = target?.[methodName]
|
|
621
|
+
if (typeof concurrentTest !== 'function') return
|
|
622
|
+
if (this.wrappedConcurrentTestFunctions.has(concurrentTest)) return
|
|
623
|
+
|
|
624
|
+
const originalConcurrentTest = getOriginalConcurrentTest(concurrentTest)
|
|
625
|
+
if (originalConcurrentTest !== concurrentTest) {
|
|
626
|
+
target[methodName] = originalConcurrentTest
|
|
627
|
+
concurrentTest = originalConcurrentTest
|
|
628
|
+
}
|
|
629
|
+
if (typeof concurrentTest !== 'function') return
|
|
630
|
+
|
|
631
|
+
const environment = this
|
|
632
|
+
shimmer.wrap(target, methodName, concurrentTest => {
|
|
633
|
+
return function wrappedConcurrentTest (testName, testFn, ...args) {
|
|
634
|
+
if (typeof testFn !== 'function') {
|
|
635
|
+
return concurrentTest.apply(this, arguments)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
environment.hasConcurrentTests = true
|
|
639
|
+
const asyncError = environment.isImpactedTestsEnabled
|
|
640
|
+
? new Error('Datadog concurrent test registration')
|
|
641
|
+
: undefined
|
|
642
|
+
const wrappedTestFn = environment.createConcurrentTestFn(testName, testFn, asyncError, state, {
|
|
643
|
+
concurrentTest,
|
|
644
|
+
concurrentTestThisArg: this,
|
|
645
|
+
serialTest,
|
|
646
|
+
serialTestThisArg,
|
|
647
|
+
sourceTestFn: environment.concurrentTestSourceFns.get(testFn),
|
|
648
|
+
})
|
|
649
|
+
return concurrentTest.call(this, testName, wrappedTestFn, ...args)
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
const wrappedConcurrentTest = target[methodName]
|
|
654
|
+
setOriginalConcurrentTest(wrappedConcurrentTest, concurrentTest)
|
|
655
|
+
this.wrappedConcurrentTestFunctions.add(wrappedConcurrentTest)
|
|
656
|
+
this.bindConcurrentEach(wrappedConcurrentTest, methodName === 'failing')
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Rebuilds a concurrent `.each` helper so each generated row calls the wrapped test function.
|
|
661
|
+
*
|
|
662
|
+
* @param {Function} concurrentTest
|
|
663
|
+
* @param {boolean} needsEachError
|
|
664
|
+
* @returns {void}
|
|
665
|
+
*/
|
|
666
|
+
bindConcurrentEach (concurrentTest, needsEachError) {
|
|
667
|
+
if (typeof concurrentTest?.each !== 'function') return
|
|
668
|
+
|
|
669
|
+
const bind = this.getJestEachBind()
|
|
670
|
+
if (typeof bind !== 'function') return
|
|
671
|
+
|
|
672
|
+
const environment = this
|
|
673
|
+
concurrentTest.each = function wrappedConcurrentEach (...eachArgs) {
|
|
674
|
+
const testParameters = getFormattedJestTestParameters(eachArgs)
|
|
675
|
+
return function (...testArgs) {
|
|
676
|
+
let parameterIndex = 0
|
|
677
|
+
const sourceTestFn = testArgs[1]
|
|
678
|
+
const concurrentEachTest = function (testName, testFn, ...callArgs) {
|
|
679
|
+
const parameters = testParameters?.[parameterIndex++]
|
|
680
|
+
if (parameters !== undefined) {
|
|
681
|
+
environment.appendNameToParams(testName, parameters)
|
|
682
|
+
}
|
|
683
|
+
if (typeof testFn === 'function' && typeof sourceTestFn === 'function') {
|
|
684
|
+
environment.concurrentTestSourceFns.set(testFn, sourceTestFn)
|
|
685
|
+
}
|
|
686
|
+
return concurrentTest.call(this, testName, testFn, ...callArgs)
|
|
687
|
+
}
|
|
688
|
+
const eachBind = bind(concurrentEachTest, false, needsEachError).apply(this, eachArgs)
|
|
689
|
+
return eachBind.apply(this, testArgs)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Creates a Jest test function wrapper and stores its concurrent execution state.
|
|
696
|
+
*
|
|
697
|
+
* @param {string} testName
|
|
698
|
+
* @param {(...args: unknown[]) => unknown} testFn
|
|
699
|
+
* @param {Error|undefined} asyncError
|
|
700
|
+
* @param {object} state
|
|
701
|
+
* @param {ConcurrentTestOptions|undefined} options
|
|
702
|
+
* @returns {(...args: unknown[]) => unknown}
|
|
703
|
+
*/
|
|
704
|
+
createConcurrentTestFn (testName, testFn, asyncError, state, options) {
|
|
705
|
+
const concurrentTestState = {
|
|
706
|
+
concurrentTest: options?.concurrentTest,
|
|
707
|
+
concurrentTestThisArg: options?.concurrentTestThisArg,
|
|
708
|
+
ctx: this.createConcurrentTestContext(testName, testFn, asyncError, state, options),
|
|
709
|
+
numExecutions: 0,
|
|
710
|
+
serialTest: options?.serialTest,
|
|
711
|
+
serialTestThisArg: options?.serialTestThisArg,
|
|
712
|
+
state,
|
|
713
|
+
testFn,
|
|
714
|
+
}
|
|
715
|
+
concurrentTestState.ctx.concurrentTestState = concurrentTestState
|
|
716
|
+
const environment = this
|
|
717
|
+
const wrappedTestFn = shimmer.wrapFunction(testFn, testFn => function (...args) {
|
|
718
|
+
return environment.runConcurrentTestFn(concurrentTestState, testFn, this, args)
|
|
719
|
+
})
|
|
720
|
+
this.concurrentTestStates.set(wrappedTestFn, concurrentTestState)
|
|
721
|
+
return wrappedTestFn
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Gets Jest's table-driven test binder from the user's Jest installation.
|
|
726
|
+
*
|
|
727
|
+
* @returns {Function|undefined}
|
|
728
|
+
*/
|
|
729
|
+
getJestEachBind () {
|
|
730
|
+
if (this.jestEachBind !== undefined) return this.jestEachBind
|
|
731
|
+
|
|
732
|
+
if (typeof jestEachBind === 'function') {
|
|
733
|
+
this.jestEachBind = jestEachBind
|
|
734
|
+
return this.jestEachBind
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
let jestEach
|
|
738
|
+
try {
|
|
739
|
+
jestEach = createRequire(path.join(this.rootDir, 'package.json'))('jest-each')
|
|
740
|
+
} catch {
|
|
741
|
+
try {
|
|
742
|
+
jestEach = createRequire(path.join(process.cwd(), 'package.json'))('jest-each')
|
|
743
|
+
} catch {
|
|
744
|
+
jestEach = undefined
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
this.jestEachBind = jestEach?.bind
|
|
748
|
+
return this.jestEachBind
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Creates the context used to start and finish a concurrent test span.
|
|
753
|
+
*
|
|
754
|
+
* @param {string} testName
|
|
755
|
+
* @param {(...args: unknown[]) => unknown} testFn
|
|
756
|
+
* @param {Error|undefined} asyncError
|
|
757
|
+
* @param {object} state
|
|
758
|
+
* @param {ConcurrentTestOptions|undefined} options
|
|
759
|
+
* @returns {object}
|
|
760
|
+
*/
|
|
761
|
+
createConcurrentTestContext (testName, testFn, asyncError, state, options) {
|
|
762
|
+
const testFullName = this.getTestNameFromAddTestEvent({ testName }, state)
|
|
763
|
+
const isNewTest = this.isKnownTestsEnabled && !this.knownTestsForThisSuite?.includes(testFullName)
|
|
764
|
+
const isAttemptToFix = this.isTestManagementTestsEnabled &&
|
|
765
|
+
this.testManagementTestsForThisSuite?.attemptToFix?.includes(testFullName)
|
|
766
|
+
const sourceTestFn = options?.sourceTestFn || testFn
|
|
767
|
+
const ctx = {
|
|
768
|
+
name: testFullName,
|
|
769
|
+
suite: this.testSuite,
|
|
770
|
+
testSourceFile: this.testSourceFile,
|
|
771
|
+
displayName: this.displayName,
|
|
772
|
+
testParameters: options?.testParameters || getTestParametersString(this.nameToParams, testName),
|
|
773
|
+
frameworkVersion: jestVersion,
|
|
774
|
+
isNew: isNewTest,
|
|
775
|
+
isEfdRetry: options?.isEfdRetry === true,
|
|
776
|
+
isAttemptToFix,
|
|
777
|
+
isAttemptToFixRetry: options?.isAttemptToFixRetry === true,
|
|
778
|
+
isJestRetry: false,
|
|
779
|
+
isDisabled: this.testManagementTestsForThisSuite?.disabled?.includes(testFullName),
|
|
780
|
+
isQuarantined: this.testManagementTestsForThisSuite?.quarantined?.includes(testFullName),
|
|
781
|
+
isModified: options?.isModified ?? this.isTestModified(asyncError, sourceTestFn),
|
|
782
|
+
hasDynamicName: isNewTest && DYNAMIC_NAME_RE.test(testFullName),
|
|
783
|
+
testSuiteAbsolutePath: this.testSuiteAbsolutePath,
|
|
784
|
+
}
|
|
785
|
+
let contexts = this.concurrentTestContexts.get(testFullName)
|
|
786
|
+
if (contexts) {
|
|
787
|
+
contexts.push(ctx)
|
|
788
|
+
} else {
|
|
789
|
+
contexts = [ctx]
|
|
790
|
+
this.concurrentTestContexts.set(testFullName, contexts)
|
|
791
|
+
}
|
|
792
|
+
return ctx
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Checks if a test overlaps modified lines for impacted-test detection.
|
|
797
|
+
*
|
|
798
|
+
* @param {Error|undefined} asyncError
|
|
799
|
+
* @param {Function|undefined} testFn
|
|
800
|
+
* @returns {boolean}
|
|
801
|
+
*/
|
|
802
|
+
isTestModified (asyncError, testFn) {
|
|
803
|
+
if (!this.isImpactedTestsEnabled || !asyncError || typeof testFn !== 'function') return false
|
|
804
|
+
|
|
805
|
+
const testStartLine = getTestLineStart(asyncError, this.testSuite)
|
|
806
|
+
const testEndLine = getTestEndLine(testFn, testStartLine)
|
|
807
|
+
return isModifiedTest(
|
|
808
|
+
this.testSourceFile,
|
|
809
|
+
testStartLine,
|
|
810
|
+
testEndLine,
|
|
811
|
+
this.modifiedFiles,
|
|
812
|
+
'jest'
|
|
813
|
+
)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Gets the next registered concurrent test context for a full Jest test name.
|
|
818
|
+
*
|
|
819
|
+
* @param {string} testName
|
|
820
|
+
* @returns {object|undefined}
|
|
821
|
+
*/
|
|
822
|
+
getNextConcurrentTestContext (testName) {
|
|
823
|
+
const contexts = this.concurrentTestContexts.get(testName)
|
|
824
|
+
if (!contexts?.length) return
|
|
825
|
+
|
|
826
|
+
const ctx = contexts.shift()
|
|
827
|
+
if (contexts.length === 0) {
|
|
828
|
+
this.concurrentTestContexts.delete(testName)
|
|
829
|
+
}
|
|
830
|
+
return ctx
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Removes one registered concurrent test context from the name fallback queue.
|
|
835
|
+
*
|
|
836
|
+
* @param {string} testName
|
|
837
|
+
* @param {object} ctx
|
|
838
|
+
* @returns {void}
|
|
839
|
+
*/
|
|
840
|
+
removeConcurrentTestContext (testName, ctx) {
|
|
841
|
+
const contexts = this.concurrentTestContexts.get(testName)
|
|
842
|
+
if (!contexts?.length) return
|
|
843
|
+
|
|
844
|
+
const contextIndex = contexts.indexOf(ctx)
|
|
845
|
+
if (contextIndex === -1) return
|
|
846
|
+
|
|
847
|
+
contexts.splice(contextIndex, 1)
|
|
848
|
+
if (contexts.length === 0) {
|
|
849
|
+
this.concurrentTestContexts.delete(testName)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Records formatted parameters for one table-driven Jest test declaration.
|
|
855
|
+
*
|
|
856
|
+
* @param {string} name
|
|
857
|
+
* @param {unknown[]} params
|
|
858
|
+
* @returns {void}
|
|
859
|
+
*/
|
|
860
|
+
setNameToParams (name, params) {
|
|
861
|
+
this.nameToParams[name] = [...params]
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Appends formatted parameters for one table-driven Jest test row.
|
|
866
|
+
*
|
|
867
|
+
* @param {string} name
|
|
868
|
+
* @param {unknown[]|object} params
|
|
869
|
+
* @returns {void}
|
|
870
|
+
*/
|
|
871
|
+
appendNameToParams (name, params) {
|
|
872
|
+
if (this.nameToParams[name]) {
|
|
873
|
+
this.nameToParams[name].push(params)
|
|
874
|
+
} else {
|
|
875
|
+
this.nameToParams[name] = [params]
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Runs a concurrent test function inside its test span context.
|
|
881
|
+
*
|
|
882
|
+
* @param {{ ctx: object, numExecutions: number }} concurrentTestState
|
|
883
|
+
* @param {(...args: unknown[]) => unknown} testFn
|
|
884
|
+
* @param {object} thisArg
|
|
885
|
+
* @param {unknown[]} args
|
|
886
|
+
* @returns {unknown|void}
|
|
887
|
+
*/
|
|
888
|
+
runConcurrentTestFn (concurrentTestState, testFn, thisArg, args) {
|
|
889
|
+
const ctx = concurrentTestState.ctx
|
|
890
|
+
if (ctx.isDisabled && !ctx.isAttemptToFix) {
|
|
891
|
+
const done = args[0]
|
|
892
|
+
if (typeof done === 'function') {
|
|
893
|
+
done()
|
|
894
|
+
}
|
|
895
|
+
return
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const runTest = () => testFn.apply(thisArg, args)
|
|
899
|
+
if (!ctx.isAttemptToFixRetry && !ctx.isEfdRetry) {
|
|
900
|
+
ctx.isJestRetry = concurrentTestState.numExecutions > 0
|
|
901
|
+
}
|
|
902
|
+
concurrentTestState.numExecutions++
|
|
903
|
+
|
|
904
|
+
if (ctx.currentStore) {
|
|
905
|
+
return testFnCh.runStores(ctx, runTest)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
let result
|
|
909
|
+
testStartCh.runStores(ctx, () => {
|
|
910
|
+
result = testFnCh.runStores(ctx, runTest)
|
|
911
|
+
})
|
|
912
|
+
return result
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Ensures Datadog handles Jest circus events even when a custom environment overrides
|
|
917
|
+
* `handleTestEvent` without calling `super.handleTestEvent`.
|
|
918
|
+
*
|
|
919
|
+
* @param {(event: object, state: object) => Promise<void>|void} datadogHandleTestEvent
|
|
920
|
+
* @returns {void}
|
|
921
|
+
*/
|
|
922
|
+
wrapCustomHandleTestEvent (datadogHandleTestEvent) {
|
|
923
|
+
const descriptor = Object.getOwnPropertyDescriptor(this, 'handleTestEvent')
|
|
924
|
+
let handleTestEvent = getWrappedCustomHandleTestEvent(this.handleTestEvent, datadogHandleTestEvent)
|
|
925
|
+
|
|
926
|
+
Object.defineProperty(this, 'handleTestEvent', {
|
|
927
|
+
configurable: true,
|
|
928
|
+
enumerable: descriptor?.enumerable ?? false,
|
|
929
|
+
get () {
|
|
930
|
+
return handleTestEvent
|
|
931
|
+
},
|
|
932
|
+
set (value) {
|
|
933
|
+
handleTestEvent = getWrappedCustomHandleTestEvent(value, datadogHandleTestEvent)
|
|
934
|
+
},
|
|
935
|
+
})
|
|
448
936
|
}
|
|
449
937
|
|
|
450
938
|
/**
|
|
@@ -558,12 +1046,47 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
558
1046
|
// Generic function to handle test retries
|
|
559
1047
|
retryTest ({
|
|
560
1048
|
jestEvent,
|
|
1049
|
+
forceSerial,
|
|
561
1050
|
retryCount,
|
|
562
1051
|
retryType,
|
|
563
1052
|
}) {
|
|
564
1053
|
const { testName, fn, timeout } = jestEvent
|
|
1054
|
+
const isAttemptToFixRetry = retryType === 'Test Management (Attempt to Fix)'
|
|
1055
|
+
const isEfdRetry = retryType === 'Impacted tests' || retryType === 'Early flake detection'
|
|
565
1056
|
for (let retryIndex = 0; retryIndex < retryCount; retryIndex++) {
|
|
566
|
-
|
|
1057
|
+
const concurrentTestState = this.concurrentTestStates.get(fn)
|
|
1058
|
+
if (concurrentTestState) {
|
|
1059
|
+
const test = forceSerial
|
|
1060
|
+
? concurrentTestState.serialTest
|
|
1061
|
+
: (
|
|
1062
|
+
concurrentTestState.concurrentTest ||
|
|
1063
|
+
getOriginalConcurrentTest(this.global.test?.concurrent)
|
|
1064
|
+
)
|
|
1065
|
+
if (typeof test !== 'function') {
|
|
1066
|
+
log.error('%s could not retry concurrent test because test is undefined', retryType)
|
|
1067
|
+
continue
|
|
1068
|
+
}
|
|
1069
|
+
const retryFn = this.createConcurrentTestFn(
|
|
1070
|
+
testName,
|
|
1071
|
+
concurrentTestState.testFn,
|
|
1072
|
+
undefined,
|
|
1073
|
+
concurrentTestState.state,
|
|
1074
|
+
{
|
|
1075
|
+
concurrentTest: concurrentTestState.concurrentTest,
|
|
1076
|
+
concurrentTestThisArg: concurrentTestState.concurrentTestThisArg,
|
|
1077
|
+
isAttemptToFixRetry,
|
|
1078
|
+
isEfdRetry,
|
|
1079
|
+
isModified: concurrentTestState.ctx.isModified,
|
|
1080
|
+
serialTest: concurrentTestState.serialTest,
|
|
1081
|
+
serialTestThisArg: concurrentTestState.serialTestThisArg,
|
|
1082
|
+
testParameters: concurrentTestState.ctx.testParameters,
|
|
1083
|
+
}
|
|
1084
|
+
)
|
|
1085
|
+
const thisArg = forceSerial
|
|
1086
|
+
? concurrentTestState.serialTestThisArg
|
|
1087
|
+
: concurrentTestState.concurrentTestThisArg
|
|
1088
|
+
test.call(thisArg, testName, retryFn, timeout)
|
|
1089
|
+
} else if (this.global.test) {
|
|
567
1090
|
this.global.test(testName, fn, timeout)
|
|
568
1091
|
} else {
|
|
569
1092
|
log.error('%s could not retry test because global.test is undefined', retryType)
|
|
@@ -579,25 +1102,37 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
579
1102
|
}
|
|
580
1103
|
|
|
581
1104
|
async handleTestEvent (event, state) {
|
|
1105
|
+
if (isDatadogJestEventHandled(event)) return
|
|
1106
|
+
markDatadogJestEventHandled(event)
|
|
1107
|
+
|
|
582
1108
|
if (super.handleTestEvent) {
|
|
583
1109
|
await super.handleTestEvent(event, state)
|
|
584
1110
|
}
|
|
585
1111
|
|
|
586
|
-
|
|
587
|
-
|
|
1112
|
+
if (event.name === 'setup') {
|
|
1113
|
+
this.wrapConcurrentTest(state)
|
|
1114
|
+
}
|
|
588
1115
|
if (event.name === 'setup' && this.global.test) {
|
|
1116
|
+
const environment = this
|
|
589
1117
|
shimmer.wrap(this.global.test, 'each', each => function (...args) {
|
|
590
1118
|
const testParameters = getFormattedJestTestParameters(args)
|
|
591
1119
|
const eachBind = each.apply(this, args)
|
|
592
1120
|
return function (...args) {
|
|
593
1121
|
const [testName] = args
|
|
594
|
-
setNameToParams(testName, testParameters)
|
|
1122
|
+
environment.setNameToParams(testName, testParameters)
|
|
595
1123
|
return eachBind.apply(this, args)
|
|
596
1124
|
}
|
|
597
1125
|
})
|
|
598
1126
|
}
|
|
599
1127
|
if (event.name === 'test_start') {
|
|
600
1128
|
const testName = getJestTestName(event.test)
|
|
1129
|
+
const concurrentTestState = this.concurrentTestStates.get(event.test.fn)
|
|
1130
|
+
let concurrentCtx = concurrentTestState?.ctx
|
|
1131
|
+
if (concurrentCtx) {
|
|
1132
|
+
this.removeConcurrentTestContext(testName, concurrentCtx)
|
|
1133
|
+
} else {
|
|
1134
|
+
concurrentCtx = this.getNextConcurrentTestContext(testName)
|
|
1135
|
+
}
|
|
601
1136
|
if (testsToBeRetried.has(testName)) {
|
|
602
1137
|
// This is needed because we're retrying tests with the same name
|
|
603
1138
|
this.resetSnapshotState()
|
|
@@ -607,7 +1142,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
607
1142
|
let isNewTest = false
|
|
608
1143
|
let numEfdRetry = null
|
|
609
1144
|
let numOfAttemptsToFixRetries = null
|
|
610
|
-
const testParameters =
|
|
1145
|
+
const testParameters = concurrentCtx?.testParameters ||
|
|
1146
|
+
concurrentTestState?.ctx?.testParameters ||
|
|
1147
|
+
getTestParametersString(this.nameToParams, event.test.name)
|
|
611
1148
|
|
|
612
1149
|
let isAttemptToFix = false
|
|
613
1150
|
let isDisabled = false
|
|
@@ -624,17 +1161,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
624
1161
|
}
|
|
625
1162
|
}
|
|
626
1163
|
|
|
627
|
-
let isModified =
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
const testEndLine = getTestEndLine(event.test.fn, testStartLine)
|
|
631
|
-
isModified = isModifiedTest(
|
|
632
|
-
this.testSourceFile,
|
|
633
|
-
testStartLine,
|
|
634
|
-
testEndLine,
|
|
635
|
-
this.modifiedFiles,
|
|
636
|
-
'jest'
|
|
637
|
-
)
|
|
1164
|
+
let isModified = this.isTestModified(event.test.asyncError, event.test.fn)
|
|
1165
|
+
if (concurrentCtx?.isModified || concurrentTestState?.ctx?.isModified) {
|
|
1166
|
+
isModified = true
|
|
638
1167
|
}
|
|
639
1168
|
|
|
640
1169
|
if (this.isKnownTestsEnabled) {
|
|
@@ -651,23 +1180,26 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
651
1180
|
|
|
652
1181
|
const isJestRetry = event.test?.invocations > 1
|
|
653
1182
|
const hasDynamicName = isNewTest && DYNAMIC_NAME_RE.test(testName)
|
|
654
|
-
const ctx = {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1183
|
+
const ctx = concurrentCtx || concurrentTestState?.ctx || {}
|
|
1184
|
+
ctx.name = testName
|
|
1185
|
+
ctx.suite = this.testSuite
|
|
1186
|
+
ctx.testSourceFile = this.testSourceFile
|
|
1187
|
+
ctx.displayName = this.displayName
|
|
1188
|
+
ctx.testParameters = testParameters
|
|
1189
|
+
ctx.frameworkVersion = jestVersion
|
|
1190
|
+
ctx.isNew = isNewTest
|
|
1191
|
+
ctx.isEfdRetry = ctx.isEfdRetry || numEfdRetry > 0
|
|
1192
|
+
ctx.isAttemptToFix = isAttemptToFix
|
|
1193
|
+
ctx.isAttemptToFixRetry = ctx.isAttemptToFixRetry || numOfAttemptsToFixRetries > 0
|
|
1194
|
+
ctx.isJestRetry = isJestRetry
|
|
1195
|
+
ctx.isDisabled = isDisabled
|
|
1196
|
+
ctx.isQuarantined = isQuarantined
|
|
1197
|
+
ctx.isModified = isModified
|
|
1198
|
+
ctx.hasDynamicName = hasDynamicName
|
|
1199
|
+
ctx.testSuiteAbsolutePath = this.testSuiteAbsolutePath
|
|
1200
|
+
if (concurrentTestState) {
|
|
1201
|
+
ctx.concurrentTestState = concurrentTestState
|
|
1202
|
+
concurrentTestState.ctx = ctx
|
|
671
1203
|
}
|
|
672
1204
|
testContexts.set(event.test, ctx)
|
|
673
1205
|
|
|
@@ -675,7 +1207,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
675
1207
|
logAttemptToFixTestExecution(this.testSuite, testName, loggedAttemptToFixTests)
|
|
676
1208
|
}
|
|
677
1209
|
|
|
678
|
-
|
|
1210
|
+
const wrapTestAndHooks = () => {
|
|
679
1211
|
let p = event.test.parent
|
|
680
1212
|
const hooks = []
|
|
681
1213
|
while (p != null) {
|
|
@@ -694,6 +1226,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
694
1226
|
})
|
|
695
1227
|
hook.fn = newHookFn
|
|
696
1228
|
}
|
|
1229
|
+
|
|
697
1230
|
const originalFn = event.test.fn
|
|
698
1231
|
originalTestFns.set(event.test, originalFn)
|
|
699
1232
|
|
|
@@ -702,7 +1235,53 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
702
1235
|
})
|
|
703
1236
|
|
|
704
1237
|
event.test.fn = newFn
|
|
705
|
-
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const isConcurrentTest = event.test.concurrent || ctx.concurrentTestState
|
|
1241
|
+
if (isConcurrentTest) {
|
|
1242
|
+
if (!ctx.currentStore && event.test.errors?.length) {
|
|
1243
|
+
testStartCh.runStores(ctx, () => {})
|
|
1244
|
+
}
|
|
1245
|
+
} else if (ctx.currentStore) {
|
|
1246
|
+
wrapTestAndHooks()
|
|
1247
|
+
} else {
|
|
1248
|
+
testStartCh.runStores(ctx, wrapTestAndHooks)
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (event.name === 'hook_start' && (event.hook.type === 'beforeEach' || event.hook.type === 'afterEach')) {
|
|
1253
|
+
const ctx = testContexts.get(event.test) || this.concurrentTestStates.get(event.test?.fn)?.ctx
|
|
1254
|
+
if (ctx?.concurrentTestState && (!ctx.isDisabled || ctx.isAttemptToFix)) {
|
|
1255
|
+
let hookFn = event.hook.fn
|
|
1256
|
+
if (originalHookFns.has(event.hook)) {
|
|
1257
|
+
hookFn = originalHookFns.get(event.hook)
|
|
1258
|
+
} else {
|
|
1259
|
+
originalHookFns.set(event.hook, hookFn)
|
|
1260
|
+
}
|
|
1261
|
+
let hookContexts = concurrentHookContextQueues.get(event.hook)
|
|
1262
|
+
if (!hookContexts) {
|
|
1263
|
+
hookContexts = []
|
|
1264
|
+
concurrentHookContextQueues.set(event.hook, hookContexts)
|
|
1265
|
+
}
|
|
1266
|
+
hookContexts.push(ctx)
|
|
1267
|
+
if (!ctx.currentStore) {
|
|
1268
|
+
testStartCh.runStores(ctx, () => {})
|
|
1269
|
+
}
|
|
1270
|
+
let concurrentHookFn = concurrentHookFns.get(event.hook)
|
|
1271
|
+
if (!concurrentHookFn) {
|
|
1272
|
+
concurrentHookFn = shimmer.wrapFunction(hookFn, hookFn => function (...args) {
|
|
1273
|
+
const hookContexts = concurrentHookContextQueues.get(event.hook)
|
|
1274
|
+
const [hookCtx] = hookContexts || []
|
|
1275
|
+
if (hookCtx) {
|
|
1276
|
+
hookContexts.shift()
|
|
1277
|
+
return testFnCh.runStores(hookCtx, () => hookFn.apply(this, args))
|
|
1278
|
+
}
|
|
1279
|
+
return hookFn.apply(this, args)
|
|
1280
|
+
})
|
|
1281
|
+
concurrentHookFns.set(event.hook, concurrentHookFn)
|
|
1282
|
+
}
|
|
1283
|
+
event.hook.fn = concurrentHookFn
|
|
1284
|
+
}
|
|
706
1285
|
}
|
|
707
1286
|
|
|
708
1287
|
if (event.name === 'hook_start' && (event.hook.type === 'beforeAll' || event.hook.type === 'afterAll')) {
|
|
@@ -719,6 +1298,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
719
1298
|
}
|
|
720
1299
|
|
|
721
1300
|
if (event.name === 'add_test') {
|
|
1301
|
+
if (event.concurrent) {
|
|
1302
|
+
this.hasConcurrentTests = true
|
|
1303
|
+
}
|
|
1304
|
+
|
|
722
1305
|
if (event.failing) {
|
|
723
1306
|
return
|
|
724
1307
|
}
|
|
@@ -741,15 +1324,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
741
1324
|
})
|
|
742
1325
|
}
|
|
743
1326
|
if (!isAttemptToFix && this.isImpactedTestsEnabled) {
|
|
744
|
-
const
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
this.testSourceFile,
|
|
748
|
-
testStartLine,
|
|
749
|
-
testEndLine,
|
|
750
|
-
this.modifiedFiles,
|
|
751
|
-
'jest'
|
|
752
|
-
)
|
|
1327
|
+
const concurrentTestState = this.concurrentTestStates.get(event.fn)
|
|
1328
|
+
const isModified = concurrentTestState?.ctx?.isModified ||
|
|
1329
|
+
this.isTestModified(event.asyncError, event.fn)
|
|
753
1330
|
if (isModified && !retriedTestsToNumAttempts.has(testFullName) && this.isEarlyFlakeDetectionEnabled) {
|
|
754
1331
|
retriedTestsToNumAttempts.set(testFullName, 0)
|
|
755
1332
|
testsToBeRetried.add(testFullName)
|
|
@@ -788,7 +1365,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
788
1365
|
status = 'fail'
|
|
789
1366
|
}
|
|
790
1367
|
// restore in case it is retried
|
|
791
|
-
|
|
1368
|
+
if (originalTestFns.has(event.test)) {
|
|
1369
|
+
event.test.fn = originalTestFns.get(event.test)
|
|
1370
|
+
}
|
|
792
1371
|
// If ATR retry is being suppressed for this test (due to EFD or Attempt to Fix taking precedence)
|
|
793
1372
|
// and the test has errors for this attempt, store the errors temporarily and clear them
|
|
794
1373
|
// so Jest won't treat this attempt as failed (the real status will be reported after retries).
|
|
@@ -857,6 +1436,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
857
1436
|
state.currentDescribeBlock = event.test.parent ?? originalDescribeBlock
|
|
858
1437
|
state.hasStarted = false
|
|
859
1438
|
this.retryTest({
|
|
1439
|
+
forceSerial: true,
|
|
860
1440
|
jestEvent: {
|
|
861
1441
|
testName: event.test.name,
|
|
862
1442
|
fn: event.test.fn,
|
|
@@ -905,14 +1485,16 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
905
1485
|
const promises = {}
|
|
906
1486
|
const numRetries = this.global[RETRY_TIMES]
|
|
907
1487
|
const numTestExecutions = event.test?.invocations
|
|
908
|
-
const
|
|
909
|
-
const
|
|
1488
|
+
const willBeRetriedByAutoTestRetry = numRetries > 0 && numTestExecutions - 1 < numRetries
|
|
1489
|
+
const isFailedTestReplayAllowed = !this.hasConcurrentTests
|
|
1490
|
+
const willBeRetriedByFailedTestReplay = isFailedTestReplayAllowed && willBeRetriedByAutoTestRetry
|
|
1491
|
+
const mightHitBreakpoint = this.isDiEnabled && isFailedTestReplayAllowed && numTestExecutions >= 2
|
|
910
1492
|
|
|
911
1493
|
// For quarantined tests, track failures so the session can be marked as passing later,
|
|
912
1494
|
// and suppress errors so Jest does not mark the test suite as failing.
|
|
913
1495
|
// The actual status ('fail') is already captured above for dd-trace reporting.
|
|
914
1496
|
// Only suppress on the final execution — not when ATR/EFD/ATF will retry the test.
|
|
915
|
-
if (!event.test?.[ATR_RETRY_SUPPRESSION_FLAG] && !
|
|
1497
|
+
if (!event.test?.[ATR_RETRY_SUPPRESSION_FLAG] && !willBeRetriedByAutoTestRetry) {
|
|
916
1498
|
const quarantineCtx = testContexts.get(event.test)
|
|
917
1499
|
if (quarantineCtx?.isQuarantined && !quarantineCtx.isAttemptToFix && event.test.errors?.length) {
|
|
918
1500
|
quarantinedFailingTests.add(`${quarantineCtx.suite} › ${quarantineCtx.name}`)
|
|
@@ -925,6 +1507,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
925
1507
|
log.warn('"ci:jest:test_done": no context found for test "%s"', testName)
|
|
926
1508
|
return
|
|
927
1509
|
}
|
|
1510
|
+
if (ctx.concurrentTestState && !ctx.currentStore && !ctx.isDisabled) {
|
|
1511
|
+
testStartCh.runStores(ctx, () => {})
|
|
1512
|
+
}
|
|
928
1513
|
|
|
929
1514
|
const finalStatus = this.getFinalStatus(testName,
|
|
930
1515
|
status,
|
|
@@ -940,18 +1525,13 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
940
1525
|
...ctx.currentStore,
|
|
941
1526
|
error: formatJestError(originalError),
|
|
942
1527
|
shouldSetProbe,
|
|
1528
|
+
shouldWaitForHitProbe: mightHitBreakpoint,
|
|
943
1529
|
promises,
|
|
944
1530
|
})
|
|
945
1531
|
}
|
|
946
1532
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if (status === 'fail' && mightHitBreakpoint) {
|
|
950
|
-
await new Promise(resolve => {
|
|
951
|
-
realSetTimeout(() => {
|
|
952
|
-
resolve()
|
|
953
|
-
}, BREAKPOINT_HIT_GRACE_PERIOD_MS)
|
|
954
|
-
})
|
|
1533
|
+
if (promises.hitBreakpointPromise) {
|
|
1534
|
+
await promises.hitBreakpointPromise
|
|
955
1535
|
}
|
|
956
1536
|
|
|
957
1537
|
let isAtrRetry = false
|
|
@@ -969,11 +1549,18 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
969
1549
|
isAtrRetry,
|
|
970
1550
|
finalStatus,
|
|
971
1551
|
earlyFlakeAbortReason: efdSlowAbortedTests.has(testName) ? 'slow' : undefined,
|
|
1552
|
+
promises,
|
|
972
1553
|
})
|
|
973
1554
|
|
|
1555
|
+
if (promises.hitBreakpointPromise) {
|
|
1556
|
+
await promises.hitBreakpointPromise
|
|
1557
|
+
}
|
|
974
1558
|
if (promises.isProbeReady) {
|
|
975
1559
|
await promises.isProbeReady
|
|
976
1560
|
}
|
|
1561
|
+
if (ctx.concurrentTestState) {
|
|
1562
|
+
ctx.currentStore = undefined
|
|
1563
|
+
}
|
|
977
1564
|
}
|
|
978
1565
|
if (event.name === 'run_finish') {
|
|
979
1566
|
for (const [test, errors] of atrSuppressedErrors) {
|
|
@@ -1003,6 +1590,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
1003
1590
|
retriedTestsToNumAttempts.clear()
|
|
1004
1591
|
attemptToFixRetriedTestsStatuses.clear()
|
|
1005
1592
|
testsToBeRetried.clear()
|
|
1593
|
+
testSuiteDatadogEnvironments.delete(this.testSuiteAbsolutePath)
|
|
1006
1594
|
}
|
|
1007
1595
|
if (event.name === 'test_skip' || event.name === 'test_todo') {
|
|
1008
1596
|
const testName = getJestTestName(event.test)
|
|
@@ -1506,11 +2094,23 @@ addHook({
|
|
|
1506
2094
|
versions: [MINIMUM_JEST_VERSION],
|
|
1507
2095
|
}, getTestEnvironment)
|
|
1508
2096
|
|
|
2097
|
+
addHook({
|
|
2098
|
+
name: 'jest-environment-node',
|
|
2099
|
+
file: 'build/index.js',
|
|
2100
|
+
versions: [MINIMUM_JEST_VERSION],
|
|
2101
|
+
}, getTestEnvironment)
|
|
2102
|
+
|
|
1509
2103
|
addHook({
|
|
1510
2104
|
name: 'jest-environment-jsdom',
|
|
1511
2105
|
versions: [MINIMUM_JEST_VERSION],
|
|
1512
2106
|
}, getTestEnvironment)
|
|
1513
2107
|
|
|
2108
|
+
addHook({
|
|
2109
|
+
name: 'jest-environment-jsdom',
|
|
2110
|
+
file: 'build/index.js',
|
|
2111
|
+
versions: [MINIMUM_JEST_VERSION],
|
|
2112
|
+
}, getTestEnvironment)
|
|
2113
|
+
|
|
1514
2114
|
addHook({
|
|
1515
2115
|
name: '@happy-dom/jest-environment',
|
|
1516
2116
|
versions: ['>=10.0.0'],
|
|
@@ -1927,6 +2527,19 @@ function cleanupTestSuiteState (testSuiteAbsolutePath) {
|
|
|
1927
2527
|
testSuiteJestObjects.delete(testSuiteAbsolutePath)
|
|
1928
2528
|
}
|
|
1929
2529
|
|
|
2530
|
+
/**
|
|
2531
|
+
* Rechecks custom handlers after Jest has finished constructing the environment.
|
|
2532
|
+
*
|
|
2533
|
+
* @param {object} environment
|
|
2534
|
+
* @returns {void}
|
|
2535
|
+
*/
|
|
2536
|
+
function wrapEnvironmentCustomHandleTestEvent (environment) {
|
|
2537
|
+
const datadogHandleTestEvent = environment[DD_JEST_HANDLE_TEST_EVENT_DATADOG]
|
|
2538
|
+
if (typeof environment.wrapCustomHandleTestEvent === 'function' && datadogHandleTestEvent) {
|
|
2539
|
+
environment.wrapCustomHandleTestEvent(datadogHandleTestEvent)
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
|
|
1930
2543
|
addHook({
|
|
1931
2544
|
name: '@jest/core',
|
|
1932
2545
|
file: 'build/TestScheduler.js',
|
|
@@ -1960,10 +2573,7 @@ if (DD_MAJOR < 6) {
|
|
|
1960
2573
|
})
|
|
1961
2574
|
}
|
|
1962
2575
|
|
|
1963
|
-
|
|
1964
|
-
name: '@jest/test-sequencer',
|
|
1965
|
-
versions: ['>=28'],
|
|
1966
|
-
}, (sequencerPackage, frameworkVersion) => {
|
|
2576
|
+
function testSequencerWrapper (sequencerPackage, frameworkVersion) {
|
|
1967
2577
|
shimmer.wrap(sequencerPackage.default.prototype, 'shard', shard => function (...args) {
|
|
1968
2578
|
const shardedTests = shard.apply(this, args)
|
|
1969
2579
|
|
|
@@ -1976,7 +2586,18 @@ addHook({
|
|
|
1976
2586
|
return applySuiteSkipping(shardedTests, rootDir, frameworkVersion)
|
|
1977
2587
|
})
|
|
1978
2588
|
return sequencerPackage
|
|
1979
|
-
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
addHook({
|
|
2592
|
+
name: '@jest/test-sequencer',
|
|
2593
|
+
versions: ['>=28'],
|
|
2594
|
+
}, testSequencerWrapper)
|
|
2595
|
+
|
|
2596
|
+
addHook({
|
|
2597
|
+
name: '@jest/test-sequencer',
|
|
2598
|
+
file: 'build/index.js',
|
|
2599
|
+
versions: ['>=28'],
|
|
2600
|
+
}, testSequencerWrapper)
|
|
1980
2601
|
|
|
1981
2602
|
addHook({
|
|
1982
2603
|
name: '@jest/core',
|
|
@@ -2021,6 +2642,9 @@ function jestAdapterWrapper (jestAdapter, jestVersion) {
|
|
|
2021
2642
|
if (!environment || !environment.testEnvironmentOptions) {
|
|
2022
2643
|
return adapter.apply(this, args)
|
|
2023
2644
|
}
|
|
2645
|
+
|
|
2646
|
+
wrapEnvironmentCustomHandleTestEvent(environment)
|
|
2647
|
+
|
|
2024
2648
|
testSuiteStartCh.publish({
|
|
2025
2649
|
testSuite: environment.testSuite,
|
|
2026
2650
|
testEnvironmentOptions: environment.testEnvironmentOptions,
|
|
@@ -2300,6 +2924,14 @@ addHook({
|
|
|
2300
2924
|
file: 'build/SearchSource.js',
|
|
2301
2925
|
}, searchSourceWrapper)
|
|
2302
2926
|
|
|
2927
|
+
addHook({
|
|
2928
|
+
name: 'jest-each',
|
|
2929
|
+
versions: [MINIMUM_JEST_VERSION],
|
|
2930
|
+
}, jestEach => {
|
|
2931
|
+
cacheJestEachBind(jestEach)
|
|
2932
|
+
return jestEach
|
|
2933
|
+
})
|
|
2934
|
+
|
|
2303
2935
|
// from 25.1.0 on, readConfigs becomes async
|
|
2304
2936
|
addHook({
|
|
2305
2937
|
name: 'jest-config',
|
|
@@ -2394,6 +3026,17 @@ function wrapJestGlobalsForRuntime (runtime) {
|
|
|
2394
3026
|
})
|
|
2395
3027
|
}
|
|
2396
3028
|
|
|
3029
|
+
function wrapConcurrentJestGlobalsForRuntime (runtime, jestGlobals) {
|
|
3030
|
+
const environment = testSuiteDatadogEnvironments.get(runtime?._testPath)
|
|
3031
|
+
environment?.wrapConcurrentTestGlobals(jestGlobals?.test, environment.concurrentTestState)
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
function cacheJestEachBind (jestEach) {
|
|
3035
|
+
if (typeof jestEach?.bind === 'function') {
|
|
3036
|
+
jestEachBind = jestEach.bind
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
2397
3040
|
function getLastLoggedReferenceError (runtime) {
|
|
2398
3041
|
const loggedReferenceErrors = runtime?.loggedReferenceErrors
|
|
2399
3042
|
if (!loggedReferenceErrors?.size) return
|
|
@@ -2472,7 +3115,11 @@ addHook({
|
|
|
2472
3115
|
shimmer.wrap(Runtime.prototype, 'requireModule', requireModule => function (from, moduleName) {
|
|
2473
3116
|
wrapJestGlobalsForRuntime(this)
|
|
2474
3117
|
try {
|
|
2475
|
-
|
|
3118
|
+
const returnedValue = requireModule.apply(this, arguments)
|
|
3119
|
+
if (moduleName === '@jest/globals') {
|
|
3120
|
+
wrapConcurrentJestGlobalsForRuntime(this, returnedValue)
|
|
3121
|
+
}
|
|
3122
|
+
return returnedValue
|
|
2476
3123
|
} catch (error) {
|
|
2477
3124
|
if (isBetweenTestsReferenceError(error)) {
|
|
2478
3125
|
reportBetweenTestsReferenceError(this, moduleName, error.message)
|
|
@@ -2505,6 +3152,9 @@ addHook({
|
|
|
2505
3152
|
let returnedValue
|
|
2506
3153
|
try {
|
|
2507
3154
|
returnedValue = requireModuleOrMock.apply(this, arguments)
|
|
3155
|
+
if (moduleName === '@jest/globals') {
|
|
3156
|
+
wrapConcurrentJestGlobalsForRuntime(this, returnedValue)
|
|
3157
|
+
}
|
|
2508
3158
|
} catch (error) {
|
|
2509
3159
|
if (isBetweenTestsReferenceError(error)) {
|
|
2510
3160
|
reportBetweenTestsReferenceError(this, moduleName, error.message)
|