Skip to content

Commit da52d23

Browse files
authored
fix(coverage): use project specific vitenode for uncovered files (#6044)
1 parent 2c926bf commit da52d23

22 files changed

+962
-7
lines changed

docs/guide/workspace.md

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

232232
## Coverage
233233

234-
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.
235-
236-
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.
234+
Coverage for workspace projects works out of the box.

packages/coverage-istanbul/src/provider.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -414,13 +414,15 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
414414
const cacheKey = new Date().getTime()
415415
const coverageMap = libCoverage.createCoverageMap({})
416416

417+
const transform = this.createUncoveredFileTransformer(this.ctx)
418+
417419
// Note that these cannot be run parallel as synchronous instrumenter.lastFileCoverage
418420
// returns the coverage of the last transformed file
419421
for (const [index, filename] of uncoveredFiles.entries()) {
420422
debug('Uncovered file %s %d/%d', filename, index, uncoveredFiles.length)
421423

422424
// Make sure file is not served from cache so that instrumenter loads up requested file coverage
423-
await this.ctx.vitenode.transformRequest(`${filename}?v=${cacheKey}`)
425+
await transform(`${filename}?v=${cacheKey}`)
424426
const lastCoverage = this.instrumenter.lastFileCoverage()
425427
coverageMap.addFileCoverage(lastCoverage)
426428
}

packages/coverage-v8/src/provider.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
368368
const transformResults = normalizeTransformResults(
369369
this.ctx.vitenode.fetchCache,
370370
)
371+
const transform = this.createUncoveredFileTransformer(this.ctx)
371372

372373
const allFiles = await this.testExclude.glob(this.ctx.config.root)
373374
let includedFiles = allFiles.map(file =>
@@ -401,7 +402,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
401402
const { originalSource } = await this.getSources(
402403
filename.href,
403404
transformResults,
404-
file => this.ctx.vitenode.transformRequest(file),
405+
transform,
405406
)
406407

407408
const coverage = {

packages/vitest/src/utils/coverage.ts

+32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { relative } from 'pathe'
22
import mm from 'micromatch'
33
import type { CoverageMap } from 'istanbul-lib-coverage'
4+
import type { Vitest } from '../node/core'
45
import type { BaseCoverageOptions, ResolvedCoverageOptions } from '../node/types/coverage'
56
import { resolveCoverageReporters } from '../node/config/resolveConfig'
67

@@ -272,6 +273,37 @@ export class BaseCoverageProvider {
272273
return chunks
273274
}, [])
274275
}
276+
277+
createUncoveredFileTransformer(ctx: Vitest) {
278+
const servers = [
279+
...ctx.projects.map(project => ({
280+
root: project.config.root,
281+
vitenode: project.vitenode,
282+
})),
283+
// Check core last as it will match all files anyway
284+
{ root: ctx.config.root, vitenode: ctx.vitenode },
285+
]
286+
287+
return async function transformFile(filename: string) {
288+
let lastError
289+
290+
for (const { root, vitenode } of servers) {
291+
if (!filename.startsWith(root)) {
292+
continue
293+
}
294+
295+
try {
296+
return await vitenode.transformRequest(filename)
297+
}
298+
catch (error) {
299+
lastError = error
300+
}
301+
}
302+
303+
// All vite-node servers failed to transform the file
304+
throw lastError
305+
}
306+
}
275307
}
276308

277309
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { readFileSync } from "node:fs";
2+
import { Plugin, defineWorkspace } from "vitest/config";
3+
import MagicString from "magic-string";
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)