diff --git a/docs/config/index.md b/docs/config/index.md index 2ab7fefa93a87..fe2103f9a3d4d 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -722,7 +722,6 @@ Clean coverage report on watch rerun - **CLI:** `--coverage.reportsDirectory=` Directory to write coverage report to. -When using `c8` provider a temporary `/tmp` directory is created for [V8 coverage results](https://nodejs.org/api/cli.html#coverage-output). #### reporter diff --git a/packages/coverage-c8/src/provider.ts b/packages/coverage-c8/src/provider.ts index 91be9b82b9cff..1a4db23a9a43f 100644 --- a/packages/coverage-c8/src/provider.ts +++ b/packages/coverage-c8/src/provider.ts @@ -1,14 +1,13 @@ import { existsSync, promises as fs } from 'fs' import _url from 'url' import type { Profiler } from 'inspector' -import { takeCoverage } from 'v8' import { extname, resolve } from 'pathe' import c from 'picocolors' import { provider } from 'std-env' import type { RawSourceMap } from 'vite-node' import { coverageConfigDefaults } from 'vitest/config' // eslint-disable-next-line no-restricted-imports -import type { CoverageC8Options, CoverageProvider, ReportContext, ResolvedCoverageOptions } from 'vitest' +import type { AfterSuiteRunMeta, CoverageC8Options, CoverageProvider, ReportContext, ResolvedCoverageOptions } from 'vitest' import type { Vitest } from 'vitest/node' import type { Report } from 'c8' // @ts-expect-error missing types @@ -16,15 +15,14 @@ import createReport from 'c8/lib/report.js' // @ts-expect-error missing types import { checkCoverages } from 'c8/lib/commands/check-coverage.js' -type Options = - & ResolvedCoverageOptions<'c8'> - & { tempDirectory: string } +type Options = ResolvedCoverageOptions<'c8'> export class C8CoverageProvider implements CoverageProvider { name = 'c8' ctx!: Vitest options!: Options + coverages: Profiler.TakePreciseCoverageReturnType[] = [] initialize(ctx: Vitest) { this.ctx = ctx @@ -35,25 +33,16 @@ export class C8CoverageProvider implements CoverageProvider { return this.options } - onBeforeFilesRun() { - process.env.NODE_V8_COVERAGE ||= this.options.tempDirectory - } - async clean(clean = true) { if (clean && existsSync(this.options.reportsDirectory)) await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }) - - if (!existsSync(this.options.tempDirectory)) - await fs.mkdir(this.options.tempDirectory, { recursive: true }) } - onAfterSuiteRun() { - takeCoverage() + onAfterSuiteRun({ coverage }: AfterSuiteRunMeta) { + this.coverages.push(coverage as Profiler.TakePreciseCoverageReturnType) } async reportCoverage({ allTestsRun }: ReportContext = {}) { - takeCoverage() - if (provider === 'stackblitz') this.ctx.logger.log(c.blue(' % ') + c.yellow('@vitest/coverage-c8 does not work on Stackblitz. Report will be empty.')) @@ -64,6 +53,9 @@ export class C8CoverageProvider implements CoverageProvider { const report = createReport(options) + // Overwrite C8's loader as results are in memory instead of file system + report._loadReports = () => this.coverages + interface MapAndSource { map: RawSourceMap; source: string | undefined } type SourceMapMeta = { url: string; filepath: string } & MapAndSource @@ -73,7 +65,7 @@ export class C8CoverageProvider implements CoverageProvider { const entries = Array .from(this.ctx.vitenode.fetchCache.entries()) - .filter(i => !i[0].includes('/node_modules/')) + .filter(entry => report._shouldInstrument(entry[0])) .map(([file, { result }]) => { if (!result.map) return null @@ -153,12 +145,6 @@ export class C8CoverageProvider implements CoverageProvider { await report.run() await checkCoverages(options, report) - - // Note that this will only clean up the V8 reports generated so far. - // There will still be a temp directory with some reports when vitest exists, - // but at least it will only contain reports of vitest's internal functions. - if (existsSync(this.options.tempDirectory)) - await fs.rm(this.options.tempDirectory, { recursive: true, force: true, maxRetries: 10 }) } } @@ -178,7 +164,6 @@ function resolveC8Options(options: CoverageC8Options, root: string): Options { // Resolved fields provider: 'c8', - tempDirectory: process.env.NODE_V8_COVERAGE || resolve(reportsDirectory, 'tmp'), reporter: Array.isArray(reporter) ? reporter : [reporter], reportsDirectory, } diff --git a/packages/coverage-c8/src/takeCoverage.ts b/packages/coverage-c8/src/takeCoverage.ts index 5669b10886710..37a94eb4faa29 100644 --- a/packages/coverage-c8/src/takeCoverage.ts +++ b/packages/coverage-c8/src/takeCoverage.ts @@ -1,10 +1,46 @@ -import v8 from 'v8' +/* + * For details about the Profiler.* messages see https://chromedevtools.github.io/devtools-protocol/v8/Profiler/ +*/ -// Flush coverage to disk +import inspector from 'node:inspector' +import type { Profiler } from 'node:inspector' -export function takeCoverage() { - if (v8.takeCoverage == null) - console.warn('[Vitest] takeCoverage is not available in this NodeJs version.\nCoverage could be incomplete. Update to NodeJs 14.18.') - else - v8.takeCoverage() +const session = new inspector.Session() + +export function startCoverage() { + session.connect() + session.post('Profiler.enable') + session.post('Profiler.startPreciseCoverage', { + callCount: true, + detailed: true, + }) +} + +export async function takeCoverage() { + return new Promise((resolve, reject) => { + session.post('Profiler.takePreciseCoverage', async (error, coverage) => { + if (error) + return reject(error) + + // Reduce amount of data sent over rpc by doing some early result filtering + const result = coverage.result.filter(filterResult) + + resolve({ result }) + }) + }) +} + +export function stopCoverage() { + session.post('Profiler.stopPreciseCoverage') + session.post('Profiler.disable') +} + +function filterResult(coverage: Profiler.ScriptCoverage): boolean { + if (!coverage.url.startsWith('file://')) + return false + + if (coverage.url.includes('/node_modules/')) + return false + + return true } diff --git a/packages/vitest/src/integrations/coverage.ts b/packages/vitest/src/integrations/coverage.ts index a14b4d5dc9f9c..7a2438c2c6cc9 100644 --- a/packages/vitest/src/integrations/coverage.ts +++ b/packages/vitest/src/integrations/coverage.ts @@ -18,17 +18,43 @@ export async function resolveCoverageProvider(provider: NonNullable { - if (options?.enabled && options?.provider) { - const { getProvider } = await resolveCoverageProvider(options.provider) - return await getProvider() - } + const coverageModule = await getCoverageModule(options) + + if (coverageModule) + return coverageModule.getProvider() + + return null +} + +export async function startCoverageInsideWorker(options: CoverageOptions) { + const coverageModule = await getCoverageModule(options) + + if (coverageModule) + return coverageModule.startCoverage?.() + return null } export async function takeCoverageInsideWorker(options: CoverageOptions) { - if (options.enabled && options.provider) { - const { takeCoverage } = await resolveCoverageProvider(options.provider) - return await takeCoverage?.() - } + const coverageModule = await getCoverageModule(options) + + if (coverageModule) + return coverageModule.takeCoverage?.() + + return null +} + +export async function stopCoverageInsideWorker(options: CoverageOptions) { + const coverageModule = await getCoverageModule(options) + + if (coverageModule) + return coverageModule.stopCoverage?.() + + return null } diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index c480b236ac4cb..85930026b5aa2 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -74,8 +74,6 @@ export function createPool(ctx: Vitest): WorkerPool { options.minThreads = 1 } - ctx.coverageProvider?.onBeforeFilesRun?.() - options.env = { TEST: 'true', VITEST: 'true', diff --git a/packages/vitest/src/runtime/entry.ts b/packages/vitest/src/runtime/entry.ts index e2c8591b99501..89639f415f7a0 100644 --- a/packages/vitest/src/runtime/entry.ts +++ b/packages/vitest/src/runtime/entry.ts @@ -6,7 +6,7 @@ import type { EnvironmentOptions, ResolvedConfig, VitestEnvironment } from '../t import { getWorkerState, resetModules } from '../utils' import { vi } from '../integrations/vi' import { envs } from '../integrations/env' -import { takeCoverageInsideWorker } from '../integrations/coverage' +import { startCoverageInsideWorker, stopCoverageInsideWorker, takeCoverageInsideWorker } from '../integrations/coverage' import { setupGlobalEnv, withEnv } from './setup.node' import { VitestTestRunner } from './runners/test' import { NodeBenchmarkRunner } from './runners/benchmark' @@ -67,6 +67,7 @@ async function getTestRunner(config: ResolvedConfig): Promise { // browser shouldn't call this! export async function run(files: string[], config: ResolvedConfig): Promise { await setupGlobalEnv(config) + await startCoverageInsideWorker(config.coverage) const workerState = getWorkerState() @@ -145,4 +146,6 @@ export async function run(files: string[], config: ResolvedConfig): Promise - onBeforeFilesRun?(): void | Promise onAfterSuiteRun(meta: AfterSuiteRunMeta): void | Promise reportCoverage(reportContext?: ReportContext): void | Promise @@ -32,10 +31,21 @@ export interface CoverageProviderModule { * Factory for creating a new coverage provider */ getProvider(): CoverageProvider | Promise + + /** + * Executed before tests are run in the worker thread. + */ + startCoverage?(): unknown | Promise + /** * Executed on after each run in the worker thread. Possible to return a payload passed to the provider */ takeCoverage?(): unknown | Promise + + /** + * Executed after all tests have been run in the worker thread. + */ + stopCoverage?(): unknown | Promise } export type CoverageReporter =