Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/config/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,12 @@ Concurrency limit used when processing the coverage results.
- **CLI:** `--coverage.customProviderModule=<path or module name>`

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=<commit/branch>`

Collect coverage only for files changed since a specified commit or branch. When set to `true`, it uses staged and unstaged changes.
7 changes: 7 additions & 0 deletions docs/guide/cli-generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@ High and low watermarks for branches in the format of `<high>,<low>`

High and low watermarks for functions in the format of `<high>,<low>`

### coverage.changed

- **CLI:** `--coverage.changed <commit/branch>`
- **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 <name>`
Expand Down
4 changes: 3 additions & 1 deletion packages/coverage-istanbul/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider<ResolvedCover
return libCoverage.createCoverageMap({})
}

async generateCoverage({ allTestsRun }: ReportContext): Promise<CoverageMap> {
async generateCoverage(reportContext: ReportContext): Promise<CoverageMap> {
this.setReportContext(reportContext)
const { allTestsRun } = reportContext
const start = debug.enabled ? performance.now() : 0

const coverageMap = this.createCoverageMap()
Expand Down
4 changes: 3 additions & 1 deletion packages/coverage-v8/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
return libCoverage.createCoverageMap({})
}

async generateCoverage({ allTestsRun }: ReportContext): Promise<CoverageMap> {
async generateCoverage(reportContext: ReportContext): Promise<CoverageMap> {
this.setReportContext(reportContext)
const { allTestsRun } = reportContext
const start = debug.enabled ? performance.now() : 0

const coverageMap = this.createCoverageMap()
Expand Down
14 changes: 14 additions & 0 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<commit/branch>',
transform(value) {
if (value === 'true' || value === 'yes' || value === true) {
return true
}
if (value === 'false' || value === 'no' || value === false) {
return false
}
return value
},
},
},
},
mode: {
Expand Down
3 changes: 3 additions & 0 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
41 changes: 33 additions & 8 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -1361,7 +1367,26 @@ export class Vitest {
}
}

private async reportCoverage(coverage: unknown, allTestsRun: boolean): Promise<void> {
private async getCoverageReportContext(allTestsRun: boolean): Promise<ReportContext> {
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
}
Comment thread
AriPerkkio marked this conversation as resolved.
Outdated

private async reportCoverage(coverage: unknown, reportContext: ReportContext): Promise<void> {
if (this.state.getCountOfFailedTests() > 0) {
await this.coverageProvider?.onTestFailure?.()

Expand All @@ -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) {
Expand Down
35 changes: 28 additions & 7 deletions packages/vitest/src/node/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
pendingPromises: Promise<void>[] = []
coverageFilesDirectory!: string
roots: string[] = []
private changedFiles?: Set<string>

_initialize(ctx: Vitest): void {
this.ctx = ctx
Expand Down Expand Up @@ -142,6 +143,14 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
: [ctx.config.root]
}

protected setReportContext(reportContext: ReportContext): void {
if (reportContext.changedFiles) {
this.changedFiles = new Set(reportContext.changedFiles.map(file => slash(file)))
return
}
this.changedFiles = undefined
}

/**
* Check if file matches `coverage.include` but not `coverage.exclude`
*/
Expand Down Expand Up @@ -192,8 +201,16 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
// 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))

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)))
Expand Down Expand Up @@ -330,11 +347,15 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
}
}

async reportCoverage(coverageMap: unknown, { allTestsRun }: ReportContext): Promise<void> {
await this.generateReports(
(coverageMap as CoverageMap) || this.createCoverageMap(),
allTestsRun,
)
async reportCoverage(coverageMap: unknown, reportContext: ReportContext): Promise<void> {
this.setReportContext(reportContext)
const finalCoverageMap = (coverageMap as CoverageMap) || this.createCoverageMap()

if (this.changedFiles) {
finalCoverageMap.filter(filename => this.changedFiles!.has(slash(filename)))
}
Comment thread
AriPerkkio marked this conversation as resolved.

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
Expand Down
12 changes: 11 additions & 1 deletion packages/vitest/src/node/types/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Comment thread
AriPerkkio marked this conversation as resolved.
Outdated
}

export interface CoverageModuleLoader extends RuntimeCoverageModuleLoader {
Expand Down Expand Up @@ -266,14 +268,22 @@ 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 {}

export interface CoverageV8Options extends BaseCoverageOptions {}

export interface CustomProviderOptions
extends Pick<BaseCoverageOptions, FieldsWithDefaultValues> {
extends Pick<BaseCoverageOptions, FieldsWithDefaultValues | 'changed'> {
/** Name of the module or path to a file to load the custom provider from */
customProviderModule: string
}
Expand Down
20 changes: 20 additions & 0 deletions test/config/test/public.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
2 changes: 2 additions & 0 deletions test/core/test/cli-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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',
Expand Down
23 changes: 23 additions & 0 deletions test/coverage-test/test/changed.test.ts
100644 → 100755
Comment thread
AriPerkkio marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
[
"<process-cwd>/fixtures/src/file-to-change.ts",
"<process-cwd>/fixtures/src/new-uncovered-file.ts",
]
`)
})
Loading