From ef11c9bc9dbdb9bbf0da13b1195e7ec164e81f31 Mon Sep 17 00:00:00 2001 From: kykim00 Date: Fri, 16 Jan 2026 06:54:30 +0900 Subject: [PATCH 1/8] feat(coverage): add coverage.changed config --- packages/vitest/src/node/cli/cli-config.ts | 14 ++++++++++++++ packages/vitest/src/node/config/resolveConfig.ts | 3 +++ packages/vitest/src/node/types/coverage.ts | 12 +++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 753ea566f674..e13f3b173e23 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -293,6 +293,20 @@ export const cliOptionsConfig: VitestCLIOptions = { }, }, }, + changed: { + description: + 'Collect coverage only for files changed since a specified commit or branch (e.g., `origin/main` or `HEAD~1`). Inherits value from `--changed` by default.', + argument: '', + transform(value) { + if (value === 'true' || value === 'yes' || value === true) { + return true + } + if (value === 'false' || value === 'no' || value === false) { + return false + } + return value + }, + }, }, }, mode: { diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index cb98436cf551..da0ff45a706a 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -389,6 +389,9 @@ export function resolveConfig( } resolved.coverage.reporter = resolveCoverageReporters(resolved.coverage.reporter) + if (resolved.coverage.changed === undefined && resolved.changed !== undefined) { + resolved.coverage.changed = resolved.changed + } if (resolved.coverage.enabled && resolved.coverage.reportsDirectory) { const reportsDirectory = resolve( diff --git a/packages/vitest/src/node/types/coverage.ts b/packages/vitest/src/node/types/coverage.ts index a148fc0901aa..b73e34089871 100644 --- a/packages/vitest/src/node/types/coverage.ts +++ b/packages/vitest/src/node/types/coverage.ts @@ -67,6 +67,8 @@ export interface CoverageProvider { export interface ReportContext { /** Indicates whether all tests were run. False when only specific tests were run. */ allTestsRun?: boolean + /** Absolute paths for files changed since a given commit/branch. */ + changedFiles?: string[] } export interface CoverageModuleLoader extends RuntimeCoverageModuleLoader { @@ -266,6 +268,14 @@ export interface BaseCoverageOptions { * @default [] */ ignoreClassMethods?: string[] + + /** + * Collect coverage only for files changed since a specified commit or branch. + * Inherits the default value from `test.changed`. + * + * @default false + */ + changed?: boolean | string } export interface CoverageIstanbulOptions extends BaseCoverageOptions {} @@ -273,7 +283,7 @@ export interface CoverageIstanbulOptions extends BaseCoverageOptions {} export interface CoverageV8Options extends BaseCoverageOptions {} export interface CustomProviderOptions - extends Pick { + extends Pick { /** Name of the module or path to a file to load the custom provider from */ customProviderModule: string } From 11a048cf2e780bcb80274d91c4d31fb720dbee34 Mon Sep 17 00:00:00 2001 From: kykim00 Date: Fri, 16 Jan 2026 06:55:35 +0900 Subject: [PATCH 2/8] feat(coverage): implement coverage.changed --- packages/coverage-istanbul/src/provider.ts | 4 ++- packages/coverage-v8/src/provider.ts | 4 ++- packages/vitest/src/node/core.ts | 41 +++++++++++++++++----- packages/vitest/src/node/coverage.ts | 35 ++++++++++++++---- 4 files changed, 67 insertions(+), 17 deletions(-) diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 6dddd56684f6..b78c27b61cd8 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -125,7 +125,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider { + async generateCoverage(reportContext: ReportContext): Promise { + this.setReportContext(reportContext) + const { allTestsRun } = reportContext const start = debug.enabled ? performance.now() : 0 const coverageMap = this.createCoverageMap() diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index 93fbb074ff43..8d6b88b683c1 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -40,7 +40,9 @@ export class V8CoverageProvider extends BaseCoverageProvider { + async generateCoverage(reportContext: ReportContext): Promise { + this.setReportContext(reportContext) + const { allTestsRun } = reportContext const start = debug.enabled ? performance.now() : 0 const coverageMap = this.createCoverageMap() diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index e77c9afd2cea..8a0678a65cda 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -12,7 +12,7 @@ import type { ProcessPool } from './pool' import type { TestModule } from './reporters/reported-tasks' import type { TestSpecification } from './test-specification' import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMode } from './types/config' -import type { CoverageProvider, ResolvedCoverageOptions } from './types/coverage' +import type { CoverageProvider, ReportContext, ResolvedCoverageOptions } from './types/coverage' import type { Reporter } from './types/reporter' import type { TestRunResult } from './types/tests' import os, { tmpdir } from 'node:os' @@ -36,7 +36,7 @@ import { resolveConfig } from './config/resolveConfig' import { getCoverageProvider } from './coverage' import { createFetchModuleFunction } from './environments/fetchModule' import { ServerModuleRunner } from './environments/serverRunner' -import { FilesNotFoundError } from './errors' +import { FilesNotFoundError, GitNotFoundError } from './errors' import { Logger } from './logger' import { collectModuleDurationsDiagnostic, collectSourceModulesLocations } from './module-diagnostic' import { VitestPackageInstaller } from './packageInstaller' @@ -743,11 +743,14 @@ export class Vitest { if (!specifications.length) { await this._traces.$('vitest.test_run', async () => { await this._testRun.start([]) - const coverage = await this.coverageProvider?.generateCoverage?.({ allTestsRun: true }) + const reportContext = this.coverageProvider + ? await this.getCoverageReportContext(true) + : { allTestsRun: true } + const coverage = await this.coverageProvider?.generateCoverage?.(reportContext) await this._testRun.end([], [], coverage) // Report coverage for uncovered files - await this.reportCoverage(coverage, true) + await this.reportCoverage(coverage, reportContext) }) if (!this.config.watch || !(this.config.changed || this.config.related?.length)) { @@ -920,12 +923,15 @@ export class Vitest { } } finally { - const coverage = await this.coverageProvider?.generateCoverage({ allTestsRun }) + const reportContext = this.coverageProvider + ? await this.getCoverageReportContext(allTestsRun) + : { allTestsRun } + const coverage = await this.coverageProvider?.generateCoverage(reportContext) const errors = this.state.getUnhandledErrors() this._checkUnhandledErrors(errors) await this._testRun.end(specs, errors, coverage) - await this.reportCoverage(coverage, allTestsRun) + await this.reportCoverage(coverage, reportContext) } })() .finally(() => { @@ -1361,7 +1367,26 @@ export class Vitest { } } - private async reportCoverage(coverage: unknown, allTestsRun: boolean): Promise { + private async getCoverageReportContext(allTestsRun: boolean): Promise { + const reportContext: ReportContext = { allTestsRun } + const coverageChanged = this._coverageOptions.changed + if (!coverageChanged) { + return reportContext + } + const { VitestGit } = await import('./git') + const vitestGit = new VitestGit(this.config.root) + const changedFiles = await vitestGit.findChangedFiles({ + changedSince: coverageChanged, + }) + if (!changedFiles) { + process.exitCode = 1 + throw new GitNotFoundError() + } + reportContext.changedFiles = Array.from(new Set(changedFiles)) + return reportContext + } + + private async reportCoverage(coverage: unknown, reportContext: ReportContext): Promise { if (this.state.getCountOfFailedTests() > 0) { await this.coverageProvider?.onTestFailure?.() @@ -1371,7 +1396,7 @@ export class Vitest { } if (this.coverageProvider) { - await this.coverageProvider.reportCoverage(coverage, { allTestsRun }) + await this.coverageProvider.reportCoverage(coverage, reportContext) // notify coverage iframe reload for (const reporter of this.reporters) { if (reporter instanceof WebSocketReporter) { diff --git a/packages/vitest/src/node/coverage.ts b/packages/vitest/src/node/coverage.ts index 2fc6e49f969b..fbeeb2754504 100644 --- a/packages/vitest/src/node/coverage.ts +++ b/packages/vitest/src/node/coverage.ts @@ -86,6 +86,7 @@ export class BaseCoverageProvider[] = [] coverageFilesDirectory!: string roots: string[] = [] + private changedFiles?: Set _initialize(ctx: Vitest): void { this.ctx = ctx @@ -142,6 +143,14 @@ export class BaseCoverageProvider slash(file))) + return + } + this.changedFiles = undefined + } + /** * Check if file matches `coverage.include` but not `coverage.exclude` */ @@ -192,8 +201,16 @@ export class BaseCoverageProvider this.isIncluded(file, root)) - if (this.ctx.config.changed) { - includedFiles = (this.ctx.config.related || []).filter(file => includedFiles.includes(file)) + if (this.changedFiles) { + includedFiles = includedFiles.filter(file => this.changedFiles!.has(slash(file))) + } + else if (this.ctx.config.changed) { + const related = this.ctx.config.related || [] + if (!related.length) { + return [] + } + const relatedSet = new Set(related.map(file => slash(file))) + includedFiles = includedFiles.filter(file => relatedSet.has(slash(file))) } return includedFiles.map(file => slash(path.resolve(root, file))) @@ -330,11 +347,15 @@ export class BaseCoverageProvider { - await this.generateReports( - (coverageMap as CoverageMap) || this.createCoverageMap(), - allTestsRun, - ) + async reportCoverage(coverageMap: unknown, reportContext: ReportContext): Promise { + this.setReportContext(reportContext) + const finalCoverageMap = (coverageMap as CoverageMap) || this.createCoverageMap() + + if (this.changedFiles) { + finalCoverageMap.filter(filename => this.changedFiles!.has(slash(filename))) + } + + await this.generateReports(finalCoverageMap, reportContext.allTestsRun) // In watch mode we need to preserve the previous results if cleanOnRerun is disabled const keepResults = !this.options.cleanOnRerun && this.ctx.config.watch From a7ff06f20748f5b110fbaebdd243918586390ad2 Mon Sep 17 00:00:00 2001 From: kykim00 Date: Fri, 16 Jan 2026 06:56:40 +0900 Subject: [PATCH 3/8] test(coverage): add coverage.changed tests --- test/core/test/cli-test.test.ts | 2 ++ test/coverage-test/test/changed.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) mode change 100644 => 100755 test/coverage-test/test/changed.test.ts diff --git a/test/core/test/cli-test.test.ts b/test/core/test/cli-test.test.ts index 0943d50149b0..ffa7d8f1a76d 100644 --- a/test/core/test/cli-test.test.ts +++ b/test/core/test/cli-test.test.ts @@ -60,6 +60,7 @@ test('nested coverage options have correct types', async () => { --coverage.thresholds.100 25 --coverage.provider v8 + --coverage.changed HEAD --coverage.reporter text --coverage.reportsDirectory .\\dist\\coverage --coverage.customProviderModule=./folder/coverage.js @@ -81,6 +82,7 @@ test('nested coverage options have correct types', async () => { enabled: true, reporter: ['text'], provider: 'v8', + changed: 'HEAD', clean: false, cleanOnRerun: true, reportsDirectory: 'dist/coverage', diff --git a/test/coverage-test/test/changed.test.ts b/test/coverage-test/test/changed.test.ts old mode 100644 new mode 100755 index 975ede7dc5a6..616675486222 --- a/test/coverage-test/test/changed.test.ts +++ b/test/coverage-test/test/changed.test.ts @@ -68,3 +68,26 @@ test('{ changed: "HEAD" }', { skip: SKIP }, async () => { } `) }) + +test('{ coverage.changed: "HEAD" }', async () => { + await runVitest({ + include: [ + 'fixtures/test/file-to-change.test.ts', + 'fixtures/test/math.test.ts', + ], + coverage: { + include: ['fixtures/src/**'], + reporter: 'json', + changed: 'HEAD', + }, + }) + + const coverageMap = await readCoverageMap() + + expect(coverageMap.files()).toMatchInlineSnapshot(` + [ + "/fixtures/src/file-to-change.ts", + "/fixtures/src/new-uncovered-file.ts", + ] + `) +}) From ccee2fb7fee7e64d3f41a7d0f82e66cc4967ad70 Mon Sep 17 00:00:00 2001 From: kykim00 Date: Fri, 16 Jan 2026 06:57:45 +0900 Subject: [PATCH 4/8] docs: document coverage.changed option (#8747) --- docs/config/coverage.md | 9 +++++++++ docs/guide/cli-generated.md | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/docs/config/coverage.md b/docs/config/coverage.md index d760620c714e..a9c9cd1eabf7 100644 --- a/docs/config/coverage.md +++ b/docs/config/coverage.md @@ -395,3 +395,12 @@ Concurrency limit used when processing the coverage results. - **CLI:** `--coverage.customProviderModule=` Specifies the module name or path for the custom coverage provider module. See [Guide - Custom Coverage Provider](/guide/coverage#custom-coverage-provider) for more information. + +## coverage.changed + +- **Type:** `boolean | string` +- **Default:** `false` (inherits from `test.changed`) +- **Available for providers:** `'v8' | 'istanbul'` +- **CLI:** `--coverage.changed`, `--coverage.changed=` + +Collect coverage only for files changed since a specified commit or branch. When set to `true`, it uses staged and unstaged changes. diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index b1a61ddf1b5d..1cdeec19b044 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -264,6 +264,13 @@ High and low watermarks for branches in the format of `,` High and low watermarks for functions in the format of `,` +### coverage.changed + +- **CLI:** `--coverage.changed ` +- **Config:** [coverage.changed](/config/coverage#coverage-changed) + +Collect coverage only for files changed since a specified commit or branch (e.g., `origin/main` or `HEAD~1`). Inherits value from `--changed` by default. + ### mode - **CLI:** `--mode ` From e845db855f9c00283beec6fb2e9314b2258a4043 Mon Sep 17 00:00:00 2001 From: kykim00 Date: Mon, 19 Jan 2026 01:03:42 +0900 Subject: [PATCH 5/8] test(coverage): add test for overriding coverage.changed configuration --- test/config/test/public.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/config/test/public.test.ts b/test/config/test/public.test.ts index 59c972f283c3..b40f3ffaf4a2 100644 --- a/test/config/test/public.test.ts +++ b/test/config/test/public.test.ts @@ -62,3 +62,23 @@ test('default value changes of coverage.exclude do not reflect to test.exclude', expect(vitestConfig.coverage.exclude).toContain('**/custom-exclude/**') expect(vitestConfig.coverage.exclude).toContain('**/example.test.ts') }) + +test('coverage.changed inherits from test.changed but can be overridden', async () => { + const { vitestConfig: inherited } = await resolveConfig({ + changed: 'HEAD', + coverage: { + reporter: 'json', + }, + }) + + expect(inherited.coverage.changed).toBe('HEAD') + + const { vitestConfig: overridden } = await resolveConfig({ + changed: 'HEAD', + coverage: { + changed: false, + }, + }) + + expect(overridden.coverage.changed).toBe(false) +}) From dc15bc9fa30ba19884307adb2a98921860b4d700 Mon Sep 17 00:00:00 2001 From: kykim00 Date: Tue, 3 Feb 2026 11:45:17 +0900 Subject: [PATCH 6/8] refactor(coverage): move changed filtering into BaseCoverageProvider --- packages/coverage-istanbul/src/provider.ts | 9 ++- packages/coverage-v8/src/provider.ts | 5 +- packages/vitest/src/node/core.ts | 41 ++--------- packages/vitest/src/node/coverage.ts | 86 ++++++++++++++-------- 4 files changed, 70 insertions(+), 71 deletions(-) diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index b78c27b61cd8..e3be70adce06 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -125,9 +125,8 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider { - this.setReportContext(reportContext) - const { allTestsRun } = reportContext + async generateCoverage({ allTestsRun }: ReportContext): Promise { + await this.updateChangedFiles() const start = debug.enabled ? performance.now() : 0 const coverageMap = this.createCoverageMap() @@ -138,6 +137,10 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider { + if (!this.options.excludeAfterRemap) { + this.filterChangedFiles(coverageMapByEnvironment) + } + // Source maps can change based on projectName and transform mode. // Coverage transform re-uses source maps so we need to separate transforms from each other. const transformedCoverage = await transformCoverage(coverageMapByEnvironment) diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index 8d6b88b683c1..e938d99bdb22 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -40,9 +40,8 @@ export class V8CoverageProvider extends BaseCoverageProvider { - this.setReportContext(reportContext) - const { allTestsRun } = reportContext + async generateCoverage({ allTestsRun }: ReportContext): Promise { + await this.updateChangedFiles() const start = debug.enabled ? performance.now() : 0 const coverageMap = this.createCoverageMap() diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 8a0678a65cda..e77c9afd2cea 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -12,7 +12,7 @@ import type { ProcessPool } from './pool' import type { TestModule } from './reporters/reported-tasks' import type { TestSpecification } from './test-specification' import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMode } from './types/config' -import type { CoverageProvider, ReportContext, ResolvedCoverageOptions } from './types/coverage' +import type { CoverageProvider, ResolvedCoverageOptions } from './types/coverage' import type { Reporter } from './types/reporter' import type { TestRunResult } from './types/tests' import os, { tmpdir } from 'node:os' @@ -36,7 +36,7 @@ import { resolveConfig } from './config/resolveConfig' import { getCoverageProvider } from './coverage' import { createFetchModuleFunction } from './environments/fetchModule' import { ServerModuleRunner } from './environments/serverRunner' -import { FilesNotFoundError, GitNotFoundError } from './errors' +import { FilesNotFoundError } from './errors' import { Logger } from './logger' import { collectModuleDurationsDiagnostic, collectSourceModulesLocations } from './module-diagnostic' import { VitestPackageInstaller } from './packageInstaller' @@ -743,14 +743,11 @@ export class Vitest { if (!specifications.length) { await this._traces.$('vitest.test_run', async () => { await this._testRun.start([]) - const reportContext = this.coverageProvider - ? await this.getCoverageReportContext(true) - : { allTestsRun: true } - const coverage = await this.coverageProvider?.generateCoverage?.(reportContext) + const coverage = await this.coverageProvider?.generateCoverage?.({ allTestsRun: true }) await this._testRun.end([], [], coverage) // Report coverage for uncovered files - await this.reportCoverage(coverage, reportContext) + await this.reportCoverage(coverage, true) }) if (!this.config.watch || !(this.config.changed || this.config.related?.length)) { @@ -923,15 +920,12 @@ export class Vitest { } } finally { - const reportContext = this.coverageProvider - ? await this.getCoverageReportContext(allTestsRun) - : { allTestsRun } - const coverage = await this.coverageProvider?.generateCoverage(reportContext) + const coverage = await this.coverageProvider?.generateCoverage({ allTestsRun }) const errors = this.state.getUnhandledErrors() this._checkUnhandledErrors(errors) await this._testRun.end(specs, errors, coverage) - await this.reportCoverage(coverage, reportContext) + await this.reportCoverage(coverage, allTestsRun) } })() .finally(() => { @@ -1367,26 +1361,7 @@ export class Vitest { } } - private async getCoverageReportContext(allTestsRun: boolean): Promise { - const reportContext: ReportContext = { allTestsRun } - const coverageChanged = this._coverageOptions.changed - if (!coverageChanged) { - return reportContext - } - const { VitestGit } = await import('./git') - const vitestGit = new VitestGit(this.config.root) - const changedFiles = await vitestGit.findChangedFiles({ - changedSince: coverageChanged, - }) - if (!changedFiles) { - process.exitCode = 1 - throw new GitNotFoundError() - } - reportContext.changedFiles = Array.from(new Set(changedFiles)) - return reportContext - } - - private async reportCoverage(coverage: unknown, reportContext: ReportContext): Promise { + private async reportCoverage(coverage: unknown, allTestsRun: boolean): Promise { if (this.state.getCountOfFailedTests() > 0) { await this.coverageProvider?.onTestFailure?.() @@ -1396,7 +1371,7 @@ export class Vitest { } if (this.coverageProvider) { - await this.coverageProvider.reportCoverage(coverage, reportContext) + await this.coverageProvider.reportCoverage(coverage, { allTestsRun }) // notify coverage iframe reload for (const reporter of this.reporters) { if (reporter instanceof WebSocketReporter) { diff --git a/packages/vitest/src/node/coverage.ts b/packages/vitest/src/node/coverage.ts index fbeeb2754504..4f337f0d0fdd 100644 --- a/packages/vitest/src/node/coverage.ts +++ b/packages/vitest/src/node/coverage.ts @@ -17,6 +17,7 @@ import c from 'tinyrainbow' import { coverageConfigDefaults } from '../defaults' import { resolveCoverageReporters } from '../node/config/resolveConfig' import { resolveCoverageProviderModule } from '../utils/coverage' +import { GitNotFoundError } from './errors' type Threshold = 'lines' | 'functions' | 'statements' | 'branches' @@ -143,12 +144,32 @@ export class BaseCoverageProvider slash(file))) + protected async updateChangedFiles(): Promise { + const coverageChanged = this.options.changed + if (!coverageChanged) { + this.changedFiles = undefined return } - this.changedFiles = undefined + const { VitestGit } = await import('./git') + const vitestGit = new VitestGit(this.ctx.config.root) + const changedFiles = await vitestGit.findChangedFiles({ + changedSince: coverageChanged, + }) + if (!changedFiles) { + process.exitCode = 1 + throw new GitNotFoundError() + } + this.changedFiles = new Set(changedFiles.map(file => slash(file))) + } + + protected filterChangedFiles(coverageMap: CoverageMap): void { + if (!this.changedFiles) { + return + } + coverageMap.filter((filename) => { + const normalized = slash(filename.split('?')[0]) + return this.changedFiles!.has(normalized) + }) } /** @@ -159,28 +180,33 @@ export class BaseCoverageProvider !filename.startsWith(root))) { + this.globCache.set(filename, false) + return false + } - if (cacheHit !== undefined) { - return cacheHit - } + // By default `coverage.include` matches all files, except "coverage.exclude" + const glob = this.options.include || '**' - // File outside project root with default allowExternal - if (this.options.allowExternal === false && roots.every(root => !filename.startsWith(root))) { - this.globCache.set(filename, false) + included = pm.isMatch(filename, glob, { + contains: true, + dot: true, + ignore: this.options.exclude, + }) - return false + this.globCache.set(filename, included) } - // By default `coverage.include` matches all files, except "coverage.exclude" - const glob = this.options.include || '**' - - const included = pm.isMatch(filename, glob, { - contains: true, - dot: true, - ignore: this.options.exclude, - }) + if (!included) { + return false + } - this.globCache.set(filename, included) + if (this.changedFiles && !this.changedFiles.has(filename)) { + return false + } return included } @@ -198,9 +224,6 @@ export class BaseCoverageProvider this.isIncluded(file, root)) - if (this.changedFiles) { includedFiles = includedFiles.filter(file => this.changedFiles!.has(slash(file))) } @@ -213,6 +236,9 @@ export class BaseCoverageProvider relatedSet.has(slash(file))) } + // Run again through picomatch as tinyglobby's exclude pattern is different ({ "exclude": ["math"] } should ignore "src/math.ts") + includedFiles = includedFiles.filter(file => this.isIncluded(file, root)) + return includedFiles.map(file => slash(path.resolve(root, file))) } @@ -347,15 +373,11 @@ export class BaseCoverageProvider { - this.setReportContext(reportContext) - const finalCoverageMap = (coverageMap as CoverageMap) || this.createCoverageMap() - - if (this.changedFiles) { - finalCoverageMap.filter(filename => this.changedFiles!.has(slash(filename))) - } - - await this.generateReports(finalCoverageMap, reportContext.allTestsRun) + async reportCoverage(coverageMap: unknown, { allTestsRun }: ReportContext): Promise { + await this.generateReports( + (coverageMap as CoverageMap) || this.createCoverageMap(), + allTestsRun, + ) // In watch mode we need to preserve the previous results if cleanOnRerun is disabled const keepResults = !this.options.cleanOnRerun && this.ctx.config.watch From 4ae999e3c2f5bec61ebf2488a978573e49c13113 Mon Sep 17 00:00:00 2001 From: kykim00 Date: Tue, 3 Feb 2026 14:42:39 +0900 Subject: [PATCH 7/8] fix(coverage): normalize changed filenames --- packages/vitest/src/node/coverage.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/node/coverage.ts b/packages/vitest/src/node/coverage.ts index 4f337f0d0fdd..ad720e67b873 100644 --- a/packages/vitest/src/node/coverage.ts +++ b/packages/vitest/src/node/coverage.ts @@ -167,8 +167,7 @@ export class BaseCoverageProvider { - const normalized = slash(filename.split('?')[0]) - return this.changedFiles!.has(normalized) + return this.changedFiles!.has(this.normalizeChangedFilename(filename)) }) } @@ -204,13 +203,27 @@ export class BaseCoverageProvider Date: Tue, 3 Feb 2026 14:46:23 +0900 Subject: [PATCH 8/8] test(coverage): add coverage.changed cases for remap and query params --- test/coverage-test/test/changed.test.ts | 34 +++++++++++- .../test/query-param-transforms.test.ts | 54 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/test/coverage-test/test/changed.test.ts b/test/coverage-test/test/changed.test.ts index 616675486222..0e0632846e9b 100755 --- a/test/coverage-test/test/changed.test.ts +++ b/test/coverage-test/test/changed.test.ts @@ -76,9 +76,41 @@ test('{ coverage.changed: "HEAD" }', async () => { 'fixtures/test/math.test.ts', ], coverage: { - include: ['fixtures/src/**'], + include: [ + 'fixtures/src/file-to-change.ts', + 'fixtures/src/new-uncovered-file.ts', + 'fixtures/src/math.ts', + ], + reporter: 'json', + changed: 'HEAD', + }, + }) + + const coverageMap = await readCoverageMap() + + expect(coverageMap.files()).toMatchInlineSnapshot(` + [ + "/fixtures/src/file-to-change.ts", + "/fixtures/src/new-uncovered-file.ts", + ] + `) +}) + +test('{ coverage.changed: "HEAD", excludeAfterRemap: true }', async () => { + await runVitest({ + include: [ + 'fixtures/test/file-to-change.test.ts', + 'fixtures/test/math.test.ts', + ], + coverage: { + include: [ + 'fixtures/src/file-to-change.ts', + 'fixtures/src/new-uncovered-file.ts', + 'fixtures/src/math.ts', + ], reporter: 'json', changed: 'HEAD', + excludeAfterRemap: true, }, }) diff --git a/test/coverage-test/test/query-param-transforms.test.ts b/test/coverage-test/test/query-param-transforms.test.ts index 65b870d595b2..f7419f9424ad 100644 --- a/test/coverage-test/test/query-param-transforms.test.ts +++ b/test/coverage-test/test/query-param-transforms.test.ts @@ -1,3 +1,5 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' import { expect } from 'vitest' import { readCoverageMap, runVitest, test } from '../utils' @@ -46,3 +48,55 @@ test('query param based transforms are resolved properly', async () => { ] `) }) + +test('query param transforms respect coverage.changed', async () => { + const filePath = resolve('./fixtures/src/query-param-transformed.ts') + const original = readFileSync(filePath, 'utf8') + writeFileSync(filePath, `${original}\nexport const changedMarker = true\n`, 'utf8') + + try { + await runVitest({ + config: 'fixtures/configs/vitest.config.query-param-transform.ts', + include: ['fixtures/test/query-param.test.ts'], + coverage: { reporter: 'json', changed: 'HEAD' }, + }) + + const coverageMap = await readCoverageMap() + + expect(coverageMap.files()).toMatchInlineSnapshot(` + [ + "/fixtures/src/query-param-transformed.ts", + ] + `) + + const coverage = coverageMap.fileCoverageFor(coverageMap.files()[0]) + + const functionCoverage = Object.keys(coverage.fnMap) + .map(index => ({ name: coverage.fnMap[index].name, hits: coverage.f[index] })) + .sort((a, b) => a.name.localeCompare(b.name)) + + expect(functionCoverage).toMatchInlineSnapshot(` + [ + { + "hits": 1, + "name": "first", + }, + { + "hits": 3, + "name": "initial", + }, + { + "hits": 1, + "name": "second", + }, + { + "hits": 0, + "name": "uncovered", + }, + ] + `) + } + finally { + writeFileSync(filePath, original, 'utf8') + } +})