diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 8c06ebd2800a..a579bf874755 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -4,7 +4,6 @@ import type { AfterSuiteRunMeta, CoverageIstanbulOptions, CoverageProvider, Repo import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config' import { BaseCoverageProvider } from 'vitest/coverage' import c from 'picocolors' -import type { ProxifiedModule } from 'magicast' import { parseModule } from 'magicast' import createDebug from 'debug' import libReport from 'istanbul-lib-report' @@ -172,7 +171,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co for (const filenames of [coveragePerProject.ssr, coveragePerProject.web]) { const coverageMapByTransformMode = libCoverage.createCoverageMap({}) - for (const chunk of toSlices(filenames, this.options.processingConcurrency)) { + for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) { if (debug.enabled) { index += chunk.length debug('Covered files %d/%d', index, total) @@ -206,7 +205,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co watermarks: this.options.watermarks, }) - if (hasTerminalReporter(this.options.reporter)) + if (this.hasTerminalReporter(this.options.reporter)) this.ctx.logger.log(c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name)) for (const reporter of this.options.reporter) { @@ -240,10 +239,8 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co this.updateThresholds({ thresholds: resolvedThresholds, perFile: this.options.thresholds.perFile, - configurationFile: { - write: () => writeFileSync(configFilePath, configModule.generate().code, 'utf-8'), - read: () => resolveConfig(configModule), - }, + configurationFile: configModule, + onUpdate: () => writeFileSync(configFilePath, configModule.generate().code, 'utf-8'), }) } } @@ -338,53 +335,3 @@ function isEmptyCoverageRange(range: libCoverage.Range) { || range.end.column === undefined ) } - -function hasTerminalReporter(reporters: Options['reporter']) { - return reporters.some(([reporter]) => - reporter === 'text' - || reporter === 'text-summary' - || reporter === 'text-lcov' - || reporter === 'teamcity') -} - -function toSlices(array: T[], size: number): T[][] { - return array.reduce((chunks, item) => { - const index = Math.max(0, chunks.length - 1) - const lastChunk = chunks[index] || [] - chunks[index] = lastChunk - - if (lastChunk.length >= size) - chunks.push([item]) - - else - lastChunk.push(item) - - return chunks - }, []) -} - -function resolveConfig(configModule: ProxifiedModule) { - const mod = configModule.exports.default - - try { - // Check for "export default { test: {...} }" - if (mod.$type === 'object') - return mod - - if (mod.$type === 'function-call') { - // "export default defineConfig({ test: {...} })" - if (mod.$args[0].$type === 'object') - return mod.$args[0] - - // "export default defineConfig(() => ({ test: {...} }))" - if (mod.$args[0].$type === 'arrow-function-expression' && mod.$args[0].$body.$type === 'object') - return mod.$args[0].$body - } - } - catch (error) { - // Reduce magicast's verbose errors to readable ones - throw new Error(error instanceof Error ? error.message : String(error)) - } - - throw new Error('Failed to update coverage thresholds. Configuration file is too complex.') -} diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index e2f7d617e086..14ebe527e7fa 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -9,7 +9,6 @@ import type { CoverageMap } from 'istanbul-lib-coverage' import libCoverage from 'istanbul-lib-coverage' import libSourceMaps from 'istanbul-lib-source-maps' import MagicString from 'magic-string' -import type { ProxifiedModule } from 'magicast' import { parseModule } from 'magicast' import remapping from '@ampproject/remapping' import { normalize, resolve } from 'pathe' @@ -162,7 +161,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage for (const [transformMode, filenames] of Object.entries(coveragePerProject) as [AfterSuiteRunMeta['transformMode'], Filename[]][]) { let merged: RawCoverage = { result: [] } - for (const chunk of toSlices(filenames, this.options.processingConcurrency)) { + for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) { if (debug.enabled) { index += chunk.length debug('Covered files %d/%d', index, total) @@ -198,7 +197,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage watermarks: this.options.watermarks, }) - if (hasTerminalReporter(this.options.reporter)) + if (this.hasTerminalReporter(this.options.reporter)) this.ctx.logger.log(c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name)) for (const reporter of this.options.reporter) { @@ -232,10 +231,8 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage this.updateThresholds({ thresholds: resolvedThresholds, perFile: this.options.thresholds.perFile, - configurationFile: { - write: () => writeFileSync(configFilePath, configModule.generate().code, 'utf-8'), - read: () => resolveConfig(configModule), - }, + configurationFile: configModule, + onUpdate: () => writeFileSync(configFilePath, configModule.generate().code, 'utf-8'), }) } } @@ -255,7 +252,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage let merged: RawCoverage = { result: [] } let index = 0 - for (const chunk of toSlices(uncoveredFiles, this.options.processingConcurrency)) { + for (const chunk of this.toSlices(uncoveredFiles, this.options.processingConcurrency)) { if (debug.enabled) { index += chunk.length debug('Uncovered files %d/%d', index, uncoveredFiles.length) @@ -334,7 +331,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage const coverageMap = libCoverage.createCoverageMap({}) let index = 0 - for (const chunk of toSlices(scriptCoverages, this.options.processingConcurrency)) { + for (const chunk of this.toSlices(scriptCoverages, this.options.processingConcurrency)) { if (debug.enabled) { index += chunk.length debug('Converting %d/%d', index, scriptCoverages.length) @@ -410,53 +407,3 @@ function normalizeTransformResults(fetchCache: Map - reporter === 'text' - || reporter === 'text-summary' - || reporter === 'text-lcov' - || reporter === 'teamcity') -} - -function toSlices(array: T[], size: number): T[][] { - return array.reduce((chunks, item) => { - const index = Math.max(0, chunks.length - 1) - const lastChunk = chunks[index] || [] - chunks[index] = lastChunk - - if (lastChunk.length >= size) - chunks.push([item]) - - else - lastChunk.push(item) - - return chunks - }, []) -} - -function resolveConfig(configModule: ProxifiedModule) { - const mod = configModule.exports.default - - try { - // Check for "export default { test: {...} }" - if (mod.$type === 'object') - return mod - - if (mod.$type === 'function-call') { - // "export default defineConfig({ test: {...} })" - if (mod.$args[0].$type === 'object') - return mod.$args[0] - - // "export default defineConfig(() => ({ test: {...} }))" - if (mod.$args[0].$type === 'arrow-function-expression' && mod.$args[0].$body.$type === 'object') - return mod.$args[0].$body - } - } - catch (error) { - // Reduce magicast's verbose errors to readable ones - throw new Error(error instanceof Error ? error.message : String(error)) - } - - throw new Error('Failed to update coverage thresholds. Configuration file is too complex.') -} diff --git a/packages/vitest/src/utils/coverage.ts b/packages/vitest/src/utils/coverage.ts index e2c5ee263fb4..846783f52b3a 100644 --- a/packages/vitest/src/utils/coverage.ts +++ b/packages/vitest/src/utils/coverage.ts @@ -18,14 +18,15 @@ export class BaseCoverageProvider { /** * Check if current coverage is above configured thresholds and bump the thresholds if needed */ - updateThresholds({ thresholds: allThresholds, perFile, configurationFile }: { + updateThresholds({ thresholds: allThresholds, perFile, configurationFile, onUpdate }: { thresholds: ResolvedThreshold[] perFile?: boolean - configurationFile: { read: () => unknown; write: () => void } + configurationFile: unknown // ProxifiedModule from magicast + onUpdate: () => void }) { let updatedThresholds = false - const config = configurationFile.read() + const config = resolveConfig(configurationFile) assertConfigurationModule(config) for (const { coverageMap, thresholds, name } of allThresholds) { @@ -63,7 +64,7 @@ export class BaseCoverageProvider { if (updatedThresholds) { // eslint-disable-next-line no-console console.log('Updating thresholds to configuration file. You may want to push with updated coverage thresholds.') - configurationFile.write() + onUpdate() } } @@ -200,6 +201,30 @@ export class BaseCoverageProvider { return resolvedReporters } + + hasTerminalReporter(reporters: ResolvedCoverageOptions['reporter']) { + return reporters.some(([reporter]) => + reporter === 'text' + || reporter === 'text-summary' + || reporter === 'text-lcov' + || reporter === 'teamcity') + } + + toSlices(array: T[], size: number): T[][] { + return array.reduce((chunks, item) => { + const index = Math.max(0, chunks.length - 1) + const lastChunk = chunks[index] || [] + chunks[index] = lastChunk + + if (lastChunk.length >= size) + chunks.push([item]) + + else + lastChunk.push(item) + + return chunks + }, []) + } } /** @@ -228,3 +253,29 @@ function assertConfigurationModule(config: unknown): asserts config is { test: { throw new Error(`Unable to parse thresholds from configuration file: ${message}`) } } + +function resolveConfig(configModule: any) { + const mod = configModule.exports.default + + try { + // Check for "export default { test: {...} }" + if (mod.$type === 'object') + return mod + + if (mod.$type === 'function-call') { + // "export default defineConfig({ test: {...} })" + if (mod.$args[0].$type === 'object') + return mod.$args[0] + + // "export default defineConfig(() => ({ test: {...} }))" + if (mod.$args[0].$type === 'arrow-function-expression' && mod.$args[0].$body.$type === 'object') + return mod.$args[0].$body + } + } + catch (error) { + // Reduce magicast's verbose errors to readable ones + throw new Error(error instanceof Error ? error.message : String(error)) + } + + throw new Error('Failed to update coverage thresholds. Configuration file is too complex.') +}