Skip to content

Commit 6f22c67

Browse files
authored
fix(coverage): invalidate circular modules correctly on rerun with coverage (#9096)
1 parent 3326cc9 commit 6f22c67

File tree

5 files changed

+61
-0
lines changed

5 files changed

+61
-0
lines changed

packages/coverage-istanbul/src/provider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider<ResolvedCover
239239
if (node.id && !this.transformedModuleIds.has(node.id)) {
240240
moduleGraph.invalidateModule(node, seen)
241241
}
242+
seen.add(node) // to avoid infinite loops in circular dependencies
242243
node.importedModules.forEach((mod) => {
243244
this.invalidateTree(mod, moduleGraph, seen)
244245
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { circularB } from './circularB'
2+
3+
export const CalledB: number[] = []
4+
5+
export function circularA() {
6+
return circularB()
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { CalledB } from './circularA'
2+
3+
export function circularB() {
4+
return CalledB.push(CalledB.length)
5+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect, it } from 'vitest'
2+
import { CalledB, circularA } from '../src/circularA'
3+
4+
it('circular', () => {
5+
CalledB.length = 0
6+
7+
circularA()
8+
9+
expect(CalledB.length).toBe(1)
10+
11+
circularA()
12+
circularA()
13+
14+
expect(CalledB).toEqual([0, 1, 2])
15+
})

test/coverage-test/test/run-dynamic-coverage.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,39 @@ test('enableCoverage() collects coverage after being called', async () => {
2626
expect(coverageMap.files()).toContain('<process-cwd>/fixtures/src/math.ts')
2727
})
2828

29+
test('enableCoverage() invalidates circular modules', async () => {
30+
await cleanupCoverageJson()
31+
32+
await expect(readCoverageMap(), 'coverage map should not be on the disk').rejects.toThrowError(/no such file/)
33+
34+
// Simulating user actions in the VSCode Vitest extension:
35+
// 1. User clicks "Run Test with Coverage" to generate coverage files normally
36+
const { ctx } = await runVitest({
37+
include: ['fixtures/test/circular.test.ts'],
38+
watch: false,
39+
coverage: {
40+
enabled: true,
41+
reporter: 'json',
42+
},
43+
})
44+
45+
const coverageMap = await readCoverageMap()
46+
expect(coverageMap.files()).toEqual([
47+
'<process-cwd>/fixtures/src/circularA.ts',
48+
'<process-cwd>/fixtures/src/circularB.ts',
49+
])
50+
51+
// 2. User reruns tests with coverage
52+
await ctx!.enableCoverage()
53+
await ctx!.rerunFiles()
54+
55+
const coverageMap2 = await readCoverageMap()
56+
expect(coverageMap2.files()).toEqual([
57+
'<process-cwd>/fixtures/src/circularA.ts',
58+
'<process-cwd>/fixtures/src/circularB.ts',
59+
])
60+
})
61+
2962
test('disableCoverage() stops collecting coverage going forward', async () => {
3063
const { ctx } = await runVitest({
3164
include: ['fixtures/test/math.test.ts'],

0 commit comments

Comments
 (0)