Skip to content

Commit 28109cc

Browse files
authored
fix!(coverage): use transformMode and workspace project based source maps (#4309)
1 parent fde1843 commit 28109cc

File tree

28 files changed

+1568
-72
lines changed

28 files changed

+1568
-72
lines changed

packages/browser/src/client/runner.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,14 @@ export function createBrowserRunner(original: any, coverageModule: CoverageHandl
4242
async onAfterRunFiles() {
4343
await super.onAfterRun?.()
4444
const coverage = await coverageModule?.takeCoverage?.()
45-
if (coverage)
46-
await rpc().onAfterSuiteRun({ coverage })
45+
46+
if (coverage) {
47+
await rpc().onAfterSuiteRun({
48+
coverage,
49+
transformMode: 'web',
50+
projectName: this.config.name,
51+
})
52+
}
4753
}
4854

4955
onCollected(files: File[]): unknown {

packages/coverage-istanbul/src/provider.ts

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { BaseCoverageProvider } from 'vitest/coverage'
66
import c from 'picocolors'
77
import libReport from 'istanbul-lib-report'
88
import reports from 'istanbul-reports'
9-
import type { CoverageMap } from 'istanbul-lib-coverage'
9+
import type { CoverageMap, CoverageMapData } from 'istanbul-lib-coverage'
1010
import libCoverage from 'istanbul-lib-coverage'
1111
import libSourceMaps from 'istanbul-lib-source-maps'
1212
import { type Instrumenter, createInstrumenter } from 'istanbul-lib-instrument'
@@ -16,6 +16,8 @@ import _TestExclude from 'test-exclude'
1616
import { COVERAGE_STORE_KEY } from './constants'
1717

1818
type Options = ResolvedCoverageOptions<'istanbul'>
19+
type CoverageByTransformMode = Record<AfterSuiteRunMeta['transformMode'], CoverageMapData[]>
20+
type ProjectName = NonNullable<AfterSuiteRunMeta['projectName']> | typeof DEFAULT_PROJECT
1921

2022
interface TestExclude {
2123
new(opts: {
@@ -31,6 +33,8 @@ interface TestExclude {
3133
}
3234
}
3335

36+
const DEFAULT_PROJECT = Symbol.for('default-project')
37+
3438
export class IstanbulCoverageProvider extends BaseCoverageProvider implements CoverageProvider {
3539
name = 'istanbul'
3640

@@ -45,7 +49,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
4549
* If storing in memory causes issues, we can simply write these into fs in `onAfterSuiteRun`
4650
* and read them back when merging coverage objects in `onAfterAllFilesRun`.
4751
*/
48-
coverages: any[] = []
52+
coverages = new Map<ProjectName, CoverageByTransformMode>()
4953

5054
initialize(ctx: Vitest) {
5155
const config: CoverageIstanbulOptions = ctx.config.coverage
@@ -106,36 +110,52 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
106110
return { code, map }
107111
}
108112

109-
onAfterSuiteRun({ coverage }: AfterSuiteRunMeta) {
110-
this.coverages.push(coverage)
113+
/*
114+
* Coverage and meta information passed from Vitest runners.
115+
* Note that adding new entries here and requiring on those without
116+
* backwards compatibility is a breaking change.
117+
*/
118+
onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta) {
119+
if (transformMode !== 'web' && transformMode !== 'ssr')
120+
throw new Error(`Invalid transform mode: ${transformMode}`)
121+
122+
let entry = this.coverages.get(projectName || DEFAULT_PROJECT)
123+
124+
if (!entry) {
125+
entry = { web: [], ssr: [] }
126+
this.coverages.set(projectName || DEFAULT_PROJECT, entry)
127+
}
128+
129+
entry[transformMode].push(coverage as CoverageMapData)
111130
}
112131

113132
async clean(clean = true) {
114133
if (clean && existsSync(this.options.reportsDirectory))
115134
await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 })
116135

117-
this.coverages = []
136+
this.coverages = new Map()
118137
}
119138

120139
async reportCoverage({ allTestsRun }: ReportContext = {}) {
121-
const mergedCoverage: CoverageMap = this.coverages.reduce((coverage, previousCoverageMap) => {
122-
const map = libCoverage.createCoverageMap(coverage)
123-
map.merge(previousCoverageMap)
124-
return map
125-
}, libCoverage.createCoverageMap({}))
126-
127-
if (this.options.all && allTestsRun)
128-
await this.includeUntestedFiles(mergedCoverage)
129-
130-
includeImplicitElseBranches(mergedCoverage)
140+
const coverageMaps = await Promise.all(
141+
Array.from(this.coverages.values()).map(coverages => [
142+
mergeAndTransformCoverage(coverages.ssr),
143+
mergeAndTransformCoverage(coverages.web),
144+
]).flat(),
145+
)
146+
147+
if (this.options.all && allTestsRun) {
148+
const coveredFiles = coverageMaps.map(map => map.files()).flat()
149+
const uncoveredCoverage = await this.getCoverageMapForUncoveredFiles(coveredFiles)
150+
151+
coverageMaps.push(await mergeAndTransformCoverage([uncoveredCoverage]))
152+
}
131153

132-
const sourceMapStore = libSourceMaps.createSourceMapStore()
133-
const coverageMap: CoverageMap = await sourceMapStore.transformCoverage(mergedCoverage)
154+
const coverageMap = mergeCoverageMaps(...coverageMaps)
134155

135156
const context = libReport.createContext({
136157
dir: this.options.reportsDirectory,
137158
coverageMap,
138-
sourceFinder: sourceMapStore.sourceFinder,
139159
watermarks: this.options.watermarks,
140160
})
141161

@@ -181,19 +201,21 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
181201
}
182202
}
183203

184-
async includeUntestedFiles(coverageMap: CoverageMap) {
204+
async getCoverageMapForUncoveredFiles(coveredFiles: string[]) {
185205
// Load, instrument and collect empty coverages from all files which
186206
// are not already in the coverage map
187207
const includedFiles = await this.testExclude.glob(this.ctx.config.root)
188208
const uncoveredFiles = includedFiles
189209
.map(file => resolve(this.ctx.config.root, file))
190-
.filter(file => !coverageMap.data[file])
210+
.filter(file => !coveredFiles.includes(file))
191211

192212
const transformResults = await Promise.all(uncoveredFiles.map(async (filename) => {
193213
const transformResult = await this.ctx.vitenode.transformRequest(filename)
194214
return { transformResult, filename }
195215
}))
196216

217+
const coverageMap = libCoverage.createCoverageMap({})
218+
197219
for (const { transformResult, filename } of transformResults) {
198220
const sourceMap = transformResult?.map
199221

@@ -209,9 +231,27 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
209231
coverageMap.addFileCoverage(lastCoverage)
210232
}
211233
}
234+
235+
return coverageMap.data
212236
}
213237
}
214238

239+
async function mergeAndTransformCoverage(coverages: CoverageMapData[]) {
240+
const mergedCoverage = mergeCoverageMaps(...coverages)
241+
includeImplicitElseBranches(mergedCoverage)
242+
243+
const sourceMapStore = libSourceMaps.createSourceMapStore()
244+
return await sourceMapStore.transformCoverage(mergedCoverage)
245+
}
246+
247+
function mergeCoverageMaps(...coverageMaps: (CoverageMap | CoverageMapData)[]) {
248+
return coverageMaps.reduce<CoverageMap>((coverage, previousCoverageMap) => {
249+
const map = libCoverage.createCoverageMap(coverage)
250+
map.merge(previousCoverageMap)
251+
return map
252+
}, libCoverage.createCoverageMap({}))
253+
}
254+
215255
/**
216256
* Remove possible query parameters from filenames
217257
* - From `/src/components/Header.component.ts?vue&type=script&src=true&lang.ts`

packages/coverage-v8/src/provider.ts

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import v8ToIstanbul from 'v8-to-istanbul'
55
import { mergeProcessCovs } from '@bcoe/v8-coverage'
66
import libReport from 'istanbul-lib-report'
77
import reports from 'istanbul-reports'
8-
import type { CoverageMap } from 'istanbul-lib-coverage'
8+
import type { CoverageMap, CoverageMapData } from 'istanbul-lib-coverage'
99
import libCoverage from 'istanbul-lib-coverage'
1010
import libSourceMaps from 'istanbul-lib-source-maps'
1111
import MagicString from 'magic-string'
@@ -39,20 +39,24 @@ interface TestExclude {
3939

4040
type Options = ResolvedCoverageOptions<'v8'>
4141
type TransformResults = Map<string, FetchResult>
42+
type RawCoverage = Profiler.TakePreciseCoverageReturnType
43+
type CoverageByTransformMode = Record<AfterSuiteRunMeta['transformMode'], RawCoverage[]>
44+
type ProjectName = NonNullable<AfterSuiteRunMeta['projectName']> | typeof DEFAULT_PROJECT
4245

4346
// TODO: vite-node should export this
4447
const WRAPPER_LENGTH = 185
4548

4649
// Note that this needs to match the line ending as well
4750
const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g
51+
const DEFAULT_PROJECT = Symbol.for('default-project')
4852

4953
export class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider {
5054
name = 'v8'
5155

5256
ctx!: Vitest
5357
options!: Options
5458
testExclude!: InstanceType<TestExclude>
55-
coverages: Profiler.TakePreciseCoverageReturnType[] = []
59+
coverages = new Map<ProjectName, CoverageByTransformMode>()
5660

5761
initialize(ctx: Vitest) {
5862
const config: CoverageV8Options = ctx.config.coverage
@@ -92,54 +96,52 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
9296
if (clean && existsSync(this.options.reportsDirectory))
9397
await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 })
9498

95-
this.coverages = []
99+
this.coverages = new Map()
96100
}
97101

98-
onAfterSuiteRun({ coverage }: AfterSuiteRunMeta) {
99-
this.coverages.push(coverage as Profiler.TakePreciseCoverageReturnType)
102+
/*
103+
* Coverage and meta information passed from Vitest runners.
104+
* Note that adding new entries here and requiring on those without
105+
* backwards compatibility is a breaking change.
106+
*/
107+
onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta) {
108+
if (transformMode !== 'web' && transformMode !== 'ssr')
109+
throw new Error(`Invalid transform mode: ${transformMode}`)
110+
111+
let entry = this.coverages.get(projectName || DEFAULT_PROJECT)
112+
113+
if (!entry) {
114+
entry = { web: [], ssr: [] }
115+
this.coverages.set(projectName || DEFAULT_PROJECT, entry)
116+
}
117+
118+
entry[transformMode].push(coverage as RawCoverage)
100119
}
101120

102121
async reportCoverage({ allTestsRun }: ReportContext = {}) {
103122
if (provider === 'stackblitz')
104123
this.ctx.logger.log(c.blue(' % ') + c.yellow('@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.'))
105124

106-
const transformResults = normalizeTransformResults(this.ctx.projects.map(project => project.vitenode.fetchCache))
107-
const merged = mergeProcessCovs(this.coverages)
108-
const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url)))
125+
const coverageMaps = await Promise.all(
126+
Array.from(this.coverages.entries()).map(([projectName, coverages]) => [
127+
this.mergeAndTransformCoverage(coverages.ssr, projectName, 'ssr'),
128+
this.mergeAndTransformCoverage(coverages.web, projectName, 'web'),
129+
]).flat(),
130+
)
109131

110132
if (this.options.all && allTestsRun) {
111-
const coveredFiles = Array.from(scriptCoverages.map(r => r.url))
112-
const untestedFiles = await this.getUntestedFiles(coveredFiles, transformResults)
133+
const coveredFiles = coverageMaps.map(map => map.files()).flat()
134+
const untestedCoverage = await this.getUntestedFiles(coveredFiles)
135+
const untestedCoverageResults = untestedCoverage.map(files => ({ result: [files] }))
113136

114-
scriptCoverages.push(...untestedFiles)
137+
coverageMaps.push(await this.mergeAndTransformCoverage(untestedCoverageResults))
115138
}
116139

117-
const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => {
118-
const sources = await this.getSources(url, transformResults, functions)
119-
120-
// If no source map was found from vite-node we can assume this file was not run in the wrapper
121-
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0
122-
123-
const converter = v8ToIstanbul(url, wrapperLength, sources)
124-
await converter.load()
125-
126-
converter.applyCoverage(functions)
127-
return converter.toIstanbul()
128-
}))
129-
130-
const mergedCoverage = converted.reduce((coverage, previousCoverageMap) => {
131-
const map = libCoverage.createCoverageMap(coverage)
132-
map.merge(previousCoverageMap)
133-
return map
134-
}, libCoverage.createCoverageMap({}))
135-
136-
const sourceMapStore = libSourceMaps.createSourceMapStore()
137-
const coverageMap: CoverageMap = await sourceMapStore.transformCoverage(mergedCoverage)
140+
const coverageMap = mergeCoverageMaps(...coverageMaps)
138141

139142
const context = libReport.createContext({
140143
dir: this.options.reportsDirectory,
141144
coverageMap,
142-
sourceFinder: sourceMapStore.sourceFinder,
143145
watermarks: this.options.watermarks,
144146
})
145147

@@ -185,11 +187,13 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
185187
}
186188
}
187189

188-
private async getUntestedFiles(testedFiles: string[], transformResults: TransformResults): Promise<Profiler.ScriptCoverage[]> {
190+
private async getUntestedFiles(testedFiles: string[]): Promise<Profiler.ScriptCoverage[]> {
191+
const transformResults = normalizeTransformResults(this.ctx.vitenode.fetchCache)
192+
189193
const includedFiles = await this.testExclude.glob(this.ctx.config.root)
190194
const uncoveredFiles = includedFiles
191195
.map(file => pathToFileURL(resolve(this.ctx.config.root, file)))
192-
.filter(file => !testedFiles.includes(file.href))
196+
.filter(file => !testedFiles.includes(file.pathname))
193197

194198
return await Promise.all(uncoveredFiles.map(async (uncoveredFile) => {
195199
const { source } = await this.getSources(uncoveredFile.href, transformResults)
@@ -247,6 +251,41 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
247251
},
248252
}
249253
}
254+
255+
private async mergeAndTransformCoverage(coverages: RawCoverage[], projectName?: ProjectName, transformMode?: 'web' | 'ssr') {
256+
const viteNode = this.ctx.projects.find(project => project.getName() === projectName)?.vitenode || this.ctx.vitenode
257+
const fetchCache = transformMode ? viteNode.fetchCaches[transformMode] : viteNode.fetchCache
258+
const transformResults = normalizeTransformResults(fetchCache)
259+
260+
const merged = mergeProcessCovs(coverages)
261+
const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url)))
262+
263+
const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => {
264+
const sources = await this.getSources(url, transformResults, functions)
265+
266+
// If no source map was found from vite-node we can assume this file was not run in the wrapper
267+
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0
268+
269+
const converter = v8ToIstanbul(url, wrapperLength, sources)
270+
await converter.load()
271+
272+
converter.applyCoverage(functions)
273+
return converter.toIstanbul()
274+
}))
275+
276+
const mergedCoverage = mergeCoverageMaps(...converted)
277+
278+
const sourceMapStore = libSourceMaps.createSourceMapStore()
279+
return sourceMapStore.transformCoverage(mergedCoverage)
280+
}
281+
}
282+
283+
function mergeCoverageMaps(...coverageMaps: (CoverageMap | CoverageMapData)[]) {
284+
return coverageMaps.reduce<CoverageMap>((coverage, previousCoverageMap) => {
285+
const map = libCoverage.createCoverageMap(coverage)
286+
map.merge(previousCoverageMap)
287+
return map
288+
}, libCoverage.createCoverageMap({}))
250289
}
251290

252291
/**
@@ -284,16 +323,14 @@ function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) {
284323
}, 0)
285324
}
286325

287-
function normalizeTransformResults(fetchCaches: Map<string, { result: FetchResult }>[]) {
326+
function normalizeTransformResults(fetchCache: Map<string, { result: FetchResult }>) {
288327
const normalized: TransformResults = new Map()
289328

290-
for (const fetchCache of fetchCaches) {
291-
for (const [key, value] of fetchCache.entries()) {
292-
const cleanEntry = cleanUrl(key)
329+
for (const [key, value] of fetchCache.entries()) {
330+
const cleanEntry = cleanUrl(key)
293331

294-
if (!normalized.has(cleanEntry))
295-
normalized.set(cleanEntry, value.result)
296-
}
332+
if (!normalized.has(cleanEntry))
333+
normalized.set(cleanEntry, value.result)
297334
}
298335

299336
return normalized

packages/vitest/src/node/pools/child.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env, forksPath }
100100
invalidates,
101101
environment,
102102
workerId,
103+
projectName: project.getName(),
103104
}
104105
try {
105106
await pool.run(data, { name, channel })

packages/vitest/src/node/pools/threads.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env, workerPath }: Po
8888
invalidates,
8989
environment,
9090
workerId,
91+
projectName: project.getName(),
9192
}
9293
try {
9394
await pool.run(data, { transferList: [workerPort], name })

packages/vitest/src/node/pools/vm-threads.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export function createVmThreadsPool(ctx: Vitest, { execArgv, env, vmPath }: Pool
9595
invalidates,
9696
environment,
9797
workerId,
98+
projectName: project.getName(),
9899
}
99100
try {
100101
await pool.run(data, { transferList: [workerPort], name })

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy