Skip to content

Commit

Permalink
feat(coverage): automatic threshold updating
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Feb 23, 2023
1 parent d6bfeef commit c357fbe
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 0 deletions.
10 changes: 10 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,16 @@ Do not show files with 100% statement, branch, and function coverage.
Check thresholds per file.
See `lines`, `functions`, `branches` and `statements` for the actual thresholds.

#### thresholdAutoUpdate

- **Type:** `boolean`
- **Default:** `false`
- **Available for providers:** `'c8' | 'istanbul'`
- **CLI:** `--coverage.thresholdAutoUpdate=<boolean>`

Update threshold values `lines`, `functions`, `branches` and `statements` to configuration file when current coverage is above the configured thresholds.
This option helps to maintain thresholds when coverage is improved.

#### lines

- **Type:** `number`
Expand Down
13 changes: 13 additions & 0 deletions packages/coverage-c8/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,18 @@ export class C8CoverageProvider extends BaseCoverageProvider implements Coverage

await report.run()
await checkCoverages(options, report)

if (this.options.thresholdAutoUpdate && allTestsRun) {
this.updateThresholds({
coverageMap: await report.getCoverageMapFromAllCoverageFiles(),
thresholds: {
branches: this.options.branches,
functions: this.options.functions,
lines: this.options.lines,
statements: this.options.statements,
},
configurationFile: this.ctx.server.config.configFile,
})
}
}
}
13 changes: 13 additions & 0 deletions packages/coverage-istanbul/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
statements: this.options.statements,
})
}

if (this.options.thresholdAutoUpdate && allTestsRun) {
this.updateThresholds({
coverageMap,
thresholds: {
branches: this.options.branches,
functions: this.options.functions,
lines: this.options.lines,
statements: this.options.statements,
},
configurationFile: this.ctx.server.config.configFile,
})
}
}

checkThresholds(coverageMap: CoverageMap, thresholds: Record<Threshold, number | undefined>) {
Expand Down
7 changes: 7 additions & 0 deletions packages/vitest/src/types/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ export interface BaseCoverageOptions {
* @default undefined
*/
statements?: number

/**
* Update threshold values automatically when current coverage is higher than earlier thresholds
*
* @default false
*/
thresholdAutoUpdate?: boolean
}

export interface CoverageIstanbulOptions extends BaseCoverageOptions {
Expand Down
55 changes: 55 additions & 0 deletions packages/vitest/src/utils/coverage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,61 @@
import { readFileSync, writeFileSync } from 'node:fs'
import type { CoverageMap } from 'istanbul-lib-coverage'
import type { BaseCoverageOptions, ResolvedCoverageOptions } from '../types'

type Threshold = 'lines' | 'functions' | 'statements' | 'branches'

const THRESHOLD_KEYS: Readonly<Threshold[]> = ['lines', 'functions', 'statements', 'branches']

export class BaseCoverageProvider {
/**
* Check if current coverage is above configured thresholds and bump the thresholds if needed
*/
updateThresholds({ configurationFile, coverageMap, thresholds }: {
coverageMap: CoverageMap
thresholds: Record<Threshold, number | undefined>
configurationFile?: string
}) {
// Thresholds cannot be updated if there is no configuration file and
// feature was enabled by CLI, e.g. --coverage.thresholdAutoUpdate
if (!configurationFile)
throw new Error('Missing configurationFile. The "coverage.thresholdAutoUpdate" can only be enabled when configuration file is used.')

const summary = coverageMap.getCoverageSummary()
const thresholdsToUpdate: Threshold[] = []

for (const key of THRESHOLD_KEYS) {
const threshold = thresholds[key] || 100
const actual = summary[key].pct

if (actual > threshold)
thresholdsToUpdate.push(key)
}

if (thresholdsToUpdate.length === 0)
return

const originalConfig = readFileSync(configurationFile, 'utf8')
let updatedConfig = originalConfig

for (const threshold of thresholdsToUpdate) {
// Find the exact match from the configuration file and replace the value
const previousThreshold = (thresholds[threshold] || 100).toString()
const pattern = new RegExp(`(${threshold}\\s*:\\s*)${previousThreshold.replace('.', '\\.')}`)
const matches = originalConfig.match(pattern)

if (matches)
updatedConfig = updatedConfig.replace(matches[0], matches[1] + summary[threshold].pct)
else
console.error(`Unable to update coverage threshold ${threshold}. No threshold found using pattern ${pattern}`)
}

if (updatedConfig !== originalConfig) {
// eslint-disable-next-line no-console
console.log('Updating thresholds to configuration file. You may want to push with updated coverage thresholds.')
writeFileSync(configurationFile, updatedConfig, 'utf-8')
}
}

/**
* Resolve reporters from various configuration options
*/
Expand Down
17 changes: 17 additions & 0 deletions test/coverage-test/coverage-report-tests/generic.report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,20 @@ test('files should not contain a setup file', () => {

expect(srcFiles).not.toContain('another-setup.ts.html')
})

test('thresholdAutoUpdate updates thresholds', async () => {
const configFilename = resolve('./vitest.config.ts')
const configContents = fs.readFileSync(configFilename, 'utf-8')

for (const threshold of ['functions', 'branches', 'lines', 'statements']) {
const match = configContents.match(new RegExp(`${threshold}: (?<coverage>[\\d|\\.]+)`))
const coverage = match?.groups?.coverage || '0'

// Configuration has fixed value of 1.01 set for each threshold
expect(parseInt(coverage)).toBeGreaterThan(1.01)
}

// Update thresholds back to fixed values
const updatedConfig = configContents.replace(/(branches|functions|lines|statements): ([\d|\.])+/g, '$1: 1.01')
fs.writeFileSync(configFilename, updatedConfig)
})
7 changes: 7 additions & 0 deletions test/coverage-test/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export default defineConfig({
['lcov', {}],
['json', { file: 'custom-json-report-name.json' }],
],

// These will be updated by tests and reseted back by generic.report.test.ts
thresholdAutoUpdate: true,
functions: 1.01,
branches: 1.01,
lines: 1.01,
statements: 1.01,
},
setupFiles: [
resolve(__dirname, './setup.ts'),
Expand Down

0 comments on commit c357fbe

Please sign in to comment.