Skip to content

Commit f5e1827

Browse files
committed
fix(coverage): use project specific vitenode for uncovered files
1 parent e9f9adc commit f5e1827

22 files changed

+970
-12
lines changed

docs/guide/workspace.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,4 @@ All configuration options that are not supported inside a project config have <N
185185

186186
## Coverage
187187

188-
Coverage for workspace projects works out of the box. But if you have [`all`](/config/#coverage-all) option enabled and use non-conventional extensions in some of your projects, you will need to have a plugin that handles this extension in your root configuration file.
189-
190-
For example, if you have a package that uses Vue files and it has its own config file, but some of the files are not imported in your tests, coverage will fail trying to analyze the usage of unused files, because it relies on the root configuration rather than project configuration.
188+
Coverage for workspace projects works out of the box.

packages/coverage-istanbul/src/provider.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -396,16 +396,20 @@ export class IstanbulCoverageProvider
396396
.filter(file => !coveredFiles.includes(file))
397397
.sort()
398398

399-
const cacheKey = new Date().getTime()
400399
const coverageMap = libCoverage.createCoverageMap({})
401400

401+
// Make sure file is not served from cache so that instrumenter loads up requested file coverage
402+
const cacheKey = new Date().getTime()
403+
404+
const transform = this.createUncoveredFileTransformer(this.ctx)
405+
402406
// Note that these cannot be run parallel as synchronous instrumenter.lastFileCoverage
403407
// returns the coverage of the last transformed file
404408
for (const [index, filename] of uncoveredFiles.entries()) {
405409
debug('Uncovered file %s %d/%d', filename, index, uncoveredFiles.length)
406410

407-
// Make sure file is not served from cache so that instrumenter loads up requested file coverage
408-
await this.ctx.vitenode.transformRequest(`${filename}?v=${cacheKey}`)
411+
await transform(`${filename}?v=${cacheKey}`)
412+
409413
const lastCoverage = this.instrumenter.lastFileCoverage()
410414
coverageMap.addFileCoverage(lastCoverage)
411415
}

packages/coverage-v8/src/provider.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ export class V8CoverageProvider
363363
const transformResults = normalizeTransformResults(
364364
this.ctx.vitenode.fetchCache,
365365
)
366+
const transform = this.createUncoveredFileTransformer(this.ctx)
366367

367368
const allFiles = await this.testExclude.glob(this.ctx.config.root)
368369
let includedFiles = allFiles.map(file =>
@@ -396,6 +397,7 @@ export class V8CoverageProvider
396397
const { originalSource, source } = await this.getSources(
397398
filename.href,
398399
transformResults,
400+
transform,
399401
)
400402

401403
// Ignore empty files, e.g. files that contain only typescript types and no runtime code
@@ -441,6 +443,7 @@ export class V8CoverageProvider
441443
private async getSources(
442444
url: string,
443445
transformResults: TransformResults,
446+
transform: ReturnType<typeof this.createUncoveredFileTransformer>,
444447
functions: Profiler.FunctionCoverage[] = [],
445448
): Promise<{
446449
source: string
@@ -458,9 +461,7 @@ export class V8CoverageProvider
458461

459462
if (!transformResult) {
460463
isExecuted = false
461-
transformResult = await this.ctx.vitenode
462-
.transformRequest(filePath)
463-
.catch(() => null)
464+
transformResult = await transform(filePath).catch(() => null)
464465
}
465466

466467
const map = transformResult?.map as EncodedSourceMap | undefined
@@ -515,6 +516,7 @@ export class V8CoverageProvider
515516
? viteNode.fetchCaches[transformMode]
516517
: viteNode.fetchCache
517518
const transformResults = normalizeTransformResults(fetchCache)
519+
const transform = this.createUncoveredFileTransformer(this.ctx)
518520

519521
const scriptCoverages = coverage.result.filter(result =>
520522
this.testExclude.shouldInstrument(fileURLToPath(result.url)),
@@ -536,6 +538,7 @@ export class V8CoverageProvider
536538
const sources = await this.getSources(
537539
url,
538540
transformResults,
541+
transform,
539542
functions,
540543
)
541544

packages/vitest/src/utils/coverage.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { relative } from 'pathe'
22
import mm from 'micromatch'
33
import type { CoverageMap } from 'istanbul-lib-coverage'
4-
import type { BaseCoverageOptions, ResolvedCoverageOptions } from '../types'
4+
import type { BaseCoverageOptions, ResolvedCoverageOptions, Vitest } from '../types'
55

66
type Threshold = 'lines' | 'functions' | 'statements' | 'branches'
77

@@ -293,6 +293,37 @@ export class BaseCoverageProvider {
293293
return chunks
294294
}, [])
295295
}
296+
297+
createUncoveredFileTransformer(ctx: Vitest) {
298+
const servers = [
299+
...ctx.projects.map(project => ({
300+
root: project.config.root,
301+
vitenode: project.vitenode,
302+
})),
303+
// Check core last as it will match all files anyway
304+
{ root: ctx.config.root, vitenode: ctx.vitenode },
305+
]
306+
307+
return async function transformFile(filename: string) {
308+
let lastError
309+
310+
for (const { root, vitenode } of servers) {
311+
if (!filename.startsWith(root)) {
312+
continue
313+
}
314+
315+
try {
316+
return await vitenode.transformRequest(filename)
317+
}
318+
catch (error) {
319+
lastError = error
320+
}
321+
}
322+
323+
// All vite-node servers failed to transform the file
324+
throw lastError
325+
}
326+
}
296327
}
297328

298329
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Plugin, defineWorkspace } from "vitest/config";
2+
import MagicString from "magic-string";
3+
import { readFileSync } from "fs";
4+
5+
export default defineWorkspace([
6+
// Project that uses its own "root" and custom transform plugin
7+
{
8+
test: {
9+
name: "custom-with-root",
10+
root: "fixtures/workspaces/custom-2",
11+
},
12+
plugins: [customFilePlugin("2")],
13+
},
14+
15+
// Project that cannot transform "*.custom-x" files
16+
{
17+
test: {
18+
name: "normal",
19+
include: ["fixtures/test/math.test.ts"],
20+
},
21+
},
22+
23+
// Project that uses default "root" and has custom transform plugin
24+
{
25+
test: {
26+
name: "custom",
27+
include: ["fixtures/test/custom-1-syntax.test.ts"],
28+
},
29+
plugins: [customFilePlugin("1")],
30+
},
31+
]);
32+
33+
/**
34+
* Plugin for transforming `.custom-1` and/or `.custom-2` files to Javascript
35+
*/
36+
function customFilePlugin(postfix: "1" | "2"): Plugin {
37+
function transform(code: MagicString) {
38+
code.replaceAll(
39+
"<function covered>",
40+
`
41+
function covered() {
42+
return "Custom-${postfix} file loaded!"
43+
}
44+
`.trim()
45+
);
46+
47+
code.replaceAll(
48+
"<function uncovered>",
49+
`
50+
function uncovered() {
51+
return "This should be uncovered!"
52+
}
53+
`.trim()
54+
);
55+
56+
code.replaceAll("<default export covered>", "export default covered()");
57+
code.replaceAll("<default export uncovered>", "export default uncovered()");
58+
}
59+
60+
return {
61+
name: `custom-${postfix}-file-plugin`,
62+
transform(_, id) {
63+
const filename = id.split("?")[0];
64+
65+
if (filename.endsWith(`.custom-${postfix}`)) {
66+
const content = readFileSync(filename, "utf8");
67+
68+
const s = new MagicString(content);
69+
transform(s);
70+
71+
return {
72+
code: s.toString(),
73+
map: s.generateMap({ hires: "boundary" }),
74+
};
75+
}
76+
},
77+
};
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<function covered>
2+
3+
<function uncovered>
4+
5+
<default export covered>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<function uncovered>
2+
3+
<default export uncovered>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { expect, test } from 'vitest'
2+
3+
// @ts-expect-error -- untyped
4+
import output from '../src/covered.custom-1'
5+
6+
test('custom file loads fine', () => {
7+
expect(output).toMatch('Custom-1 file loaded!')
8+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<function covered>
2+
3+
<function uncovered>
4+
5+
<default export covered>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<function uncovered>
2+
3+
<default export uncovered>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { expect, test } from 'vitest'
2+
3+
// @ts-expect-error -- untyped
4+
import output from '../src/covered.custom-2'
5+
6+
test('custom-2 file loads fine', () => {
7+
expect(output).toMatch('Custom-2 file loaded!')
8+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"path": "<process-cwd>/fixtures/src/covered.custom-1",
3+
"statementMap": {
4+
"0": {
5+
"start": {
6+
"line": 1,
7+
"column": 0
8+
},
9+
"end": {
10+
"line": 1,
11+
"column": 18
12+
}
13+
},
14+
"1": {
15+
"start": {
16+
"line": 3,
17+
"column": 0
18+
},
19+
"end": {
20+
"line": 3,
21+
"column": 20
22+
}
23+
}
24+
},
25+
"fnMap": {
26+
"0": {
27+
"name": "covered",
28+
"decl": {
29+
"start": {
30+
"line": 1,
31+
"column": 0
32+
},
33+
"end": {
34+
"line": 1,
35+
"column": 18
36+
}
37+
},
38+
"loc": {
39+
"start": {
40+
"line": 1,
41+
"column": 0
42+
},
43+
"end": {
44+
"line": 1,
45+
"column": 18
46+
}
47+
}
48+
},
49+
"1": {
50+
"name": "uncovered",
51+
"decl": {
52+
"start": {
53+
"line": 3,
54+
"column": 0
55+
},
56+
"end": {
57+
"line": 3,
58+
"column": 20
59+
}
60+
},
61+
"loc": {
62+
"start": {
63+
"line": 3,
64+
"column": 0
65+
},
66+
"end": {
67+
"line": 3,
68+
"column": 20
69+
}
70+
}
71+
}
72+
},
73+
"branchMap": {},
74+
"s": {
75+
"0": 1,
76+
"1": 0
77+
},
78+
"f": {
79+
"0": 1,
80+
"1": 0
81+
},
82+
"b": {}
83+
}

0 commit comments

Comments
 (0)