Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
13 changes: 13 additions & 0 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ function transformNestedBoolean(value: unknown) {
return value
}

function transformBoolean(value: unknown) {
if (value === 'true' || value === 'yes' || value === true) {
return true
}
if (value === 'false' || value === 'no' || value === false) {
return false
}
return value
}

export const cliOptionsConfig: VitestCLIOptions = {
root: {
description: 'Root path',
Expand Down Expand Up @@ -254,6 +264,9 @@ export const cliOptionsConfig: VitestCLIOptions = {
autoUpdate: {
description:
'Update threshold values: "lines", "functions", "branches" and "statements" to configuration file when current coverage is above the configured thresholds (default: `false`)',
argument: '<boolean|function>',
subcommands: null,
transform: transformBoolean,
},
lines: {
description:
Expand Down
7 changes: 5 additions & 2 deletions packages/vitest/src/node/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,13 +578,16 @@ export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istan

updatedThresholds = true

const thresholdFormatter = typeof this.options.thresholds?.autoUpdate === 'function' ? this.options.thresholds?.autoUpdate : (value: number | undefined) => value

for (const [threshold, newValue] of thresholdsToUpdate) {
const formattedValue = thresholdFormatter(newValue)
if (name === GLOBAL_THRESHOLDS_KEY) {
config.test.coverage.thresholds[threshold] = newValue
config.test.coverage.thresholds[threshold] = formattedValue
}
else {
const glob = config.test.coverage.thresholds[name as Threshold] as ResolvedThreshold['thresholds']
glob[threshold] = newValue
glob[threshold] = formattedValue
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/node/types/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,11 @@ interface Thresholds {

/**
* Update threshold values automatically when current coverage is higher than earlier thresholds
* Also can accept a function to format the new threshold values
*
* @default false
*/
autoUpdate?: boolean
autoUpdate?: boolean | ((newThreshold: number | undefined) => number | undefined)

/** Thresholds for statements */
statements?: number
Expand Down
11 changes: 11 additions & 0 deletions test/core/test/cli-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ test('fails when an array is passed down for a single value', async () => {
.toThrowErrorMatchingInlineSnapshot(`[Error: Expected a single value for option "--coverage.provider <name>", received ["v8", "istanbul"]]`)
})

test('coverage autoUpdate accepts boolean values from CLI', async () => {
expect(getCLIOptions('--coverage.thresholds.autoUpdate true').coverage.thresholds.autoUpdate).toBe(true)
expect(getCLIOptions('--coverage.thresholds.autoUpdate false').coverage.thresholds.autoUpdate).toBe(false)
expect(getCLIOptions('--coverage.thresholds.autoUpdate yes').coverage.thresholds.autoUpdate).toBe(true)
expect(getCLIOptions('--coverage.thresholds.autoUpdate no').coverage.thresholds.autoUpdate).toBe(false)
})

test('coverage autoUpdate preserves non-boolean values from CLI', async () => {
expect(getCLIOptions('--coverage.thresholds.autoUpdate someFunction').coverage.thresholds.autoUpdate).toBe('someFunction')
})

test('bench only options', async () => {
expect(() =>
parseArguments('--compare file.json').matchedCommand?.checkUnknownOptions(),
Expand Down
74 changes: 71 additions & 3 deletions test/coverage-test/test/threshold-auto-update.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,79 @@ export default config
await expect(updateThresholds(config)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Failed to update coverage thresholds. Configuration file is too complex.]`)
})

test('autoUpdate function rounds down decimal coverage to whole numbers', async () => {
// Create a config with autoUpdate function that rounds down to whole numbers
const configWithFunction = JSON.stringify(defineConfig({
test: {
coverage: {
thresholds: {
autoUpdate: (value: number | undefined) => value ? Math.floor(value) : value,
lines: 80, // Will be updated from 85.67 -> 85 (rounded down)
functions: 75, // Will be updated from 78.34 -> 78 (rounded down)
branches: 70, // Will be updated from 72.89 -> 72 (rounded down)
},
},
},
}), null, 2)

const config = parseModule(`export default ${configWithFunction}`)

// Use realistic coverage values with decimal places
const summaryData = { total: 0, covered: 0, skipped: 0 }
const thresholds = [{
name: 'global',
thresholds: { lines: 80, functions: 75, branches: 70 },
coverageMap: {
getCoverageSummary: () => createCoverageSummary({
lines: { pct: 85.67, ...summaryData },
functions: { pct: 78.34, ...summaryData },
branches: { pct: 72.89, ...summaryData },
statements: { pct: 65.12, ...summaryData },
}),
} as CoverageMap,
}]

const provider = new BaseCoverageProvider()
provider._initialize({
_coverageOptions: {
thresholds: {
autoUpdate: (value: number | undefined) => value ? Math.floor(value) : value,
},
},
logger: { log: () => {} },
} as any)

let updatedConfig = ''
await provider.updateThresholds({
thresholds,
configurationFile: config,
onUpdate: () => { updatedConfig = config.generate().code },
})

// Verify the function rounded down the decimal values to whole numbers
expect(updatedConfig).toContain('"lines": 85')
expect(updatedConfig).toContain('"functions": 78')
expect(updatedConfig).toContain('"branches": 72')

// Verify decimal values aren't present in the config
expect(updatedConfig).not.toContain('85.67')
expect(updatedConfig).not.toContain('78.34')
expect(updatedConfig).not.toContain('72.89')
})

async function updateThresholds(configurationFile: ReturnType<typeof parseModule>) {
const summaryData = { total: 0, covered: 0, skipped: 0 }

const configCode = configurationFile.generate().code
let actualThresholds = initialThresholds

if (configCode.includes('"lines": 30')) {
actualThresholds = { lines: 30, branches: 50, functions: 30, statements: 30 }
}

const thresholds = [{
name: 'global',
thresholds: initialThresholds,
thresholds: actualThresholds,
coverageMap: {
getCoverageSummary: () => createCoverageSummary({
lines: { pct: coveredThresholds.lines, ...summaryData },
Expand All @@ -149,9 +217,9 @@ async function updateThresholds(configurationFile: ReturnType<typeof parseModule
const provider = new BaseCoverageProvider()

provider._initialize({
config: { coverage: { } },
config: { coverage: { thresholds: { autoUpdate: true } } },
logger: { log: () => {} },
_coverageOptions: {},
_coverageOptions: { thresholds: { autoUpdate: true } },
} as any)

provider.updateThresholds({
Expand Down
Loading