From 5acd01017bfc55f802b77a2cabdde7ad90262140 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Mon, 2 Jun 2025 14:49:14 +0200 Subject: [PATCH 1/7] new exporter --- ci/init.js | 12 +++ ext/exporters.d.ts | 3 +- ext/exporters.js | 3 +- .../src/helpers/hooks.js | 3 +- .../datadog-instrumentations/src/vitest.js | 93 +++++++++++++++---- packages/datadog-plugin-vitest/src/index.js | 17 ++++ .../exporters/test-worker/index.js | 6 +- .../exporters/test-worker/writer.js | 15 ++- packages/dd-trace/src/exporter.js | 1 + packages/dd-trace/src/plugins/index.js | 1 + packages/dd-trace/src/plugins/util/test.js | 4 + 11 files changed, 132 insertions(+), 26 deletions(-) diff --git a/ci/init.js b/ci/init.js index d7ef64daf3d..2817e817b74 100644 --- a/ci/init.js +++ b/ci/init.js @@ -8,6 +8,9 @@ const isCucumberWorker = !!process.env.CUCUMBER_WORKER_ID const isMochaWorker = !!process.env.MOCHA_WORKER_ID const isPlaywrightWorker = !!process.env.DD_PLAYWRIGHT_WORKER +// we can't use VITEST_WORKER_ID because it's set _after_ the worker is initialized +// maybe we can intercept tinypool to inject it to be sure +const isVitestWorker = !!process.env.TINYPOOL_WORKER_ID const packageManagers = [ 'npm', @@ -15,6 +18,8 @@ const packageManagers = [ 'pnpm' ] +console.log('init', isVitestWorker) + const isPackageManager = () => { return packageManagers.some(packageManager => process.argv[1]?.includes(`bin/${packageManager}`)) } @@ -75,6 +80,13 @@ if (isPlaywrightWorker) { } } +if (isVitestWorker) { + console.log('initializing as vites tworker') + options.experimental = { + exporter: 'vitest_worker' + } +} + if (shouldInit) { tracer.init(options) tracer.use('fs', false) diff --git a/ext/exporters.d.ts b/ext/exporters.d.ts index f6fc4fddaab..3d36140c56d 100644 --- a/ext/exporters.d.ts +++ b/ext/exporters.d.ts @@ -6,7 +6,8 @@ declare const exporters: { JEST_WORKER: 'jest_worker', CUCUMBER_WORKER: 'cucumber_worker', MOCHA_WORKER: 'mocha_worker', - PLAYWRIGHT_WORKER: 'playwright_worker' + PLAYWRIGHT_WORKER: 'playwright_worker', + VITEST_WORKER: 'vitest_worker' } export = exporters diff --git a/ext/exporters.js b/ext/exporters.js index e4ed4e84360..acdef949d9a 100644 --- a/ext/exporters.js +++ b/ext/exporters.js @@ -7,5 +7,6 @@ module.exports = { JEST_WORKER: 'jest_worker', CUCUMBER_WORKER: 'cucumber_worker', MOCHA_WORKER: 'mocha_worker', - PLAYWRIGHT_WORKER: 'playwright_worker' + PLAYWRIGHT_WORKER: 'playwright_worker', + VITEST_WORKER: 'vitest_worker' } diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 314a847dc44..7753f76bc21 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -132,5 +132,6 @@ module.exports = { vm: () => require('../vm'), when: () => require('../when'), winston: () => require('../winston'), - workerpool: () => require('../mocha') + workerpool: () => require('../mocha'), + tinypool: { esmFirst: true, fn: () => require('../vitest') } } diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index 7513e33efcc..f79d6436259 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -1,7 +1,8 @@ +const v8 = require('node:v8') + const { addHook, channel, AsyncResource } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') const log = require('../../dd-trace/src/log') - // test hooks const testStartCh = channel('ci:vitest:test:start') const testFinishTimeCh = channel('ci:vitest:test:finish-time') @@ -26,6 +27,8 @@ const knownTestsCh = channel('ci:vitest:known-tests') const isEarlyFlakeDetectionFaultyCh = channel('ci:vitest:is-early-flake-detection-faulty') const testManagementTestsCh = channel('ci:vitest:test-management-tests') +const workerReporterCh = channel('ci:vitest:worker-report:trace') + const taskToCtx = new WeakMap() const taskToStatuses = new WeakMap() const newTasks = new WeakSet() @@ -61,6 +64,8 @@ function getProvidedContext () { _ddIsFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled } = globalThis.__vitest_worker__.providedContext + // console.log('globalThis.__vitest_worker__', globalThis.__vitest_worker__) + return { isDiEnabled: _ddIsDiEnabled, isEarlyFlakeDetectionEnabled: _ddIsEarlyFlakeDetectionEnabled, @@ -187,6 +192,11 @@ function getSortWrapper (sort) { let knownTests = {} let testManagementTests = {} + // console.log('this.ctx.vitest', this.ctx.vitest) + // console.log('this.ctx.rpc', this.ctx.rpc) + console.log('__vitest_worker__', global.__vitest_worker__) + // console.log('this.ctx.getCoreWorkspaceProject()', this.ctx.getCoreWorkspaceProject()) + // console.log('this.ctx', this) try { const { err, libraryConfig } = await getChannelPromise(libraryConfigurationCh) if (!err) { @@ -262,24 +272,25 @@ function getSortWrapper (sort) { log.warn('Could not send Dynamic Instrumentation configuration to workers.') } } - - if (isTestManagementTestsEnabled) { - const { err, testManagementTests: receivedTestManagementTests } = await getChannelPromise(testManagementTestsCh) - if (!err) { - testManagementTests = receivedTestManagementTests - try { - const workspaceProject = this.ctx.getCoreWorkspaceProject() - workspaceProject._provided._ddIsTestManagementTestsEnabled = isTestManagementTestsEnabled - workspaceProject._provided._ddTestManagementAttemptToFixRetries = testManagementAttemptToFixRetries - workspaceProject._provided._ddTestManagementTests = testManagementTests - } catch { - log.warn('Could not send test management tests to workers so Test Management will not work.') - } - } else { - isTestManagementTestsEnabled = false - log.error('Could not get test management tests.') - } - } + debugger + + // if (isTestManagementTestsEnabled) { + // const { err, testManagementTests: receivedTestManagementTests } = await getChannelPromise(testManagementTestsCh) + // if (!err) { + // testManagementTests = receivedTestManagementTests + // try { + // const workspaceProject = this.ctx.getCoreWorkspaceProject() + // workspaceProject._provided._ddIsTestManagementTestsEnabled = isTestManagementTestsEnabled + // workspaceProject._provided._ddTestManagementAttemptToFixRetries = testManagementAttemptToFixRetries + // workspaceProject._provided._ddTestManagementTests = testManagementTests + // } catch { + // log.warn('Could not send test management tests to workers so Test Management will not work.') + // } + // } else { + // isTestManagementTestsEnabled = false + // log.error('Could not get test management tests.') + // } + // } let testCodeCoverageLinesTotal @@ -344,6 +355,47 @@ function getCreateCliWrapper (vitestPackage, frameworkVersion) { return vitestPackage } + +// UNUSED RIGHT NOW, but we can use it to bind the async resource to the test fn +// getFn is what's used to get the test fn to run it with vitest: +// https://github.com/vitest-dev/vitest/blob/0cbad1b0d0d56f1ec60f8496678d1435f8bb8977/packages/runner/src/run.ts#L315-L321 +let getFn = null + +// run in workers only +addHook({ + name: '@vitest/runner', + versions: ['>=1.6.0'], + file: 'dist/index.js' +}, (suitePackage) => { + getFn = suitePackage.getFn + + return suitePackage +}) + +addHook({ + name: 'tinypool', + versions: ['>=1.0.0'], + file: 'dist/index.js' +}, (TinyPool) => { + debugger + // we can pass handle here to the worker, and then use it to send messages to the main process + shimmer.wrap(TinyPool.prototype, 'run', run => function (_, { channel }) { + const res = run.apply(this, arguments) + + this.threads.forEach(thread => { + thread.process.on('message', (message) => { + if (message.__tinypool_worker_message__ && message.data) { + workerReporterCh.publish(message.data) + } + }) + }) + + return res + }) + + return TinyPool +}) + addHook({ name: 'vitest', versions: ['>=1.6.0'], @@ -354,6 +406,7 @@ addHook({ // `onBeforeRunTask` is run before any repetition or attempt is run // `onBeforeRunTask` is an async function shimmer.wrap(VitestTestRunner.prototype, 'onBeforeRunTask', onBeforeRunTask => function (task) { + // console.log('on before run task', process.env) const testName = getTestName(task) const { @@ -420,6 +473,7 @@ addHook({ // `onAfterRunTask` is run after all repetitions or attempts are run // `onAfterRunTask` is an async function shimmer.wrap(VitestTestRunner.prototype, 'onAfterRunTask', onAfterRunTask => function (task) { + // console.log('task', task) const { isEarlyFlakeDetectionEnabled, isTestManagementTestsEnabled } = getProvidedContext() if (isTestManagementTestsEnabled) { @@ -819,6 +873,7 @@ addHook({ testSuiteFinishCh.publish({ status: testSuiteResult.state, onFinish, ...testSuiteCtx.currentStore }) + // console.log('test usite finish!') // TODO: fix too frequent flushes await onFinishPromise diff --git a/packages/datadog-plugin-vitest/src/index.js b/packages/datadog-plugin-vitest/src/index.js index 93066f71d5e..64a4c076b12 100644 --- a/packages/datadog-plugin-vitest/src/index.js +++ b/packages/datadog-plugin-vitest/src/index.js @@ -35,6 +35,7 @@ const { TELEMETRY_TEST_SESSION } = require('../../dd-trace/src/ci-visibility/telemetry') const { DD_MAJOR } = require('../../../version') +const id = require('../../dd-trace/src/id') // Milliseconds that we subtract from the error test duration // so that they do not overlap with the following test @@ -386,6 +387,22 @@ class VitestPlugin extends CiPlugin { }) this.tracer._exporter.flush(onFinish) }) + + this.addSub('ci:vitest:worker-report:trace', (traces) => { + // it has no test session or test module id so there are hanging + const formattedTraces = JSON.parse(traces).map(trace => { + return trace.map(span => ({ + ...span, + span_id: id(span.span_id), + trace_id: id(span.trace_id), + parent_id: id(span.parent_id) + })) + }) + + formattedTraces.forEach(trace => { + this.tracer._exporter.export(trace) + }) + }) } getTestProperties (testManagementTests, testSuite, testName) { diff --git a/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js b/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js index 4aca67ee72f..0145c5b2205 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +++ b/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js @@ -7,7 +7,8 @@ const { CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, MOCHA_WORKER_TRACE_PAYLOAD_CODE, JEST_WORKER_LOGS_PAYLOAD_CODE, - PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE + PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE, + VITEST_WORKER_TRACE_PAYLOAD_CODE } = require('../../../plugins/util/test') function getInterprocessTraceCode () { @@ -23,6 +24,9 @@ function getInterprocessTraceCode () { if (process.env.DD_PLAYWRIGHT_WORKER) { return PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE } + if (process.env.VITEST_WORKER_ID) { + return VITEST_WORKER_TRACE_PAYLOAD_CODE + } return null } diff --git a/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js b/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js index f32698df944..84f8352ac1f 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js @@ -34,10 +34,19 @@ class Writer { // See cucumber code: // https://github.com/cucumber/cucumber-js/blob/5ce371870b677fe3d1a14915dc535688946f734c/src/runtime/parallel/run_worker.ts#L13 if (process.send) { // it only works if process.send is available - process.send([this._interprocessCode, data], () => { - onDone() - }) + if (process.env.TINYPOOL_WORKER_ID) { + // in vitest we have to trick the main process into thinking these are messages from + // tinypool so they are not rejected + process.send({ __tinypool_worker_message__: true, data }, () => { + onDone() + }) + } else { + process.send([this._interprocessCode, data], () => { + onDone() + }) + } } + // onDone() } } diff --git a/packages/dd-trace/src/exporter.js b/packages/dd-trace/src/exporter.js index 220e18fe82e..5a1b6874568 100644 --- a/packages/dd-trace/src/exporter.js +++ b/packages/dd-trace/src/exporter.js @@ -21,6 +21,7 @@ module.exports = name => { case exporters.CUCUMBER_WORKER: case exporters.MOCHA_WORKER: case exporters.PLAYWRIGHT_WORKER: + case exporters.VITEST_WORKER: return require('./ci-visibility/exporters/test-worker') default: return inAWSLambda && !usingLambdaExtension ? require('./exporters/log') : require('./exporters/agent') diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 55a1a7a7432..b768d28c78f 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -69,6 +69,7 @@ module.exports = { get 'mocha-each' () { return require('../../../datadog-plugin-mocha/src') }, get vitest () { return require('../../../datadog-plugin-vitest/src') }, get workerpool () { return require('../../../datadog-plugin-mocha/src') }, + get tinypool () { return require('../../../datadog-plugin-vitest/src') }, get moleculer () { return require('../../../datadog-plugin-moleculer/src') }, get mongodb () { return require('../../../datadog-plugin-mongodb-core/src') }, get 'mongodb-core' () { return require('../../../datadog-plugin-mongodb-core/src') }, diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 99338f14981..afa4d3ddd69 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -99,6 +99,9 @@ const MOCHA_WORKER_TRACE_PAYLOAD_CODE = 80 // playwright worker variables const PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE = 90 +// vitest worker variables +const VITEST_WORKER_TRACE_PAYLOAD_CODE = 100 + // Early flake detection util strings const EFD_STRING = "Retried by Datadog's Early Flake Detection" const EFD_TEST_NAME_REGEX = new RegExp(EFD_STRING + ' \\(#\\d+\\): ', 'g') @@ -172,6 +175,7 @@ module.exports = { CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, MOCHA_WORKER_TRACE_PAYLOAD_CODE, PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE, + VITEST_WORKER_TRACE_PAYLOAD_CODE, TEST_SOURCE_START, TEST_SKIPPED_BY_ITR, TEST_IS_NEW, From 0fd3ec07dca732f838a1b3714ecfed8c24ff5e3b Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Tue, 3 Jun 2025 15:21:56 +0200 Subject: [PATCH 2/7] better process handler --- packages/datadog-instrumentations/src/vitest.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index f79d6436259..2b5e8dcbc27 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -372,17 +372,22 @@ addHook({ return suitePackage }) +const processToHandler = new WeakMap() + addHook({ name: 'tinypool', versions: ['>=1.0.0'], file: 'dist/index.js' }, (TinyPool) => { - debugger // we can pass handle here to the worker, and then use it to send messages to the main process shimmer.wrap(TinyPool.prototype, 'run', run => function (_, { channel }) { const res = run.apply(this, arguments) this.threads.forEach(thread => { + if (processToHandler.has(thread.process)) { + return + } + processToHandler.set(thread.process, true) thread.process.on('message', (message) => { if (message.__tinypool_worker_message__ && message.data) { workerReporterCh.publish(message.data) From 1db246769de33f94085eb40c9b1f7306d44773c4 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Wed, 11 Jun 2025 16:40:56 +0200 Subject: [PATCH 3/7] fix performance --- .../datadog-instrumentations/src/vitest.js | 37 ++++++++++--------- .../exporters/test-worker/writer.js | 8 +++- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index e7aca478577..8c9941ec3e7 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -369,7 +369,6 @@ function getCreateCliWrapper (vitestPackage, frameworkVersion) { return vitestPackage } - // UNUSED RIGHT NOW, but we can use it to bind the async resource to the test fn // getFn is what's used to get the test fn to run it with vitest: // https://github.com/vitest-dev/vitest/blob/0cbad1b0d0d56f1ec60f8496678d1435f8bb8977/packages/runner/src/run.ts#L315-L321 @@ -386,29 +385,31 @@ addHook({ return suitePackage }) -const processToHandler = new WeakMap() +const processToHandler = new WeakSet() + +function threadHandler (thread) { + if (processToHandler.has(thread.process)) { + return + } + processToHandler.add(thread.process) + thread.process.on('message', (message) => { + if (message.__tinypool_worker_message__ && message.data) { + workerReporterCh.publish(message.data) + } + }) +} addHook({ name: 'tinypool', versions: ['>=1.0.0'], file: 'dist/index.js' }, (TinyPool) => { - // we can pass handle here to the worker, and then use it to send messages to the main process - shimmer.wrap(TinyPool.prototype, 'run', run => function (_, { channel }) { - const res = run.apply(this, arguments) - - this.threads.forEach(thread => { - if (processToHandler.has(thread.process)) { - return - } - processToHandler.set(thread.process, true) - thread.process.on('message', (message) => { - if (message.__tinypool_worker_message__ && message.data) { - workerReporterCh.publish(message.data) - } - }) - }) - + shimmer.wrap(TinyPool.prototype, 'run', run => async function () { + // we need to do this before and after because the threads list gets recycled + // (the processes are re-created) + this.threads.forEach(threadHandler) + const res = await run.apply(this, arguments) + this.threads.forEach(threadHandler) return res }) diff --git a/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js b/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js index 84f8352ac1f..a045fcf7670 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js @@ -1,5 +1,8 @@ 'use strict' const { JSONEncoder } = require('../../encode/json-encoder') +const { getEnvironmentVariable } = require('../../../config-helper') + +const isVitestWorker = !!getEnvironmentVariable('TINYPOOL_WORKER_ID') class Writer { constructor (interprocessCode) { @@ -34,7 +37,7 @@ class Writer { // See cucumber code: // https://github.com/cucumber/cucumber-js/blob/5ce371870b677fe3d1a14915dc535688946f734c/src/runtime/parallel/run_worker.ts#L13 if (process.send) { // it only works if process.send is available - if (process.env.TINYPOOL_WORKER_ID) { + if (isVitestWorker) { // in vitest we have to trick the main process into thinking these are messages from // tinypool so they are not rejected process.send({ __tinypool_worker_message__: true, data }, () => { @@ -45,8 +48,9 @@ class Writer { onDone() }) } + } else { + onDone() } - // onDone() } } From d60730dd3e459f46d4f266bd0650eab8e660759a Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Wed, 11 Jun 2025 17:01:37 +0200 Subject: [PATCH 4/7] remove unused code --- .../src/ci-visibility/exporters/test-worker/index.js | 6 +----- packages/dd-trace/src/plugins/util/test.js | 4 ---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js b/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js index 95bdeb813bd..3575fb10758 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +++ b/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js @@ -7,8 +7,7 @@ const { CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, MOCHA_WORKER_TRACE_PAYLOAD_CODE, JEST_WORKER_LOGS_PAYLOAD_CODE, - PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE, - VITEST_WORKER_TRACE_PAYLOAD_CODE + PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE } = require('../../../plugins/util/test') const { getEnvironmentVariable } = require('../../../config-helper') @@ -25,9 +24,6 @@ function getInterprocessTraceCode () { if (getEnvironmentVariable('DD_PLAYWRIGHT_WORKER')) { return PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE } - if (process.env.VITEST_WORKER_ID) { - return VITEST_WORKER_TRACE_PAYLOAD_CODE - } return null } diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 72969a7a7dc..1a11f4bc7ac 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -115,9 +115,6 @@ const MOCHA_WORKER_TRACE_PAYLOAD_CODE = 80 // playwright worker variables const PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE = 90 -// vitest worker variables -const VITEST_WORKER_TRACE_PAYLOAD_CODE = 100 - // Early flake detection util strings const EFD_STRING = "Retried by Datadog's Early Flake Detection" const EFD_TEST_NAME_REGEX = new RegExp(EFD_STRING + String.raw` \(#\d+\): `, 'g') @@ -197,7 +194,6 @@ module.exports = { CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, MOCHA_WORKER_TRACE_PAYLOAD_CODE, PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE, - VITEST_WORKER_TRACE_PAYLOAD_CODE, TEST_SOURCE_START, TEST_SKIPPED_BY_ITR, TEST_IS_NEW, From d846d486c817ec3503b8ae6dfb5041128cc9fc9f Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Wed, 25 Jun 2025 15:21:01 +0200 Subject: [PATCH 5/7] clean up and add test --- integration-tests/vitest/vitest.spec.js | 8 +++ .../datadog-instrumentations/src/vitest.js | 58 +++++++------------ packages/datadog-plugin-vitest/src/index.js | 7 ++- .../exporters/test-worker/index.js | 10 +++- .../exporters/test-worker/writer.js | 22 +++---- packages/dd-trace/src/plugins/util/test.js | 6 ++ 6 files changed, 58 insertions(+), 53 deletions(-) diff --git a/integration-tests/vitest/vitest.spec.js b/integration-tests/vitest/vitest.spec.js index 38ceb1e2be4..986fe2cd6a0 100644 --- a/integration-tests/vitest/vitest.spec.js +++ b/integration-tests/vitest/vitest.spec.js @@ -2054,5 +2054,13 @@ versions.forEach((version) => { }) }) }) + + it.skip('does not blow up when tinypool is used outside of a test', (done) => { + // childProcess = exec('node ./ci-visibility/run-workerpool.js', { + // cwd, + // env: getCiVisAgentlessConfig(receiver.port), + // stdio: 'pipe' + // }) + }) }) }) diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index db816754705..ad2b88b3373 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -1,6 +1,13 @@ +'use strict' + const { addHook, channel } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') const log = require('../../dd-trace/src/log') +const { + VITEST_WORKER_TRACE_PAYLOAD_CODE, + VITEST_WORKER_LOGS_PAYLOAD_CODE +} = require('../../dd-trace/src/plugins/util/test') + // test hooks const testStartCh = channel('ci:vitest:test:start') const testFinishTimeCh = channel('ci:vitest:test:finish-time') @@ -27,7 +34,8 @@ const isEarlyFlakeDetectionFaultyCh = channel('ci:vitest:is-early-flake-detectio const testManagementTestsCh = channel('ci:vitest:test-management-tests') const impactedTestsCh = channel('ci:vitest:modified-tests') -const workerReporterCh = channel('ci:vitest:worker-report:trace') +const workerReportTraceCh = channel('ci:vitest:worker-report:trace') +const workerReportLogsCh = channel('ci:vitest:worker-report:logs') const taskToCtx = new WeakMap() const taskToStatuses = new WeakMap() @@ -39,6 +47,7 @@ const modifiedTasks = new WeakSet() let isRetryReasonEfd = false let isRetryReasonAttemptToFix = false const switchedStatuses = new WeakSet() +const workerProcesses = new WeakSet() const BREAKPOINT_HIT_GRACE_PERIOD_MS = 400 @@ -66,8 +75,6 @@ function getProvidedContext () { _ddModifiedTests: modifiedTests } = globalThis.__vitest_worker__.providedContext - // console.log('globalThis.__vitest_worker__', globalThis.__vitest_worker__) - return { isDiEnabled: _ddIsDiEnabled, isEarlyFlakeDetectionEnabled: _ddIsEarlyFlakeDetectionEnabled, @@ -195,11 +202,6 @@ function getSortWrapper (sort, frameworkVersion) { let testManagementAttemptToFixRetries = 0 let isDiEnabled = false - // console.log('this.ctx.vitest', this.ctx.vitest) - // console.log('this.ctx.rpc', this.ctx.rpc) - console.log('__vitest_worker__', global.__vitest_worker__) - // console.log('this.ctx.getCoreWorkspaceProject()', this.ctx.getCoreWorkspaceProject()) - // console.log('this.ctx', this) try { const { err, libraryConfig } = await getChannelPromise(libraryConfigurationCh, frameworkVersion) if (!err) { @@ -275,7 +277,6 @@ function getSortWrapper (sort, frameworkVersion) { log.warn('Could not send Dynamic Instrumentation configuration to workers.') } } - debugger if (isTestManagementTestsEnabled) { const { err, testManagementTests: receivedTestManagementTests } = await getChannelPromise(testManagementTestsCh) @@ -369,32 +370,18 @@ function getCreateCliWrapper (vitestPackage, frameworkVersion) { return vitestPackage } -// UNUSED RIGHT NOW, but we can use it to bind the async resource to the test fn -// getFn is what's used to get the test fn to run it with vitest: -// https://github.com/vitest-dev/vitest/blob/0cbad1b0d0d56f1ec60f8496678d1435f8bb8977/packages/runner/src/run.ts#L315-L321 -let getFn = null - -// run in workers only -addHook({ - name: '@vitest/runner', - versions: ['>=1.6.0'], - file: 'dist/index.js' -}, (suitePackage) => { - getFn = suitePackage.getFn - - return suitePackage -}) - -const processToHandler = new WeakSet() - function threadHandler (thread) { - if (processToHandler.has(thread.process)) { + if (workerProcesses.has(thread.process)) { return } - processToHandler.add(thread.process) + workerProcesses.add(thread.process) thread.process.on('message', (message) => { if (message.__tinypool_worker_message__ && message.data) { - workerReporterCh.publish(message.data) + if (message.interprocessCode === VITEST_WORKER_TRACE_PAYLOAD_CODE) { + workerReportTraceCh.publish(message.data) + } else if (message.interprocessCode === VITEST_WORKER_LOGS_PAYLOAD_CODE) { + workerReportLogsCh.publish(message.data) + } } }) } @@ -405,12 +392,11 @@ addHook({ file: 'dist/index.js' }, (TinyPool) => { shimmer.wrap(TinyPool.prototype, 'run', run => async function () { - // we need to do this before and after because the threads list gets recycled - // (the processes are re-created) + // We have to do this before and after because the threads list gets recycled, that is, the processes are re-created this.threads.forEach(threadHandler) - const res = await run.apply(this, arguments) + const runResult = await run.apply(this, arguments) this.threads.forEach(threadHandler) - return res + return runResult }) return TinyPool @@ -426,7 +412,6 @@ addHook({ // `onBeforeRunTask` is run before any repetition or attempt is run // `onBeforeRunTask` is an async function shimmer.wrap(VitestTestRunner.prototype, 'onBeforeRunTask', onBeforeRunTask => function (task) { - // console.log('on before run task', process.env) const testName = getTestName(task) const { @@ -512,7 +497,6 @@ addHook({ // `onAfterRunTask` is run after all repetitions or attempts are run // `onAfterRunTask` is an async function shimmer.wrap(VitestTestRunner.prototype, 'onAfterRunTask', onAfterRunTask => function (task) { - // console.log('task', task) const { isEarlyFlakeDetectionEnabled, isTestManagementTestsEnabled } = getProvidedContext() if (isTestManagementTestsEnabled) { @@ -913,8 +897,6 @@ addHook({ testSuiteFinishCh.publish({ status: testSuiteResult.state, onFinish, ...testSuiteCtx.currentStore }) - // console.log('test usite finish!') - // TODO: fix too frequent flushes await onFinishPromise return startTestsResponse diff --git a/packages/datadog-plugin-vitest/src/index.js b/packages/datadog-plugin-vitest/src/index.js index b320f837c07..40e699c3c34 100644 --- a/packages/datadog-plugin-vitest/src/index.js +++ b/packages/datadog-plugin-vitest/src/index.js @@ -404,7 +404,6 @@ class VitestPlugin extends CiPlugin { }) this.addSub('ci:vitest:worker-report:trace', (traces) => { - // it has no test session or test module id so there are hanging const formattedTraces = JSON.parse(traces).map(trace => { return trace.map(span => ({ ...span, @@ -418,6 +417,12 @@ class VitestPlugin extends CiPlugin { this.tracer._exporter.export(trace) }) }) + + this.addSub('ci:vitest:worker-report:logs', (logsPayloads) => { + JSON.parse(logsPayloads).forEach(({ testConfiguration, logMessage }) => { + this.tracer._exporter.exportDiLogs(testConfiguration, logMessage) + }) + }) } getTestProperties (testManagementTests, testSuite, testName) { diff --git a/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js b/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js index 3575fb10758..81970d015c8 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +++ b/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js @@ -7,7 +7,9 @@ const { CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, MOCHA_WORKER_TRACE_PAYLOAD_CODE, JEST_WORKER_LOGS_PAYLOAD_CODE, - PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE + PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE, + VITEST_WORKER_TRACE_PAYLOAD_CODE, + VITEST_WORKER_LOGS_PAYLOAD_CODE } = require('../../../plugins/util/test') const { getEnvironmentVariable } = require('../../../config-helper') @@ -24,6 +26,9 @@ function getInterprocessTraceCode () { if (getEnvironmentVariable('DD_PLAYWRIGHT_WORKER')) { return PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE } + if (getEnvironmentVariable('TINYPOOL_WORKER_ID')) { + return VITEST_WORKER_TRACE_PAYLOAD_CODE + } return null } @@ -39,6 +44,9 @@ function getInterprocessLogsCode () { if (getEnvironmentVariable('JEST_WORKER_ID')) { return JEST_WORKER_LOGS_PAYLOAD_CODE } + if (getEnvironmentVariable('TINYPOOL_WORKER_ID')) { + return VITEST_WORKER_LOGS_PAYLOAD_CODE + } return null } diff --git a/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js b/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js index a045fcf7670..50914ba9608 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js @@ -2,8 +2,6 @@ const { JSONEncoder } = require('../../encode/json-encoder') const { getEnvironmentVariable } = require('../../../config-helper') -const isVitestWorker = !!getEnvironmentVariable('TINYPOOL_WORKER_ID') - class Writer { constructor (interprocessCode) { this._encoder = new JSONEncoder() @@ -37,17 +35,15 @@ class Writer { // See cucumber code: // https://github.com/cucumber/cucumber-js/blob/5ce371870b677fe3d1a14915dc535688946f734c/src/runtime/parallel/run_worker.ts#L13 if (process.send) { // it only works if process.send is available - if (isVitestWorker) { - // in vitest we have to trick the main process into thinking these are messages from - // tinypool so they are not rejected - process.send({ __tinypool_worker_message__: true, data }, () => { - onDone() - }) - } else { - process.send([this._interprocessCode, data], () => { - onDone() - }) - } + const isVitestWorker = !!getEnvironmentVariable('TINYPOOL_WORKER_ID') + + const payload = isVitestWorker + ? { __tinypool_worker_message__: true, interprocessCode: this._interprocessCode, data } + : [this._interprocessCode, data] + + process.send(payload, () => { + onDone() + }) } else { onDone() } diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 26db429ac31..87320edb6c2 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -116,6 +116,10 @@ const MOCHA_WORKER_TRACE_PAYLOAD_CODE = 80 // playwright worker variables const PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE = 90 +// vitest worker variables +const VITEST_WORKER_TRACE_PAYLOAD_CODE = 100 +const VITEST_WORKER_LOGS_PAYLOAD_CODE = 102 + // Early flake detection util strings const EFD_STRING = "Retried by Datadog's Early Flake Detection" const EFD_TEST_NAME_REGEX = new RegExp(EFD_STRING + String.raw` \(#\d+\): `, 'g') @@ -211,6 +215,8 @@ module.exports = { CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, MOCHA_WORKER_TRACE_PAYLOAD_CODE, PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE, + VITEST_WORKER_TRACE_PAYLOAD_CODE, + VITEST_WORKER_LOGS_PAYLOAD_CODE, TEST_SOURCE_START, TEST_SKIPPED_BY_ITR, TEST_IS_NEW, From bc7089f7d1e925c861ada55c7695a818a1e46956 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Wed, 25 Jun 2025 15:40:59 +0200 Subject: [PATCH 6/7] add tinypool test --- .../ci-visibility/run-tinypool.mjs | 13 ++++++++++ .../ci-visibility/tinypool-worker.mjs | 3 +++ integration-tests/vitest/vitest.spec.js | 26 ++++++++++++++----- 3 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 integration-tests/ci-visibility/run-tinypool.mjs create mode 100644 integration-tests/ci-visibility/tinypool-worker.mjs diff --git a/integration-tests/ci-visibility/run-tinypool.mjs b/integration-tests/ci-visibility/run-tinypool.mjs new file mode 100644 index 00000000000..b6791bfb2c8 --- /dev/null +++ b/integration-tests/ci-visibility/run-tinypool.mjs @@ -0,0 +1,13 @@ +import Tinypool from 'tinypool' + +const pool = new Tinypool({ + filename: new URL('./tinypool-worker.mjs', import.meta.url).href, +}) + +const result = await pool.run({ a: 4, b: 6 }) +// eslint-disable-next-line no-console +console.log('result', result) + +// Make sure to destroy pool once it's not needed anymore +// This terminates all pool's idle workers +await pool.destroy() diff --git a/integration-tests/ci-visibility/tinypool-worker.mjs b/integration-tests/ci-visibility/tinypool-worker.mjs new file mode 100644 index 00000000000..e06ea054b1b --- /dev/null +++ b/integration-tests/ci-visibility/tinypool-worker.mjs @@ -0,0 +1,3 @@ +export default ({ a, b }) => { + return a + b +} diff --git a/integration-tests/vitest/vitest.spec.js b/integration-tests/vitest/vitest.spec.js index 986fe2cd6a0..22f5f88cc3a 100644 --- a/integration-tests/vitest/vitest.spec.js +++ b/integration-tests/vitest/vitest.spec.js @@ -67,7 +67,8 @@ versions.forEach((version) => { sandbox = await createSandbox([ `vitest@${version}`, `@vitest/coverage-istanbul@${version}`, - `@vitest/coverage-v8@${version}` + `@vitest/coverage-v8@${version}`, + 'tinypool' ], true) cwd = sandbox.folder }) @@ -2055,12 +2056,23 @@ versions.forEach((version) => { }) }) - it.skip('does not blow up when tinypool is used outside of a test', (done) => { - // childProcess = exec('node ./ci-visibility/run-workerpool.js', { - // cwd, - // env: getCiVisAgentlessConfig(receiver.port), - // stdio: 'pipe' - // }) + it('does not blow up when tinypool is used outside of a test', (done) => { + childProcess = exec('node ./ci-visibility/run-tinypool.mjs', { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + stdio: 'pipe' + }) + childProcess.stdout.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.stderr.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.on('exit', (code) => { + assert.include(testOutput, 'result 10') + assert.equal(code, 0) + done() + }) }) }) }) From 92c2083565ad601dfa11748cacebd3f18c3fdc9b Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Thu, 26 Jun 2025 16:35:34 +0200 Subject: [PATCH 7/7] remove unnecessary logs --- ci/init.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ci/init.js b/ci/init.js index 7a71eb30a78..c55cbf6ce52 100644 --- a/ci/init.js +++ b/ci/init.js @@ -8,8 +8,7 @@ const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID') const isCucumberWorker = !!getEnvironmentVariable('CUCUMBER_WORKER_ID') const isMochaWorker = !!getEnvironmentVariable('MOCHA_WORKER_ID') -// we can't use VITEST_WORKER_ID because it's set _after_ the worker is initialized -// maybe we can intercept tinypool to inject it to be sure +// We can't use VITEST_WORKER_ID because it's set _after_ the worker is initialized const isVitestWorker = !!getEnvironmentVariable('TINYPOOL_WORKER_ID') const isPlaywrightWorker = !!getEnvironmentVariable('DD_PLAYWRIGHT_WORKER') @@ -19,8 +18,6 @@ const packageManagers = [ 'pnpm' ] -console.log('init', isVitestWorker) - const isPackageManager = () => { return packageManagers.some(packageManager => process.argv[1]?.includes(`bin/${packageManager}`)) } @@ -82,7 +79,6 @@ if (isPlaywrightWorker) { } if (isVitestWorker) { - console.log('initializing as vites tworker') options.experimental = { exporter: 'vitest_worker' }