Skip to content

Commit 600b44d

Browse files
authoredMar 14, 2024··
feat(coverage): support --changed option (#5314)
1 parent 4f9dfc8 commit 600b44d

File tree

11 files changed

+343
-32
lines changed

11 files changed

+343
-32
lines changed
 

‎docs/guide/cli.md

+2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ vitest --api=false
137137

138138
To run tests against changes made in the last commit, you can use `--changed HEAD~1`. You can also pass commit hash (e.g. `--changed 09a9920`) or branch name (e.g. `--changed origin/develop`).
139139

140+
When used with code coverage the report will contain only the files that were related to the changes.
141+
140142
If paired with the [`forceRerunTriggers`](/config/#forcereruntriggers) config option it will run the whole test suite if at least one of the files listed in the `forceRerunTriggers` list changes. By default, changes to the Vitest config file and `package.json` will always rerun the whole suite.
141143

142144
### shard

‎packages/coverage-istanbul/src/provider.ts

+9-11
Original file line numberDiff line numberDiff line change
@@ -253,27 +253,25 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
253253
}
254254

255255
async getCoverageMapForUncoveredFiles(coveredFiles: string[]) {
256-
// Load, instrument and collect empty coverages from all files which
257-
// are not already in the coverage map
258-
const includedFiles = await this.testExclude.glob(this.ctx.config.root)
256+
const allFiles = await this.testExclude.glob(this.ctx.config.root)
257+
let includedFiles = allFiles.map(file => resolve(this.ctx.config.root, file))
258+
259+
if (this.ctx.config.changed)
260+
includedFiles = (this.ctx.config.related || []).filter(file => includedFiles.includes(file))
261+
259262
const uncoveredFiles = includedFiles
260-
.map(file => resolve(this.ctx.config.root, file))
261263
.filter(file => !coveredFiles.includes(file))
262264

265+
const cacheKey = new Date().getTime()
263266
const coverageMap = libCoverage.createCoverageMap({})
264267

265268
// Note that these cannot be run parallel as synchronous instrumenter.lastFileCoverage
266269
// returns the coverage of the last transformed file
267270
for (const [index, filename] of uncoveredFiles.entries()) {
268271
debug('Uncovered file %s %d/%d', filename, index, uncoveredFiles.length)
269272

270-
// Make sure file is not served from cache
271-
// so that instrumenter loads up requested file coverage
272-
if (this.ctx.vitenode.fetchCache.has(filename))
273-
this.ctx.vitenode.fetchCache.delete(filename)
274-
275-
await this.ctx.vitenode.transformRequest(filename)
276-
273+
// Make sure file is not served from cache so that instrumenter loads up requested file coverage
274+
await this.ctx.vitenode.transformRequest(`${filename}?v=${cacheKey}`)
277275
const lastCoverage = this.instrumenter.lastFileCoverage()
278276
coverageMap.addFileCoverage(lastCoverage)
279277
}

‎packages/coverage-v8/src/provider.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,14 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
246246
private async getUntestedFiles(testedFiles: string[]): Promise<RawCoverage> {
247247
const transformResults = normalizeTransformResults(this.ctx.vitenode.fetchCache)
248248

249-
const includedFiles = await this.testExclude.glob(this.ctx.config.root)
249+
const allFiles = await this.testExclude.glob(this.ctx.config.root)
250+
let includedFiles = allFiles.map(file => resolve(this.ctx.config.root, file))
251+
252+
if (this.ctx.config.changed)
253+
includedFiles = (this.ctx.config.related || []).filter(file => includedFiles.includes(file))
254+
250255
const uncoveredFiles = includedFiles
251-
.map(file => pathToFileURL(resolve(this.ctx.config.root, file)))
256+
.map(file => pathToFileURL(file))
252257
.filter(file => !testedFiles.includes(file.pathname))
253258

254259
let merged: RawCoverage = { result: [] }

‎packages/vitest/src/node/core.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -382,10 +382,6 @@ export class Vitest {
382382
}
383383
}
384384

385-
// all subsequent runs will treat this as a fresh run
386-
this.config.changed = false
387-
this.config.related = undefined
388-
389385
if (files.length) {
390386
// populate once, update cache on watch
391387
await this.cache.stats.populateStats(this.config.root, files)
@@ -532,6 +528,10 @@ export class Vitest {
532528

533529
this.runningPromise = undefined
534530
this.isFirstRun = false
531+
532+
// all subsequent runs will treat this as a fresh run
533+
this.config.changed = false
534+
this.config.related = undefined
535535
})
536536

537537
return await this.runningPromise

‎test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap

+83
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,89 @@ exports[`istanbul json report 1`] = `
11341134
},
11351135
},
11361136
},
1137+
"<process-cwd>/src/file-to-change.ts": {
1138+
"b": {},
1139+
"branchMap": {},
1140+
"f": {
1141+
"0": 0,
1142+
"1": 0,
1143+
},
1144+
"fnMap": {
1145+
"0": {
1146+
"decl": {
1147+
"end": {
1148+
"column": 22,
1149+
"line": 1,
1150+
},
1151+
"start": {
1152+
"column": 16,
1153+
"line": 1,
1154+
},
1155+
},
1156+
"loc": {
1157+
"end": {
1158+
"column": null,
1159+
"line": 3,
1160+
},
1161+
"start": {
1162+
"column": 22,
1163+
"line": 1,
1164+
},
1165+
},
1166+
"name": "run",
1167+
},
1168+
"1": {
1169+
"decl": {
1170+
"end": {
1171+
"column": 36,
1172+
"line": 5,
1173+
},
1174+
"start": {
1175+
"column": 16,
1176+
"line": 5,
1177+
},
1178+
},
1179+
"loc": {
1180+
"end": {
1181+
"column": null,
1182+
"line": 7,
1183+
},
1184+
"start": {
1185+
"column": 36,
1186+
"line": 5,
1187+
},
1188+
},
1189+
"name": "uncoveredFunction",
1190+
},
1191+
},
1192+
"path": "<process-cwd>/src/file-to-change.ts",
1193+
"s": {
1194+
"0": 0,
1195+
"1": 0,
1196+
},
1197+
"statementMap": {
1198+
"0": {
1199+
"end": {
1200+
"column": null,
1201+
"line": 2,
1202+
},
1203+
"start": {
1204+
"column": 2,
1205+
"line": 2,
1206+
},
1207+
},
1208+
"1": {
1209+
"end": {
1210+
"column": null,
1211+
"line": 6,
1212+
},
1213+
"start": {
1214+
"column": 2,
1215+
"line": 6,
1216+
},
1217+
},
1218+
},
1219+
},
11371220
"<process-cwd>/src/function-count.ts": {
11381221
"b": {},
11391222
"branchMap": {},

‎test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap

+147
Original file line numberDiff line numberDiff line change
@@ -2777,6 +2777,153 @@ exports[`v8 json report 1`] = `
27772777
},
27782778
},
27792779
},
2780+
"<process-cwd>/src/file-to-change.ts": {
2781+
"all": true,
2782+
"b": {
2783+
"0": [
2784+
0,
2785+
],
2786+
},
2787+
"branchMap": {
2788+
"0": {
2789+
"line": 1,
2790+
"loc": {
2791+
"end": {
2792+
"column": 1,
2793+
"line": 7,
2794+
},
2795+
"start": {
2796+
"column": 0,
2797+
"line": 1,
2798+
},
2799+
},
2800+
"locations": [
2801+
{
2802+
"end": {
2803+
"column": 1,
2804+
"line": 7,
2805+
},
2806+
"start": {
2807+
"column": 0,
2808+
"line": 1,
2809+
},
2810+
},
2811+
],
2812+
"type": "branch",
2813+
},
2814+
},
2815+
"f": {
2816+
"0": 0,
2817+
},
2818+
"fnMap": {
2819+
"0": {
2820+
"decl": {
2821+
"end": {
2822+
"column": 1,
2823+
"line": 7,
2824+
},
2825+
"start": {
2826+
"column": 0,
2827+
"line": 1,
2828+
},
2829+
},
2830+
"line": 1,
2831+
"loc": {
2832+
"end": {
2833+
"column": 1,
2834+
"line": 7,
2835+
},
2836+
"start": {
2837+
"column": 0,
2838+
"line": 1,
2839+
},
2840+
},
2841+
"name": "(empty-report)",
2842+
},
2843+
},
2844+
"path": "<process-cwd>/src/file-to-change.ts",
2845+
"s": {
2846+
"0": 0,
2847+
"1": 0,
2848+
"2": 0,
2849+
"3": 0,
2850+
"4": 0,
2851+
"5": 0,
2852+
"6": 0,
2853+
},
2854+
"statementMap": {
2855+
"0": {
2856+
"end": {
2857+
"column": 23,
2858+
"line": 1,
2859+
},
2860+
"start": {
2861+
"column": 0,
2862+
"line": 1,
2863+
},
2864+
},
2865+
"1": {
2866+
"end": {
2867+
"column": 51,
2868+
"line": 2,
2869+
},
2870+
"start": {
2871+
"column": 0,
2872+
"line": 2,
2873+
},
2874+
},
2875+
"2": {
2876+
"end": {
2877+
"column": 1,
2878+
"line": 3,
2879+
},
2880+
"start": {
2881+
"column": 0,
2882+
"line": 3,
2883+
},
2884+
},
2885+
"3": {
2886+
"end": {
2887+
"column": 0,
2888+
"line": 4,
2889+
},
2890+
"start": {
2891+
"column": 0,
2892+
"line": 4,
2893+
},
2894+
},
2895+
"4": {
2896+
"end": {
2897+
"column": 37,
2898+
"line": 5,
2899+
},
2900+
"start": {
2901+
"column": 0,
2902+
"line": 5,
2903+
},
2904+
},
2905+
"5": {
2906+
"end": {
2907+
"column": 14,
2908+
"line": 6,
2909+
},
2910+
"start": {
2911+
"column": 0,
2912+
"line": 6,
2913+
},
2914+
},
2915+
"6": {
2916+
"end": {
2917+
"column": 1,
2918+
"line": 7,
2919+
},
2920+
"start": {
2921+
"column": 0,
2922+
"line": 7,
2923+
},
2924+
},
2925+
},
2926+
},
27802927
"<process-cwd>/src/function-count.ts": {
27812928
"all": false,
27822929
"b": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expect, test } from 'vitest'
2+
import libCoverage from 'istanbul-lib-coverage'
3+
4+
import { readCoverageJson } from './utils'
5+
6+
test('report contains only the changed files', async () => {
7+
const coverageJson = await readCoverageJson('./coverage/coverage-final.json')
8+
const coverageMap = libCoverage.createCoverageMap(coverageJson as any)
9+
10+
expect(coverageMap.files()).toMatchInlineSnapshot(`
11+
[
12+
"<process-cwd>/src/file-to-change.ts",
13+
"<process-cwd>/src/new-uncovered-file.ts",
14+
]
15+
`)
16+
17+
const uncoveredFile = coverageMap.fileCoverageFor('<process-cwd>/src/new-uncovered-file.ts').toSummary()
18+
expect(uncoveredFile.lines.pct).toBe(0)
19+
20+
const changedFile = coverageMap.fileCoverageFor('<process-cwd>/src/file-to-change.ts').toSummary()
21+
expect(changedFile.lines.pct).toBeGreaterThanOrEqual(50)
22+
})

‎test/coverage-test/coverage-report-tests/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ interface CoverageFinalJson {
1717
* Read JSON coverage report from file system.
1818
* Normalizes paths to keep contents consistent between OS's
1919
*/
20-
export async function readCoverageJson() {
21-
const jsonReport = JSON.parse(readFileSync('./coverage/custom-json-report-name.json', 'utf8')) as CoverageFinalJson
20+
export async function readCoverageJson(name = './coverage/custom-json-report-name.json') {
21+
const jsonReport = JSON.parse(readFileSync(name, 'utf8')) as CoverageFinalJson
2222

2323
const normalizedReport: CoverageFinalJson['default'] = {}
2424

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { test } from 'vitest'
2+
import { run } from '../src/file-to-change'
3+
4+
test('test case for changed file', () => {
5+
run()
6+
})
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function run() {
2+
return 'This file will be modified by test cases'
3+
}
4+
5+
export function uncoveredFunction() {
6+
return 1 + 2
7+
}

‎test/coverage-test/testing-options.mjs

+54-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
12
import { startVitest } from 'vitest/node'
23

3-
/** @type {Record<string, Partial<import('vitest/config').UserConfig['test']>>[]} */
4+
/**
5+
* @typedef {NonNullable<import('vitest/config').UserConfig['test']>} Config
6+
* @typedef { () => void | Promise<void> } Callback
7+
* @typedef {{ testConfig: Config, assertionConfig?: Config, after?: Callback, before?: Callback }} TestCase
8+
*/
9+
10+
/** @type {TestCase[]} */
411
const testCases = [
512
{
613
testConfig: {
@@ -51,10 +58,44 @@ const testCases = [
5158
},
5259
assertionConfig: null,
5360
},
61+
{
62+
testConfig: {
63+
name: 'changed',
64+
changed: 'HEAD',
65+
coverage: {
66+
include: ['src'],
67+
reporter: 'json',
68+
all: true,
69+
},
70+
},
71+
assertionConfig: {
72+
include: ['coverage-report-tests/changed.test.ts'],
73+
},
74+
before: () => {
75+
let content = readFileSync('./src/file-to-change.ts', 'utf8')
76+
content = content.replace('This file will be modified by test cases', 'Changed!')
77+
writeFileSync('./src/file-to-change.ts', content, 'utf8')
78+
79+
writeFileSync('./src/new-uncovered-file.ts', `
80+
// This file is not covered by any tests but should be picked by --changed
81+
export default function helloworld() {
82+
return 'Hello world'
83+
}
84+
`.trim(), 'utf8')
85+
},
86+
after: () => {
87+
let content = readFileSync('./src/file-to-change.ts', 'utf8')
88+
content = content.replace('Changed!', 'This file will be modified by test cases')
89+
writeFileSync('./src/file-to-change.ts', content, 'utf8')
90+
rmSync('./src/new-uncovered-file.ts')
91+
},
92+
},
5493
]
5594

5695
for (const provider of ['v8', 'istanbul']) {
57-
for (const { testConfig, assertionConfig } of testCases) {
96+
for (const { after, before, testConfig, assertionConfig } of testCases) {
97+
await before?.()
98+
5899
// Run test case
59100
await startVitest('test', ['option-tests/'], {
60101
config: false,
@@ -65,23 +106,23 @@ for (const provider of ['v8', 'istanbul']) {
65106
enabled: true,
66107
clean: true,
67108
all: false,
109+
reporter: [],
68110
provider,
69111
...testConfig.coverage,
70112
},
71113
})
72114

73-
checkExit()
74-
75-
if (!assertionConfig)
76-
continue
77-
78115
// Check generated coverage report
79-
await startVitest('test', ['coverage-report-tests'], {
80-
config: false,
81-
watch: false,
82-
...assertionConfig,
83-
name: `${provider} - assert ${testConfig.name}`,
84-
})
116+
if (assertionConfig) {
117+
await startVitest('test', ['coverage-report-tests'], {
118+
config: false,
119+
watch: false,
120+
...assertionConfig,
121+
name: `${provider} - assert ${testConfig.name}`,
122+
})
123+
}
124+
125+
await after?.()
85126

86127
checkExit()
87128
}

0 commit comments

Comments
 (0)
Please sign in to comment.