Skip to content

Commit 095c639

Browse files
authoredFeb 14, 2023
feat!: coverage-c8 to use V8 profiler directly instead of NODE_V8_COVERAGE (#2786)
1 parent 489b247 commit 095c639

File tree

11 files changed

+128
-52
lines changed

11 files changed

+128
-52
lines changed
 

‎docs/config/index.md

-1
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,6 @@ Clean coverage report on watch rerun
722722
- **CLI:** `--coverage.reportsDirectory=<path>`
723723

724724
Directory to write coverage report to.
725-
When using `c8` provider a temporary `/tmp` directory is created for [V8 coverage results](https://nodejs.org/api/cli.html#coverage-output).
726725

727726
#### reporter
728727

‎packages/coverage-c8/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"prepublishOnly": "pnpm build"
4343
},
4444
"peerDependencies": {
45-
"vitest": ">=0.28.0 <1"
45+
"vitest": ">=0.29.0 <1"
4646
},
4747
"dependencies": {
4848
"c8": "^7.12.0",

‎packages/coverage-c8/src/provider.ts

+10-23
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
11
import { existsSync, promises as fs } from 'fs'
22
import _url from 'url'
33
import type { Profiler } from 'inspector'
4-
import { takeCoverage } from 'v8'
54
import { extname, resolve } from 'pathe'
65
import c from 'picocolors'
76
import { provider } from 'std-env'
87
import type { RawSourceMap } from 'vite-node'
98
import { coverageConfigDefaults } from 'vitest/config'
109
// eslint-disable-next-line no-restricted-imports
11-
import type { CoverageC8Options, CoverageProvider, ReportContext, ResolvedCoverageOptions } from 'vitest'
10+
import type { AfterSuiteRunMeta, CoverageC8Options, CoverageProvider, ReportContext, ResolvedCoverageOptions } from 'vitest'
1211
import type { Vitest } from 'vitest/node'
1312
import type { Report } from 'c8'
1413
// @ts-expect-error missing types
1514
import createReport from 'c8/lib/report.js'
1615
// @ts-expect-error missing types
1716
import { checkCoverages } from 'c8/lib/commands/check-coverage.js'
1817

19-
type Options =
20-
& ResolvedCoverageOptions<'c8'>
21-
& { tempDirectory: string }
18+
type Options = ResolvedCoverageOptions<'c8'>
2219

2320
export class C8CoverageProvider implements CoverageProvider {
2421
name = 'c8'
2522

2623
ctx!: Vitest
2724
options!: Options
25+
coverages: Profiler.TakePreciseCoverageReturnType[] = []
2826

2927
initialize(ctx: Vitest) {
3028
this.ctx = ctx
@@ -35,25 +33,18 @@ export class C8CoverageProvider implements CoverageProvider {
3533
return this.options
3634
}
3735

38-
onBeforeFilesRun() {
39-
process.env.NODE_V8_COVERAGE ||= this.options.tempDirectory
40-
}
41-
4236
async clean(clean = true) {
4337
if (clean && existsSync(this.options.reportsDirectory))
4438
await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 })
4539

46-
if (!existsSync(this.options.tempDirectory))
47-
await fs.mkdir(this.options.tempDirectory, { recursive: true })
40+
this.coverages = []
4841
}
4942

50-
onAfterSuiteRun() {
51-
takeCoverage()
43+
onAfterSuiteRun({ coverage }: AfterSuiteRunMeta) {
44+
this.coverages.push(coverage as Profiler.TakePreciseCoverageReturnType)
5245
}
5346

5447
async reportCoverage({ allTestsRun }: ReportContext = {}) {
55-
takeCoverage()
56-
5748
if (provider === 'stackblitz')
5849
this.ctx.logger.log(c.blue(' % ') + c.yellow('@vitest/coverage-c8 does not work on Stackblitz. Report will be empty.'))
5950

@@ -64,6 +55,9 @@ export class C8CoverageProvider implements CoverageProvider {
6455

6556
const report = createReport(options)
6657

58+
// Overwrite C8's loader as results are in memory instead of file system
59+
report._loadReports = () => this.coverages
60+
6761
interface MapAndSource { map: RawSourceMap; source: string | undefined }
6862
type SourceMapMeta = { url: string; filepath: string } & MapAndSource
6963

@@ -73,7 +67,7 @@ export class C8CoverageProvider implements CoverageProvider {
7367

7468
const entries = Array
7569
.from(this.ctx.vitenode.fetchCache.entries())
76-
.filter(i => !i[0].includes('/node_modules/'))
70+
.filter(entry => report._shouldInstrument(entry[0]))
7771
.map(([file, { result }]) => {
7872
if (!result.map)
7973
return null
@@ -153,12 +147,6 @@ export class C8CoverageProvider implements CoverageProvider {
153147

154148
await report.run()
155149
await checkCoverages(options, report)
156-
157-
// Note that this will only clean up the V8 reports generated so far.
158-
// There will still be a temp directory with some reports when vitest exists,
159-
// but at least it will only contain reports of vitest's internal functions.
160-
if (existsSync(this.options.tempDirectory))
161-
await fs.rm(this.options.tempDirectory, { recursive: true, force: true, maxRetries: 10 })
162150
}
163151
}
164152

@@ -178,7 +166,6 @@ function resolveC8Options(options: CoverageC8Options, root: string): Options {
178166

179167
// Resolved fields
180168
provider: 'c8',
181-
tempDirectory: process.env.NODE_V8_COVERAGE || resolve(reportsDirectory, 'tmp'),
182169
reporter: Array.isArray(reporter) ? reporter : [reporter],
183170
reportsDirectory,
184171
}
+43-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,46 @@
1-
import v8 from 'v8'
1+
/*
2+
* For details about the Profiler.* messages see https://chromedevtools.github.io/devtools-protocol/v8/Profiler/
3+
*/
24

3-
// Flush coverage to disk
5+
import inspector from 'node:inspector'
6+
import type { Profiler } from 'node:inspector'
47

5-
export function takeCoverage() {
6-
if (v8.takeCoverage == null)
7-
console.warn('[Vitest] takeCoverage is not available in this NodeJs version.\nCoverage could be incomplete. Update to NodeJs 14.18.')
8-
else
9-
v8.takeCoverage()
8+
const session = new inspector.Session()
9+
10+
export function startCoverage() {
11+
session.connect()
12+
session.post('Profiler.enable')
13+
session.post('Profiler.startPreciseCoverage', {
14+
callCount: true,
15+
detailed: true,
16+
})
17+
}
18+
19+
export async function takeCoverage() {
20+
return new Promise((resolve, reject) => {
21+
session.post('Profiler.takePreciseCoverage', async (error, coverage) => {
22+
if (error)
23+
return reject(error)
24+
25+
// Reduce amount of data sent over rpc by doing some early result filtering
26+
const result = coverage.result.filter(filterResult)
27+
28+
resolve({ result })
29+
})
30+
})
31+
}
32+
33+
export function stopCoverage() {
34+
session.post('Profiler.stopPreciseCoverage')
35+
session.post('Profiler.disable')
36+
}
37+
38+
function filterResult(coverage: Profiler.ScriptCoverage): boolean {
39+
if (!coverage.url.startsWith('file://'))
40+
return false
41+
42+
if (coverage.url.includes('/node_modules/'))
43+
return false
44+
45+
return true
1046
}

‎packages/vitest/src/integrations/coverage.ts

+35-11
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ export const CoverageProviderMap: Record<string, string> = {
1010
istanbul: '@vitest/coverage-istanbul',
1111
}
1212

13-
async function resolveCoverageProviderModule(options: CoverageOptions & Required<Pick<CoverageOptions, 'provider'>>, loader: Loader) {
13+
async function resolveCoverageProviderModule(options: CoverageOptions | undefined, loader: Loader) {
14+
if (!options?.enabled || !options.provider)
15+
return null
16+
1417
const provider = options.provider
1518

1619
if (provider === 'c8' || provider === 'istanbul')
@@ -31,17 +34,38 @@ async function resolveCoverageProviderModule(options: CoverageOptions & Required
3134
return customProviderModule.default
3235
}
3336

34-
export async function getCoverageProvider(options: CoverageOptions, loader: Loader): Promise<CoverageProvider | null> {
35-
if (options.enabled && options.provider) {
36-
const { getProvider } = await resolveCoverageProviderModule(options, loader)
37-
return await getProvider()
38-
}
37+
export async function getCoverageProvider(options: CoverageOptions | undefined, loader: Loader): Promise<CoverageProvider | null> {
38+
const coverageModule = await resolveCoverageProviderModule(options, loader)
39+
40+
if (coverageModule)
41+
return coverageModule.getProvider()
42+
3943
return null
4044
}
4145

42-
export async function takeCoverageInsideWorker(options: CoverageOptions, loader: Loader) {
43-
if (options.enabled && options.provider) {
44-
const { takeCoverage } = await resolveCoverageProviderModule(options, loader)
45-
return await takeCoverage?.()
46-
}
46+
export async function startCoverageInsideWorker(options: CoverageOptions | undefined, loader: Loader) {
47+
const coverageModule = await resolveCoverageProviderModule(options, loader)
48+
49+
if (coverageModule)
50+
return coverageModule.startCoverage?.()
51+
52+
return null
53+
}
54+
55+
export async function takeCoverageInsideWorker(options: CoverageOptions | undefined, loader: Loader) {
56+
const coverageModule = await resolveCoverageProviderModule(options, loader)
57+
58+
if (coverageModule)
59+
return coverageModule.takeCoverage?.()
60+
61+
return null
62+
}
63+
64+
export async function stopCoverageInsideWorker(options: CoverageOptions | undefined, loader: Loader) {
65+
const coverageModule = await resolveCoverageProviderModule(options, loader)
66+
67+
if (coverageModule)
68+
return coverageModule.stopCoverage?.()
69+
70+
return null
4771
}

‎packages/vitest/src/node/pool.ts

-2
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ export function createPool(ctx: Vitest): WorkerPool {
7474
options.minThreads = 1
7575
}
7676

77-
ctx.coverageProvider?.onBeforeFilesRun?.()
78-
7977
options.env = {
8078
TEST: 'true',
8179
VITEST: 'true',

‎packages/vitest/src/runtime/entry.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import type { EnvironmentOptions, ResolvedConfig, VitestEnvironment } from '../t
77
import { getWorkerState, resetModules } from '../utils'
88
import { vi } from '../integrations/vi'
99
import { envs } from '../integrations/env'
10-
import { takeCoverageInsideWorker } from '../integrations/coverage'
1110
import { distDir } from '../constants'
11+
import { startCoverageInsideWorker, stopCoverageInsideWorker, takeCoverageInsideWorker } from '../integrations/coverage'
1212
import { setupGlobalEnv, withEnv } from './setup.node'
1313
import { rpc } from './rpc'
1414
import type { VitestExecutor } from './execute'
@@ -79,6 +79,7 @@ async function getTestRunner(config: ResolvedConfig, executor: VitestExecutor):
7979
// browser shouldn't call this!
8080
export async function run(files: string[], config: ResolvedConfig, executor: VitestExecutor): Promise<void> {
8181
await setupGlobalEnv(config)
82+
await startCoverageInsideWorker(config.coverage, executor)
8283

8384
const workerState = getWorkerState()
8485

@@ -159,4 +160,6 @@ export async function run(files: string[], config: ResolvedConfig, executor: Vit
159160
})
160161
}
161162
}
163+
164+
await stopCoverageInsideWorker(config.coverage, executor)
162165
}

‎packages/vitest/src/types/coverage.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export interface CoverageProvider {
1010
resolveOptions(): ResolvedCoverageOptions
1111
clean(clean?: boolean): void | Promise<void>
1212

13-
onBeforeFilesRun?(): void | Promise<void>
1413
onAfterSuiteRun(meta: AfterSuiteRunMeta): void | Promise<void>
1514

1615
reportCoverage(reportContext?: ReportContext): void | Promise<void>
@@ -32,10 +31,21 @@ export interface CoverageProviderModule {
3231
* Factory for creating a new coverage provider
3332
*/
3433
getProvider(): CoverageProvider | Promise<CoverageProvider>
34+
35+
/**
36+
* Executed before tests are run in the worker thread.
37+
*/
38+
startCoverage?(): unknown | Promise<unknown>
39+
3540
/**
3641
* Executed on after each run in the worker thread. Possible to return a payload passed to the provider
3742
*/
3843
takeCoverage?(): unknown | Promise<unknown>
44+
45+
/**
46+
* Executed after all tests have been run in the worker thread.
47+
*/
48+
stopCoverage?(): unknown | Promise<unknown>
3949
}
4050

4151
export type CoverageReporter =

‎test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ exports[`custom json report 1`] = `
66
"initialized with context",
77
"resolveOptions",
88
"clean with force",
9-
"onBeforeFilesRun",
109
"onAfterSuiteRun with {\\"coverage\\":{\\"customCoverage\\":\\"Coverage report passed from workers to main thread\\"}}",
1110
"reportCoverage with {\\"allTestsRun\\":true}",
1211
],

‎test/coverage-test/custom-provider.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,30 @@ const CustomCoverageProviderModule: CoverageProviderModule = {
99
},
1010

1111
takeCoverage() {
12+
// @ts-expect-error -- untyped
13+
globalThis.CUSTOM_PROVIDER_TAKE_COVERAGE = true
14+
15+
// @ts-expect-error -- untyped
16+
if (!globalThis.CUSTOM_PROVIDER_START_COVERAGE)
17+
throw new Error('takeCoverage was called before startCoverage!')
18+
1219
return { customCoverage: 'Coverage report passed from workers to main thread' }
1320
},
21+
22+
startCoverage() {
23+
// @ts-expect-error -- untyped
24+
globalThis.CUSTOM_PROVIDER_START_COVERAGE = true
25+
},
26+
27+
stopCoverage() {
28+
// @ts-expect-error -- untyped
29+
if (!globalThis.CUSTOM_PROVIDER_START_COVERAGE)
30+
throw new Error('stopCoverage was called before startCoverage!')
31+
32+
// @ts-expect-error -- untyped
33+
if (!globalThis.CUSTOM_PROVIDER_TAKE_COVERAGE)
34+
throw new Error('stopCoverage was called before takeCoverage!')
35+
},
1436
}
1537

1638
/**
@@ -33,10 +55,6 @@ class CustomCoverageProvider implements CoverageProvider {
3355
this.calls.add(`clean ${force ? 'with' : 'without'} force`)
3456
}
3557

36-
onBeforeFilesRun() {
37-
this.calls.add('onBeforeFilesRun')
38-
}
39-
4058
onAfterSuiteRun(meta: AfterSuiteRunMeta) {
4159
this.calls.add(`onAfterSuiteRun with ${JSON.stringify(meta)}`)
4260
}

‎test/coverage-test/test/configuration-options.test-d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ test('provider module', () => {
116116
}
117117
},
118118
takeCoverage() {},
119+
startCoverage() {},
120+
stopCoverage() {},
119121
})
120122
})
121123

0 commit comments

Comments
 (0)
Please sign in to comment.