Skip to content

Commit 63db106

Browse files
authored
[test-optimization] [SDTEST-1990] Make compatible Test Optimization instrumentations with Node 24 (#5711)
* Add node24 * Make it work for Jest but custom tags * Fix Jest * Fix Mocha * Update subscription channel Mocha * Fix propagation error in Mocha * Fix active span in Mocha * Fix Cucumber * Partial fix Vitest * Add OPTION_OVERRIDE * Update CLI Vitest * Fix Playwright
1 parent 34f2e55 commit 63db106

File tree

12 files changed

+503
-473
lines changed

12 files changed

+503
-473
lines changed

.github/workflows/project.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ jobs:
7070
DD_SERVICE: dd-trace-js-integration-tests
7171
DD_CIVISIBILITY_AGENTLESS_ENABLED: 1
7272
DD_API_KEY: ${{ secrets.DD_API_KEY }}
73+
OPTIONS_OVERRIDE: 1
7374
steps:
7475
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
7576
- uses: ./.github/actions/node
@@ -92,6 +93,7 @@ jobs:
9293
DD_SERVICE: dd-trace-js-integration-tests
9394
DD_CIVISIBILITY_AGENTLESS_ENABLED: 1
9495
DD_API_KEY: ${{ secrets.DD_API_KEY }}
96+
OPTIONS_OVERRIDE: 1
9597
steps:
9698
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
9799
- uses: ./.github/actions/node
@@ -148,17 +150,24 @@ jobs:
148150
CYPRESS_VERSION: ${{ matrix.cypress-version }}
149151
NODE_OPTIONS: '-r ./ci/init'
150152
CYPRESS_MODULE_TYPE: ${{ matrix.module-type }}
153+
OPTIONS_OVERRIDE: 1
151154

152155

153156
integration-vitest:
154157
runs-on: ubuntu-latest
158+
strategy:
159+
matrix:
160+
version: [oldest, latest]
155161
env:
156162
DD_SERVICE: dd-trace-js-integration-tests
157163
DD_CIVISIBILITY_AGENTLESS_ENABLED: 1
158164
DD_API_KEY: ${{ secrets.DD_API_KEY }}
165+
OPTIONS_OVERRIDE: 1
159166
steps:
160167
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
161-
- uses: ./.github/actions/node/active-lts
168+
- uses: ./.github/actions/node
169+
with:
170+
version: ${{ matrix.version }}
162171
- uses: ./.github/actions/install
163172
- run: yarn test:integration:vitest
164173
env:

packages/datadog-instrumentations/src/cucumber.js

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const log = require('../../dd-trace/src/log')
88
const testStartCh = channel('ci:cucumber:test:start')
99
const testRetryCh = channel('ci:cucumber:test:retry')
1010
const testFinishCh = channel('ci:cucumber:test:finish') // used for test steps too
11+
const testFnCh = channel('ci:cucumber:test:fn')
1112

1213
const testStepStartCh = channel('ci:cucumber:test-step:start')
1314

@@ -52,7 +53,7 @@ const patched = new WeakSet()
5253

5354
const lastStatusByPickleId = new Map()
5455
const numRetriesByPickleId = new Map()
55-
const numAttemptToAsyncResource = new Map()
56+
const numAttemptToCtx = new Map()
5657
const newTestsByTestFullname = new Map()
5758

5859
let eventDataCollector = null
@@ -227,16 +228,12 @@ function wrapRun (pl, isLatestVersion) {
227228
patched.add(pl)
228229

229230
shimmer.wrap(pl.prototype, 'run', run => function () {
230-
if (!testStartCh.hasSubscribers) {
231+
if (!testFinishCh.hasSubscribers) {
231232
return run.apply(this, arguments)
232233
}
233234

234235
let numAttempt = 0
235236

236-
const asyncResource = new AsyncResource('bound-anonymous-fn')
237-
238-
numAttemptToAsyncResource.set(numAttempt, asyncResource)
239-
240237
const testFileAbsolutePath = this.pickle.uri
241238

242239
const testSourceLine = this.gherkinDocument?.feature?.location?.line
@@ -247,9 +244,9 @@ function wrapRun (pl, isLatestVersion) {
247244
testSourceLine,
248245
isParallel: !!process.env.CUCUMBER_WORKER_ID
249246
}
250-
asyncResource.runInAsyncScope(() => {
251-
testStartCh.publish(testStartPayload)
252-
})
247+
const ctx = testStartPayload
248+
numAttemptToCtx.set(numAttempt, ctx)
249+
testStartCh.runStores(ctx, () => { })
253250
const promises = {}
254251
try {
255252
this.eventBroadcaster.on('envelope', shimmer.wrapFunction(null, () => async (testCase) => {
@@ -265,31 +262,27 @@ function wrapRun (pl, isLatestVersion) {
265262
// ignore error
266263
}
267264

268-
const failedAttemptAsyncResource = numAttemptToAsyncResource.get(numAttempt)
265+
const failedAttemptCtx = numAttemptToCtx.get(numAttempt)
269266
const isFirstAttempt = numAttempt++ === 0
270267
const isAtrRetry = !isFirstAttempt && isFlakyTestRetriesEnabled
271268

272269
if (promises.hitBreakpointPromise) {
273270
await promises.hitBreakpointPromise
274271
}
275272

276-
failedAttemptAsyncResource.runInAsyncScope(() => {
277-
// the current span will be finished and a new one will be created
278-
testRetryCh.publish({ isFirstAttempt, error, isAtrRetry })
279-
})
273+
// the current span will be finished and a new one will be created
274+
testRetryCh.publish({ isFirstAttempt, error, isAtrRetry, ...failedAttemptCtx.currentStore })
280275

281-
const newAsyncResource = new AsyncResource('bound-anonymous-fn')
282-
numAttemptToAsyncResource.set(numAttempt, newAsyncResource)
276+
const newCtx = { ...testStartPayload, promises }
277+
numAttemptToCtx.set(numAttempt, newCtx)
283278

284-
newAsyncResource.runInAsyncScope(() => {
285-
testStartCh.publish({ ...testStartPayload, promises }) // a new span will be created
286-
})
279+
testStartCh.runStores(newCtx, () => { })
287280
}
288281
}
289282
}))
290283
let promise
291284

292-
asyncResource.runInAsyncScope(() => {
285+
testFnCh.runStores(ctx, () => {
293286
promise = run.apply(this, arguments)
294287
})
295288
promise.finally(async () => {
@@ -343,39 +336,40 @@ function wrapRun (pl, isLatestVersion) {
343336
isEfdRetry = numRetries > 0
344337
}
345338

346-
const attemptAsyncResource = numAttemptToAsyncResource.get(numAttempt)
339+
const attemptCtx = numAttemptToCtx.get(numAttempt)
347340

348341
const error = getErrorFromCucumberResult(result)
349342

350343
if (promises.hitBreakpointPromise) {
351344
await promises.hitBreakpointPromise
352345
}
353-
attemptAsyncResource.runInAsyncScope(() => {
354-
testFinishCh.publish({
355-
status,
356-
skipReason,
357-
error,
358-
isNew,
359-
isEfdRetry,
360-
isFlakyRetry: numAttempt > 0,
361-
isAttemptToFix,
362-
isAttemptToFixRetry,
363-
hasFailedAllRetries,
364-
hasPassedAllRetries,
365-
hasFailedAttemptToFix,
366-
isDisabled,
367-
isQuarantined
368-
})
346+
testFinishCh.publish({
347+
status,
348+
skipReason,
349+
error,
350+
isNew,
351+
isEfdRetry,
352+
isFlakyRetry: numAttempt > 0,
353+
isAttemptToFix,
354+
isAttemptToFixRetry,
355+
hasFailedAllRetries,
356+
hasPassedAllRetries,
357+
hasFailedAttemptToFix,
358+
isDisabled,
359+
isQuarantined,
360+
...attemptCtx.currentStore
369361
})
370362
})
371363
return promise
372364
} catch (err) {
373-
errorCh.publish(err)
374-
throw err
365+
ctx.err = err
366+
errorCh.runStores(ctx, () => {
367+
throw err
368+
})
375369
}
376370
})
377371
shimmer.wrap(pl.prototype, 'runStep', runStep => function () {
378-
if (!testStepStartCh.hasSubscribers) {
372+
if (!testFinishCh.hasSubscribers) {
379373
return runStep.apply(this, arguments)
380374
}
381375
const testStep = arguments[0]
@@ -387,9 +381,8 @@ function wrapRun (pl, isLatestVersion) {
387381
resource = testStep.isHook ? 'hook' : testStep.pickleStep.text
388382
}
389383

390-
const asyncResource = new AsyncResource('bound-anonymous-fn')
391-
return asyncResource.runInAsyncScope(() => {
392-
testStepStartCh.publish({ resource })
384+
const ctx = { resource }
385+
return testStepStartCh.runStores(ctx, () => {
393386
try {
394387
const promise = runStep.apply(this, arguments)
395388

@@ -398,12 +391,14 @@ function wrapRun (pl, isLatestVersion) {
398391
? getStatusFromResultLatest(result)
399392
: getStatusFromResult(result)
400393

401-
testFinishCh.publish({ isStep: true, status, skipReason, errorMessage })
394+
testFinishCh.publish({ isStep: true, status, skipReason, errorMessage, ...ctx.currentStore })
402395
})
403396
return promise
404397
} catch (err) {
405-
errorCh.publish(err)
406-
throw err
398+
ctx.err = err
399+
errorCh.runStores(ctx, () => {
400+
throw err
401+
})
407402
}
408403
})
409404
})

packages/datadog-instrumentations/src/jest.js

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const testStartCh = channel('ci:jest:test:start')
4141
const testSkippedCh = channel('ci:jest:test:skip')
4242
const testFinishCh = channel('ci:jest:test:finish')
4343
const testErrCh = channel('ci:jest:test:err')
44+
const testFnCh = channel('ci:jest:test:fn')
4445

4546
const skippableSuitesCh = channel('ci:jest:test-suite:skippable')
4647
const libraryConfigurationCh = channel('ci:jest:library-configuration')
@@ -79,7 +80,7 @@ let testManagementAttemptToFixRetries = 0
7980

8081
const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
8182

82-
const asyncResources = new WeakMap()
83+
const testContexts = new WeakMap()
8384
const originalTestFns = new WeakMap()
8485
const originalHookFns = new WeakMap()
8586
const retriedTestsToNumAttempts = new Map()
@@ -307,8 +308,6 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
307308
const testParameters = getTestParametersString(this.nameToParams, event.test.name)
308309
// Async resource for this test is created here
309310
// It is used later on by the test_done handler
310-
const asyncResource = new AsyncResource('bound-anonymous-fn')
311-
asyncResources.set(event.test, asyncResource)
312311
const testName = getJestTestName(event.test)
313312
const originalTestName = removeEfdStringFromTestName(removeAttemptToFixStringFromTestName(testName))
314313

@@ -336,33 +335,46 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
336335
}
337336

338337
const isJestRetry = event.test?.invocations > 1
339-
asyncResource.runInAsyncScope(() => {
340-
testStartCh.publish({
341-
name: originalTestName,
342-
suite: this.testSuite,
343-
testSourceFile: this.testSourceFile,
344-
displayName: this.displayName,
345-
testParameters,
346-
frameworkVersion: jestVersion,
347-
isNew: isNewTest,
348-
isEfdRetry: numEfdRetry > 0,
349-
isAttemptToFix,
350-
isAttemptToFixRetry: numOfAttemptsToFixRetries > 0,
351-
isJestRetry,
352-
isDisabled,
353-
isQuarantined
354-
})
338+
const ctx = {
339+
name: originalTestName,
340+
suite: this.testSuite,
341+
testSourceFile: this.testSourceFile,
342+
displayName: this.displayName,
343+
testParameters,
344+
frameworkVersion: jestVersion,
345+
isNew: isNewTest,
346+
isEfdRetry: numEfdRetry > 0,
347+
isAttemptToFix,
348+
isAttemptToFixRetry: numOfAttemptsToFixRetries > 0,
349+
isJestRetry,
350+
isDisabled,
351+
isQuarantined
352+
}
353+
testContexts.set(event.test, ctx)
354+
355+
testStartCh.runStores(ctx, () => {
355356
for (const hook of event.test.parent.hooks) {
356357
let hookFn = hook.fn
357358
if (!originalHookFns.has(hook)) {
358359
originalHookFns.set(hook, hookFn)
359360
} else {
360361
hookFn = originalHookFns.get(hook)
361362
}
362-
hook.fn = asyncResource.bind(hookFn)
363+
const wrapperHook = function () {
364+
return testFnCh.runStores(ctx, () => hookFn.apply(this, arguments))
365+
}
366+
// If we don't do this, the timeout will be not be triggered
367+
Object.defineProperty(wrapperHook, 'length', { value: hookFn.length })
368+
hook.fn = wrapperHook
369+
}
370+
const originalFn = event.test.fn
371+
originalTestFns.set(event.test, originalFn)
372+
const wrapper = function () {
373+
return testFnCh.runStores(ctx, () => originalFn.apply(this, arguments))
363374
}
364-
originalTestFns.set(event.test, event.test.fn)
365-
event.test.fn = asyncResource.bind(event.test.fn)
375+
// If we don't do this, the timeout will be not be triggered
376+
Object.defineProperty(wrapper, 'length', { value: originalFn.length })
377+
event.test.fn = wrapper
366378
})
367379
}
368380

@@ -460,16 +472,15 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
460472
const willBeRetried = numRetries > 0 && numTestExecutions - 1 < numRetries
461473
const mightHitBreakpoint = this.isDiEnabled && numTestExecutions >= 2
462474

463-
const asyncResource = asyncResources.get(event.test)
475+
const ctx = testContexts.get(event.test)
464476

465477
if (status === 'fail') {
466478
const shouldSetProbe = this.isDiEnabled && willBeRetried && numTestExecutions === 1
467-
asyncResource.runInAsyncScope(() => {
468-
testErrCh.publish({
469-
error: formatJestError(event.test.errors[0]),
470-
shouldSetProbe,
471-
promises
472-
})
479+
testErrCh.publish({
480+
...ctx.currentStore,
481+
error: formatJestError(event.test.errors[0]),
482+
shouldSetProbe,
483+
promises
473484
})
474485
}
475486

@@ -488,15 +499,14 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
488499
isAtrRetry = true
489500
}
490501

491-
asyncResource.runInAsyncScope(() => {
492-
testFinishCh.publish({
493-
status,
494-
testStartLine: getTestLineStart(event.test.asyncError, this.testSuite),
495-
attemptToFixPassed,
496-
failedAllTests,
497-
attemptToFixFailed,
498-
isAtrRetry
499-
})
502+
testFinishCh.publish({
503+
...ctx.currentStore,
504+
status,
505+
testStartLine: getTestLineStart(event.test.asyncError, this.testSuite),
506+
attemptToFixPassed,
507+
failedAllTests,
508+
attemptToFixFailed,
509+
isAtrRetry
500510
})
501511

502512
if (promises.isProbeReady) {

0 commit comments

Comments
 (0)