From 4166c413ba897a6987e18884ffea3cacd6e6cd9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Tue, 28 Nov 2023 20:30:59 +0200 Subject: [PATCH] fix(coverage): improve memory usage by writing temporary files on file system (#4603) --- docs/config/index.md | 9 + packages/coverage-istanbul/package.json | 2 + packages/coverage-istanbul/src/provider.ts | 131 ++++++++---- packages/coverage-v8/package.json | 2 + packages/coverage-v8/src/provider.ts | 197 ++++++++++++------ packages/vitest/src/defaults.ts | 2 + packages/vitest/src/types/coverage.ts | 7 + pnpm-lock.yaml | 20 +- .../istanbul.report.test.ts.snap | 25 ++- .../__snapshots__/v8.report.test.ts.snap | 40 ++-- .../test/configuration-options.test-d.ts | 1 + 11 files changed, 307 insertions(+), 129 deletions(-) diff --git a/docs/config/index.md b/docs/config/index.md index 2f0e4fbe159f..eeb7bf9e5d88 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1323,6 +1323,15 @@ See [istanbul documentation](https://github.com/istanbuljs/nyc#ignoring-methods) Watermarks for statements, lines, branches and functions. See [istanbul documentation](https://github.com/istanbuljs/nyc#high-and-low-watermarks) for more information. +#### coverage.processingConcurrency + +- **Type:** `boolean` +- **Default:** `Math.min(20, os.cpu().length)` +- **Available for providers:** `'v8' | 'istanbul'` +- **CLI:** `--coverage.processingConcurrency=` + +Concurrency limit used when processing the coverage results. + #### coverage.customProviderModule - **Type:** `string` diff --git a/packages/coverage-istanbul/package.json b/packages/coverage-istanbul/package.json index 750b4d8c8362..df23530e6da3 100644 --- a/packages/coverage-istanbul/package.json +++ b/packages/coverage-istanbul/package.json @@ -45,6 +45,7 @@ "vitest": "^1.0.0-0" }, "dependencies": { + "debug": "^4.3.4", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-instrument": "^6.0.1", "istanbul-lib-report": "^3.0.1", @@ -55,6 +56,7 @@ "test-exclude": "^6.0.0" }, "devDependencies": { + "@types/debug": "^4.1.12", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-lib-instrument": "^1.7.7", "@types/istanbul-lib-report": "^3.0.3", diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index ea91fc5fa7b7..1eb6a70597c2 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -5,9 +5,10 @@ import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/c import { BaseCoverageProvider } from 'vitest/coverage' import c from 'picocolors' import { parseModule } from 'magicast' +import createDebug from 'debug' import libReport from 'istanbul-lib-report' import reports from 'istanbul-reports' -import type { CoverageMap, CoverageMapData } from 'istanbul-lib-coverage' +import type { CoverageMap } from 'istanbul-lib-coverage' import libCoverage from 'istanbul-lib-coverage' import libSourceMaps from 'istanbul-lib-source-maps' import { type Instrumenter, createInstrumenter } from 'istanbul-lib-instrument' @@ -17,7 +18,8 @@ import _TestExclude from 'test-exclude' import { COVERAGE_STORE_KEY } from './constants' type Options = ResolvedCoverageOptions<'istanbul'> -type CoverageByTransformMode = Record +type Filename = string +type CoverageFilesByTransformMode = Record type ProjectName = NonNullable | typeof DEFAULT_PROJECT interface TestExclude { @@ -35,6 +37,8 @@ interface TestExclude { } const DEFAULT_PROJECT = Symbol.for('default-project') +const debug = createDebug('vitest:coverage') +let uniqueId = 0 export class IstanbulCoverageProvider extends BaseCoverageProvider implements CoverageProvider { name = 'istanbul' @@ -44,13 +48,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co instrumenter!: Instrumenter testExclude!: InstanceType - /** - * Coverage objects collected from workers. - * Some istanbul utilizers write these into file system instead of storing in memory. - * If storing in memory causes issues, we can simply write these into fs in `onAfterSuiteRun` - * and read them back when merging coverage objects in `onAfterAllFilesRun`. - */ - coverages = new Map() + coverageFiles = new Map() + coverageFilesDirectory!: string + pendingPromises: Promise[] = [] initialize(ctx: Vitest) { const config: CoverageIstanbulOptions = ctx.config.coverage @@ -96,6 +96,8 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co extension: this.options.extension, relativePath: !this.options.allowExternal, }) + + this.coverageFilesDirectory = resolve(this.options.reportsDirectory, '.tmp') } resolveOptions() { @@ -121,43 +123,79 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co * backwards compatibility is a breaking change. */ onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta) { + if (!coverage) + return + if (transformMode !== 'web' && transformMode !== 'ssr') throw new Error(`Invalid transform mode: ${transformMode}`) - let entry = this.coverages.get(projectName || DEFAULT_PROJECT) + let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT) if (!entry) { entry = { web: [], ssr: [] } - this.coverages.set(projectName || DEFAULT_PROJECT, entry) + this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry) } - entry[transformMode].push(coverage as CoverageMapData) + const filename = resolve(this.coverageFilesDirectory, `coverage-${uniqueId++}.json`) + entry[transformMode].push(filename) + + const promise = fs.writeFile(filename, JSON.stringify(coverage), 'utf-8') + this.pendingPromises.push(promise) } async clean(clean = true) { if (clean && existsSync(this.options.reportsDirectory)) await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }) - this.coverages = new Map() + if (existsSync(this.coverageFilesDirectory)) + await fs.rm(this.coverageFilesDirectory, { recursive: true, force: true, maxRetries: 10 }) + + await fs.mkdir(this.coverageFilesDirectory, { recursive: true }) + + this.coverageFiles = new Map() + this.pendingPromises = [] } async reportCoverage({ allTestsRun }: ReportContext = {}) { - const coverageMaps = await Promise.all( - Array.from(this.coverages.values()).map(coverages => [ - mergeAndTransformCoverage(coverages.ssr), - mergeAndTransformCoverage(coverages.web), - ]).flat(), - ) + const coverageMap = libCoverage.createCoverageMap({}) + let index = 0 + const total = this.pendingPromises.length + + await Promise.all(this.pendingPromises) + this.pendingPromises = [] + + for (const coveragePerProject of this.coverageFiles.values()) { + for (const filenames of [coveragePerProject.ssr, coveragePerProject.web]) { + const coverageMapByTransformMode = libCoverage.createCoverageMap({}) + + for (const chunk of toSlices(filenames, this.options.processingConcurrency)) { + if (debug.enabled) { + index += chunk.length + debug('Covered files %d/%d', index, total) + } + + await Promise.all(chunk.map(async (filename) => { + const contents = await fs.readFile(filename, 'utf-8') + const coverage = JSON.parse(contents) as CoverageMap + + coverageMapByTransformMode.merge(coverage) + })) + } + + // Source maps can change based on projectName and transform mode. + // Coverage transform re-uses source maps so we need to separate transforms from each other. + const transformedCoverage = await transformCoverage(coverageMapByTransformMode) + coverageMap.merge(transformedCoverage) + } + } if (this.options.all && allTestsRun) { - const coveredFiles = coverageMaps.map(map => map.files()).flat() + const coveredFiles = coverageMap.files() const uncoveredCoverage = await this.getCoverageMapForUncoveredFiles(coveredFiles) - coverageMaps.push(await mergeAndTransformCoverage([uncoveredCoverage])) + coverageMap.merge(await transformCoverage(uncoveredCoverage)) } - const coverageMap = mergeCoverageMaps(...coverageMaps) - const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, @@ -206,6 +244,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co }) } } + + await fs.rm(this.coverageFilesDirectory, { recursive: true }) + this.coverageFiles = new Map() } async getCoverageMapForUncoveredFiles(coveredFiles: string[]) { @@ -218,31 +259,31 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co const coverageMap = libCoverage.createCoverageMap({}) - for (const filename of uncoveredFiles) { + // Note that these cannot be run parallel as synchronous instrumenter.lastFileCoverage + // returns the coverage of the last transformed file + for (const [index, filename] of uncoveredFiles.entries()) { + debug('Uncovered file %s %d/%d', filename, index, uncoveredFiles.length) + + // Make sure file is not served from cache + // so that instrumenter loads up requested file coverage + if (this.ctx.vitenode.fetchCache.has(filename)) + this.ctx.vitenode.fetchCache.delete(filename) + await this.ctx.vitenode.transformRequest(filename) const lastCoverage = this.instrumenter.lastFileCoverage() coverageMap.addFileCoverage(lastCoverage) } - return coverageMap.data + return coverageMap } } -async function mergeAndTransformCoverage(coverages: CoverageMapData[]) { - const mergedCoverage = mergeCoverageMaps(...coverages) - includeImplicitElseBranches(mergedCoverage) +async function transformCoverage(coverageMap: CoverageMap) { + includeImplicitElseBranches(coverageMap) const sourceMapStore = libSourceMaps.createSourceMapStore() - return await sourceMapStore.transformCoverage(mergedCoverage) -} - -function mergeCoverageMaps(...coverageMaps: (CoverageMap | CoverageMapData)[]) { - return coverageMaps.reduce((coverage, previousCoverageMap) => { - const map = libCoverage.createCoverageMap(coverage) - map.merge(previousCoverageMap) - return map - }, libCoverage.createCoverageMap({})) + return await sourceMapStore.transformCoverage(coverageMap) } /** @@ -302,3 +343,19 @@ function hasTerminalReporter(reporters: Options['reporter']) { || 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 + }, []) +} diff --git a/packages/coverage-v8/package.json b/packages/coverage-v8/package.json index 7b708c552566..c16e64a46791 100644 --- a/packages/coverage-v8/package.json +++ b/packages/coverage-v8/package.json @@ -47,6 +47,7 @@ "dependencies": { "@ampproject/remapping": "^2.2.1", "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^4.0.1", @@ -59,6 +60,7 @@ "v8-to-istanbul": "^9.2.0" }, "devDependencies": { + "@types/debug": "^4.1.12", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-lib-report": "^3.0.3", "@types/istanbul-lib-source-maps": "^4.0.4", diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index b88dc80af2e2..d3f95a874886 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -5,7 +5,7 @@ import v8ToIstanbul from 'v8-to-istanbul' import { mergeProcessCovs } from '@bcoe/v8-coverage' import libReport from 'istanbul-lib-report' import reports from 'istanbul-reports' -import type { CoverageMap, CoverageMapData } from 'istanbul-lib-coverage' +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' @@ -14,6 +14,7 @@ import remapping from '@ampproject/remapping' import { normalize, resolve } from 'pathe' import c from 'picocolors' import { provider } from 'std-env' +import createDebug from 'debug' import { cleanUrl } from 'vite-node/utils' import type { EncodedSourceMap, FetchResult } from 'vite-node' import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config' @@ -40,8 +41,9 @@ interface TestExclude { type Options = ResolvedCoverageOptions<'v8'> type TransformResults = Map +type Filename = string type RawCoverage = Profiler.TakePreciseCoverageReturnType -type CoverageByTransformMode = Record +type CoverageFilesByTransformMode = Record type ProjectName = NonNullable | typeof DEFAULT_PROJECT // TODO: vite-node should export this @@ -51,13 +53,19 @@ const WRAPPER_LENGTH = 185 const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g const DEFAULT_PROJECT = Symbol.for('default-project') +const debug = createDebug('vitest:coverage') +let uniqueId = 0 + export class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider { name = 'v8' ctx!: Vitest options!: Options testExclude!: InstanceType - coverages = new Map() + + coverageFiles = new Map() + coverageFilesDirectory!: string + pendingPromises: Promise[] = [] initialize(ctx: Vitest) { const config: CoverageV8Options = ctx.config.coverage @@ -91,6 +99,8 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage extension: this.options.extension, relativePath: !this.options.allowExternal, }) + + this.coverageFilesDirectory = resolve(this.options.reportsDirectory, '.tmp') } resolveOptions() { @@ -101,7 +111,13 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage if (clean && existsSync(this.options.reportsDirectory)) await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }) - this.coverages = new Map() + if (existsSync(this.coverageFilesDirectory)) + await fs.rm(this.coverageFilesDirectory, { recursive: true, force: true, maxRetries: 10 }) + + await fs.mkdir(this.coverageFilesDirectory, { recursive: true }) + + this.coverageFiles = new Map() + this.pendingPromises = [] } /* @@ -113,37 +129,65 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage if (transformMode !== 'web' && transformMode !== 'ssr') throw new Error(`Invalid transform mode: ${transformMode}`) - let entry = this.coverages.get(projectName || DEFAULT_PROJECT) + let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT) if (!entry) { entry = { web: [], ssr: [] } - this.coverages.set(projectName || DEFAULT_PROJECT, entry) + this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry) } - entry[transformMode].push(coverage as RawCoverage) + const filename = resolve(this.coverageFilesDirectory, `coverage-${uniqueId++}.json`) + entry[transformMode].push(filename) + + const promise = fs.writeFile(filename, JSON.stringify(coverage), 'utf-8') + this.pendingPromises.push(promise) } async reportCoverage({ allTestsRun }: ReportContext = {}) { if (provider === 'stackblitz') this.ctx.logger.log(c.blue(' % ') + c.yellow('@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.')) - const coverageMaps = await Promise.all( - Array.from(this.coverages.entries()).map(([projectName, coverages]) => [ - this.mergeAndTransformCoverage(coverages.ssr, projectName, 'ssr'), - this.mergeAndTransformCoverage(coverages.web, projectName, 'web'), - ]).flat(), - ) + const coverageMap = libCoverage.createCoverageMap({}) + let index = 0 + const total = this.pendingPromises.length + + await Promise.all(this.pendingPromises) + this.pendingPromises = [] + + for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) { + 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)) { + if (debug.enabled) { + index += chunk.length + debug('Covered files %d/%d', index, total) + } + + await Promise.all(chunk.map(async (filename) => { + const contents = await fs.readFile(filename, 'utf-8') + const coverage = JSON.parse(contents) as RawCoverage + merged = mergeProcessCovs([merged, coverage]) + })) + } + + const converted = await this.convertCoverage(merged, projectName, transformMode) + + // Source maps can change based on projectName and transform mode. + // Coverage transform re-uses source maps so we need to separate transforms from each other. + const transformedCoverage = await transformCoverage(converted) + coverageMap.merge(transformedCoverage) + } + } if (this.options.all && allTestsRun) { - const coveredFiles = coverageMaps.map(map => map.files()).flat() + const coveredFiles = coverageMap.files() const untestedCoverage = await this.getUntestedFiles(coveredFiles) - const untestedCoverageResults = untestedCoverage.map(files => ({ result: [files] })) - coverageMaps.push(await this.mergeAndTransformCoverage(untestedCoverageResults)) + const converted = await this.convertCoverage(untestedCoverage) + coverageMap.merge(await transformCoverage(converted)) } - const coverageMap = mergeCoverageMaps(...coverageMaps) - const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, @@ -191,10 +235,13 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage }, }) } + + this.coverageFiles = new Map() + await fs.rm(this.coverageFilesDirectory, { recursive: true }) } } - private async getUntestedFiles(testedFiles: string[]): Promise { + private async getUntestedFiles(testedFiles: string[]): Promise { const transformResults = normalizeTransformResults(this.ctx.vitenode.fetchCache) const includedFiles = await this.testExclude.glob(this.ctx.config.root) @@ -202,25 +249,41 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage .map(file => pathToFileURL(resolve(this.ctx.config.root, file))) .filter(file => !testedFiles.includes(file.pathname)) - return await Promise.all(uncoveredFiles.map(async (uncoveredFile) => { - const { source } = await this.getSources(uncoveredFile.href, transformResults) - - return { - url: uncoveredFile.href, - scriptId: '0', - // Create a made up function to mark whole file as uncovered. Note that this does not exist in source maps. - functions: [{ - ranges: [{ - startOffset: 0, - endOffset: source.length, - count: 0, - }], - isBlockCoverage: true, - // This is magical value that indicates an empty report: https://github.com/istanbuljs/v8-to-istanbul/blob/fca5e6a9e6ef38a9cdc3a178d5a6cf9ef82e6cab/lib/v8-to-istanbul.js#LL131C40-L131C40 - functionName: '(empty-report)', - }], + let merged: RawCoverage = { result: [] } + let index = 0 + + for (const chunk of toSlices(uncoveredFiles, this.options.processingConcurrency)) { + if (debug.enabled) { + index += chunk.length + debug('Uncovered files %d/%d', index, uncoveredFiles.length) } - })) + + const coverages = await Promise.all(chunk.map(async (filename) => { + const { source } = await this.getSources(filename.href, transformResults) + + const coverage = { + url: filename.href, + scriptId: '0', + // Create a made up function to mark whole file as uncovered. Note that this does not exist in source maps. + functions: [{ + ranges: [{ + startOffset: 0, + endOffset: source.length, + count: 0, + }], + isBlockCoverage: true, + // This is magical value that indicates an empty report: https://github.com/istanbuljs/v8-to-istanbul/blob/fca5e6a9e6ef38a9cdc3a178d5a6cf9ef82e6cab/lib/v8-to-istanbul.js#LL131C40-L131C40 + functionName: '(empty-report)', + }], + } + + return { result: [coverage] } + })) + + merged = mergeProcessCovs([merged, ...coverages]) + } + + return merged } private async getSources(url: string, transformResults: TransformResults, functions: Profiler.FunctionCoverage[] = []): Promise<{ @@ -259,40 +322,42 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } } - private async mergeAndTransformCoverage(coverages: RawCoverage[], projectName?: ProjectName, transformMode?: 'web' | 'ssr') { + private async convertCoverage(coverage: RawCoverage, projectName?: ProjectName, transformMode?: 'web' | 'ssr'): Promise { const viteNode = this.ctx.projects.find(project => project.getName() === projectName)?.vitenode || this.ctx.vitenode const fetchCache = transformMode ? viteNode.fetchCaches[transformMode] : viteNode.fetchCache const transformResults = normalizeTransformResults(fetchCache) - const merged = mergeProcessCovs(coverages) - const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url))) + const scriptCoverages = coverage.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url))) + const coverageMap = libCoverage.createCoverageMap({}) + let index = 0 - const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => { - const sources = await this.getSources(url, transformResults, functions) + for (const chunk of toSlices(scriptCoverages, this.options.processingConcurrency)) { + if (debug.enabled) { + index += chunk.length + debug('Converting %d/%d', index, scriptCoverages.length) + } - // If no source map was found from vite-node we can assume this file was not run in the wrapper - const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0 + await Promise.all(chunk.map(async ({ url, functions }) => { + const sources = await this.getSources(url, transformResults, functions) - const converter = v8ToIstanbul(url, wrapperLength, sources) - await converter.load() + // If no source map was found from vite-node we can assume this file was not run in the wrapper + const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0 - converter.applyCoverage(functions) - return converter.toIstanbul() - })) + const converter = v8ToIstanbul(url, wrapperLength, sources) + await converter.load() - const mergedCoverage = mergeCoverageMaps(...converted) + converter.applyCoverage(functions) + coverageMap.merge(converter.toIstanbul()) + })) + } - const sourceMapStore = libSourceMaps.createSourceMapStore() - return sourceMapStore.transformCoverage(mergedCoverage) + return coverageMap } } -function mergeCoverageMaps(...coverageMaps: (CoverageMap | CoverageMapData)[]) { - return coverageMaps.reduce((coverage, previousCoverageMap) => { - const map = libCoverage.createCoverageMap(coverage) - map.merge(previousCoverageMap) - return map - }, libCoverage.createCoverageMap({})) +async function transformCoverage(coverageMap: CoverageMap) { + const sourceMapStore = libSourceMaps.createSourceMapStore() + return await sourceMapStore.transformCoverage(coverageMap) } /** @@ -350,3 +415,19 @@ function hasTerminalReporter(reporters: Options['reporter']) { || 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 + }, []) +} diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts index 74a8e19250af..6d4d0d590db1 100644 --- a/packages/vitest/src/defaults.ts +++ b/packages/vitest/src/defaults.ts @@ -1,3 +1,4 @@ +import { cpus } from 'node:os' import type { BenchmarkUserOptions, CoverageV8Options, ResolvedCoverageOptions, UserConfig } from './types' import { isCI } from './utils/env' @@ -42,6 +43,7 @@ export const coverageConfigDefaults: ResolvedCoverageOptions = { reporter: [['text', {}], ['html', {}], ['clover', {}], ['json', {}]], extension: ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts', '.tsx', '.jsx', '.vue', '.svelte', '.marko'], allowExternal: false, + processingConcurrency: Math.min(20, cpus().length), } export const fakeTimersDefaults = { diff --git a/packages/vitest/src/types/coverage.ts b/packages/vitest/src/types/coverage.ts index bcd96182ef9b..9b88026200fe 100644 --- a/packages/vitest/src/types/coverage.ts +++ b/packages/vitest/src/types/coverage.ts @@ -79,6 +79,7 @@ type FieldsWithDefaultValues = | 'extension' | 'reportOnFailure' | 'allowExternal' + | 'processingConcurrency' export type ResolvedCoverageOptions = & CoverageOptions @@ -204,6 +205,12 @@ export interface BaseCoverageOptions { * @default false */ allowExternal?: boolean + + /** + * Concurrency limit used when processing the coverage results. + * Defaults to `Math.min(20, os.cpu().length)` + */ + processingConcurrency?: number } export interface CoverageIstanbulOptions extends BaseCoverageOptions { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3e3ae6cfe4e..b85e08c62343 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -914,6 +914,9 @@ importers: packages/coverage-istanbul: dependencies: + debug: + specifier: ^4.3.4 + version: 4.3.4(supports-color@8.1.1) istanbul-lib-coverage: specifier: ^3.2.2 version: 3.2.2 @@ -939,6 +942,9 @@ importers: specifier: ^6.0.0 version: 6.0.0 devDependencies: + '@types/debug': + specifier: ^4.1.12 + version: 4.1.12 '@types/istanbul-lib-coverage': specifier: ^2.0.6 version: 2.0.6 @@ -969,6 +975,9 @@ importers: '@bcoe/v8-coverage': specifier: ^0.2.3 version: 0.2.3 + debug: + specifier: ^4.3.4 + version: 4.3.4(supports-color@8.1.1) istanbul-lib-coverage: specifier: ^3.2.2 version: 3.2.2 @@ -1000,6 +1009,9 @@ importers: specifier: ^9.2.0 version: 9.2.0 devDependencies: + '@types/debug': + specifier: ^4.1.12 + version: 4.1.12 '@types/istanbul-lib-coverage': specifier: ^2.0.6 version: 2.0.6 @@ -8956,12 +8968,6 @@ packages: '@types/ms': 0.7.31 dev: true - /@types/debug@4.1.8: - resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} - dependencies: - '@types/ms': 0.7.31 - dev: true - /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: @@ -25701,7 +25707,7 @@ packages: '@vue/compiler-sfc': optional: true dependencies: - '@types/debug': 4.1.8 + '@types/debug': 4.1.12 debug: 4.3.4(supports-color@8.1.1) deep-equal: 2.2.1 extract-comments: 1.1.0 diff --git a/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap b/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap index 852af788a740..8d0773c323e3 100644 --- a/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap +++ b/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap @@ -462,12 +462,23 @@ exports[`istanbul json report 1`] = ` "s": { "0": 1, "1": 1, - "2": 3, - "3": 1, - "4": 2, + "2": 1, + "3": 3, + "4": 1, + "5": 2, }, "statementMap": { "0": { + "end": { + "column": null, + "line": 4, + }, + "start": { + "column": 14, + "line": 4, + }, + }, + "1": { "end": { "column": null, "line": 6, @@ -477,7 +488,7 @@ exports[`istanbul json report 1`] = ` "line": 6, }, }, - "1": { + "2": { "end": { "column": null, "line": 7, @@ -487,7 +498,7 @@ exports[`istanbul json report 1`] = ` "line": 7, }, }, - "2": { + "3": { "end": { "column": 55, "line": 7, @@ -497,7 +508,7 @@ exports[`istanbul json report 1`] = ` "line": 7, }, }, - "3": { + "4": { "end": { "column": null, "line": 9, @@ -507,7 +518,7 @@ exports[`istanbul json report 1`] = ` "line": 9, }, }, - "4": { + "5": { "end": { "column": 30, "line": 14, diff --git a/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap b/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap index 941d848ed151..863ebec77577 100644 --- a/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap +++ b/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap @@ -3315,7 +3315,7 @@ exports[`v8 json report 1`] = ` 0, ], "10": [ - 2, + 1, ], "2": [ 0, @@ -3324,7 +3324,7 @@ exports[`v8 json report 1`] = ` 1, ], "4": [ - 3, + 1, ], "5": [ 0, @@ -3336,10 +3336,10 @@ exports[`v8 json report 1`] = ` 1, ], "8": [ - 1, + 2, ], "9": [ - 1, + 3, ], }, "branchMap": { @@ -3399,8 +3399,8 @@ exports[`v8 json report 1`] = ` "line": 24, "loc": { "end": { - "column": 1, - "line": 31, + "column": 3, + "line": 27, }, "start": { "column": 33, @@ -3410,8 +3410,8 @@ exports[`v8 json report 1`] = ` "locations": [ { "end": { - "column": 1, - "line": 31, + "column": 3, + "line": 27, }, "start": { "column": 33, @@ -3477,8 +3477,8 @@ exports[`v8 json report 1`] = ` "line": 16, "loc": { "end": { - "column": 1, - "line": 31, + "column": 3, + "line": 19, }, "start": { "column": 31, @@ -3488,8 +3488,8 @@ exports[`v8 json report 1`] = ` "locations": [ { "end": { - "column": 1, - "line": 31, + "column": 3, + "line": 19, }, "start": { "column": 31, @@ -3581,8 +3581,8 @@ exports[`v8 json report 1`] = ` "line": 24, "loc": { "end": { - "column": 3, - "line": 27, + "column": 1, + "line": 31, }, "start": { "column": 33, @@ -3592,8 +3592,8 @@ exports[`v8 json report 1`] = ` "locations": [ { "end": { - "column": 3, - "line": 27, + "column": 1, + "line": 31, }, "start": { "column": 33, @@ -3607,8 +3607,8 @@ exports[`v8 json report 1`] = ` "line": 16, "loc": { "end": { - "column": 3, - "line": 19, + "column": 1, + "line": 31, }, "start": { "column": 31, @@ -3618,8 +3618,8 @@ exports[`v8 json report 1`] = ` "locations": [ { "end": { - "column": 3, - "line": 19, + "column": 1, + "line": 31, }, "start": { "column": 31, diff --git a/test/coverage-test/test/configuration-options.test-d.ts b/test/coverage-test/test/configuration-options.test-d.ts index 8af873a44229..d4dad6bb4771 100644 --- a/test/coverage-test/test/configuration-options.test-d.ts +++ b/test/coverage-test/test/configuration-options.test-d.ts @@ -117,6 +117,7 @@ test('provider module', () => { reportsDirectory: 'string', reportOnFailure: true, allowExternal: true, + processingConcurrency: 1, } }, clean(_: boolean) {},