From 188a95fd3ca4ba7cdc4b5a185ef3a6232eb7a95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Thu, 26 Mar 2026 15:34:18 +0200 Subject: [PATCH] feat(coverage): v8 to track `node:child_process` and `node:worker_threads` contexts --- packages/coverage-v8/package.json | 8 +- packages/coverage-v8/rollup.config.js | 7 +- packages/coverage-v8/src/index.ts | 38 ++++++- .../src/intercept-new-run-context.ts | 51 +++++++++ packages/coverage-v8/src/provider.ts | 15 ++- packages/vitest/src/defaults.ts | 1 + packages/vitest/src/integrations/coverage.ts | 5 +- packages/vitest/src/node/cli/cli-config.ts | 3 + .../vitest/src/node/config/serializeConfig.ts | 1 + packages/vitest/src/node/coverage.ts | 4 +- packages/vitest/src/node/types/coverage.ts | 9 ++ packages/vitest/src/runtime/config.ts | 1 + packages/vitest/src/utils/coverage.ts | 2 +- pnpm-lock.yaml | 14 +++ pnpm-workspace.yaml | 1 + .../fixtures/src/start-fork-and-thread.ts | 27 +++++ .../fixtures/src/worker-or-process.js | 30 +++++ .../fixtures/src/worker-or-process.ts | 45 ++++++++ .../fixtures/test/child-process.test.ts | 14 +++ .../fixtures/test/worker-thread.test.ts | 14 +++ .../test/configuration-options.test-d.ts | 2 +- .../test/extended-run-context.v8.test.ts | 105 ++++++++++++++++++ 22 files changed, 379 insertions(+), 18 deletions(-) create mode 100644 packages/coverage-v8/src/intercept-new-run-context.ts create mode 100644 test/coverage-test/fixtures/src/start-fork-and-thread.ts create mode 100644 test/coverage-test/fixtures/src/worker-or-process.js create mode 100644 test/coverage-test/fixtures/src/worker-or-process.ts create mode 100644 test/coverage-test/fixtures/test/child-process.test.ts create mode 100644 test/coverage-test/fixtures/test/worker-thread.test.ts create mode 100644 test/coverage-test/test/extended-run-context.v8.test.ts diff --git a/packages/coverage-v8/package.json b/packages/coverage-v8/package.json index 67c97f5b6e7f..6ca2ac20de59 100644 --- a/packages/coverage-v8/package.json +++ b/packages/coverage-v8/package.json @@ -32,6 +32,10 @@ "types": "./dist/browser.d.ts", "default": "./dist/browser.js" }, + "./intercept-new-run-context": { + "types": "./dist/intercept-new-run-context.d.ts", + "default": "./dist/intercept-new-run-context.js" + }, "./*": "./*" }, "main": "./dist/index.js", @@ -57,13 +61,15 @@ "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "workspace:*", "ast-v8-to-istanbul": "^1.0.0", + "get-port-please": "catalog:", "istanbul-lib-coverage": "catalog:", "istanbul-lib-report": "catalog:", "istanbul-reports": "catalog:", "magicast": "catalog:", "obug": "catalog:", "std-env": "catalog:", - "tinyrainbow": "catalog:" + "tinyrainbow": "catalog:", + "ws": "catalog:" }, "devDependencies": { "@types/istanbul-lib-coverage": "catalog:", diff --git a/packages/coverage-v8/rollup.config.js b/packages/coverage-v8/rollup.config.js index 083d2b01d84f..2d5e34dc5b71 100644 --- a/packages/coverage-v8/rollup.config.js +++ b/packages/coverage-v8/rollup.config.js @@ -10,9 +10,10 @@ const require = createRequire(import.meta.url) const pkg = require('./package.json') const entries = { - index: 'src/index.ts', - browser: 'src/browser.ts', - provider: 'src/provider.ts', + 'index': 'src/index.ts', + 'browser': 'src/browser.ts', + 'provider': 'src/provider.ts', + 'intercept-new-run-context': 'src/intercept-new-run-context.ts', } const external = [ diff --git a/packages/coverage-v8/src/index.ts b/packages/coverage-v8/src/index.ts index 7218cf94204e..ff9514b7c244 100644 --- a/packages/coverage-v8/src/index.ts +++ b/packages/coverage-v8/src/index.ts @@ -3,21 +3,41 @@ import type { CoverageProviderModule } from 'vitest/node' import type { ScriptCoverageWithOffset, V8CoverageProvider } from './provider' import inspector from 'node:inspector/promises' import { fileURLToPath } from 'node:url' +import { getPort } from 'get-port-please' import { normalize } from 'pathe' import { provider } from 'std-env' +import { WebSocketServer } from 'ws' import { loadProvider } from './load-provider' -const session = new inspector.Session() +let session: inspector.Session | null = null let enabled = false -const mod: CoverageProviderModule = { - async startCoverage({ isolate }) { +const mod: CoverageProviderModule & { wss: WebSocketServer | undefined; extendedContextCoverage: Profiler.ScriptCoverage[] } = { + wss: undefined, + extendedContextCoverage: [], + + async startCoverage({ isolate, trackProcessAndWorker }) { if (isolate === false && enabled) { return } enabled = true + if (trackProcessAndWorker) { + const port = await getPort() + this.wss = new WebSocketServer({ port }) + + this.wss.on('connection', socket => socket.on('message', (raw) => { + const result: ScriptCoverageWithOffset[] = JSON.parse(raw.toString()) + this.extendedContextCoverage.push(...(result || [])) + })) + + process.env.NODE_OPTIONS ||= '' + process.env.NODE_OPTIONS += ' --import @vitest/coverage-v8/intercept-new-run-context' + process.env.VITEST_WS_PORT = `${port}` + } + + session ||= new inspector.Session() session.connect() await session.post('Profiler.enable') await session.post('Profiler.startPreciseCoverage', { callCount: true, detailed: true }) @@ -28,11 +48,16 @@ const mod: CoverageProviderModule = { return { result: [] } } + if (!session) { + throw new Error('V8 provider missing inspector session.') + } + + this.wss?.clients.forEach(client => client.send('take-coverage')) const coverage = await session.post('Profiler.takePreciseCoverage') const result: ScriptCoverageWithOffset[] = [] // Reduce amount of data sent over rpc by doing some early result filtering - for (const entry of coverage.result) { + for (const entry of [...coverage.result, ...this.extendedContextCoverage.splice(0)]) { if (filterResult(entry)) { result.push({ ...entry, @@ -49,9 +74,14 @@ const mod: CoverageProviderModule = { return } + if (!session) { + throw new Error('V8 provider missing inspector session.') + } + await session.post('Profiler.stopPreciseCoverage') await session.post('Profiler.disable') session.disconnect() + this.wss?.close() }, async getProvider(): Promise { diff --git a/packages/coverage-v8/src/intercept-new-run-context.ts b/packages/coverage-v8/src/intercept-new-run-context.ts new file mode 100644 index 000000000000..942d0d1a446b --- /dev/null +++ b/packages/coverage-v8/src/intercept-new-run-context.ts @@ -0,0 +1,51 @@ +import type { ScriptCoverageWithOffset } from './provider' +import { WebSocket } from 'ws' +import provider from './index' + +// eslint-disable-next-line antfu/no-top-level-await -- This should be blocking module loading +await initialize().catch((error) => { + console.error('[vitest-coverage] Error initializing process/thread intercepting:', error) + throw error +}) + +async function initialize() { + let reportedCoverage = false + + const ws = new WebSocket(`ws://localhost:${Number(process.env.VITEST_WS_PORT)}`) + + // @ts-expect-error -- untyped + ws.on('open', () => ws._socket?.unref?.()) + + await provider.startCoverage?.({ + isolate: true, + + // Environment options that were set by parent should inherit, no need to add more ws servers + trackProcessAndWorker: false, + }) + + onMessage(message => message === 'take-coverage' && takeCoverage()) + process.on('beforeExit', takeCoverage) + + async function takeCoverage() { + if (reportedCoverage) { + return + } + + reportedCoverage = true + + const coverage = await provider.takeCoverage?.({ + // Start offset should be 0 as these run outside of Vite + moduleExecutionInfo: undefined, + }) as { result: ScriptCoverageWithOffset[] } + + ws.send(JSON.stringify(coverage.result.map(entry => ({ ...entry, isExtendedContext: true })))) + + await provider.stopCoverage?.({ isolate: true }) + + ws.close() + } + + async function onMessage(callback: (message: unknown) => void) { + ws.on('message', raw => callback(raw.toString())) + } +} diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index f46be5d9bdeb..c343143944cd 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -20,6 +20,9 @@ import { version } from '../package.json' with { type: 'json' } export interface ScriptCoverageWithOffset extends Profiler.ScriptCoverage { startOffset: number + + /** Whether script ran outside Vite, e.g. in sub-processes or worker threads */ + isExtendedContext?: boolean } interface RawCoverage { result: ScriptCoverageWithOffset[] } @@ -331,8 +334,9 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage private async getSources( url: string, - onTransform: (filepath: string) => Promise, + onTransform: (filepath: string, isExtendedContext?: ScriptCoverageWithOffset['isExtendedContext']) => Promise, functions: Profiler.FunctionCoverage[] = [], + isExtendedContext: ScriptCoverageWithOffset['isExtendedContext'] = false, ): Promise<{ code: string map?: Vite.Rollup.SourceMap @@ -342,7 +346,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage ? url.slice(8) : removeStartsWith(url, FILE_PROTOCOL) // TODO: do we still need to "catch" here? why would it fail? - const transformResult = await onTransform(filepath).catch(() => null) + const transformResult = await onTransform(filepath, isExtendedContext).catch(() => null) const map = transformResult?.map as Vite.Rollup.SourceMap | undefined const code = transformResult?.code @@ -385,8 +389,8 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage throw new Error(`Cannot access browser module graph because it was torn down.`) } - const onTransform = async (filepath: string) => { - const result = await this.transformFile(filepath, project, environment) + const onTransform = async (filepath: string, isExtendedContext: ScriptCoverageWithOffset['isExtendedContext'] = false) => { + const result = await this.transformFile(filepath, project, environment, !isExtendedContext) if (result && environment === '__browser__' && project.browser) { return { ...result, code: `${result.code}// ` } } @@ -423,7 +427,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } await Promise.all( - chunk.map(async ({ url, functions, startOffset }) => { + chunk.map(async ({ url, functions, startOffset, isExtendedContext }) => { let timeout: ReturnType | undefined let start: number | undefined @@ -436,6 +440,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage url, onTransform, functions, + isExtendedContext, ) coverageMap.merge(await this.remapCoverage( diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts index 4f06742b3b8a..520c90fac972 100644 --- a/packages/vitest/src/defaults.ts +++ b/packages/vitest/src/defaults.ts @@ -53,6 +53,7 @@ export const coverageConfigDefaults: Required = { diff --git a/packages/vitest/src/integrations/coverage.ts b/packages/vitest/src/integrations/coverage.ts index df9681490a06..c43a92a8eee4 100644 --- a/packages/vitest/src/integrations/coverage.ts +++ b/packages/vitest/src/integrations/coverage.ts @@ -10,7 +10,10 @@ export async function startCoverageInsideWorker( const coverageModule = await resolveCoverageProviderModule(options, loader) if (coverageModule) { - return coverageModule.startCoverage?.(runtimeOptions) + return coverageModule.startCoverage?.({ + ...runtimeOptions, + trackProcessAndWorker: options?.trackProcessAndWorker ?? false, + }) } return null diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 852372f76824..756f2882cc5b 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -326,6 +326,9 @@ export const cliOptionsConfig: VitestCLIOptions = { description: 'Directory of HTML coverage output to be served in UI mode and HTML reporter.', argument: '', }, + trackProcessAndWorker: { + description: 'Track coverage of the `node:child_process` and `node:worker_threads` spawned during test run. Supported only by `v8` provider. (default: false)', + }, }, }, mode: { diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts index ca0115c32eb8..c4d8ecd221bd 100644 --- a/packages/vitest/src/node/config/serializeConfig.ts +++ b/packages/vitest/src/node/config/serializeConfig.ts @@ -58,6 +58,7 @@ export function serializeConfig(project: TestProject): SerializedConfig { ? coverage.customProviderModule : undefined, htmlDir: coverage.htmlDir, + trackProcessAndWorker: coverage.trackProcessAndWorker ?? false, } })(config.coverage), fakeTimers: config.fakeTimers, diff --git a/packages/vitest/src/node/coverage.ts b/packages/vitest/src/node/coverage.ts index a8ecec1f381b..b06f272d9ee2 100644 --- a/packages/vitest/src/node/coverage.ts +++ b/packages/vitest/src/node/coverage.ts @@ -663,11 +663,11 @@ export class BaseCoverageProvider { // TODO: should this be abstracted in `project`/`vitest` instead? // if we decide to keep `viteModuleRunner: false`, we will need to abstract transformation in both main thread and tests // custom --import=module.registerHooks need to be transformed as well somehow - async transformFile(url: string, project: TestProject, viteEnvironment: string): Promise { + async transformFile(url: string, project: TestProject, viteEnvironment: string, isTransformedByVite = true): Promise { const config = project.config // vite is disabled, should transform manually if possible - if (config.experimental.viteModuleRunner === false) { + if (config.experimental.viteModuleRunner === false || !isTransformedByVite) { const pathname = url.split('?')[0] const filename = pathname.startsWith('file://') ? fileURLToPath(pathname) : pathname const extension = path.extname(filename) diff --git a/packages/vitest/src/node/types/coverage.ts b/packages/vitest/src/node/types/coverage.ts index 7c11d86cbd58..415f48e79f30 100644 --- a/packages/vitest/src/node/types/coverage.ts +++ b/packages/vitest/src/node/types/coverage.ts @@ -111,6 +111,7 @@ export type FieldsWithDefaultValues | 'ignoreClassMethods' | 'skipFull' | 'watermarks' + | 'trackProcessAndWorker' export type ResolvedCoverageOptions = CoverageOptions @@ -264,6 +265,14 @@ export interface CoverageOptions { */ processingConcurrency?: number + /** + * Track coverage of the `node:child_process` and `node:worker_threads` spawned during test run. + * Supported only by `v8` provider. + * + * @default false + */ + trackProcessAndWorker?: boolean + /** * Set to array of class method names to ignore for coverage * diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index f2e2cbbf8304..c28500d1a890 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -155,6 +155,7 @@ export interface SerializedCoverageConfig { htmlDir: string | undefined enabled: boolean customProviderModule: string | undefined + trackProcessAndWorker: boolean } export type RuntimeConfig = Pick< diff --git a/packages/vitest/src/utils/coverage.ts b/packages/vitest/src/utils/coverage.ts index b3a1d23c2391..219b419b394f 100644 --- a/packages/vitest/src/utils/coverage.ts +++ b/packages/vitest/src/utils/coverage.ts @@ -15,7 +15,7 @@ export interface RuntimeCoverageProviderModule { /** * Executed before tests are run in the worker thread. */ - startCoverage?: (runtimeOptions: { isolate: boolean }) => unknown | Promise + startCoverage?: (runtimeOptions: { isolate: boolean; trackProcessAndWorker?: boolean }) => unknown | Promise /** * Executed on after each run in the worker thread. Possible to return a payload passed to the provider diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da59eedd0a83..5193121ae42c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ catalogs: flatted: specifier: ^3.4.2 version: 3.4.2 + get-port-please: + specifier: ^3.2.0 + version: 3.2.0 istanbul-lib-coverage: specifier: ^3.2.2 version: 3.2.2 @@ -699,6 +702,9 @@ importers: ast-v8-to-istanbul: specifier: ^1.0.0 version: 1.0.0 + get-port-please: + specifier: 'catalog:' + version: 3.2.0 istanbul-lib-coverage: specifier: 'catalog:' version: 3.2.2 @@ -720,6 +726,9 @@ importers: tinyrainbow: specifier: 'catalog:' version: 3.1.0 + ws: + specifier: 'catalog:' + version: 8.19.0 devDependencies: '@types/istanbul-lib-coverage': specifier: 'catalog:' @@ -7303,6 +7312,9 @@ packages: get-own-enumerable-property-symbols@3.0.2: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-port@7.0.0: resolution: {integrity: sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==} engines: {node: '>=16'} @@ -16238,6 +16250,8 @@ snapshots: get-own-enumerable-property-symbols@3.0.2: {} + get-port-please@3.2.0: {} + get-port@7.0.0: {} get-proto@1.0.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c18207096e7e..3f716450b659 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -67,6 +67,7 @@ catalog: cac: ^6.7.14 chai: ^6.2.2 flatted: ^3.4.2 + get-port-please: ^3.2.0 istanbul-lib-coverage: ^3.2.2 istanbul-lib-report: ^3.0.1 istanbul-lib-source-maps: ^5.0.6 diff --git a/test/coverage-test/fixtures/src/start-fork-and-thread.ts b/test/coverage-test/fixtures/src/start-fork-and-thread.ts new file mode 100644 index 000000000000..486ec7801715 --- /dev/null +++ b/test/coverage-test/fixtures/src/start-fork-and-thread.ts @@ -0,0 +1,27 @@ +import { fork } from 'node:child_process'; +import { resolve } from 'node:path'; +import { Worker } from 'node:worker_threads'; + +export async function runFork(filename: string) { + const child = fork(resolve(import.meta.dirname, filename)); + const onExit = new Promise((resolve) => child.on('exit', resolve)); + + const onResponse = new Promise<{ result: number }>((resolve) => child.on('message', resolve)); + const response = await onResponse; + + await onExit; + + return response.result; +} + +export async function runThread(filename: string) { + const worker = new Worker(resolve(import.meta.dirname, filename), { env: process.env }); + const onExit = new Promise((resolve) => worker.on('exit', resolve)); + + const onResponse = new Promise<{ result: number }>((resolve) => worker.on('message', resolve)); + const response = await onResponse; + + await onExit; + + return response.result; +} \ No newline at end of file diff --git a/test/coverage-test/fixtures/src/worker-or-process.js b/test/coverage-test/fixtures/src/worker-or-process.js new file mode 100644 index 000000000000..6a0709e84a97 --- /dev/null +++ b/test/coverage-test/fixtures/src/worker-or-process.js @@ -0,0 +1,30 @@ +import { isMainThread, parentPort } from "node:worker_threads"; +import { subtract } from "./math.ts"; + +/** + * Comment that adds padding + */ +function execute(options) { + return subtract(options.left, options.right); +} + +const result = execute({ left: 10, right: 5 }); + +if (isMainThread && process.send) { + // Comment + // Comment + process.send({ result }); + // Comment +} +else if (parentPort) { + // Comment + // Comment + // Comment + // Comment + parentPort.postMessage({ result }); +} +else { + // Comment + // Comment + throw new Error("Where is this running?") +} diff --git a/test/coverage-test/fixtures/src/worker-or-process.ts b/test/coverage-test/fixtures/src/worker-or-process.ts new file mode 100644 index 000000000000..bdff704d6f47 --- /dev/null +++ b/test/coverage-test/fixtures/src/worker-or-process.ts @@ -0,0 +1,45 @@ +import { isMainThread, parentPort } from "node:worker_threads"; +import { subtract } from "./math.ts"; + +interface Options { + left: number; + right: number; +} + +/** + * Comment that adds padding + */ +function execute(options: Options) { + return subtract(options.left, options.right); +} + +const result = execute({ left: 10, right: 5 }); + +if (isMainThread && process.send) { + // Comment + interface Padding { + left: number; + right: number; + } + // Comment + process.send({ result }); + interface Padding2 { + left: number; + right: number; + } + // Comment +} +else if (parentPort) { + // Comment + // Comment + // Comment + // Comment + parentPort.postMessage({ result }); +} +else { + // Comment + type A = "B" + type B = "C" + // Comment + throw new Error("Where is this running?" as unknown as A | B) +} diff --git a/test/coverage-test/fixtures/test/child-process.test.ts b/test/coverage-test/fixtures/test/child-process.test.ts new file mode 100644 index 000000000000..d309ef3b4f49 --- /dev/null +++ b/test/coverage-test/fixtures/test/child-process.test.ts @@ -0,0 +1,14 @@ +import { test , expect } from "vitest" +import { runFork } from "../src/start-fork-and-thread"; + +test("child process typescript", async () => { + const result = await runFork('worker-or-process.ts'); + expect(result).toBe(5); +}) + +test("child process javascript source file", async () => { + const result = await runFork('worker-or-process.js'); + expect(result).toBe(5); +}) + +test.todo("child process transpiled javascript with source maps") diff --git a/test/coverage-test/fixtures/test/worker-thread.test.ts b/test/coverage-test/fixtures/test/worker-thread.test.ts new file mode 100644 index 000000000000..6da007c31f3a --- /dev/null +++ b/test/coverage-test/fixtures/test/worker-thread.test.ts @@ -0,0 +1,14 @@ +import { test , expect } from "vitest" +import { runThread } from "../src/start-fork-and-thread"; + +test("worker thread", async () => { + const result = await runThread('worker-or-process.ts'); + expect(result).toBe(5); +}) + +test("worker thread javascript source file", async () => { + const result = await runThread('worker-or-process.js'); + expect(result).toBe(5); +}) + +test.todo("worker thread transpiled javascript with source maps") diff --git a/test/coverage-test/test/configuration-options.test-d.ts b/test/coverage-test/test/configuration-options.test-d.ts index be45d91f16f7..212fa2a916ff 100644 --- a/test/coverage-test/test/configuration-options.test-d.ts +++ b/test/coverage-test/test/configuration-options.test-d.ts @@ -100,7 +100,7 @@ test('provider module', () => { branches: [80, 95], lines: [80, 95], }, - + trackProcessAndWorker: false, } }, clean(_?: boolean) {}, diff --git a/test/coverage-test/test/extended-run-context.v8.test.ts b/test/coverage-test/test/extended-run-context.v8.test.ts new file mode 100644 index 000000000000..5aab2292fd68 --- /dev/null +++ b/test/coverage-test/test/extended-run-context.v8.test.ts @@ -0,0 +1,105 @@ +import { expect } from 'vitest' +import { readCoverageMap, runVitest, test } from '../utils' + +test('{ trackProcessAndWorker: true } includes files from child process', async () => { + await runVitest({ + include: ['fixtures/test/child-process.test.ts'], + coverage: { + trackProcessAndWorker: true, + reporter: 'json', + }, + }) + const coverageMap = await readCoverageMap() + const files = coverageMap.files() + + expect(files).toMatchInlineSnapshot(` + [ + "/fixtures/src/math.ts", + "/fixtures/src/start-fork-and-thread.ts", + "/fixtures/src/worker-or-process.js", + "/fixtures/src/worker-or-process.ts", + ] + `) + + { + /* See {@link file://./../fixtures/src/worker-or-process.ts} */ + const fileCoverage = coverageMap.fileCoverageFor('/fixtures/src/worker-or-process.ts') + const lines = fileCoverage.getLineCoverage() + + expect.soft(lines[25]).toBe(1) + expect.soft(lines[37]).toBe(0) + expect.soft(lines[44]).toBe(0) + } + + { + /* See {@link file://./../fixtures/src/worker-or-process.js} */ + const fileCoverage = coverageMap.fileCoverageFor('/fixtures/src/worker-or-process.js') + const lines = fileCoverage.getLineCoverage() + + expect.soft(lines[16]).toBe(1) + expect.soft(lines[24]).toBe(0) + expect.soft(lines[29]).toBe(0) + } +}) + +test('{ trackProcessAndWorker: true } includes files from worker thread', async () => { + await runVitest({ + include: ['fixtures/test/worker-thread.test.ts'], + coverage: { + trackProcessAndWorker: true, + reporter: 'json', + }, + }) + const coverageMap = await readCoverageMap() + const files = coverageMap.files() + + expect(files).toMatchInlineSnapshot(` + [ + "/fixtures/src/math.ts", + "/fixtures/src/start-fork-and-thread.ts", + "/fixtures/src/worker-or-process.js", + "/fixtures/src/worker-or-process.ts", + ] + `) + + { + /* See {@link file://./../fixtures/src/worker-or-process.ts} */ + const fileCoverage = coverageMap.fileCoverageFor('/fixtures/src/worker-or-process.ts') + const lines = fileCoverage.getLineCoverage() + + expect.soft(lines[25]).toBe(0) + expect.soft(lines[37]).toBe(1) + expect.soft(lines[44]).toBe(0) + } + + { + /* See {@link file://./../fixtures/src/worker-or-process.js} */ + const fileCoverage = coverageMap.fileCoverageFor('/fixtures/src/worker-or-process.js') + const lines = fileCoverage.getLineCoverage() + + expect.soft(lines[16]).toBe(0) + expect.soft(lines[24]).toBe(1) + expect.soft(lines[29]).toBe(0) + } +}) + +test('{ trackProcessAndWorker: false } does not include files from child process or worker thread', async () => { + await runVitest({ + include: ['fixtures/test/child-process.test.ts', 'fixtures/test/worker-thread.test.ts'], + coverage: { + trackProcessAndWorker: false, + reporter: 'json', + }, + }) + const coverageMap = await readCoverageMap() + const files = coverageMap.files() + + expect(files).toMatchInlineSnapshot(` + [ + "/fixtures/src/start-fork-and-thread.ts", + ] + `) +}) + +test.todo('{ trackProcessAndWorker: true } includes files from process inside process') +test.todo('{ trackProcessAndWorker: true } includes files from worker inside worker')