Skip to content
18 changes: 18 additions & 0 deletions docs/advanced/api/vitest.md
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,24 @@ Creates a coverage provider if `coverage` is enabled in the config. This is done
This method will also clean all previous reports if [`coverage.clean`](/config/#coverage-clean) is not set to `false`.
:::

## enableCoverage <Version>4.0.0</Version> {#enablecoverage}

```ts
function enableCoverage(): Promise<void>
```

This method enables coverage for tests that run after this call. `enableCoverage` doesn't run any tests; it only sets up Vitest to collect coverage.

It creates a new coverage provider if one doesn't already exist.

## disableCoverage <Version>4.0.0</Version> {#disablecoverage}

```ts
function disableCoverage(): void
```

This method disables coverage collection for tests that run afterwards.

## experimental_parseSpecification <Version>4.0.0</Version> <Badge type="warning">experimental</Badge> {#parsespecification}

```ts
Expand Down
7 changes: 4 additions & 3 deletions packages/browser/src/node/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export default (parentServer: ParentBrowserProject, base = '/'): Plugin[] => {
}

if (parentServer.vitest.coverageProvider) {
const coverage = parentServer.vitest.config.coverage
const coverage = parentServer.vitest._coverageOptions
const provider = coverage.provider
if (provider === 'v8') {
const path = tryResolve('@vitest/coverage-v8', [parentServer.config.root])
Expand Down Expand Up @@ -630,7 +630,8 @@ function getRequire() {

function resolveCoverageFolder(vitest: Vitest) {
const options = vitest.config
const htmlReporter = options.coverage?.enabled
const coverageOptions = vitest._coverageOptions
const htmlReporter = coverageOptions?.enabled
? toArray(options.coverage.reporter).find((reporter) => {
if (typeof reporter === 'string') {
return reporter === 'html'
Expand All @@ -647,7 +648,7 @@ function resolveCoverageFolder(vitest: Vitest) {
// reportsDirectory not resolved yet
const root = resolve(
options.root || process.cwd(),
options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory,
coverageOptions.reportsDirectory || coverageConfigDefaults.reportsDirectory,
)

const subdir
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/cli/cli-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export async function startVitest(
cliFilters,
)

if (mode === 'test' && ctx.config.coverage.enabled) {
const provider = ctx.config.coverage.provider || 'v8'
if (mode === 'test' && ctx._coverageOptions.enabled) {
const provider = ctx._coverageOptions.provider || 'v8'
const requiredPackages = CoverageProviderMap[provider]

if (requiredPackages) {
Expand Down
76 changes: 58 additions & 18 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import type { ProcessPool, WorkspaceSpec } from './pool'
import type { TestModule } from './reporters/reported-tasks'
import type { TestSpecification } from './spec'
import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMode } from './types/config'
import type { CoverageProvider } from './types/coverage'
import type { CoverageProvider, ResolvedCoverageOptions } from './types/coverage'
import type { Reporter } from './types/reporter'
import type { TestRunResult } from './types/tests'
import os from 'node:os'
import { getTasks, hasFailed, limitConcurrency } from '@vitest/runner/utils'
import { SnapshotManager } from '@vitest/snapshot/manager'
import { noop, toArray } from '@vitest/utils'
import { deepClone, deepMerge, noop, toArray } from '@vitest/utils'
import { normalize, relative } from 'pathe'
import { version } from '../../package.json' with { type: 'json' }
import { WebSocketReporter } from '../api/setup'
Expand Down Expand Up @@ -86,7 +86,6 @@ export class Vitest {
public projects: TestProject[] = []

/** @internal */ configOverride: Partial<ResolvedConfig> = {}
/** @internal */ coverageProvider: CoverageProvider | null | undefined
/** @internal */ filenamePattern?: string[]
/** @internal */ runningPromise?: Promise<TestRunResult>
/** @internal */ closingPromise?: Promise<void>
Expand All @@ -110,6 +109,7 @@ export class Vitest {
private _state?: StateManager
private _cache?: VitestCache
private _snapshot?: SnapshotManager
private _coverageProvider?: CoverageProvider | null | undefined

constructor(
public readonly mode: VitestRunMode,
Expand Down Expand Up @@ -202,10 +202,10 @@ export class Vitest {
this.pool = undefined
this.closingPromise = undefined
this.projects = []
this.coverageProvider = undefined
this.runningPromise = undefined
this.coreWorkspaceProject = undefined
this.specifications.clearCache()
this._coverageProvider = undefined
this._onUserTestsRerun = []

this._vite = server
Expand Down Expand Up @@ -305,6 +305,43 @@ export class Vitest {
await Promise.all(this._onSetServer.map(fn => fn()))
}

/** @internal */
get coverageProvider(): CoverageProvider | null | undefined {
if (this.configOverride.coverage?.enabled === false) {
return null
}
return this._coverageProvider
}

public async enableCoverage(): Promise<void> {
this.configOverride.coverage = {} as any
this.configOverride.coverage!.enabled = true
await this.createCoverageProvider()
}

public disableCoverage(): void {
this.configOverride.coverage ??= {} as any
this.configOverride.coverage!.enabled = false
}

private _coverageOverrideCache = new WeakMap<ResolvedCoverageOptions, ResolvedCoverageOptions>()

/** @internal */
get _coverageOptions(): ResolvedCoverageOptions {
if (!this.configOverride.coverage) {
return this.config.coverage
}
if (!this._coverageOverrideCache.has(this.configOverride.coverage)) {
const coverage = deepClone(this.config.coverage)
const options = deepMerge(coverage, this.configOverride)
this._coverageOverrideCache.set(
this.configOverride.coverage,
options,
)
}
return this._coverageOverrideCache.get(this.configOverride.coverage)!
}

/**
* Inject new test projects into the workspace.
* @param config Glob, config path or a custom config options.
Expand Down Expand Up @@ -392,12 +429,12 @@ export class Vitest {
* Creates a coverage provider if `coverage` is enabled in the config.
*/
public async createCoverageProvider(): Promise<CoverageProvider | null> {
if (this.coverageProvider) {
return this.coverageProvider
if (this._coverageProvider) {
return this._coverageProvider
}
const coverageProvider = await this.initCoverageProvider()
if (coverageProvider) {
await coverageProvider.clean(this.config.coverage.clean)
await coverageProvider.clean(this._coverageOptions.clean)
}
return coverageProvider || null
}
Expand Down Expand Up @@ -437,18 +474,21 @@ export class Vitest {
}

private async initCoverageProvider(): Promise<CoverageProvider | null | undefined> {
if (this.coverageProvider !== undefined) {
if (this._coverageProvider != null) {
return
}
this.coverageProvider = await getCoverageProvider(
this.config.coverage as unknown as SerializedCoverageConfig,
const coverageConfig = (this.configOverride.coverage
? this.getRootProject().serializedConfig.coverage
: this.config.coverage) as unknown as SerializedCoverageConfig
this._coverageProvider = await getCoverageProvider(
coverageConfig,
this.runner,
)
if (this.coverageProvider) {
await this.coverageProvider.initialize(this)
this.config.coverage = this.coverageProvider.resolveOptions()
if (this._coverageProvider) {
await this._coverageProvider.initialize(this)
this.config.coverage = this._coverageProvider.resolveOptions()
}
return this.coverageProvider
return this._coverageProvider
}

/**
Expand Down Expand Up @@ -546,7 +586,7 @@ export class Vitest {
async start(filters?: string[]): Promise<TestRunResult> {
try {
await this.initCoverageProvider()
await this.coverageProvider?.clean(this.config.coverage.clean)
await this.coverageProvider?.clean(this._coverageOptions.clean)
}
finally {
await this.report('onInit', this)
Expand Down Expand Up @@ -595,7 +635,7 @@ export class Vitest {
async init(): Promise<void> {
try {
await this.initCoverageProvider()
await this.coverageProvider?.clean(this.config.coverage.clean)
await this.coverageProvider?.clean(this._coverageOptions.clean)
}
finally {
await this.report('onInit', this)
Expand Down Expand Up @@ -702,7 +742,7 @@ export class Vitest {
this.snapshot.clear()
this.state.clearErrors()

if (!this.isFirstRun && this.config.coverage.cleanOnRerun) {
if (!this.isFirstRun && this._coverageOptions.cleanOnRerun) {
await this.coverageProvider?.clean()
}

Expand Down Expand Up @@ -1106,7 +1146,7 @@ export class Vitest {
if (this.state.getCountOfFailedTests() > 0) {
await this.coverageProvider?.onTestFailure?.()

if (!this.config.coverage.reportOnFailure) {
if (!this._coverageOptions.reportOnFailure) {
return
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan
)
}

const config = ctx.config.coverage as Options
const config = ctx._coverageOptions as Options

this.options = {
...coverageConfigDefaults,
Expand Down
2 changes: 2 additions & 0 deletions test/coverage-test/test/mixed-versions-warning.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ test('v8 provider logs warning if versions do not match', async () => {
version: '1.0.0',
logger: { warn },
config: configDefaults,
_coverageOptions: configDefaults.coverage,
} as any)

expect(warn).toHaveBeenCalled()
Expand All @@ -36,6 +37,7 @@ test('istanbul provider logs warning if versions do not match', async () => {
version: '1.0.0',
logger: { warn },
config: configDefaults,
_coverageOptions: configDefaults.coverage,
} as any)

expect(warn).toHaveBeenCalled()
Expand Down
62 changes: 62 additions & 0 deletions test/coverage-test/test/run-dynamic-coverage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect } from 'vitest'
import { cleanupCoverageJson, readCoverageMap, runVitest, test } from '../utils'

test('enableCoverage() collects coverage after being called', async () => {
await cleanupCoverageJson()

// Run a minimal suite where coverage starts disabled, then enable it and rerun.
const { ctx } = await runVitest({
include: ['fixtures/test/math.test.ts'],
coverage: {
// start disabled and turn on dynamically
enabled: false,
reporter: 'json',
include: [
'fixtures/src/math.ts',
'fixtures/src/untested-file.ts',
],
},
})

await expect(readCoverageMap(), 'coverage map is not on the disk').rejects.toThrowError(/no such file/)

await ctx!.enableCoverage()
expect(ctx!.coverageProvider).toBeTruthy()

await ctx!.rerunFiles()

const coverageMap = await readCoverageMap()
expect(coverageMap.files()).toContain('<process-cwd>/fixtures/src/math.ts')
})

test('disableCoverage() stops collecting coverage going forward', async () => {
const { ctx } = await runVitest({
include: ['fixtures/test/math.test.ts'],
coverage: {
enabled: true,
reporter: 'json',
include: [
'fixtures/src/math.ts',
'fixtures/src/untested-file.ts',
],
// try to clean on rerun when provider is enabled
cleanOnRerun: true,
},
})

// Initial run collects coverage
const initialMap = await readCoverageMap()
expect(initialMap.files()).toContain('<process-cwd>/fixtures/src/math.ts')
expect(ctx!.coverageProvider).toBeTruthy()

// Disable coverage and rerun
ctx!.disableCoverage()
expect(ctx!.coverageProvider).toBeNull()

await cleanupCoverageJson()

await ctx!.rerunFiles()

await expect(readCoverageMap(), 'coverage map is not on the disk').rejects.toThrowError(/no such file/)
expect(ctx!.coverageProvider).toBeNull()
})
1 change: 1 addition & 0 deletions test/coverage-test/test/threshold-auto-update.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ async function updateThresholds(configurationFile: ReturnType<typeof parseModule
provider._initialize({
config: { coverage: { } },
logger: { log: () => {} },
_coverageOptions: {},
} as any)

provider.updateThresholds({
Expand Down
9 changes: 8 additions & 1 deletion test/coverage-test/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { CoverageSummary, FileCoverageData } from 'istanbul-lib-coverage'
import type { TestFunction } from 'vitest'
import type { TestUserConfig } from 'vitest/node'
import { readFileSync } from 'node:fs'
import { existsSync, readFileSync } from 'node:fs'
import { unlink } from 'node:fs/promises'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { stripVTControlCharacters } from 'node:util'
Expand Down Expand Up @@ -67,6 +68,12 @@ export async function runVitest(config: TestUserConfig, options = { throwOnError
return result
}

export async function cleanupCoverageJson(name = './coverage/coverage-final.json') {
if (existsSync(name)) {
await unlink(name)
}
}

/**
* Read JSON coverage report from file system.
* Normalizes paths to keep contents consistent between OS's
Expand Down
Loading