@vitest/coverage-v8
Advanced tools
Comparing version 0.34.6 to 1.0.4
import { V8CoverageProvider } from './provider.js'; | ||
import 'node:inspector'; | ||
import 'vitest/coverage'; | ||
@@ -4,0 +3,0 @@ import 'vitest'; |
@@ -1,2 +0,1 @@ | ||
import { Profiler } from 'node:inspector'; | ||
import { BaseCoverageProvider } from 'vitest/coverage'; | ||
@@ -20,2 +19,6 @@ import { CoverageProvider, AfterSuiteRunMeta, ReportContext, ResolvedCoverageOptions } from 'vitest'; | ||
type Options = ResolvedCoverageOptions<'v8'>; | ||
type Filename = string; | ||
type CoverageFilesByTransformMode = Record<AfterSuiteRunMeta['transformMode'], Filename[]>; | ||
type ProjectName = NonNullable<AfterSuiteRunMeta['projectName']> | typeof DEFAULT_PROJECT; | ||
declare const DEFAULT_PROJECT: unique symbol; | ||
declare class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider { | ||
@@ -26,12 +29,15 @@ name: string; | ||
testExclude: InstanceType<TestExclude>; | ||
coverages: Profiler.TakePreciseCoverageReturnType[]; | ||
coverageFiles: Map<ProjectName, CoverageFilesByTransformMode>; | ||
coverageFilesDirectory: string; | ||
pendingPromises: Promise<void>[]; | ||
initialize(ctx: Vitest): void; | ||
resolveOptions(): Options; | ||
clean(clean?: boolean): Promise<void>; | ||
onAfterSuiteRun({ coverage }: AfterSuiteRunMeta): void; | ||
onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta): void; | ||
reportCoverage({ allTestsRun }?: ReportContext): Promise<void>; | ||
private getUntestedFiles; | ||
private getSources; | ||
private convertCoverage; | ||
} | ||
export { V8CoverageProvider }; |
@@ -1,3 +0,3 @@ | ||
import { existsSync, promises } from 'fs'; | ||
import { fileURLToPath, pathToFileURL } from 'url'; | ||
import { existsSync, promises, writeFileSync } from 'node:fs'; | ||
import { pathToFileURL, fileURLToPath } from 'node:url'; | ||
import v8ToIstanbul from 'v8-to-istanbul'; | ||
@@ -10,6 +10,8 @@ import { mergeProcessCovs } from '@bcoe/v8-coverage'; | ||
import MagicString from 'magic-string'; | ||
import { parseModule } from 'magicast'; | ||
import remapping from '@ampproject/remapping'; | ||
import c from 'picocolors'; | ||
import { provider } from 'std-env'; | ||
import { builtinModules } from 'module'; | ||
import createDebug from 'debug'; | ||
import { builtinModules } from 'node:module'; | ||
import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config'; | ||
@@ -172,2 +174,5 @@ import { BaseCoverageProvider } from 'vitest/coverage'; | ||
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; | ||
class V8CoverageProvider extends BaseCoverageProvider { | ||
@@ -178,3 +183,5 @@ name = "v8"; | ||
testExclude; | ||
coverages = []; | ||
coverageFiles = /* @__PURE__ */ new Map(); | ||
coverageFilesDirectory; | ||
pendingPromises = []; | ||
initialize(ctx) { | ||
@@ -191,6 +198,9 @@ const config = ctx.config.coverage; | ||
reportsDirectory: resolve(ctx.config.root, config.reportsDirectory || coverageConfigDefaults.reportsDirectory), | ||
lines: config["100"] ? 100 : config.lines, | ||
functions: config["100"] ? 100 : config.functions, | ||
branches: config["100"] ? 100 : config.branches, | ||
statements: config["100"] ? 100 : config.statements | ||
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 | ||
} | ||
}; | ||
@@ -205,2 +215,3 @@ this.testExclude = new _TestExclude({ | ||
}); | ||
this.coverageFilesDirectory = resolve(this.options.reportsDirectory, ".tmp"); | ||
} | ||
@@ -213,6 +224,25 @@ resolveOptions() { | ||
await promises.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }); | ||
this.coverages = []; | ||
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 }) { | ||
this.coverages.push(coverage); | ||
/* | ||
* Coverage and meta information passed from Vitest runners. | ||
* Note that adding new entries here and requiring on those without | ||
* backwards compatibility is a breaking change. | ||
*/ | ||
onAfterSuiteRun({ coverage, transformMode, projectName }) { | ||
if (transformMode !== "web" && transformMode !== "ssr") | ||
throw new Error(`Invalid transform mode: ${transformMode}`); | ||
let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT); | ||
if (!entry) { | ||
entry = { web: [], ssr: [] }; | ||
this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry); | ||
} | ||
const filename = resolve(this.coverageFilesDirectory, `coverage-${uniqueId++}.json`); | ||
entry[transformMode].push(filename); | ||
const promise = promises.writeFile(filename, JSON.stringify(coverage), "utf-8"); | ||
this.pendingPromises.push(promise); | ||
} | ||
@@ -222,29 +252,35 @@ async reportCoverage({ allTestsRun } = {}) { | ||
this.ctx.logger.log(c.blue(" % ") + c.yellow("@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.")); | ||
const transformResults = normalizeTransformResults(this.ctx.projects.map((project) => project.vitenode.fetchCache)); | ||
const merged = mergeProcessCovs(this.coverages); | ||
const scriptCoverages = merged.result.filter((result) => this.testExclude.shouldInstrument(fileURLToPath(result.url))); | ||
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)) { | ||
let merged = { 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 promises.readFile(filename, "utf-8"); | ||
const coverage = JSON.parse(contents); | ||
merged = mergeProcessCovs([merged, coverage]); | ||
})); | ||
} | ||
const converted = await this.convertCoverage(merged, projectName, transformMode); | ||
const transformedCoverage = await transformCoverage(converted); | ||
coverageMap.merge(transformedCoverage); | ||
} | ||
} | ||
if (this.options.all && allTestsRun) { | ||
const coveredFiles = Array.from(scriptCoverages.map((r) => r.url)); | ||
const untestedFiles = await this.getUntestedFiles(coveredFiles, transformResults); | ||
scriptCoverages.push(...untestedFiles); | ||
const coveredFiles = coverageMap.files(); | ||
const untestedCoverage = await this.getUntestedFiles(coveredFiles); | ||
const converted = await this.convertCoverage(untestedCoverage); | ||
coverageMap.merge(await transformCoverage(converted)); | ||
} | ||
const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => { | ||
const sources = await this.getSources(url, transformResults, functions); | ||
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0; | ||
const converter = v8ToIstanbul(url, wrapperLength, sources); | ||
await converter.load(); | ||
converter.applyCoverage(functions); | ||
return converter.toIstanbul(); | ||
})); | ||
const mergedCoverage = converted.reduce((coverage, previousCoverageMap) => { | ||
const map = libCoverage.createCoverageMap(coverage); | ||
map.merge(previousCoverageMap); | ||
return map; | ||
}, libCoverage.createCoverageMap({})); | ||
const sourceMapStore = libSourceMaps.createSourceMapStore(); | ||
const coverageMap = await sourceMapStore.transformCoverage(mergedCoverage); | ||
const context = libReport.createContext({ | ||
dir: this.options.reportsDirectory, | ||
coverageMap, | ||
sourceFinder: sourceMapStore.sourceFinder, | ||
watermarks: this.options.watermarks | ||
@@ -261,57 +297,70 @@ }); | ||
} | ||
if (this.options.branches || this.options.functions || this.options.lines || this.options.statements) { | ||
this.checkThresholds({ | ||
if (this.options.thresholds) { | ||
const resolvedThresholds = this.resolveThresholds({ | ||
coverageMap, | ||
thresholds: { | ||
branches: this.options.branches, | ||
functions: this.options.functions, | ||
lines: this.options.lines, | ||
statements: this.options.statements | ||
}, | ||
perFile: this.options.perFile | ||
thresholds: this.options.thresholds, | ||
createCoverageMap: () => libCoverage.createCoverageMap({}) | ||
}); | ||
} | ||
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 | ||
}, | ||
perFile: this.options.perFile, | ||
configurationFile: this.ctx.server.config.configFile | ||
this.checkThresholds({ | ||
thresholds: resolvedThresholds, | ||
perFile: this.options.thresholds.perFile | ||
}); | ||
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 = parseModule(await promises.readFile(configFilePath, "utf8")); | ||
this.updateThresholds({ | ||
thresholds: resolvedThresholds, | ||
perFile: this.options.thresholds.perFile, | ||
configurationFile: { | ||
write: () => writeFileSync(configFilePath, configModule.generate().code, "utf-8"), | ||
read: () => configModule.exports.default.$type === "function-call" ? configModule.exports.default.$args[0] : configModule.exports.default | ||
} | ||
}); | ||
} | ||
this.coverageFiles = /* @__PURE__ */ new Map(); | ||
await promises.rm(this.coverageFilesDirectory, { recursive: true }); | ||
} | ||
} | ||
async getUntestedFiles(testedFiles, transformResults) { | ||
async getUntestedFiles(testedFiles) { | ||
const transformResults = normalizeTransformResults(this.ctx.vitenode.fetchCache); | ||
const includedFiles = await this.testExclude.glob(this.ctx.config.root); | ||
const uncoveredFiles = includedFiles.map((file) => pathToFileURL(resolve(this.ctx.config.root, file))).filter((file) => !testedFiles.includes(file.href)); | ||
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)" | ||
}] | ||
}; | ||
})); | ||
const uncoveredFiles = includedFiles.map((file) => pathToFileURL(resolve(this.ctx.config.root, file))).filter((file) => !testedFiles.includes(file.pathname)); | ||
let merged = { 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; | ||
} | ||
async getSources(url, transformResults, functions = []) { | ||
var _a; | ||
const filePath = normalize(fileURLToPath(url)); | ||
const transformResult = transformResults.get(filePath); | ||
const map = transformResult == null ? void 0 : transformResult.map; | ||
const code = transformResult == null ? void 0 : transformResult.code; | ||
const sourcesContent = ((_a = map == null ? void 0 : map.sourcesContent) == null ? void 0 : _a[0]) || await promises.readFile(filePath, "utf-8").catch(() => { | ||
const map = transformResult?.map; | ||
const code = transformResult?.code; | ||
const sourcesContent = map?.sourcesContent?.[0] || await promises.readFile(filePath, "utf-8").catch(() => { | ||
const length = findLongestFunctionLength(functions); | ||
@@ -335,3 +384,30 @@ return ".".repeat(length); | ||
} | ||
async convertCoverage(coverage, projectName, transformMode) { | ||
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 scriptCoverages = coverage.result.filter((result) => this.testExclude.shouldInstrument(fileURLToPath(result.url))); | ||
const coverageMap = libCoverage.createCoverageMap({}); | ||
let index = 0; | ||
for (const chunk of toSlices(scriptCoverages, this.options.processingConcurrency)) { | ||
if (debug.enabled) { | ||
index += chunk.length; | ||
debug("Converting %d/%d", index, scriptCoverages.length); | ||
} | ||
await Promise.all(chunk.map(async ({ url, functions }) => { | ||
const sources = await this.getSources(url, transformResults, functions); | ||
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0; | ||
const converter = v8ToIstanbul(url, wrapperLength, sources); | ||
await converter.load(); | ||
converter.applyCoverage(functions); | ||
coverageMap.merge(converter.toIstanbul()); | ||
})); | ||
} | ||
return coverageMap; | ||
} | ||
} | ||
async function transformCoverage(coverageMap) { | ||
const sourceMapStore = libSourceMaps.createSourceMapStore(); | ||
return await sourceMapStore.transformCoverage(coverageMap); | ||
} | ||
function removeViteHelpersFromSourceMaps(source, map) { | ||
@@ -343,3 +419,3 @@ if (!source || !source.match(VITE_EXPORTS_LINE_PATTERN)) | ||
const mapWithoutHelpers = sourceWithoutHelpers.generateMap({ | ||
hires: true | ||
hires: "boundary" | ||
}); | ||
@@ -358,10 +434,8 @@ const combinedMap = remapping( | ||
} | ||
function normalizeTransformResults(fetchCaches) { | ||
function normalizeTransformResults(fetchCache) { | ||
const normalized = /* @__PURE__ */ new Map(); | ||
for (const fetchCache of fetchCaches) { | ||
for (const [key, value] of fetchCache.entries()) { | ||
const cleanEntry = cleanUrl(key); | ||
if (!normalized.has(cleanEntry)) | ||
normalized.set(cleanEntry, value.result); | ||
} | ||
for (const [key, value] of fetchCache.entries()) { | ||
const cleanEntry = cleanUrl(key); | ||
if (!normalized.has(cleanEntry)) | ||
normalized.set(cleanEntry, value.result); | ||
} | ||
@@ -373,3 +447,15 @@ return normalized; | ||
} | ||
function 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; | ||
}, []); | ||
} | ||
export { V8CoverageProvider }; |
{ | ||
"name": "@vitest/coverage-v8", | ||
"type": "module", | ||
"version": "0.34.6", | ||
"version": "1.0.4", | ||
"description": "V8 coverage provider for Vitest", | ||
@@ -29,3 +29,3 @@ "author": "Anthony Fu <anthonyfu117@hotmail.com>", | ||
"types": "./dist/index.d.ts", | ||
"import": "./dist/index.js" | ||
"default": "./dist/index.js" | ||
}, | ||
@@ -41,3 +41,3 @@ "./*": "./*" | ||
"peerDependencies": { | ||
"vitest": ">=0.32.0 <1" | ||
"vitest": "^1.0.0" | ||
}, | ||
@@ -47,20 +47,23 @@ "dependencies": { | ||
"@bcoe/v8-coverage": "^0.2.3", | ||
"istanbul-lib-coverage": "^3.2.0", | ||
"debug": "^4.3.4", | ||
"istanbul-lib-coverage": "^3.2.2", | ||
"istanbul-lib-report": "^3.0.1", | ||
"istanbul-lib-source-maps": "^4.0.1", | ||
"istanbul-reports": "^3.1.5", | ||
"magic-string": "^0.30.1", | ||
"istanbul-reports": "^3.1.6", | ||
"magic-string": "^0.30.5", | ||
"magicast": "^0.3.2", | ||
"picocolors": "^1.0.0", | ||
"std-env": "^3.3.3", | ||
"std-env": "^3.5.0", | ||
"test-exclude": "^6.0.0", | ||
"v8-to-istanbul": "^9.1.0" | ||
"v8-to-istanbul": "^9.2.0" | ||
}, | ||
"devDependencies": { | ||
"@types/istanbul-lib-coverage": "^2.0.4", | ||
"@types/istanbul-lib-report": "^3.0.0", | ||
"@types/istanbul-lib-source-maps": "^4.0.1", | ||
"@types/istanbul-reports": "^3.0.1", | ||
"@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", | ||
"@types/istanbul-reports": "^3.0.4", | ||
"pathe": "^1.1.1", | ||
"vite-node": "0.34.6", | ||
"vitest": "0.34.6" | ||
"vite-node": "1.0.4", | ||
"vitest": "1.0.4" | ||
}, | ||
@@ -67,0 +70,0 @@ "scripts": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
23417
544
0
1
14
8
6
+ Addeddebug@^4.3.4
+ Addedmagicast@^0.3.2
+ Added@babel/helper-string-parser@7.24.8(transitive)
+ Added@babel/helper-validator-identifier@7.24.7(transitive)
+ Added@babel/parser@7.25.6(transitive)
+ Added@babel/types@7.25.6(transitive)
+ Added@vitest/expect@1.6.0(transitive)
+ Added@vitest/runner@1.6.0(transitive)
+ Added@vitest/snapshot@1.6.0(transitive)
+ Added@vitest/spy@1.6.0(transitive)
+ Added@vitest/utils@1.6.0(transitive)
+ Addedcross-spawn@7.0.3(transitive)
+ Addedestree-walker@3.0.3(transitive)
+ Addedexeca@8.0.1(transitive)
+ Addedget-stream@8.0.1(transitive)
+ Addedhuman-signals@5.0.0(transitive)
+ Addedis-stream@3.0.0(transitive)
+ Addedisexe@2.0.0(transitive)
+ Addedjs-tokens@9.0.0(transitive)
+ Addedlocal-pkg@0.5.0(transitive)
+ Addedmagicast@0.3.5(transitive)
+ Addedmerge-stream@2.0.0(transitive)
+ Addedmimic-fn@4.0.0(transitive)
+ Addednpm-run-path@5.3.0(transitive)
+ Addedonetime@6.0.0(transitive)
+ Addedp-limit@5.0.0(transitive)
+ Addedpath-key@3.1.14.0.0(transitive)
+ Addedshebang-command@2.0.0(transitive)
+ Addedshebang-regex@3.0.0(transitive)
+ Addedsignal-exit@4.1.0(transitive)
+ Addedstrip-final-newline@3.0.0(transitive)
+ Addedstrip-literal@2.1.0(transitive)
+ Addedtinypool@0.8.4(transitive)
+ Addedto-fast-properties@2.0.0(transitive)
+ Addedvite-node@1.6.0(transitive)
+ Addedvitest@1.6.0(transitive)
+ Addedwhich@2.0.2(transitive)
- Removed@types/chai@4.3.19(transitive)
- Removed@types/chai-subset@1.3.5(transitive)
- Removed@types/node@22.5.5(transitive)
- Removed@vitest/expect@0.34.6(transitive)
- Removed@vitest/runner@0.34.6(transitive)
- Removed@vitest/snapshot@0.34.6(transitive)
- Removed@vitest/spy@0.34.6(transitive)
- Removed@vitest/utils@0.34.6(transitive)
- Removedlocal-pkg@0.4.3(transitive)
- Removedp-limit@4.0.0(transitive)
- Removedstrip-literal@1.3.0(transitive)
- Removedtinypool@0.7.0(transitive)
- Removedundici-types@6.19.8(transitive)
- Removedvite-node@0.34.6(transitive)
- Removedvitest@0.34.6(transitive)
Updatedistanbul-lib-coverage@^3.2.2
Updatedistanbul-reports@^3.1.6
Updatedmagic-string@^0.30.5
Updatedstd-env@^3.5.0
Updatedv8-to-istanbul@^9.2.0