UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

1,579 lines (1,375 loc) 48.2 kB
'use strict' const path = require('node:path') const { fileURLToPath } = require('node:url') const { MessagePort } = require('node:worker_threads') const shimmer = require('../../datadog-shimmer') const log = require('../../dd-trace/src/log') const { VITEST_WORKER_TRACE_PAYLOAD_CODE, VITEST_WORKER_LOGS_PAYLOAD_CODE, getMaxEfdRetryCount, collectTestOptimizationSummariesFromTraces, logTestOptimizationSummary, getTestOptimizationRequestResults, getTestSuitePath, isModifiedTest, } = require('../../dd-trace/src/plugins/util/test') const { getChannelPromise } = require('./helpers/channel') const { addHook } = require('./helpers/instrument') const noWorkerInit = require('./vitest-main-no-worker-init') const { testStartCh, testPassCh, testErrorCh, testSkipCh, testSuiteStartCh, testSuiteFinishCh, testSuiteErrorCh, testSessionStartCh, testSessionFinishCh, testSessionConfigurationCh, libraryConfigurationCh, knownTestsCh, isEarlyFlakeDetectionFaultyCh, testManagementTestsCh, modifiedFilesCh, workerReportTraceCh, workerReportLogsCh, codeCoverageReportCh, findExportByName, getTypeTasks, getWorkspaceProject, setProvidedContext, getVitestTestProperties, } = require('./vitest-util') const newTestsWithDynamicNames = new Set() const attemptToFixExecutions = new Map() const workerProcesses = new WeakSet() const mainProcessSetupStates = new WeakMap() const coverageWrappedProviders = new WeakSet() const finishWrappedContexts = new WeakSet() let isFlakyTestRetriesEnabled = false let flakyTestRetriesCount = 0 let isEarlyFlakeDetectionEnabled = false let earlyFlakeDetectionNumRetries = 0 let earlyFlakeDetectionSlowTestRetries = {} let isEarlyFlakeDetectionFaulty = false let isKnownTestsEnabled = false let isTestManagementTestsEnabled = false let isImpactedTestsEnabled = false let testManagementAttemptToFixRetries = 0 let isDiEnabled = false let testCodeCoverageLinesTotal let coverageRootDir let requestErrorTags = {} let isSessionStarted = false let isVitestNoWorkerInitActive = false let vitestPool = null let isMessagePortWrapped = false const tinyPoolClassWrappers = new WeakMap() function getConfiguredEfdRetryCount (slowTestRetries, fallbackRetryCount) { if (!slowTestRetries || !Object.keys(slowTestRetries).length) { return fallbackRetryCount } return getMaxEfdRetryCount(slowTestRetries) } function getTestCommand () { return `vitest ${process.argv.slice(2).join(' ')}` } function isValidKnownTests (receivedKnownTests) { return !!receivedKnownTests.vitest } function isReporterPackage (vitestPackage) { return vitestPackage.B?.name === 'BaseSequencer' } function isReporterPackageNew (vitestPackage) { return vitestPackage.e?.name === 'BaseSequencer' } function isReporterPackageNewest (vitestPackage) { return vitestPackage.h?.name === 'BaseSequencer' } function getBaseSequencerExport (vitestPackage) { return findExportByName(vitestPackage, 'BaseSequencer') } function isCliApiPackage (vitestPackage) { return !!findExportByName(vitestPackage, 'startVitest') } function getVitestExport (vitestPackage) { return findExportByName(vitestPackage, 'Vitest') } function getTypecheckerExport (vitestPackage) { return findExportByName(vitestPackage, 'Typechecker') } function getForksPoolWorkerExport (vitestPackage) { return findExportByName(vitestPackage, 'ForksPoolWorker') } function getThreadsPoolWorkerExport (vitestPackage) { return findExportByName(vitestPackage, 'ThreadsPoolWorker') } function getSessionStatus (state) { if (state.getCountOfFailedTests() > 0) { return 'fail' } if (state.pathsSet.size === 0) { return 'skip' } return 'pass' } function getTestFilepathsFromSpecifications (testSpecifications) { if (!Array.isArray(testSpecifications) || !testSpecifications.length) { return } return testSpecifications.map(testSpecification => { const testFile = Array.isArray(testSpecification) ? testSpecification[1] : testSpecification return testFile?.moduleId || testFile?.filepath || testFile }) } function getTestSpecificationsKey (testSpecifications) { if (!Array.isArray(testSpecifications) || !testSpecifications.length) return const keyParts = [] for (const testSpecification of testSpecifications) { const testFile = Array.isArray(testSpecification) ? testSpecification[1] : testSpecification const testFilepath = testFile?.moduleId || testFile?.filepath || testFile if (!testFilepath) continue const projectName = getProjectName(getTestSpecificationProject(testSpecification)) || '' const pool = getTestSpecificationPool(testSpecification) || '' keyParts.push(`${projectName}\0${pool}\0${testFilepath}`) } if (!keyParts.length) return keyParts.sort() return keyParts.join('\0') } function getTestFilepaths (ctx, testSpecifications) { const testFilepaths = getTestFilepathsFromSpecifications(testSpecifications) if (testFilepaths) { return testFilepaths } const getFilePaths = ctx.getTestFilepaths || ctx._globTestFilepaths return getFilePaths.call(ctx) } /** * @typedef {{ * isAttemptToFix?: boolean, * isDisabled?: boolean, * isQuarantined?: boolean * }} VitestTestManagementProperties * * @typedef {{ * testSuite?: string, * knownTests?: string[], * testManagementTests?: Record<string, VitestTestManagementProperties>, * isModified?: boolean * }} VitestTestProperties */ /** * Normalize a Vitest test file path to the test suite path used by Test Optimization APIs. * * @param {string} testFilepath * @param {string} repositoryRoot * @returns {string} */ function getNormalizedTestSuitePath (testFilepath, repositoryRoot) { const testSuiteAbsolutePath = path.isAbsolute(testFilepath) ? testFilepath : path.join(repositoryRoot, testFilepath) return getTestSuitePath(testSuiteAbsolutePath, repositoryRoot) } /** * Build simplified Test Management metadata grouped by normalized test suite path. * * @param {{ vitest?: { suites?: Record<string, { tests?: Record<string, { properties?: { * attempt_to_fix?: boolean, * disabled?: boolean, * quarantined?: boolean * } }> }> } }} testManagementTests * @returns {Record<string, Record<string, VitestTestManagementProperties>>} */ function getTestManagementTestsBySuite (testManagementTests) { const testManagementTestsBySuite = {} const suites = testManagementTests?.vitest?.suites if (!suites) return testManagementTestsBySuite for (const [testSuite, suite] of Object.entries(suites)) { const tests = suite?.tests if (!tests) continue const testsByName = {} let hasTests = false for (const [testName, test] of Object.entries(tests)) { const properties = test?.properties const testProperties = { isAttemptToFix: properties?.attempt_to_fix, isDisabled: properties?.disabled, isQuarantined: properties?.quarantined, } testsByName[testName] = testProperties hasTests = true } if (hasTests) { testManagementTestsBySuite[testSuite] = testsByName } } return testManagementTestsBySuite } /** * Build a set-like object for test suites modified in the current pull request diff. * * @param {Record<string, number[]>|undefined} modifiedFiles * @returns {Record<string, boolean>} */ function getImpactedTestSuites (modifiedFiles) { const impactedTestSuites = {} if (!modifiedFiles) return impactedTestSuites for (const testSuite of Object.keys(modifiedFiles)) { if (isModifiedTest(testSuite, 0, 0, modifiedFiles, 'vitest')) { impactedTestSuites[testSuite] = true } } return impactedTestSuites } /** * Build the worker-ready test metadata map keyed by Vitest's absolute filepath. * * @param {string[]} testFilepaths * @param {string} repositoryRoot * @param {Record<string, string[]>|undefined} knownTestsBySuite * @param {Record<string, Record<string, VitestTestManagementProperties>>|undefined} testManagementTestsBySuite * @param {Record<string, boolean>|undefined} impactedTestSuites * @returns {Record<string, VitestTestProperties>} */ function getTestPropertiesByFilepath ( testFilepaths, repositoryRoot, knownTestsBySuite, testManagementTestsBySuite, impactedTestSuites ) { const testPropertiesByFilepath = {} if (!Array.isArray(testFilepaths)) return testPropertiesByFilepath for (const testFilepath of testFilepaths) { if (typeof testFilepath !== 'string') continue const testSuite = getNormalizedTestSuitePath(testFilepath, repositoryRoot) const testProperties = { testSuite } const hasProperties = knownTestsBySuite !== undefined || testManagementTestsBySuite !== undefined || impactedTestSuites !== undefined if (knownTestsBySuite) { testProperties.knownTests = knownTestsBySuite[testSuite] || [] } if (testManagementTestsBySuite) { testProperties.testManagementTests = testManagementTestsBySuite[testSuite] || {} } if (impactedTestSuites?.[testSuite]) { testProperties.isModified = true } if (hasProperties) { testPropertiesByFilepath[testFilepath] = testProperties } } return testPropertiesByFilepath } function wrapCoverageProvider (ctx) { const { coverageProvider } = ctx if (!coverageProvider?.generateCoverage || coverageWrappedProviders.has(coverageProvider)) { return } coverageWrappedProviders.add(coverageProvider) // Capture coverage root directory from config (default is 'coverage' in cwd) try { const coverageConfig = ctx.config?.coverage const reportsDirectory = coverageConfig?.reportsDirectory || 'coverage' const rootDir = ctx.config?.root || process.cwd() coverageRootDir = path.isAbsolute(reportsDirectory) ? reportsDirectory : path.join(rootDir, reportsDirectory) } catch { // Fallback to cwd if we can't get config coverageRootDir = process.cwd() } shimmer.wrap(coverageProvider, 'generateCoverage', generateCoverage => async function () { const totalCodeCoverage = await generateCoverage.apply(this, arguments) try { testCodeCoverageLinesTotal = totalCodeCoverage.getCoverageSummary().lines.pct } catch { // ignore errors } return totalCodeCoverage }) } function wrapSessionFinish (ctx) { if (finishWrappedContexts.has(ctx)) { return } finishWrappedContexts.add(ctx) shimmer.wrap(ctx, 'exit', getFinishWrapper) shimmer.wrap(ctx, 'close', getFinishWrapper) } function resetLibraryConfig () { isFlakyTestRetriesEnabled = false flakyTestRetriesCount = 0 isEarlyFlakeDetectionEnabled = false earlyFlakeDetectionNumRetries = 0 earlyFlakeDetectionSlowTestRetries = {} isEarlyFlakeDetectionFaulty = false isDiEnabled = false isKnownTestsEnabled = false isTestManagementTestsEnabled = false isImpactedTestsEnabled = false testManagementAttemptToFixRetries = 0 } function applyLibraryConfig (libraryConfig) { isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {} isEarlyFlakeDetectionFaulty = false isDiEnabled = libraryConfig.isDiEnabled isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries isImpactedTestsEnabled = libraryConfig.isImpactedTestsEnabled } function resetMainProcessProvidedContext (ctx) { setProvidedContext(ctx, { _ddIsDiEnabled: false, _ddIsEarlyFlakeDetectionEnabled: false, _ddEarlyFlakeDetectionNumRetries: 0, _ddEarlyFlakeDetectionSlowTestRetries: {}, _ddIsFlakyTestRetriesEnabled: false, _ddFlakyTestRetriesCount: 0, _ddFlakyTestRetriesIncludesUnnamedProject: false, _ddFlakyTestRetriesProjectNames: undefined, _ddIsImpactedTestsEnabled: false, _ddIsKnownTestsEnabled: false, _ddIsTestManagementTestsEnabled: false, _ddTestManagementAttemptToFixRetries: 0, _ddTestPropertiesByFilepath: {}, }, 'Could not reset Test Optimization context for workers.') } /** * Merges request error tags from a Test Optimization request response into the tags propagated by no-worker mode. * * @param {{ requestErrorTags?: Record<string, string> }|undefined} requestResponse - Request response. */ function mergeRequestErrorTags (requestResponse) { if (requestResponse?.requestErrorTags) { requestErrorTags = { ...requestErrorTags, ...requestResponse.requestErrorTags, } } } async function runMainProcessSetup (ctx, frameworkVersion, testSpecifications, shouldInstallNoWorkerInit) { if (!testSessionFinishCh.hasSubscribers) { return } let repositoryRoot = process.cwd() let testSessionConfiguration let testFilepaths let shouldSendTestProperties = false let testPropertiesByFilepath let knownTests let knownTestsBySuite let testManagementTests let testManagementTestsBySuite let modifiedFiles let impactedTestSuites const getCurrentTestFilepaths = async () => { if (testFilepaths === undefined) { testFilepaths = await getTestFilepaths(ctx, testSpecifications) } return testFilepaths } try { const { err, libraryConfig, requestErrorTags: receivedRequestErrorTags = {}, } = await getChannelPromise(libraryConfigurationCh, { frameworkVersion, isVitestNoWorkerInitActive: shouldInstallNoWorkerInit, }) requestErrorTags = receivedRequestErrorTags if (err) { resetLibraryConfig() } else { applyLibraryConfig(libraryConfig) } } catch { requestErrorTags = {} resetLibraryConfig() } resetMainProcessProvidedContext(ctx) if (testSessionConfigurationCh.hasSubscribers) { testSessionConfiguration = await getChannelPromise(testSessionConfigurationCh, { frameworkVersion }) || {} const { testSessionId, testModuleId, testCommand, repositoryRoot: receivedRepositoryRoot, codeOwnersEntries, } = testSessionConfiguration repositoryRoot = receivedRepositoryRoot || repositoryRoot if (!shouldInstallNoWorkerInit) { setProvidedContext(ctx, { _ddTestSessionId: testSessionId, _ddTestModuleId: testModuleId, _ddTestCommand: testCommand, _ddRepositoryRoot: repositoryRoot, _ddCodeOwnersEntries: codeOwnersEntries, }, 'Could not send test session configuration to workers.') } } const { knownTestsResponse, testManagementTestsResponse, } = await getTestOptimizationRequestResults({ isKnownTestsEnabled, isTestManagementTestsEnabled, getKnownTests: () => getChannelPromise(knownTestsCh), getTestManagementTests: () => getChannelPromise(testManagementTestsCh), }) mergeRequestErrorTags(knownTestsResponse) mergeRequestErrorTags(testManagementTestsResponse) const flakyTestRetriesConfiguration = configureFlakyTestRetries(ctx, testSpecifications) if (flakyTestRetriesConfiguration) { setProvidedContext(ctx, { _ddIsFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled, _ddFlakyTestRetriesCount: flakyTestRetriesCount, _ddFlakyTestRetriesIncludesUnnamedProject: flakyTestRetriesConfiguration.includesUnnamedProject, _ddFlakyTestRetriesProjectNames: flakyTestRetriesConfiguration.projectNames, }, 'Could not send library configuration to workers.') } if (isKnownTestsEnabled) { const currentKnownTestsResponse = knownTestsResponse || await getChannelPromise(knownTestsCh) if (!currentKnownTestsResponse || currentKnownTestsResponse.err) { isEarlyFlakeDetectionEnabled = false } else { knownTests = currentKnownTestsResponse.knownTests const currentTestFilepaths = await getCurrentTestFilepaths() if (isValidKnownTests(knownTests)) { isEarlyFlakeDetectionFaultyCh.publish({ knownTests: knownTests.vitest, testFilepaths: currentTestFilepaths, onDone: (isFaulty) => { isEarlyFlakeDetectionFaulty = isFaulty }, }) if (isEarlyFlakeDetectionFaulty) { isEarlyFlakeDetectionEnabled = false log.warn('New test detection is disabled because the number of new tests is too high.') } else { knownTestsBySuite = knownTests.vitest shouldSendTestProperties = true if (!shouldInstallNoWorkerInit) { setProvidedContext(ctx, { _ddIsKnownTestsEnabled: isKnownTestsEnabled, _ddIsEarlyFlakeDetectionEnabled: isEarlyFlakeDetectionEnabled, _ddEarlyFlakeDetectionNumRetries: getConfiguredEfdRetryCount(earlyFlakeDetectionSlowTestRetries, earlyFlakeDetectionNumRetries), _ddEarlyFlakeDetectionSlowTestRetries: earlyFlakeDetectionSlowTestRetries, }, 'Could not send known tests to workers so Early Flake Detection will not work.') } } } else { isEarlyFlakeDetectionFaulty = true isEarlyFlakeDetectionEnabled = false } } } if (!shouldInstallNoWorkerInit && isDiEnabled) { setProvidedContext(ctx, { _ddIsDiEnabled: isDiEnabled, }, 'Could not send Dynamic Instrumentation configuration to workers.') } if (isTestManagementTestsEnabled) { const testManagementResponse = testManagementTestsResponse || await getChannelPromise(testManagementTestsCh) if (!testManagementResponse || testManagementResponse.err) { isTestManagementTestsEnabled = false log.error('Could not get test management tests.') } else { const { testManagementTests: receivedTestManagementTests } = testManagementResponse testManagementTests = receivedTestManagementTests testManagementTestsBySuite = getTestManagementTestsBySuite(receivedTestManagementTests) shouldSendTestProperties = true if (!shouldInstallNoWorkerInit) { setProvidedContext(ctx, { _ddIsTestManagementTestsEnabled: isTestManagementTestsEnabled, _ddTestManagementAttemptToFixRetries: testManagementAttemptToFixRetries, }, 'Could not send test management tests to workers so Test Management will not work.') } } } if (isImpactedTestsEnabled) { const modifiedFilesResponse = await getChannelPromise(modifiedFilesCh) if (!modifiedFilesResponse || modifiedFilesResponse.err) { log.error('Could not get modified tests.') } else { modifiedFiles = modifiedFilesResponse.modifiedFiles impactedTestSuites = getImpactedTestSuites(modifiedFiles) shouldSendTestProperties = true if (!shouldInstallNoWorkerInit) { setProvidedContext(ctx, { _ddIsImpactedTestsEnabled: isImpactedTestsEnabled, }, 'Could not send modified tests to workers so Impacted Tests will not work.') } } } if (shouldSendTestProperties) { testPropertiesByFilepath = getTestPropertiesByFilepath( await getCurrentTestFilepaths(), repositoryRoot, knownTestsBySuite, testManagementTestsBySuite, impactedTestSuites ) if (!shouldInstallNoWorkerInit) { setProvidedContext(ctx, { _ddTestPropertiesByFilepath: testPropertiesByFilepath, }, 'Could not send test properties to workers so some Test Optimization features will not work.') } } if (shouldInstallNoWorkerInit) { noWorkerInit.configure(ctx, frameworkVersion, testSpecifications, { knownTests, knownTestsBySuite, modifiedFiles, repositoryRoot, flakyTestRetriesConfiguration, testManagementTests, testManagementTestsBySuite, testPropertiesByFilepath: testPropertiesByFilepath || {}, testSessionConfiguration, }, { getConfiguredEfdRetryCount, state: getNoWorkerInitState(), }) } wrapCoverageProvider(ctx) wrapSessionFinish(ctx) return shouldInstallNoWorkerInit } function getNoWorkerInitState () { return { attemptToFixExecutions, earlyFlakeDetectionNumRetries, earlyFlakeDetectionSlowTestRetries, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, isFlakyTestRetriesEnabled, isKnownTestsEnabled, newTestsWithDynamicNames, requestErrorTags, testManagementAttemptToFixRetries, } } function ensureMainProcessSetup (ctx, frameworkVersion, testSpecifications, shouldDeactivateOnFallback = false) { const shouldInstallNoWorkerInit = shouldUseNoWorkerInit(ctx, frameworkVersion, testSpecifications) const specificationsKey = getTestSpecificationsKey(testSpecifications) let setupState = mainProcessSetupStates.get(ctx) if (shouldDeactivateOnFallback && setupState?.shouldInstallNoWorkerInit && !shouldInstallNoWorkerInit) { noWorkerInit.deactivate(ctx) } if ( !setupState || setupState.specificationsKey !== specificationsKey || setupState.shouldInstallNoWorkerInit !== shouldInstallNoWorkerInit ) { setupState = { setupPromise: runMainProcessSetup(ctx, frameworkVersion, testSpecifications, shouldInstallNoWorkerInit), shouldInstallNoWorkerInit, specificationsKey, } mainProcessSetupStates.set(ctx, setupState) } return setupState.setupPromise } function shouldUseNoWorkerInit (ctx, frameworkVersion, testSpecifications) { return noWorkerInit.shouldUse(ctx, frameworkVersion, testSpecifications, { hasVitestWorkerPoolTestSpecification, isVitestWorkerPool, }) } function configureFlakyTestRetries (ctx, testSpecifications) { if (!isFlakyTestRetriesEnabled || flakyTestRetriesCount <= 0) return let configured = false let includesUnnamedProject = false const projectNames = [] for (const { config, projectName } of getVitestProjectConfigs(ctx, testSpecifications)) { if (!config.retry) { config.retry = flakyTestRetriesCount configured = true if (projectName) { projectNames.push(projectName) } else { includesUnnamedProject = true } } } if (!configured) return return { includesUnnamedProject, projectNames, } } function getVitestProjectConfigs (ctx, testSpecifications) { const entries = [] addTestSpecificationConfigs(entries, testSpecifications) if (entries.length > 0) { return entries } const selectedProjectNames = getSelectedProjectNames() addSelectedInlineProjectConfigs(entries, safeConfig(ctx), selectedProjectNames) addSelectedRuntimeProjectConfigs(entries, ctx?.projects, selectedProjectNames) if (entries.length > 0) { return entries } if (Array.isArray(ctx?.projects)) { for (const project of ctx.projects) { addConfig(entries, safeConfig(project), getProjectName(project)) } if (entries.length > 0) { return entries } } addConfig(entries, safeConfig(ctx)) addConfig(entries, safeConfig(safeWorkspaceProject(ctx))) return entries } function addTestSpecificationConfigs (entries, testSpecifications) { if (!Array.isArray(testSpecifications)) return for (const testSpecification of testSpecifications) { const project = getTestSpecificationProject(testSpecification) addConfig(entries, safeConfig(project), getProjectName(project)) } } function addSelectedInlineProjectConfigs (entries, rootConfig, selectedProjectNames) { if (selectedProjectNames.length === 0 || !Array.isArray(rootConfig?.projects)) return for (const project of rootConfig.projects) { const config = getInlineProjectConfig(project) const projectName = getProjectName(project) if (selectedProjectNames.includes(projectName)) { addConfig(entries, config, projectName) } } } function addSelectedRuntimeProjectConfigs (entries, projects, selectedProjectNames) { if (selectedProjectNames.length === 0 || !Array.isArray(projects)) return for (const project of projects) { const projectName = getProjectName(project) if (selectedProjectNames.includes(projectName)) { addConfig(entries, safeConfig(project), projectName) } } } function getSelectedProjectNames () { const names = [] for (let index = 0; index < process.argv.length; index++) { const argument = process.argv[index] if (argument === '--project' && process.argv[index + 1]) { names.push(process.argv[index + 1]) index++ } else if (argument.startsWith('--project=')) { names.push(argument.slice('--project='.length)) } } return names } function getInlineProjectConfig (project) { return project?.test || project } function getProjectName (project) { return normalizeProjectName(project?.name || project?.config?.name || project?.test?.name) } function normalizeProjectName (name) { if (typeof name === 'string') return name const label = name?.label return typeof label === 'string' ? label : undefined } function addConfig (entries, config, projectName) { if (config && !entries.some(entry => entry.config === config || (projectName && entry.projectName === projectName))) { entries.push({ config, projectName }) } } function safeConfig (project) { let config try { config = project?.config } catch {} return config } function safeWorkspaceProject (ctx) { let project try { project = getWorkspaceProject(ctx) } catch {} return project } function getSortWrapper (sort, frameworkVersion) { return async function () { await ensureMainProcessSetup(this.ctx, frameworkVersion, arguments[0]) return sort.apply(this, arguments) } } function getFinishWrapper (exitOrClose) { let isClosed = false return async function () { if (isClosed) { // needed because exit calls close return exitOrClose.apply(this, arguments) } isClosed = true if (!testSessionFinishCh.hasSubscribers) { return exitOrClose.apply(this, arguments) } const failedSuites = this.state.getFailedFilepaths() let error if (failedSuites.length) { error = new Error(`Test suites failed: ${failedSuites.length}.`) } const flushPromise = getChannelPromise(testSessionFinishCh, { status: getSessionStatus(this.state), testCodeCoverageLinesTotal, error, isEarlyFlakeDetectionEnabled, isEarlyFlakeDetectionFaulty, isTestManagementTestsEnabled, requestErrorTags, vitestPool, isVitestNoWorkerInitActive, }) logTestOptimizationSummary({ attemptToFixExecutions, newTestsWithDynamicNames }) await flushPromise // If coverage was generated, publish coverage report channel for upload if (coverageRootDir && codeCoverageReportCh.hasSubscribers) { await getChannelPromise(codeCoverageReportCh, { rootDir: coverageRootDir }) } return exitOrClose.apply(this, arguments) } } function getCliOrStartVitestWrapper (frameworkVersion) { return function (oldCliOrStartVitest) { return function (...args) { if (!testSessionStartCh.hasSubscribers || isSessionStarted) { return oldCliOrStartVitest.apply(this, args) } isSessionStarted = true testSessionStartCh.publish({ command: getTestCommand(), frameworkVersion }) return oldCliOrStartVitest.apply(this, args) } } } function isForkPool (pool) { return pool === 'forks' || pool === 'vmForks' } function isThreadPool (pool) { return pool === 'threads' || pool === 'vmThreads' } function isVitestWorkerPool (pool) { return isForkPool(pool) || isThreadPool(pool) } function getTestSpecificationProject (testSpecification) { if (Array.isArray(testSpecification)) { return testSpecification[0] } return testSpecification?.project } function getTestSpecificationOptions (testSpecification) { if (Array.isArray(testSpecification)) { return testSpecification[2] } return testSpecification } function getTestSpecificationPool (testSpecification) { const options = getTestSpecificationOptions(testSpecification) const project = getTestSpecificationProject(testSpecification) return options?.pool || project?.config?.pool || project?.serializedConfig?.pool || project?.pool || testSpecification?.pool } function hasVitestWorkerPoolTestSpecification (testSpecifications) { if (!Array.isArray(testSpecifications)) { return false } for (const testSpecification of testSpecifications) { if (isVitestWorkerPool(getTestSpecificationPool(testSpecification))) { return true } } return false } function shouldMarkVitestWorkerEnv (pool, testSpecifications, shouldSkipWorkerInit) { if (!shouldSkipWorkerInit) { return isVitestWorkerPool(pool) || hasVitestWorkerPoolTestSpecification(testSpecifications) || (!testSpecifications && pool === undefined) } return isVitestWorkerPool(pool) || pool === undefined || hasVitestWorkerPoolTestSpecification(testSpecifications) } function markVitestWorkerEnv (ctx, testSpecifications, shouldSkipWorkerInit = false) { const config = ctx?.config isVitestNoWorkerInitActive = shouldSkipWorkerInit if (!config || !shouldMarkVitestWorkerEnv(config.pool, testSpecifications, shouldSkipWorkerInit)) { return } config.env = getVitestWorkerEnv(config.env, shouldSkipWorkerInit) } function wrapVitestRunFiles (Vitest, frameworkVersion) { if (!Vitest?.prototype?.runFiles) { return } shimmer.wrap(Vitest.prototype, 'runFiles', runFiles => async function (testSpecifications) { const shouldSkipWorkerInit = await ensureMainProcessSetup(this, frameworkVersion, testSpecifications, true) markVitestWorkerEnv(this, testSpecifications, shouldSkipWorkerInit) return runFiles.apply(this, arguments) }) if (Vitest.prototype.collectTests) { shimmer.wrap(Vitest.prototype, 'collectTests', collectTests => function (testSpecifications) { const shouldSkipWorkerInit = shouldUseNoWorkerInit(this, frameworkVersion, testSpecifications) markVitestWorkerEnv(this, testSpecifications, shouldSkipWorkerInit) return collectTests.apply(this, arguments) }) } } function getTypecheckTaskStatus (task) { const state = task.result?.state if (state === 'fail') return 'fail' if (state === 'skip' || task.mode === 'skip' || task.mode === 'todo') return 'skip' return 'pass' } /** * Return whether a typecheck suite name represents the synthetic file-level suite. * * @param {string|undefined} suiteName * @param {string} testSuiteAbsolutePath * @returns {boolean} */ function isTypecheckFileSuiteName (suiteName, testSuiteAbsolutePath) { if (!suiteName || !testSuiteAbsolutePath) return false const normalizedSuiteName = path.normalize(suiteName).replaceAll('\\', '/') const normalizedSuitePath = path.normalize(testSuiteAbsolutePath).replaceAll('\\', '/') return normalizedSuitePath === normalizedSuiteName || normalizedSuitePath.endsWith(`/${normalizedSuiteName}`) } /** * Return a typecheck test name with describe/suite prefixes and without the file-level suite prefix. * * @param {object} task * @param {string} testSuiteAbsolutePath * @returns {string} */ function getTypecheckTestName (task, testSuiteAbsolutePath) { let testName = task.name || task.fullTestName let currentTask = task.suite while (currentTask) { if (currentTask.name && !isTypecheckFileSuiteName(currentTask.name, testSuiteAbsolutePath)) { testName = `${currentTask.name} ${testName}` } currentTask = currentTask.suite } return testName } /** * Return Test Optimization metadata prepared in Vitest's main process setup. * * @param {object|undefined} ctx * @returns {{ testPropertiesByFilepath: object }} */ function getMainProcessProvidedContext (ctx) { try { const workspaceProject = getWorkspaceProject(ctx) const providedContext = workspaceProject.getProvidedContext?.() || workspaceProject._provided || {} return { testPropertiesByFilepath: providedContext._ddTestPropertiesByFilepath || {}, } } catch { return { testPropertiesByFilepath: {}, } } } /** * Return the Vitest context that owns a Typechecker instance. * * @param {object} typechecker * @returns {object|undefined} */ function getTypecheckerVitestContext (typechecker) { return typechecker.ctx || typechecker.project?.vitest } /** * Apply Test Management result semantics to Vitest's returned typecheck task result. * * @param {object} task * @param {string} status * @param {{ * isAttemptToFix: boolean, * isDisabled: boolean, * isQuarantined: boolean * }} testManagement */ function updateTypecheckTaskResultForTestManagement (task, status, testManagement) { const { isAttemptToFix, isDisabled, isQuarantined } = testManagement if (isAttemptToFix || !task.result) return if (isDisabled) { task.mode = 'skip' task.result.state = 'skip' task.result.errors = [] return } if (isQuarantined && status === 'fail') { task.result.state = 'pass' task.result.errors = [] } } /** * Recompute suite/file typecheck results after Test Management rewrites child test results. * * @param {object} task * @returns {string} */ function updateTypecheckTaskTreeResult (task) { if (!Array.isArray(task.tasks)) return getTypecheckTaskStatus(task) let hasPassedTest = false let hasSkippedTest = false for (const childTask of task.tasks) { const status = updateTypecheckTaskTreeResult(childTask) if (status === 'fail') { task.result = { ...task.result, state: 'fail', } return 'fail' } if (status === 'skip') { hasSkippedTest = true } else { hasPassedTest = true } } if (task.result?.errors?.length) return 'fail' if (!hasPassedTest && !hasSkippedTest) return getTypecheckTaskStatus(task) task.result = { ...task.result, state: hasPassedTest ? 'pass' : 'skip', errors: [], } return task.result.state } /** * Recompute the aggregate typecheck result after Test Management rewrites file results. * * @param {{ * files?: object[], * errors?: object[], * diagnostics?: object[], * sourceErrors?: object[], * state?: string * }} result * @returns {boolean} */ function updateTypecheckResult (result) { if (result.sourceErrors?.length) return false let hasPassedFile = false let hasSkippedFile = false for (const file of result.files) { const status = getTypecheckTaskStatus(file) if (status === 'fail') return false if (status === 'skip') { hasSkippedFile = true } else { hasPassedFile = true } } if (!hasPassedFile && !hasSkippedFile) return false result.state = hasPassedFile ? 'pass' : 'skip' result.errors = [] result.diagnostics = [] result.sourceErrors = [] return true } /** * Clear Vitest's typechecker exit code after all typecheck failures were handled by Test Management. * * @param {{ process?: { exitCode?: number|null } }} typechecker */ function clearTypecheckerExitCode (typechecker) { if (typechecker?.process?.exitCode == null) return typechecker.process.exitCode = 0 } function reportTypecheckTest (task, testSuiteAbsolutePath, providedContext) { const testName = getTypecheckTestName(task, testSuiteAbsolutePath) const testProperties = getVitestTestProperties(providedContext, testSuiteAbsolutePath, testName) const isAttemptToFix = testProperties.isAttemptToFix === true const isDisabled = testProperties.isDisabled === true const isQuarantined = testProperties.isQuarantined === true const isModified = testProperties.isModified === true const isSkippedByTestManagement = !isAttemptToFix && isDisabled const status = getTypecheckTaskStatus(task) if (status === 'skip' || isSkippedByTestManagement) { testSkipCh.publish({ testName, testSuiteAbsolutePath, isNew: testProperties.isNew, isDisabled, }) updateTypecheckTaskResultForTestManagement(task, status, { isAttemptToFix, isDisabled, isQuarantined }) return } const ctx = { testName, testSuiteAbsolutePath, isRetry: false, isNew: testProperties.isNew, hasDynamicName: false, mightHitProbe: false, isAttemptToFix, isDisabled, isQuarantined, isModified, } testStartCh.runStores(ctx, () => {}) const finalStatus = !isAttemptToFix && isQuarantined ? 'skip' : undefined if (status === 'fail') { testErrorCh.publish({ error: task.result?.errors?.[0], finalStatus, ...ctx.currentStore, }) } else { testPassCh.publish({ task, finalStatus, ...ctx.currentStore, }) } updateTypecheckTaskResultForTestManagement(task, status, { isAttemptToFix, isDisabled, isQuarantined }) } async function reportTypecheckFile (file, sessionConfiguration, frameworkVersion, providedContext) { const testSuiteAbsolutePath = file.filepath const testSuiteCtx = { testSuiteAbsolutePath, frameworkVersion, testSessionId: sessionConfiguration.testSessionId, testModuleId: sessionConfiguration.testModuleId, testCommand: sessionConfiguration.testCommand, repositoryRoot: sessionConfiguration.repositoryRoot, codeOwnersEntries: sessionConfiguration.codeOwnersEntries, } testSuiteStartCh.runStores(testSuiteCtx, () => {}) for (const task of getTypeTasks(file.tasks)) { reportTypecheckTest(task, testSuiteAbsolutePath, providedContext) } updateTypecheckTaskTreeResult(file) const testSuiteError = file.result?.errors?.[0] if (testSuiteError) { testSuiteCtx.error = testSuiteError testSuiteErrorCh.runStores(testSuiteCtx, () => {}) } await getChannelPromise(testSuiteFinishCh, { frameworkVersion, status: getTypecheckTaskStatus(file), ...testSuiteCtx.currentStore, }) } async function reportTypecheckResults (result, frameworkVersion, ctx, typechecker) { if (!testSuiteFinishCh.hasSubscribers) return if (!Array.isArray(result?.files)) return if (ctx) { await ensureMainProcessSetup(ctx, frameworkVersion, result.files) } const providedContext = getMainProcessProvidedContext(ctx) const sessionConfiguration = testSessionConfigurationCh.hasSubscribers ? await getChannelPromise(testSessionConfigurationCh, { frameworkVersion }) || {} : {} await Promise.all(result.files.map(file => reportTypecheckFile( file, sessionConfiguration, frameworkVersion, providedContext ))) if (updateTypecheckResult(result)) { clearTypecheckerExitCode(typechecker) } } function wrapTypechecker (Typechecker, frameworkVersion) { if (!Typechecker?.prototype?.prepareResults) return shimmer.wrap(Typechecker.prototype, 'prepareResults', prepareResults => async function () { const result = await prepareResults.apply(this, arguments) await reportTypecheckResults(result, frameworkVersion, getTypecheckerVitestContext(this), this) return result }) } function getTypecheckerWrapper (vitestPackage, frameworkVersion) { const typechecker = getTypecheckerExport(vitestPackage) if (typechecker) { wrapTypechecker(typechecker.value, frameworkVersion) } return vitestPackage } function getCreateCliWrapper (vitestPackage, frameworkVersion) { const createCliExport = findExportByName(vitestPackage, 'createCLI') if (!createCliExport) { return vitestPackage } shimmer.wrap(vitestPackage, createCliExport.key, getCliOrStartVitestWrapper(frameworkVersion)) return vitestPackage } function threadHandler (thread) { const { runtime } = thread let workerProcess if (runtime === 'child_process') { vitestPool = 'child_process' workerProcess = thread.process } else if (runtime === 'worker_threads') { vitestPool = 'worker_threads' workerProcess = thread.thread } else { vitestPool = 'unknown' } if (!workerProcess) { log.error('Vitest error: could not get process or thread from TinyPool#run') return } if (workerProcesses.has(workerProcess)) { return } workerProcesses.add(workerProcess) workerProcess.on('message', (message) => { if (message.__tinypool_worker_message__ && message.data) { handleWorkerReport(message.interprocessCode, message.data) } }) } function isVitestTinypoolOptions (options) { if (options?.env?.VITEST !== 'true' || typeof options.filename !== 'string') return false let filename = options.filename if (filename.startsWith('file:')) { try { filename = fileURLToPath(filename) } catch { return false } } const workerPath = path.normalize(filename) const workerDir = path.dirname(workerPath) const packageDir = path.dirname(workerDir) return path.basename(workerPath) === 'worker.js' && path.basename(workerDir) === 'dist' && path.basename(packageDir) === 'vitest' } function markVitestTinypoolOptions (options) { if (!isVitestTinypoolOptions(options)) return options.env = getVitestWorkerEnv(options.env, isVitestNoWorkerInitActive) } function getVitestWorkerEnv (env = {}, shouldSkipWorkerInit = false) { return noWorkerInit.configureWorkerEnv({ ...env, DD_VITEST_WORKER: '1', }, shouldSkipWorkerInit) } function wrapTinyPoolRun (TinyPool) { if (!TinyPool?.prototype?.run) return shimmer.wrap(TinyPool.prototype, 'run', run => async function () { // 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 runResult = await run.apply(this, arguments) this.threads.forEach(threadHandler) return runResult }) } function wrapTinyPoolClass (TinyPool) { if (typeof TinyPool !== 'function') return TinyPool const wrappedTinyPool = tinyPoolClassWrappers.get(TinyPool) if (wrappedTinyPool) return wrappedTinyPool class DatadogTinyPool extends TinyPool { constructor (options) { markVitestTinypoolOptions(options) super(options) } } tinyPoolClassWrappers.set(TinyPool, DatadogTinyPool) wrapTinyPoolRun(DatadogTinyPool) return DatadogTinyPool } function wrapTinyPool (TinyPool) { if (typeof TinyPool === 'function') { return wrapTinyPoolClass(TinyPool) } const defaultTinyPool = wrapTinyPoolClass(TinyPool?.default) if (defaultTinyPool) { TinyPool.default = defaultTinyPool } const namedTinyPool = TinyPool?.Tinypool === TinyPool?.default ? defaultTinyPool : wrapTinyPoolClass(TinyPool?.Tinypool) if (namedTinyPool) { TinyPool.Tinypool = namedTinyPool } return TinyPool } function getWrappedOn (on) { return function (event, callback) { if (event !== 'message') { return on.apply(this, arguments) } // `arguments[1]` is the callback function, which // we modify to intercept our messages to not interfere // with vitest's own messages arguments[1] = shimmer.wrapFunction(callback, callback => function (message) { if (message.type !== 'Buffer' && Array.isArray(message)) { const [interprocessCode, data] = message if (handleWorkerReport(interprocessCode, data)) { // If we execute the callback vitest crashes, as the message is not supported return } } return callback.apply(this, arguments) }) return on.apply(this, arguments) } } function handleWorkerReport (interprocessCode, data) { if (interprocessCode === VITEST_WORKER_TRACE_PAYLOAD_CODE) { collectTestOptimizationSummariesFromTraces(data, { newTestsWithDynamicNames, attemptToFixExecutions, }) workerReportTraceCh.publish(data) return true } if (interprocessCode === VITEST_WORKER_LOGS_PAYLOAD_CODE) { workerReportLogsCh.publish(data) return true } return false } function wrapMessagePortOn () { if (isMessagePortWrapped) return isMessagePortWrapped = true shimmer.wrap(MessagePort.prototype, 'on', getWrappedOn) shimmer.wrap(MessagePort.prototype, 'addListener', getWrappedOn) } function getStartVitestWrapper (cliApiPackage, frameworkVersion) { if (!isCliApiPackage(cliApiPackage)) { return cliApiPackage } const startVitestExport = findExportByName(cliApiPackage, 'startVitest') shimmer.wrap(cliApiPackage, startVitestExport.key, getCliOrStartVitestWrapper(frameworkVersion)) wrapMessagePortOn() const vitest = getVitestExport(cliApiPackage) if (vitest) { wrapVitestRunFiles(vitest.value, frameworkVersion) } const forksPoolWorker = getForksPoolWorkerExport(cliApiPackage) if (forksPoolWorker) { // function is async shimmer.wrap(forksPoolWorker.value.prototype, 'start', start => function (...args) { vitestPool = 'child_process' this.env = getVitestWorkerEnv(this.env, isVitestNoWorkerInitActive) return start.apply(this, args) }) shimmer.wrap(forksPoolWorker.value.prototype, 'on', getWrappedOn) } const threadsPoolWorker = getThreadsPoolWorkerExport(cliApiPackage) if (threadsPoolWorker) { // function is async shimmer.wrap(threadsPoolWorker.value.prototype, 'start', start => function (...args) { vitestPool = 'worker_threads' this.env = getVitestWorkerEnv(this.env, isVitestNoWorkerInitActive) return start.apply(this, args) }) shimmer.wrap(threadsPoolWorker.value.prototype, 'on', getWrappedOn) } return cliApiPackage } addHook({ name: 'tinypool', // version from tinypool@0.8 was used in vitest@1.6.0 versions: ['>=0.8.0'], }, (TinyPool) => { return wrapTinyPool(TinyPool) }) // There are multiple index* files across different versions of vitest, // so we check for the existence of BaseSequencer to determine if we are in the right file addHook({ name: 'vitest', versions: ['>=1.6.0 <2.0.0'], filePattern: 'dist/vendor/index.*', }, (vitestPackage) => { if (isReporterPackage(vitestPackage)) { shimmer.wrap(vitestPackage.B.prototype, 'sort', getSortWrapper) } return vitestPackage }) addHook({ name: 'vitest', versions: ['>=2.0.0 <2.0.5'], filePattern: 'dist/vendor/index.*', }, (vitestPackage) => { if (isReporterPackageNew(vitestPackage)) { shimmer.wrap(vitestPackage.e.prototype, 'sort', getSortWrapper) } return vitestPackage }) addHook({ name: 'vitest', versions: ['>=2.0.5 <2.1.0'], filePattern: 'dist/chunks/index.*', }, (vitestPackage) => { if (isReporterPackageNewest(vitestPackage)) { shimmer.wrap(vitestPackage.h.prototype, 'sort', getSortWrapper) } return vitestPackage }) addHook({ name: 'vitest', versions: ['>=3.2.0 <4.0.0'], filePattern: 'dist/chunks/typechecker.*', }, getTypecheckerWrapper) addHook({ name: 'vitest', versions: ['>=4.0.0'], filePattern: 'dist/chunks/index.*', }, getTypecheckerWrapper) addHook({ name: 'vitest', versions: ['>=2.1.0 <3.0.0'], filePattern: 'dist/chunks/RandomSequencer.*', }, (randomSequencerPackage) => { shimmer.wrap(randomSequencerPackage.B.prototype, 'sort', getSortWrapper) return randomSequencerPackage }) addHook({ name: 'vitest', versions: ['>=3.0.9'], filePattern: 'dist/chunks/coverage.*', }, (coveragePackage) => { const baseSequencer = getBaseSequencerExport(coveragePackage) if (baseSequencer) { shimmer.wrap(baseSequencer.value.prototype, 'sort', getSortWrapper) } return coveragePackage }) addHook({ name: 'vitest', versions: ['>=3.0.0 <3.0.9'], filePattern: 'dist/chunks/resolveConfig.*', }, (resolveConfigPackage) => { shimmer.wrap(resolveConfigPackage.B.prototype, 'sort', getSortWrapper) return resolveConfigPackage }) // Can't specify file because compiled vitest includes hashes in their files // Following 3 wrappers are for test session start addHook({ name: 'vitest', versions: ['>=1.6.0 <2.0.5'], filePattern: 'dist/vendor/cac.*', }, getCreateCliWrapper) addHook({ name: 'vitest', versions: ['>=2.0.5'], filePattern: 'dist/chunks/cac.*', }, getCreateCliWrapper) addHook({ name: 'vitest', versions: ['>=1.6.0 <2.0.5'], filePattern: 'dist/vendor/cli-api.*', }, getStartVitestWrapper) addHook({ name: 'vitest', versions: ['>=2.0.5'], filePattern: 'dist/chunks/cli-api.*', }, getStartVitestWrapper)