import { existsSync, promises, readdirSync, writeFileSync } from 'node:fs'; import { resolve, relative } from 'pathe'; import { c as coverageConfigDefaults, r as resolveCoverageReporters, m as mm } from './chunks/resolveConfig.Dha6ilPI.js'; import c from 'tinyrainbow'; import 'node:path'; import 'node:fs/promises'; import 'node:process'; import 'node:module'; import 'node:url'; import 'node:assert'; import 'node:v8'; import 'node:util'; import './chunks/constants.fzPh7AOq.js'; import 'node:os'; import './chunks/env.CmHVDJnw.js'; import 'std-env'; import '@vitest/runner/utils'; import './chunks/base.DwXGwWst.js'; import '@vitest/utils'; import 'node:crypto'; import './chunks/RandomSequencer.Bh5-tlNJ.js'; import 'node:perf_hooks'; import '@vitest/utils/source-map'; import 'tinyexec'; import 'path'; import 'fs'; import 'module'; import 'vite'; import 'vite-node/utils'; import './chunks/_commonjsHelpers.BFTU3MAI.js'; import 'util'; import 'node:events'; import 'tinypool'; import './chunks/index.BpSiYbpB.js'; import 'node:worker_threads'; import './path.js'; const THRESHOLD_KEYS = [ "lines", "functions", "statements", "branches" ]; const GLOBAL_THRESHOLDS_KEY = "global"; const DEFAULT_PROJECT = Symbol.for("default-project"); let uniqueId = 0; class BaseCoverageProvider { ctx; name; version; options; coverageFiles = /* @__PURE__ */ new Map(); pendingPromises = []; coverageFilesDirectory; _initialize(ctx) { this.ctx = ctx; if (ctx.version !== this.version) { ctx.logger.warn( c.yellow( `Loaded ${c.inverse(c.yellow(` vitest@${ctx.version} `))} and ${c.inverse(c.yellow(` @vitest/coverage-${this.name}@${this.version} `))}. Running mixed versions is not supported and may lead into bugs Update your dependencies and make sure the versions match.` ) ); } const config = ctx.config.coverage; this.options = { ...coverageConfigDefaults, // User's options ...config, // Resolved fields provider: this.name, reportsDirectory: resolve( ctx.config.root, config.reportsDirectory || coverageConfigDefaults.reportsDirectory ), reporter: resolveCoverageReporters( config.reporter || coverageConfigDefaults.reporter ), thresholds: config.thresholds && { ...config.thresholds, lines: config.thresholds["100"] ? 100 : config.thresholds.lines, branches: config.thresholds["100"] ? 100 : config.thresholds.branches, functions: config.thresholds["100"] ? 100 : config.thresholds.functions, statements: config.thresholds["100"] ? 100 : config.thresholds.statements } }; const shard = this.ctx.config.shard; const tempDirectory = `.tmp${shard ? `-${shard.index}-${shard.count}` : ""}`; this.coverageFilesDirectory = resolve( this.options.reportsDirectory, tempDirectory ); } createCoverageMap() { throw new Error("BaseReporter's createCoverageMap was not overwritten"); } async generateReports(_, __) { throw new Error("BaseReporter's generateReports was not overwritten"); } async parseConfigModule(_) { throw new Error("BaseReporter's parseConfigModule was not overwritten"); } resolveOptions() { return this.options; } async clean(clean = true) { if (clean && existsSync(this.options.reportsDirectory)) { await promises.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }); } if (existsSync(this.coverageFilesDirectory)) { await promises.rm(this.coverageFilesDirectory, { recursive: true, force: true, maxRetries: 10 }); } await promises.mkdir(this.coverageFilesDirectory, { recursive: true }); this.coverageFiles = /* @__PURE__ */ new Map(); this.pendingPromises = []; } onAfterSuiteRun({ coverage, transformMode, projectName, testFiles }) { if (!coverage) { return; } if (transformMode !== "web" && transformMode !== "ssr" && transformMode !== "browser") { throw new Error(`Invalid transform mode: ${transformMode}`); } let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT); if (!entry) { entry = { web: {}, ssr: {}, browser: {} }; this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry); } const testFilenames = testFiles.join(); const filename = resolve( this.coverageFilesDirectory, `coverage-${uniqueId++}.json` ); entry[transformMode][testFilenames] = filename; const promise = promises.writeFile(filename, JSON.stringify(coverage), "utf-8"); this.pendingPromises.push(promise); } async readCoverageFiles({ onFileRead, onFinished, onDebug }) { 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, coverageByTestfiles] of Object.entries(coveragePerProject)) { const filenames = Object.values(coverageByTestfiles); const project = this.ctx.getProjectByName(projectName); for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) { if (onDebug.enabled) { index += chunk.length; onDebug("Covered files %d/%d", index, total); } await Promise.all( chunk.map(async (filename) => { const contents = await promises.readFile(filename, "utf-8"); const coverage = JSON.parse(contents); onFileRead(coverage); }) ); } await onFinished(project, transformMode); } } } async cleanAfterRun() { this.coverageFiles = /* @__PURE__ */ new Map(); await promises.rm(this.coverageFilesDirectory, { recursive: true }); if (readdirSync(this.options.reportsDirectory).length === 0) { await promises.rm(this.options.reportsDirectory, { recursive: true }); } } async onTestFailure() { if (!this.options.reportOnFailure) { await this.cleanAfterRun(); } } async reportCoverage(coverageMap, { allTestsRun }) { await this.generateReports( coverageMap || this.createCoverageMap(), allTestsRun ); const keepResults = !this.options.cleanOnRerun && this.ctx.config.watch; if (!keepResults) { await this.cleanAfterRun(); } } async reportThresholds(coverageMap, allTestsRun) { const resolvedThresholds = this.resolveThresholds(coverageMap); this.checkThresholds(resolvedThresholds); if (this.options.thresholds?.autoUpdate && allTestsRun) { if (!this.ctx.server.config.configFile) { throw new Error( 'Missing configurationFile. The "coverage.thresholds.autoUpdate" can only be enabled when configuration file is used.' ); } const configFilePath = this.ctx.server.config.configFile; const configModule = await this.parseConfigModule(configFilePath); await this.updateThresholds({ thresholds: resolvedThresholds, configurationFile: configModule, onUpdate: () => writeFileSync( configFilePath, configModule.generate().code, "utf-8" ) }); } } /** * Constructs collected coverage and users' threshold options into separate sets * where each threshold set holds their own coverage maps. Threshold set is either * for specific files defined by glob pattern or global for all other files. */ resolveThresholds(coverageMap) { const resolvedThresholds = []; const files = coverageMap.files(); const globalCoverageMap = this.createCoverageMap(); for (const key of Object.keys(this.options.thresholds)) { if (key === "perFile" || key === "autoUpdate" || key === "100" || THRESHOLD_KEYS.includes(key)) { continue; } const glob = key; const globThresholds = resolveGlobThresholds(this.options.thresholds[glob]); const globCoverageMap = this.createCoverageMap(); const matchingFiles = files.filter( (file) => mm.isMatch(relative(this.ctx.config.root, file), glob) ); for (const file of matchingFiles) { const fileCoverage = coverageMap.fileCoverageFor(file); globCoverageMap.addFileCoverage(fileCoverage); } resolvedThresholds.push({ name: glob, coverageMap: globCoverageMap, thresholds: globThresholds }); } for (const file of files) { const fileCoverage = coverageMap.fileCoverageFor(file); globalCoverageMap.addFileCoverage(fileCoverage); } resolvedThresholds.unshift({ name: GLOBAL_THRESHOLDS_KEY, coverageMap: globalCoverageMap, thresholds: { branches: this.options.thresholds?.branches, functions: this.options.thresholds?.functions, lines: this.options.thresholds?.lines, statements: this.options.thresholds?.statements } }); return resolvedThresholds; } /** * Check collected coverage against configured thresholds. Sets exit code to 1 when thresholds not reached. */ checkThresholds(allThresholds) { for (const { coverageMap, thresholds, name } of allThresholds) { if (thresholds.branches === void 0 && thresholds.functions === void 0 && thresholds.lines === void 0 && thresholds.statements === void 0) { continue; } const summaries = this.options.thresholds?.perFile ? coverageMap.files().map((file) => ({ file, summary: coverageMap.fileCoverageFor(file).toSummary() })) : [{ file: null, summary: coverageMap.getCoverageSummary() }]; for (const { summary, file } of summaries) { for (const thresholdKey of THRESHOLD_KEYS) { const threshold = thresholds[thresholdKey]; if (threshold !== void 0) { const coverage = summary.data[thresholdKey].pct; if (coverage < threshold) { process.exitCode = 1; let errorMessage = `ERROR: Coverage for ${thresholdKey} (${coverage}%) does not meet ${name === GLOBAL_THRESHOLDS_KEY ? name : `"${name}"`} threshold (${threshold}%)`; if (this.options.thresholds?.perFile && file) { errorMessage += ` for ${relative("./", file).replace(/\\/g, "/")}`; } this.ctx.logger.error(errorMessage); } } } } } } /** * Check if current coverage is above configured thresholds and bump the thresholds if needed */ async updateThresholds({ thresholds: allThresholds, onUpdate, configurationFile }) { let updatedThresholds = false; const config = resolveConfig(configurationFile); assertConfigurationModule(config); for (const { coverageMap, thresholds, name } of allThresholds) { const summaries = this.options.thresholds?.perFile ? coverageMap.files().map( (file) => coverageMap.fileCoverageFor(file).toSummary() ) : [coverageMap.getCoverageSummary()]; const thresholdsToUpdate = []; for (const key of THRESHOLD_KEYS) { const threshold = thresholds[key] ?? 100; const actual = Math.min( ...summaries.map((summary) => summary[key].pct) ); if (actual > threshold) { thresholdsToUpdate.push([key, actual]); } } if (thresholdsToUpdate.length === 0) { continue; } updatedThresholds = true; for (const [threshold, newValue] of thresholdsToUpdate) { if (name === GLOBAL_THRESHOLDS_KEY) { config.test.coverage.thresholds[threshold] = newValue; } else { const glob = config.test.coverage.thresholds[name]; glob[threshold] = newValue; } } } if (updatedThresholds) { this.ctx.logger.log("Updating thresholds to configuration file. You may want to push with updated coverage thresholds."); onUpdate(); } } async mergeReports(coverageMaps) { const coverageMap = this.createCoverageMap(); for (const coverage of coverageMaps) { coverageMap.merge(coverage); } await this.generateReports(coverageMap, true); } hasTerminalReporter(reporters) { return reporters.some( ([reporter]) => reporter === "text" || reporter === "text-summary" || reporter === "text-lcov" || reporter === "teamcity" ); } toSlices(array, size) { 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; }, []); } createUncoveredFileTransformer(ctx) { const servers = [ ...ctx.projects.map((project) => ({ root: project.config.root, vitenode: project.vitenode })), // Check core last as it will match all files anyway { root: ctx.config.root, vitenode: ctx.vitenode } ]; return async function transformFile(filename) { let lastError; for (const { root, vitenode } of servers) { if (!filename.startsWith(root)) { continue; } try { return await vitenode.transformRequest(filename); } catch (error) { lastError = error; } } throw lastError; }; } } function resolveGlobThresholds(thresholds) { if (!thresholds || typeof thresholds !== "object") { return {}; } if (100 in thresholds && thresholds[100] === true) { return { lines: 100, branches: 100, functions: 100, statements: 100 }; } return { lines: "lines" in thresholds && typeof thresholds.lines === "number" ? thresholds.lines : void 0, branches: "branches" in thresholds && typeof thresholds.branches === "number" ? thresholds.branches : void 0, functions: "functions" in thresholds && typeof thresholds.functions === "number" ? thresholds.functions : void 0, statements: "statements" in thresholds && typeof thresholds.statements === "number" ? thresholds.statements : void 0 }; } function assertConfigurationModule(config) { try { if (typeof config.test.coverage.thresholds !== "object") { throw new TypeError( "Expected config.test.coverage.thresholds to be an object" ); } } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error( `Unable to parse thresholds from configuration file: ${message}` ); } } function resolveConfig(configModule) { const mod = configModule.exports.default; try { if (mod.$type === "object") { return mod; } let config = resolveDefineConfig(mod); if (config) { return config; } if (mod.$type === "function-call" && mod.$callee === "mergeConfig") { config = resolveMergeConfig(mod); if (config) { return config; } } } catch (error) { throw new Error(error instanceof Error ? error.message : String(error)); } throw new Error( "Failed to update coverage thresholds. Configuration file is too complex." ); } function resolveDefineConfig(mod) { if (mod.$type === "function-call" && mod.$callee === "defineConfig") { if (mod.$args[0].$type === "object") { return mod.$args[0]; } if (mod.$args[0].$type === "arrow-function-expression") { if (mod.$args[0].$body.$type === "object") { return mod.$args[0].$body; } const config = resolveMergeConfig(mod.$args[0].$body); if (config) { return config; } } } } function resolveMergeConfig(mod) { if (mod.$type === "function-call" && mod.$callee === "mergeConfig") { for (const arg of mod.$args) { const config = resolveDefineConfig(arg); if (config) { return config; } } } } export { BaseCoverageProvider };