Skip to content

Commit 88bde99

Browse files
authored
fix(coverage): cleanOnRerun: false to invalidate previous results (#6592)
1 parent c5e2909 commit 88bde99

File tree

11 files changed

+258
-40
lines changed

11 files changed

+258
-40
lines changed

docs/config/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1229,7 +1229,7 @@ Clean coverage results before running tests
12291229
- **Available for providers:** `'v8' | 'istanbul'`
12301230
- **CLI:** `--coverage.cleanOnRerun`, `--coverage.cleanOnRerun=false`
12311231

1232-
Clean coverage report on watch rerun
1232+
Clean coverage report on watch rerun. Set to `false` to preserve coverage results from previous run in watch mode.
12331233

12341234
#### coverage.reportsDirectory
12351235

packages/browser/src/client/tester/runner.ts

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export function createBrowserRunner(
9191
if (coverage) {
9292
await rpc().onAfterSuiteRun({
9393
coverage,
94+
testFiles: files.map(file => file.name),
9495
transformMode: 'browser',
9596
projectName: this.config.name,
9697
})

packages/coverage-istanbul/src/provider.ts

+35-13
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,30 @@ import { version } from '../package.json' with { type: 'json' }
3535
import { COVERAGE_STORE_KEY } from './constants'
3636

3737
type Options = ResolvedCoverageOptions<'istanbul'>
38-
type Filename = string
39-
type CoverageFilesByTransformMode = Record<
40-
AfterSuiteRunMeta['transformMode'],
41-
Filename[]
38+
39+
/**
40+
* Holds info about raw coverage results that are stored on file system:
41+
*
42+
* ```json
43+
* "project-a": {
44+
* "web": {
45+
* "tests/math.test.ts": "coverage-1.json",
46+
* "tests/utils.test.ts": "coverage-2.json",
47+
* // ^^^^^^^^^^^^^^^ Raw coverage on file system
48+
* },
49+
* "ssr": { ... },
50+
* "browser": { ... },
51+
* },
52+
* "project-b": ...
53+
* ```
54+
*/
55+
type CoverageFiles = Map<
56+
NonNullable<AfterSuiteRunMeta['projectName']> | typeof DEFAULT_PROJECT,
57+
Record<
58+
AfterSuiteRunMeta['transformMode'],
59+
{ [TestFilenames: string]: string }
60+
>
4261
>
43-
type ProjectName =
44-
| NonNullable<AfterSuiteRunMeta['projectName']>
45-
| typeof DEFAULT_PROJECT
4662

4763
interface TestExclude {
4864
new (opts: {
@@ -70,7 +86,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
7086
instrumenter!: Instrumenter
7187
testExclude!: InstanceType<TestExclude>
7288

73-
coverageFiles: Map<ProjectName, CoverageFilesByTransformMode> = new Map()
89+
coverageFiles: CoverageFiles = new Map()
7490
coverageFilesDirectory!: string
7591
pendingPromises: Promise<void>[] = []
7692

@@ -188,7 +204,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
188204
* Note that adding new entries here and requiring on those without
189205
* backwards compatibility is a breaking change.
190206
*/
191-
onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta): void {
207+
onAfterSuiteRun({ coverage, transformMode, projectName, testFiles }: AfterSuiteRunMeta): void {
192208
if (!coverage) {
193209
return
194210
}
@@ -200,15 +216,18 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
200216
let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT)
201217

202218
if (!entry) {
203-
entry = { web: [], ssr: [], browser: [] }
219+
entry = { web: {}, ssr: {}, browser: {} }
204220
this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry)
205221
}
206222

223+
const testFilenames = testFiles.join()
207224
const filename = resolve(
208225
this.coverageFilesDirectory,
209226
`coverage-${uniqueId++}.json`,
210227
)
211-
entry[transformMode].push(filename)
228+
229+
// If there's a result from previous run, overwrite it
230+
entry[transformMode][testFilenames] = filename
212231

213232
const promise = fs.writeFile(filename, JSON.stringify(coverage), 'utf-8')
214233
this.pendingPromises.push(promise)
@@ -246,12 +265,13 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
246265
this.pendingPromises = []
247266

248267
for (const coveragePerProject of this.coverageFiles.values()) {
249-
for (const filenames of [
268+
for (const coverageByTestfiles of [
250269
coveragePerProject.ssr,
251270
coveragePerProject.web,
252271
coveragePerProject.browser,
253272
]) {
254273
const coverageMapByTransformMode = libCoverage.createCoverageMap({})
274+
const filenames = Object.values(coverageByTestfiles)
255275

256276
for (const chunk of this.toSlices(
257277
filenames,
@@ -281,7 +301,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
281301
}
282302
}
283303

284-
if (this.options.all && allTestsRun) {
304+
// Include untested files when all tests were run (not a single file re-run)
305+
// or if previous results are preserved by "cleanOnRerun: false"
306+
if (this.options.all && (allTestsRun || !this.options.cleanOnRerun)) {
285307
const coveredFiles = coverageMap.files()
286308
const uncoveredCoverage = await this.getCoverageMapForUncoveredFiles(
287309
coveredFiles,

packages/coverage-v8/src/provider.ts

+36-14
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,31 @@ interface TestExclude {
5353

5454
type Options = ResolvedCoverageOptions<'v8'>
5555
type TransformResults = Map<string, FetchResult>
56-
type Filename = string
5756
type RawCoverage = Profiler.TakePreciseCoverageReturnType
58-
type CoverageFilesByTransformMode = Record<
59-
AfterSuiteRunMeta['transformMode'],
60-
Filename[]
57+
58+
/**
59+
* Holds info about raw coverage results that are stored on file system:
60+
*
61+
* ```json
62+
* "project-a": {
63+
* "web": {
64+
* "tests/math.test.ts": "coverage-1.json",
65+
* "tests/utils.test.ts": "coverage-2.json",
66+
* // ^^^^^^^^^^^^^^^ Raw coverage on file system
67+
* },
68+
* "ssr": { ... },
69+
* "browser": { ... },
70+
* },
71+
* "project-b": ...
72+
* ```
73+
*/
74+
type CoverageFiles = Map<
75+
NonNullable<AfterSuiteRunMeta['projectName']> | typeof DEFAULT_PROJECT,
76+
Record<
77+
AfterSuiteRunMeta['transformMode'],
78+
{ [TestFilenames: string]: string }
79+
>
6180
>
62-
type ProjectName =
63-
| NonNullable<AfterSuiteRunMeta['projectName']>
64-
| typeof DEFAULT_PROJECT
6581

6682
type Entries<T> = [keyof T, T[keyof T]][]
6783

@@ -86,7 +102,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
86102
options!: Options
87103
testExclude!: InstanceType<TestExclude>
88104

89-
coverageFiles: Map<ProjectName, CoverageFilesByTransformMode> = new Map()
105+
coverageFiles: CoverageFiles = new Map()
90106
coverageFilesDirectory!: string
91107
pendingPromises: Promise<void>[] = []
92108

@@ -181,23 +197,26 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
181197
* Note that adding new entries here and requiring on those without
182198
* backwards compatibility is a breaking change.
183199
*/
184-
onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta): void {
200+
onAfterSuiteRun({ coverage, transformMode, projectName, testFiles }: AfterSuiteRunMeta): void {
185201
if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') {
186202
throw new Error(`Invalid transform mode: ${transformMode}`)
187203
}
188204

189205
let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT)
190206

191207
if (!entry) {
192-
entry = { web: [], ssr: [], browser: [] }
208+
entry = { web: { }, ssr: { }, browser: { } }
193209
this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry)
194210
}
195211

212+
const testFilenames = testFiles.join()
196213
const filename = resolve(
197214
this.coverageFilesDirectory,
198215
`coverage-${uniqueId++}.json`,
199216
)
200-
entry[transformMode].push(filename)
217+
218+
// If there's a result from previous run, overwrite it
219+
entry[transformMode][testFilenames] = filename
201220

202221
const promise = fs.writeFile(filename, JSON.stringify(coverage), 'utf-8')
203222
this.pendingPromises.push(promise)
@@ -212,9 +231,10 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
212231
this.pendingPromises = []
213232

214233
for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) {
215-
for (const [transformMode, filenames] of Object.entries(coveragePerProject) as Entries<CoverageFilesByTransformMode>) {
234+
for (const [transformMode, coverageByTestfiles] of Object.entries(coveragePerProject) as Entries<typeof coveragePerProject>) {
216235
let merged: RawCoverage = { result: [] }
217236

237+
const filenames = Object.values(coverageByTestfiles)
218238
const project = this.ctx.projects.find(p => p.getName() === projectName) || this.ctx.getCoreWorkspaceProject()
219239

220240
for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) {
@@ -245,7 +265,9 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
245265
}
246266
}
247267

248-
if (this.options.all && allTestsRun) {
268+
// Include untested files when all tests were run (not a single file re-run)
269+
// or if previous results are preserved by "cleanOnRerun: false"
270+
if (this.options.all && (allTestsRun || !this.options.cleanOnRerun)) {
249271
const coveredFiles = coverageMap.files()
250272
const untestedCoverage = await this.getUntestedFiles(coveredFiles)
251273

@@ -519,7 +541,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
519541
private async convertCoverage(
520542
coverage: RawCoverage,
521543
project: WorkspaceProject = this.ctx.getCoreWorkspaceProject(),
522-
transformMode?: keyof CoverageFilesByTransformMode,
544+
transformMode?: AfterSuiteRunMeta['transformMode'],
523545
): Promise<CoverageMap> {
524546
let fetchCache = project.vitenode.fetchCache
525547

packages/vitest/src/public/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ export type {
264264
} from '../integrations/spy'
265265
export type { BrowserUI } from '../types/ui'
266266

267-
/** @deprecated import from `vitest/node` instead */
267+
/** @deprecated import from `vitest/reporter` instead */
268268
export type Reporter = Reporter_
269269
/** @deprecated import from `vitest/node` instead */
270270
export type Vitest = Vitest_

packages/vitest/src/runtime/runners/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export async function resolveTestRunner(
9090
if (coverage) {
9191
rpc().onAfterSuiteRun({
9292
coverage,
93+
testFiles: files.map(file => file.name).sort(),
9394
transformMode: state.environment.transformMode,
9495
projectName: state.ctx.projectName,
9596
})

packages/vitest/src/types/general.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface ModuleCache {
2424

2525
export interface AfterSuiteRunMeta {
2626
coverage?: unknown
27+
testFiles: string[]
2728
transformMode: TransformMode | 'browser'
2829
projectName?: string
2930
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { expect, test } from 'vitest'
2+
import * as math from '../src/math'
3+
4+
// This line will be changed by clean-on-rerun.test.ts
5+
const methodToTest = 'sum'
6+
7+
test(`run ${methodToTest}`, () => {
8+
expect(() => math[methodToTest](1, 2)).not.toThrow()
9+
})

test/coverage-test/test/changed.test.ts

+8-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
22
import { resolve } from 'node:path'
3-
import { afterAll, beforeAll, expect } from 'vitest'
3+
import { beforeAll, expect } from 'vitest'
44
import { readCoverageMap, runVitest, test } from '../utils'
55

66
// Note that this test may fail if you have new files in "vitest/test/coverage/src"
@@ -11,23 +11,21 @@ const FILE_TO_CHANGE = resolve('./fixtures/src/file-to-change.ts')
1111
const NEW_UNCOVERED_FILE = resolve('./fixtures/src/new-uncovered-file.ts')
1212

1313
beforeAll(() => {
14-
let content = readFileSync(FILE_TO_CHANGE, 'utf8')
15-
content = content.replace('This file will be modified by test cases', 'Changed!')
16-
writeFileSync(FILE_TO_CHANGE, content, 'utf8')
14+
const original = readFileSync(FILE_TO_CHANGE, 'utf8')
15+
const changed = original.replace('This file will be modified by test cases', 'Changed!')
16+
writeFileSync(FILE_TO_CHANGE, changed, 'utf8')
1717

1818
writeFileSync(NEW_UNCOVERED_FILE, `
1919
// This file is not covered by any tests but should be picked by --changed
2020
export default function helloworld() {
2121
return 'Hello world'
2222
}
2323
`.trim(), 'utf8')
24-
})
2524

26-
afterAll(() => {
27-
let content = readFileSync(FILE_TO_CHANGE, 'utf8')
28-
content = content.replace('Changed!', 'This file will be modified by test cases')
29-
writeFileSync(FILE_TO_CHANGE, content, 'utf8')
30-
rmSync(NEW_UNCOVERED_FILE)
25+
return function restore() {
26+
writeFileSync(FILE_TO_CHANGE, original, 'utf8')
27+
rmSync(NEW_UNCOVERED_FILE)
28+
}
3129
})
3230

3331
test('{ changed: "HEAD" }', async () => {

0 commit comments

Comments
 (0)