feat!(coverage): v8 to support only AST based remapping (#8064)

This commit is contained in:
Ari Perkkiö 2025-06-18 12:29:05 +03:00 committed by GitHub
parent 41a111c35b
commit 176133ed0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 355 additions and 1177 deletions

View File

@ -1628,51 +1628,11 @@ Sets thresholds to 100 for files matching the glob pattern.
}
```
#### coverage.ignoreEmptyLines
- **Type:** `boolean`
- **Default:** `true` (`false` in v1)
- **Available for providers:** `'v8'`
- **CLI:** `--coverage.ignoreEmptyLines=<boolean>`
Ignore empty lines, comments and other non-runtime code, e.g. Typescript types. Requires `experimentalAstAwareRemapping: false`.
This option works only if the used compiler removes comments and other non-runtime code from the transpiled code.
By default Vite uses ESBuild which removes comments and Typescript types from `.ts`, `.tsx` and `.jsx` files.
If you want to apply ESBuild to other files as well, define them in [`esbuild` options](https://vitejs.dev/config/shared-options.html#esbuild):
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
esbuild: {
// Transpile all files with ESBuild to remove comments from code coverage.
// Required for `test.coverage.ignoreEmptyLines` to work:
include: ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.ts', '**/*.tsx'],
},
test: {
coverage: {
provider: 'v8',
ignoreEmptyLines: true,
},
},
})
```
#### coverage.experimentalAstAwareRemapping
- **Type:** `boolean`
- **Default:** `false`
- **Available for providers:** `'v8'`
- **CLI:** `--coverage.experimentalAstAwareRemapping=<boolean>`
Remap coverage with experimental AST based analysis. Provides more accurate results compared to default mode.
#### coverage.ignoreClassMethods
- **Type:** `string[]`
- **Default:** `[]`
- **Available for providers:** `'istanbul'`
- **Available for providers:** `'v8' | 'istanbul'`
- **CLI:** `--coverage.ignoreClassMethods=<method>`
Set to array of class method names to ignore for coverage.

View File

@ -336,9 +336,8 @@ Please refer to the type definition for more details.
Both coverage providers have their own ways how to ignore code from coverage reports:
- [`v8`](https://github.com/istanbuljs/v8-to-istanbul#ignoring-uncovered-lines)
- [`v8`](https://github.com/AriPerkkio/ast-v8-to-istanbul?tab=readme-ov-file#ignoring-code)
- [`istanbul`](https://github.com/istanbuljs/nyc#parsing-hints-ignoring-lines)
- `v8` with [`experimentalAstAwareRemapping: true`](https://vitest.dev/config/#coverage-experimentalAstAwareRemapping) see [ast-v8-to-istanbul | Ignoring code](https://github.com/AriPerkkio/ast-v8-to-istanbul?tab=readme-ov-file#ignoring-code)
When using TypeScript the source codes are transpiled using `esbuild`, which strips all comments from the source codes ([esbuild#516](https://github.com/evanw/esbuild/issues/516)).
Comments which are considered as [legal comments](https://esbuild.github.io/api/#legal-comments) are preserved.

View File

@ -21,6 +21,21 @@ export default defineConfig({
})
```
### V8 Code Coverage Major Changes
Vitest's V8 code coverage provider is now using more accurate coverage result remapping logic.
It is expected for users to see changes in their coverage reports when updating from Vitest v3.
In the past Vitest used [`v8-to-istanbul`](https://github.com/istanbuljs/v8-to-istanbul) for remapping V8 coverage results into your source files.
This method wasn't very accurate and provided plenty of false positives in the coverage reports.
We've now developed a new package that utilizes AST based analysis for the V8 coverage.
This allows V8 reports to be as accurate as `@vitest/coverage-istanbul` reports.
- Coverage ignore hints have updated. See [Coverage | Ignoring Code](/guide/coverage.html#ignoring-code).
- `coverage.ignoreEmptyLines` is removed. Lines without runtime code are no longer included in reports.
- `coverage.experimentalAstAwareRemapping` is removed. This option is now enabled by default, and is the only supported remapping method.
- `coverage.ignoreClassMethods` is now supported by V8 provider too.
### Removed options `coverage.all` and `coverage.extensions`
In previous versions Vitest included all uncovered files in coverage report by default.

View File

@ -86,7 +86,6 @@
"@sinonjs/fake-timers@14.0.0": "patches/@sinonjs__fake-timers@14.0.0.patch",
"cac@6.7.14": "patches/cac@6.7.14.patch",
"@types/sinonjs__fake-timers@8.1.5": "patches/@types__sinonjs__fake-timers@8.1.5.patch",
"v8-to-istanbul@9.3.0": "patches/v8-to-istanbul@9.3.0.patch",
"acorn@8.11.3": "patches/acorn@8.11.3.patch"
},
"onlyBuiltDependencies": [

View File

@ -75,7 +75,6 @@
"@types/istanbul-reports": "catalog:",
"@vitest/browser": "workspace:*",
"pathe": "catalog:",
"v8-to-istanbul": "^9.3.0",
"vite-node": "workspace:*",
"vitest": "workspace:*"
}

View File

@ -6,7 +6,6 @@ import type { AfterSuiteRunMeta } from 'vitest'
import type { CoverageProvider, ReportContext, ResolvedCoverageOptions, TestProject, Vitest } from 'vitest/node'
import { promises as fs } from 'node:fs'
import { fileURLToPath, pathToFileURL } from 'node:url'
import remapping from '@ampproject/remapping'
// @ts-expect-error -- untyped
import { mergeProcessCovs } from '@bcoe/v8-coverage'
import astV8ToIstanbul from 'ast-v8-to-istanbul'
@ -15,12 +14,10 @@ import libCoverage from 'istanbul-lib-coverage'
import libReport from 'istanbul-lib-report'
import libSourceMaps from 'istanbul-lib-source-maps'
import reports from 'istanbul-reports'
import MagicString from 'magic-string'
import { parseModule } from 'magicast'
import { normalize } from 'pathe'
import { provider } from 'std-env'
import c from 'tinyrainbow'
import v8ToIstanbul from 'v8-to-istanbul'
import { cleanUrl } from 'vite-node/utils'
import { BaseCoverageProvider } from 'vitest/coverage'
@ -34,11 +31,6 @@ export interface ScriptCoverageWithOffset extends Profiler.ScriptCoverage {
type TransformResults = Map<string, FetchResult>
interface RawCoverage { result: ScriptCoverageWithOffset[] }
// Note that this needs to match the line ending as well
const VITE_EXPORTS_LINE_PATTERN
= /Object\.defineProperty\(__vite_ssr_exports__.*\n/g
const DECORATOR_METADATA_PATTERN
= /_ts_metadata\("design:paramtypes", \[[^\]]*\]\),*/g
const FILE_PROTOCOL = 'file://'
const debug = createDebug('vitest:coverage')
@ -188,22 +180,11 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
transform,
)
coverageMap.merge(await this.v8ToIstanbul(
coverageMap.merge(await this.remapCoverage(
url.href,
0,
sources,
[{
ranges: [
{
startOffset: 0,
endOffset: sources.originalSource.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)',
}],
[],
))
if (debug.enabled) {
@ -219,118 +200,109 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
return coverageMap
}
private async v8ToIstanbul(filename: string, wrapperLength: number, sources: Awaited<ReturnType<typeof this.getSources>>, functions: Profiler.FunctionCoverage[]) {
if (this.options.experimentalAstAwareRemapping) {
let ast
try {
ast = await parseAstAsync(sources.source)
}
catch (error) {
this.ctx.logger.error(`Failed to parse ${filename}. Excluding it from coverage.\n`, error)
return {}
}
return await astV8ToIstanbul({
code: sources.source,
sourceMap: sources.sourceMap?.sourcemap,
ast,
coverage: { functions, url: filename },
ignoreClassMethods: this.options.ignoreClassMethods,
wrapperLength,
ignoreNode: (node, type) => {
// SSR transformed imports
if (
type === 'statement'
&& node.type === 'VariableDeclarator'
&& node.id.type === 'Identifier'
&& node.id.name.startsWith('__vite_ssr_import_')
) {
return true
}
// SSR transformed exports vite@>6.3.5
if (
type === 'statement'
&& node.type === 'ExpressionStatement'
&& node.expression.type === 'AssignmentExpression'
&& node.expression.left.type === 'MemberExpression'
&& node.expression.left.object.type === 'Identifier'
&& node.expression.left.object.name === '__vite_ssr_exports__'
) {
return true
}
// SSR transformed exports vite@^6.3.5
if (
type === 'statement'
&& node.type === 'VariableDeclarator'
&& node.id.type === 'Identifier'
&& node.id.name === '__vite_ssr_export_default__'
) {
return true
}
// in-source test with "if (import.meta.vitest)"
if (
(type === 'branch' || type === 'statement')
&& node.type === 'IfStatement'
&& node.test.type === 'MemberExpression'
&& node.test.property.type === 'Identifier'
&& node.test.property.name === 'vitest'
) {
// SSR
if (
node.test.object.type === 'Identifier'
&& node.test.object.name === '__vite_ssr_import_meta__'
) {
return 'ignore-this-and-nested-nodes'
}
// Web
if (
node.test.object.type === 'MetaProperty'
&& node.test.object.meta.name === 'import'
&& node.test.object.property.name === 'meta'
) {
return 'ignore-this-and-nested-nodes'
}
}
// Browser mode's "import.meta.env ="
if (
type === 'statement'
&& node.type === 'ExpressionStatement'
&& node.expression.type === 'AssignmentExpression'
&& node.expression.left.type === 'MemberExpression'
&& node.expression.left.object.type === 'MetaProperty'
&& node.expression.left.object.meta.name === 'import'
&& node.expression.left.object.property.name === 'meta'
&& node.expression.left.property.type === 'Identifier'
&& node.expression.left.property.name === 'env') {
return true
}
},
},
)
}
const converter = v8ToIstanbul(
filename,
wrapperLength,
sources,
undefined,
this.options.ignoreEmptyLines,
)
await converter.load()
private async remapCoverage(filename: string, wrapperLength: number, result: Awaited<ReturnType<typeof this.getSources>>, functions: Profiler.FunctionCoverage[]) {
let ast
try {
converter.applyCoverage(functions)
ast = await parseAstAsync(result.code)
}
catch (error) {
this.ctx.logger.error(`Failed to convert coverage for ${filename}.\n`, error)
this.ctx.logger.error(`Failed to parse ${filename}. Excluding it from coverage.\n`, error)
return {}
}
return converter.toIstanbul()
return await astV8ToIstanbul({
code: result.code,
sourceMap: result.map,
ast,
coverage: { functions, url: filename },
ignoreClassMethods: this.options.ignoreClassMethods,
wrapperLength,
ignoreNode: (node, type) => {
// SSR transformed imports
if (
type === 'statement'
&& node.type === 'VariableDeclarator'
&& node.id.type === 'Identifier'
&& node.id.name.startsWith('__vite_ssr_import_')
) {
return true
}
// SSR transformed exports vite@>6.3.5
if (
type === 'statement'
&& node.type === 'ExpressionStatement'
&& node.expression.type === 'AssignmentExpression'
&& node.expression.left.type === 'MemberExpression'
&& node.expression.left.object.type === 'Identifier'
&& node.expression.left.object.name === '__vite_ssr_exports__'
) {
return true
}
// SSR transformed exports vite@^6.3.5
if (
type === 'statement'
&& node.type === 'VariableDeclarator'
&& node.id.type === 'Identifier'
&& node.id.name === '__vite_ssr_export_default__'
) {
return true
}
// in-source test with "if (import.meta.vitest)"
if (
(type === 'branch' || type === 'statement')
&& node.type === 'IfStatement'
&& node.test.type === 'MemberExpression'
&& node.test.property.type === 'Identifier'
&& node.test.property.name === 'vitest'
) {
// SSR
if (
node.test.object.type === 'Identifier'
&& node.test.object.name === '__vite_ssr_import_meta__'
) {
return 'ignore-this-and-nested-nodes'
}
// Web
if (
node.test.object.type === 'MetaProperty'
&& node.test.object.meta.name === 'import'
&& node.test.object.property.name === 'meta'
) {
return 'ignore-this-and-nested-nodes'
}
}
// Browser mode's "import.meta.env ="
if (
type === 'statement'
&& node.type === 'ExpressionStatement'
&& node.expression.type === 'AssignmentExpression'
&& node.expression.left.type === 'MemberExpression'
&& node.expression.left.object.type === 'MetaProperty'
&& node.expression.left.object.meta.name === 'import'
&& node.expression.left.object.property.name === 'meta'
&& node.expression.left.property.type === 'Identifier'
&& node.expression.left.property.name === 'env') {
return true
}
// SWC's decorators
if (
type === 'statement'
&& node.type === 'ExpressionStatement'
&& node.expression.type === 'CallExpression'
&& node.expression.callee.type === 'Identifier'
&& node.expression.callee.name === '_ts_decorate') {
return 'ignore-this-and-nested-nodes'
}
},
},
)
}
private async getSources<TransformResult extends (FetchResult | Awaited<ReturnType<typeof this.ctx.vitenode.transformRequest>>)>(
@ -339,9 +311,8 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
onTransform: (filepath: string) => Promise<TransformResult>,
functions: Profiler.FunctionCoverage[] = [],
): Promise<{
source: string
originalSource: string
sourceMap?: { sourcemap: EncodedSourceMap }
code: string
map?: EncodedSourceMap
}> {
const filePath = normalize(fileURLToPath(url))
@ -353,45 +324,32 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
const map = transformResult?.map as EncodedSourceMap | undefined
const code = transformResult?.code
const sourcesContent = map?.sourcesContent || []
if (!sourcesContent[0]) {
sourcesContent[0] = await fs.readFile(filePath, 'utf-8').catch(() => {
if (!code) {
const original = await fs.readFile(filePath, 'utf-8').catch(() => {
// If file does not exist construct a dummy source for it.
// These can be files that were generated dynamically during the test run and were removed after it.
const length = findLongestFunctionLength(functions)
return '/'.repeat(length)
})
return { code: original }
}
// These can be uncovered files picked by "coverage.include" or files that are loaded outside vite-node
if (!map) {
return {
source: code || sourcesContent[0],
originalSource: sourcesContent[0],
// Vue needs special handling for "map.sources"
if (map) {
map.sources ||= []
map.sources = map.sources
.filter(source => source != null)
.map(source => new URL(source, url).href)
if (map.sources.length === 0) {
map.sources.push(url)
}
}
const sources = (map.sources || [])
.filter(source => source != null)
.map(source => new URL(source, url).href)
if (sources.length === 0) {
sources.push(url)
}
return {
originalSource: sourcesContent[0],
source: code || sourcesContent[0],
sourceMap: {
sourcemap: excludeGeneratedCode(code, {
...map,
version: 3,
sources,
sourcesContent,
}),
},
}
return { code, map }
}
private async convertCoverage(
@ -464,7 +422,7 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
functions,
)
coverageMap.merge(await this.v8ToIstanbul(
coverageMap.merge(await this.remapCoverage(
url,
startOffset,
sources,
@ -491,42 +449,6 @@ async function transformCoverage(coverageMap: CoverageMap) {
return await sourceMapStore.transformCoverage(coverageMap)
}
/**
* Remove generated code from the source maps:
* - Vite's export helpers: e.g. `Object.defineProperty(__vite_ssr_exports__, "sum", { enumerable: true, configurable: true, get(){ return sum }});`
* - SWC's decorator metadata: e.g. `_ts_metadata("design:paramtypes", [\ntypeof Request === "undefined" ? Object : Request\n]),`
*/
function excludeGeneratedCode(
source: string | undefined,
map: EncodedSourceMap,
) {
if (!source) {
return map
}
if (
!source.match(VITE_EXPORTS_LINE_PATTERN)
&& !source.match(DECORATOR_METADATA_PATTERN)
) {
return map
}
const trimmed = new MagicString(source)
trimmed.replaceAll(VITE_EXPORTS_LINE_PATTERN, '\n')
trimmed.replaceAll(DECORATOR_METADATA_PATTERN, match =>
'\n'.repeat(match.split('\n').length - 1))
const trimmedMap = trimmed.generateMap({ hires: 'boundary' })
// A merged source map where the first one excludes generated parts
const combinedMap = remapping(
[{ ...trimmedMap, version: 3 }, map],
() => null,
)
return combinedMap as EncodedSourceMap
}
/**
* Find the function with highest `endOffset` to determine the length of the file
*/

View File

@ -44,7 +44,6 @@ export const coverageConfigDefaults: ResolvedCoverageOptions = {
],
allowExternal: false,
excludeAfterRemap: false,
ignoreEmptyLines: true,
processingConcurrency: Math.min(
20,
os.availableParallelism?.() ?? os.cpus().length,

View File

@ -248,9 +248,7 @@ export interface BaseCoverageOptions {
* Defaults to `Math.min(20, os.availableParallelism?.() ?? os.cpus().length)`
*/
processingConcurrency?: number
}
export interface CoverageIstanbulOptions extends BaseCoverageOptions {
/**
* Set to array of class method names to ignore for coverage
*
@ -259,27 +257,9 @@ export interface CoverageIstanbulOptions extends BaseCoverageOptions {
ignoreClassMethods?: string[]
}
export interface CoverageV8Options extends BaseCoverageOptions {
/**
* Ignore empty lines, comments and other non-runtime code, e.g. Typescript types
* - Requires `experimentalAstAwareRemapping: false`
*/
ignoreEmptyLines?: boolean
export interface CoverageIstanbulOptions extends BaseCoverageOptions {}
/**
* Remap coverage with experimental AST based analysis
* - Provides more accurate results compared to default mode
*/
experimentalAstAwareRemapping?: boolean
/**
* Set to array of class method names to ignore for coverage.
* - Requires `experimentalAstAwareRemapping: true`
*
* @default []
*/
ignoreClassMethods?: string[]
}
export interface CoverageV8Options extends BaseCoverageOptions {}
export interface CustomProviderOptions
extends Pick<BaseCoverageOptions, FieldsWithDefaultValues> {

View File

@ -1,174 +0,0 @@
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 4f7e3bc8d1bba4feb51044ff9eb77b41f972f957..0000000000000000000000000000000000000000
diff --git a/index.d.ts b/index.d.ts
index ee7b286844f2bf96357218166e26e1c338f774cf..657531b7c75f43e9a4e957dd1f10797e44da5bb1 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,5 +1,7 @@
/// <reference types="node" />
+// Patch applied: https://github.com/istanbuljs/v8-to-istanbul/pull/244
+
import { Profiler } from 'inspector'
import { CoverageMapData } from 'istanbul-lib-coverage'
import { SourceMapInput } from '@jridgewell/trace-mapping'
@@ -20,6 +22,6 @@ declare class V8ToIstanbul {
toIstanbul(): CoverageMapData
}
-declare function v8ToIstanbul(scriptPath: string, wrapperLength?: number, sources?: Sources, excludePath?: (path: string) => boolean): V8ToIstanbul
+declare function v8ToIstanbul(scriptPath: string, wrapperLength?: number, sources?: Sources, excludePath?: (path: string) => boolean, excludeEmptyLines?: boolean): V8ToIstanbul
export = v8ToIstanbul
diff --git a/index.js b/index.js
index 4db27a7d84324d0e6605c5506e3eee5665ddfeb0..7bfb839634b1e3c54efedc3c270d82edc4167a64 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,6 @@
+// Patch applied: https://github.com/istanbuljs/v8-to-istanbul/pull/244
const V8ToIstanbul = require('./lib/v8-to-istanbul')
-module.exports = function (path, wrapperLength, sources, excludePath) {
- return new V8ToIstanbul(path, wrapperLength, sources, excludePath)
+module.exports = function (path, wrapperLength, sources, excludePath, excludeEmptyLines) {
+ return new V8ToIstanbul(path, wrapperLength, sources, excludePath, excludeEmptyLines)
}
diff --git a/lib/source.js b/lib/source.js
index d8ebc215f6ad83d472abafe976935acfe5c61b04..021fd2aed1f73ebb4adc449ce6e96f2d89c295a5 100644
--- a/lib/source.js
+++ b/lib/source.js
@@ -1,23 +1,32 @@
+// Patch applied: https://github.com/istanbuljs/v8-to-istanbul/pull/244
const CovLine = require('./line')
const { sliceRange } = require('./range')
-const { originalPositionFor, generatedPositionFor, GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND } = require('@jridgewell/trace-mapping')
+const { originalPositionFor, generatedPositionFor, eachMapping, GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND } = require('@jridgewell/trace-mapping')
module.exports = class CovSource {
- constructor (sourceRaw, wrapperLength) {
+ constructor (sourceRaw, wrapperLength, traceMap) {
sourceRaw = sourceRaw ? sourceRaw.trimEnd() : ''
this.lines = []
this.eof = sourceRaw.length
this.shebangLength = getShebangLength(sourceRaw)
this.wrapperLength = wrapperLength - this.shebangLength
- this._buildLines(sourceRaw)
+ this._buildLines(sourceRaw, traceMap)
}
- _buildLines (source) {
+ _buildLines (source, traceMap) {
let position = 0
let ignoreCount = 0
let ignoreAll = false
+ const linesToCover = traceMap && this._parseLinesToCover(traceMap)
+
for (const [i, lineStr] of source.split(/(?<=\r?\n)/u).entries()) {
- const line = new CovLine(i + 1, position, lineStr)
+ const lineNumber = i + 1
+ const line = new CovLine(lineNumber, position, lineStr)
+
+ if (linesToCover && !linesToCover.has(lineNumber)) {
+ line.ignore = true
+ }
+
if (ignoreCount > 0) {
line.ignore = true
ignoreCount--
@@ -125,6 +134,18 @@ module.exports = class CovSource {
if (this.lines[line - 1] === undefined) return this.eof
return Math.min(this.lines[line - 1].startCol + relCol, this.lines[line - 1].endCol)
}
+
+ _parseLinesToCover (traceMap) {
+ const linesToCover = new Set()
+
+ eachMapping(traceMap, (mapping) => {
+ if (mapping.originalLine !== null) {
+ linesToCover.add(mapping.originalLine)
+ }
+ })
+
+ return linesToCover
+ }
}
// this implementation is pulled over from istanbul-lib-sourcemap:
diff --git a/lib/v8-to-istanbul.js b/lib/v8-to-istanbul.js
index 3616437b00658861dc5a8910c64d1449e9fdf467..4642ca4818ce982e2f186abe4289793768e7cdf9 100644
--- a/lib/v8-to-istanbul.js
+++ b/lib/v8-to-istanbul.js
@@ -1,3 +1,4 @@
+// Patch applied: https://github.com/istanbuljs/v8-to-istanbul/pull/244
const assert = require('assert')
const convertSourceMap = require('convert-source-map')
const util = require('util')
@@ -8,14 +9,9 @@ const CovBranch = require('./branch')
const CovFunction = require('./function')
const CovSource = require('./source')
const { sliceRange } = require('./range')
-const compatError = Error(`requires Node.js ${require('../package.json').engines.node}`)
-const { readFileSync } = require('fs')
-let readFile = () => { throw compatError }
-try {
- readFile = require('fs').promises.readFile
-} catch (_err) {
- // most likely we're on an older version of Node.js.
-}
+const { readFileSync, promises } = require('fs')
+const readFile = promises.readFile
+
const { TraceMap } = require('@jridgewell/trace-mapping')
const isOlderNode10 = /^v10\.(([0-9]\.)|(1[0-5]\.))/u.test(process.version)
const isNode8 = /^v8\./.test(process.version)
@@ -25,12 +21,13 @@ const isNode8 = /^v8\./.test(process.version)
const cjsWrapperLength = isOlderNode10 ? require('module').wrapper[0].length : 0
module.exports = class V8ToIstanbul {
- constructor (scriptPath, wrapperLength, sources, excludePath) {
+ constructor (scriptPath, wrapperLength, sources, excludePath, excludeEmptyLines) {
assert(typeof scriptPath === 'string', 'scriptPath must be a string')
assert(!isNode8, 'This module does not support node 8 or lower, please upgrade to node 10')
this.path = parsePath(scriptPath)
this.wrapperLength = wrapperLength === undefined ? cjsWrapperLength : wrapperLength
this.excludePath = excludePath || (() => false)
+ this.excludeEmptyLines = excludeEmptyLines === true
this.sources = sources || {}
this.generatedLines = []
this.branches = {}
@@ -58,8 +55,8 @@ module.exports = class V8ToIstanbul {
if (!this.sourceMap.sourcesContent) {
this.sourceMap.sourcesContent = await this.sourcesContentFromSources()
}
- this.covSources = this.sourceMap.sourcesContent.map((rawSource, i) => ({ source: new CovSource(rawSource, this.wrapperLength), path: this.sourceMap.sources[i] }))
- this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength)
+ this.covSources = this.sourceMap.sourcesContent.map((rawSource, i) => ({ source: new CovSource(rawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null), path: this.sourceMap.sources[i] }))
+ this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null)
} else {
const candidatePath = this.rawSourceMap.sourcemap.sources.length >= 1 ? this.rawSourceMap.sourcemap.sources[0] : this.rawSourceMap.sourcemap.file
this.path = this._resolveSource(this.rawSourceMap, candidatePath || this.path)
@@ -82,8 +79,8 @@ module.exports = class V8ToIstanbul {
// We fallback to reading the original source from disk.
originalRawSource = await readFile(this.path, 'utf8')
}
- this.covSources = [{ source: new CovSource(originalRawSource, this.wrapperLength), path: this.path }]
- this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength)
+ this.covSources = [{ source: new CovSource(originalRawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null), path: this.path }]
+ this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null)
}
} else {
this.covSources = [{ source: new CovSource(rawSource, this.wrapperLength), path: this.path }]
@@ -281,8 +278,10 @@ module.exports = class V8ToIstanbul {
s: {}
}
source.lines.forEach((line, index) => {
- statements.statementMap[`${index}`] = line.toIstanbul()
- statements.s[`${index}`] = line.ignore ? 1 : line.count
+ if (!line.ignore) {
+ statements.statementMap[`${index}`] = line.toIstanbul()
+ statements.s[`${index}`] = line.count
+ }
})
return statements
}

16
pnpm-lock.yaml generated
View File

@ -143,9 +143,6 @@ patchedDependencies:
cac@6.7.14:
hash: a8f0f3517a47ce716ed90c0cfe6ae382ab763b021a664ada2a608477d0621588
path: patches/cac@6.7.14.patch
v8-to-istanbul@9.3.0:
hash: fc8eccce7f8e7c27b0d0b1c63e93ff3adaa8c05c33d5603bacf58ea6d12951f3
path: patches/v8-to-istanbul@9.3.0.patch
importers:
@ -633,9 +630,6 @@ importers:
pathe:
specifier: 'catalog:'
version: 2.0.3
v8-to-istanbul:
specifier: ^9.3.0
version: 9.3.0(patch_hash=fc8eccce7f8e7c27b0d0b1c63e93ff3adaa8c05c33d5603bacf58ea6d12951f3)
vite-node:
specifier: workspace:*
version: link:../vite-node
@ -8434,10 +8428,6 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
v8-to-istanbul@9.3.0:
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
engines: {node: '>=10.12.0'}
varint@6.0.0:
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
@ -16596,12 +16586,6 @@ snapshots:
utils-merge@1.0.1: {}
v8-to-istanbul@9.3.0(patch_hash=fc8eccce7f8e7c27b0d0b1c63e93ff3adaa8c05c33d5603bacf58ea6d12951f3):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
'@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0
varint@6.0.0: {}
vary@1.1.2: {}

View File

@ -1,3 +1,3 @@
export function uncovered() {
return 0
export function uncovered(condition: boolean) {
return condition ? 1 : 0
}

View File

@ -394,7 +394,7 @@ test('coverage.autoUpdate cannot update thresholds when configuration file doesn
})
test('boolean flag 100 should not crash CLI', async () => {
let { stderr } = await runVitestCli('--coverage.enabled', '--coverage.thresholds.100', '--coverage.include=fixtures/coverage-test')
let { stderr } = await runVitestCli('--coverage.enabled', '--coverage.thresholds.100', '--coverage.include=fixtures/coverage-test', '--passWithNoTests')
// non-zero coverage shows up, which is non-deterministic, so strip it.
stderr = stderr.replace(/\([0-9.]+%\) does/g, '(0%) does')

View File

@ -1,4 +1,4 @@
/* v8 ignore next 4 */
// padding
/* istanbul ignore next -- @preserve */
export function first() {
return "First"

View File

@ -1,7 +1,7 @@
import libCoverage from 'istanbul-lib-coverage'
import { expect } from 'vitest'
import * as transpiled from '../fixtures/src/pre-bundle/bundle.js'
import { coverageTest, formatSummary, isV8Provider, normalizeURL, readCoverageJson, runVitest, test } from '../utils.js'
import { coverageTest, formatSummary, normalizeURL, readCoverageJson, runVitest, test } from '../utils.js'
test('bundled code with source maps to originals', async () => {
await runVitest({
@ -31,42 +31,22 @@ test('bundled code with source maps to originals', async () => {
[second.path]: formatSummary(second.toSummary()),
}
if (isV8Provider()) {
expect(summary).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/pre-bundle/first.ts": {
"branches": "1/1 (100%)",
"functions": "1/2 (50%)",
"lines": "4/6 (66.66%)",
"statements": "4/6 (66.66%)",
},
"<process-cwd>/fixtures/src/pre-bundle/second.ts": {
"branches": "1/1 (100%)",
"functions": "1/2 (50%)",
"lines": "4/6 (66.66%)",
"statements": "4/6 (66.66%)",
},
}
`)
}
else {
expect(summary).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/pre-bundle/first.ts": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/src/pre-bundle/second.ts": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
}
`)
}
expect(summary).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/pre-bundle/first.ts": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/src/pre-bundle/second.ts": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
}
`)
})
coverageTest('run bundled sources', () => {

View File

@ -1,7 +1,7 @@
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { beforeAll, expect } from 'vitest'
import { isV8Provider, readCoverageMap, runVitest, test } from '../utils'
import { readCoverageMap, runVitest, test } from '../utils'
// Note that this test may fail if you have new files in "vitest/test/coverage/src"
// and have not yet committed those
@ -51,40 +51,20 @@ test('{ changed: "HEAD" }', async () => {
const uncoveredFile = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/new-uncovered-file.ts')
const changedFile = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/file-to-change.ts')
if (isV8Provider()) {
expect([uncoveredFile, changedFile]).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/file-to-change.ts": {
"branches": "1/1 (100%)",
"functions": "1/2 (50%)",
"lines": "4/6 (66.66%)",
"statements": "4/6 (66.66%)",
},
"<process-cwd>/fixtures/src/new-uncovered-file.ts": {
"branches": "1/1 (100%)",
"functions": "1/1 (100%)",
"lines": "0/3 (0%)",
"statements": "0/3 (0%)",
},
}
`)
}
else {
expect([uncoveredFile, changedFile]).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/file-to-change.ts": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/src/new-uncovered-file.ts": {
"branches": "0/0 (100%)",
"functions": "0/1 (0%)",
"lines": "0/1 (0%)",
"statements": "0/1 (0%)",
},
}
`)
}
expect([uncoveredFile, changedFile]).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/file-to-change.ts": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/src/new-uncovered-file.ts": {
"branches": "0/0 (100%)",
"functions": "0/1 (0%)",
"lines": "0/1 (0%)",
"statements": "0/1 (0%)",
},
}
`)
}, SKIP)

View File

@ -72,23 +72,6 @@ test('provider options, generic', () => {
})
})
test('provider specific options, v8', () => {
assertType<Coverage>({
provider: 'v8',
experimentalAstAwareRemapping: true,
})
})
test('provider specific options, istanbul', () => {
assertType<Coverage>({
provider: 'istanbul',
ignoreClassMethods: ['string'],
// @ts-expect-error -- v8 specific error
experimentalAstAwareRemapping: true,
})
})
test('provider specific options, custom', () => {
assertType<Coverage>({
provider: 'custom',

View File

@ -1,6 +1,6 @@
import { expect } from 'vitest'
import { DecoratorsTester } from '../fixtures/src/decorators'
import { coverageTest, isV8Provider, normalizeURL, readCoverageMap, runVitest, test } from '../utils'
import { coverageTest, normalizeURL, readCoverageMap, runVitest, test } from '../utils'
test('decorators generated metadata is ignored', async () => {
await runVitest({
@ -14,15 +14,8 @@ test('decorators generated metadata is ignored', async () => {
const lineCoverage = fileCoverage.getLineCoverage()
const branchCoverage = fileCoverage.getBranchCoverageByLine()
// Decorator should not be uncovered - on V8 this is marked as covered, on Istanbul it's excluded from report
if (isV8Provider()) {
expect(lineCoverage['4']).toBe(1)
expect(branchCoverage['4'].coverage).toBe(100)
}
else {
expect(lineCoverage['4']).toBeUndefined()
expect(branchCoverage['4']).toBeUndefined()
}
expect(lineCoverage['4']).toBeUndefined()
expect(branchCoverage['4']).toBeUndefined()
// Covered branch should be marked correctly
expect(lineCoverage['7']).toBe(1)

View File

@ -1,184 +0,0 @@
import { beforeAll, expect } from 'vitest'
import { add } from '../fixtures/src/empty-lines'
import { coverageTest, describe, normalizeURL, readCoverageMap, runVitest, test } from '../utils'
type CoveredLine = 1
type UncoveredLine = 0
type IgnoredLine = undefined
// Key is 1-based line number
type LineCoverage = Record<number, CoveredLine | UncoveredLine | IgnoredLine>
describe('include empty lines', () => {
let coveredFileLines: LineCoverage
let uncoveredFileLines: LineCoverage
beforeAll(async () => {
await runVitest({
include: [normalizeURL(import.meta.url)],
coverage: {
reporter: 'json',
ignoreEmptyLines: false,
include: [
'**/fixtures/src/empty-lines.ts',
'**/fixtures/src/untested-file.ts',
'**/fixtures/src/types-only.ts',
],
},
})
;({ coveredFileLines, uncoveredFileLines } = await readCoverage())
})
test('lines are included', async () => {
for (const line of range(29)) {
expect(coveredFileLines[line], `Line #${line}`).not.toBe(undefined)
expect(coveredFileLines[line], `Line #${line}`).toBeTypeOf('number')
}
for (const lines of [range(37), range(4, { base: 44 })]) {
for (const line of lines) {
expect(uncoveredFileLines[line], `Line #${line}`).not.toBe(undefined)
expect(uncoveredFileLines[line], `Line #${line}`).toBeTypeOf('number')
}
}
})
test('lines with ignore hints are ignored', () => {
for (const line of range(6, { base: 38 })) {
expect(uncoveredFileLines[line], `Line #${line}`).toBe(undefined)
}
})
})
describe('ignore empty lines', () => {
let coveredFileLines: LineCoverage
let uncoveredFileLines: LineCoverage
let typesOnlyFileLines: LineCoverage
beforeAll(async () => {
await runVitest({
include: [normalizeURL(import.meta.url)],
coverage: {
reporter: 'json',
include: [
'**/fixtures/src/empty-lines.ts',
'**/fixtures/src/untested-file.ts',
'**/fixtures/src/types-only.ts',
],
},
})
;({ coveredFileLines, uncoveredFileLines, typesOnlyFileLines } = await readCoverage())
})
test('file containing only types has no uncovered lines', () => {
expect(typesOnlyFileLines[1]).toBe(undefined)
expect(typesOnlyFileLines[2]).toBe(undefined)
expect(typesOnlyFileLines[3]).toBe(undefined)
})
test('empty lines are ignored', async () => {
expect(coveredFileLines[12]).toBe(undefined)
expect(coveredFileLines[14]).toBe(undefined)
expect(coveredFileLines[19]).toBe(undefined)
expect(coveredFileLines[27]).toBe(undefined)
expect(coveredFileLines[30]).toBe(undefined)
expect(uncoveredFileLines[5]).toBe(undefined)
expect(uncoveredFileLines[7]).toBe(undefined)
})
test('comments are ignored', async () => {
expect(coveredFileLines[1]).toBe(undefined)
expect(coveredFileLines[3]).toBe(undefined)
expect(coveredFileLines[4]).toBe(undefined)
expect(coveredFileLines[5]).toBe(undefined)
expect(coveredFileLines[6]).toBe(undefined)
expect(coveredFileLines[7]).toBe(undefined)
expect(coveredFileLines[9]).toBe(undefined)
expect(coveredFileLines[16]).toBe(undefined)
expect(uncoveredFileLines[1]).toBe(undefined)
expect(uncoveredFileLines[2]).toBe(undefined)
expect(uncoveredFileLines[3]).toBe(undefined)
expect(uncoveredFileLines[4]).toBe(undefined)
expect(uncoveredFileLines[6]).toBe(undefined)
expect(uncoveredFileLines[13]).toBe(undefined)
expect(uncoveredFileLines[20]).toBe(undefined)
expect(uncoveredFileLines[34]).toBe(undefined)
expect(uncoveredFileLines[45]).toBe(undefined)
})
test('ignore hints are ignored', () => {
expect(uncoveredFileLines[38]).toBe(undefined)
expect(uncoveredFileLines[39]).toBe(undefined)
expect(uncoveredFileLines[40]).toBe(undefined)
expect(uncoveredFileLines[41]).toBe(undefined)
expect(uncoveredFileLines[42]).toBe(undefined)
expect(uncoveredFileLines[43]).toBe(undefined)
})
test('typescript types are ignored', () => {
expect(coveredFileLines[13]).toBe(undefined)
expect(coveredFileLines[20]).toBe(undefined)
expect(coveredFileLines[21]).toBe(undefined)
expect(coveredFileLines[22]).toBe(undefined)
expect(coveredFileLines[23]).toBe(undefined)
expect(coveredFileLines[24]).toBe(undefined)
expect(coveredFileLines[25]).toBe(undefined)
expect(coveredFileLines[26]).toBe(undefined)
expect(uncoveredFileLines[17]).toBe(undefined)
expect(uncoveredFileLines[25]).toBe(undefined)
expect(uncoveredFileLines[26]).toBe(undefined)
expect(uncoveredFileLines[27]).toBe(undefined)
expect(uncoveredFileLines[28]).toBe(undefined)
expect(uncoveredFileLines[29]).toBe(undefined)
expect(uncoveredFileLines[30]).toBe(undefined)
expect(uncoveredFileLines[31]).toBe(undefined)
})
test('runtime code is not ignored', () => {
// Covered
expect(coveredFileLines[2]).toBe(1)
expect(coveredFileLines[8]).toBe(1)
expect(coveredFileLines[15]).toBe(1)
expect(coveredFileLines[28]).toBe(1)
// Uncovered
expect(coveredFileLines[10]).toBe(0)
expect(coveredFileLines[17]).toBe(0)
// Uncovered
expect(uncoveredFileLines[8]).toBe(0)
expect(uncoveredFileLines[9]).toBe(0)
expect(uncoveredFileLines[10]).toBe(0)
expect(uncoveredFileLines[12]).toBe(0)
expect(uncoveredFileLines[14]).toBe(0)
expect(uncoveredFileLines[19]).toBe(0)
expect(uncoveredFileLines[21]).toBe(0)
expect(uncoveredFileLines[24]).toBe(0)
expect(uncoveredFileLines[33]).toBe(0)
expect(uncoveredFileLines[35]).toBe(0)
expect(uncoveredFileLines[46]).toBe(0)
})
})
coverageTest('cover some lines', () => {
expect(add(10, 20)).toBe(30)
})
async function readCoverage() {
const coverageMap = await readCoverageMap()
const coveredFileLines = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/empty-lines.ts').getLineCoverage() as LineCoverage
const uncoveredFileLines = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/untested-file.ts').getLineCoverage() as LineCoverage
const typesOnlyFileLines = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/types-only.ts').getLineCoverage() as LineCoverage
return { coveredFileLines, uncoveredFileLines, typesOnlyFileLines }
}
function range(count: number, options: { base: number } = { base: 1 }) {
return Array.from({ length: count }).fill(0).map((_, i) => options.base + i)
}

View File

@ -1,6 +1,6 @@
import { createRequire } from 'node:module'
import { expect } from 'vitest'
import { coverageTest, isExperimentalV8Provider, isV8Provider, normalizeURL, readCoverageMap, runVitest, test } from '../utils'
import { coverageTest, isV8Provider, normalizeURL, readCoverageMap, runVitest, test } from '../utils'
test('does not crash when file outside Vite is loaded (#5639)', async () => {
await runVitest({
@ -11,7 +11,7 @@ test('does not crash when file outside Vite is loaded (#5639)', async () => {
const coverageMap = await readCoverageMap()
const fileCoverage = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/load-outside-vite.cjs')
if (isV8Provider() || isExperimentalV8Provider()) {
if (isV8Provider()) {
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "0/0 (100%)",

View File

@ -4,7 +4,7 @@
*/
import { expect } from 'vitest'
import { isExperimentalV8Provider, isV8Provider, readCoverageMap, runVitest, test } from '../utils'
import { isV8Provider, readCoverageMap, runVitest, test } from '../utils'
test('ignore hints work', async () => {
await runVitest({
@ -20,10 +20,6 @@ test('ignore hints work', async () => {
expect(lines[12]).toBeGreaterThanOrEqual(1)
if (isV8Provider()) {
expect(lines[15]).toBeUndefined()
expect(lines[18]).toBeGreaterThanOrEqual(1)
}
else if (isExperimentalV8Provider()) {
expect(lines[15]).toBeUndefined()
expect(lines[18]).toBeUndefined()
}

View File

@ -1,5 +1,5 @@
import { expect } from 'vitest'
import { isV8Provider, readCoverageMap, runVitest, test } from '../utils'
import { readCoverageMap, runVitest, test } from '../utils'
test('in-source tests work', async () => {
const { stdout } = await runVitest({
@ -25,31 +25,14 @@ test('in-source tests work', async () => {
// If-branch is not taken - makes sure source maps are correct in in-source testing too
expect(fileCoverage.getUncoveredLines()).toContain('5')
if (isV8Provider()) {
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "2/4 (50%)",
"functions": "2/2 (100%)",
"lines": "10/12 (83.33%)",
"statements": "10/12 (83.33%)",
}
`)
}
else {
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "2/4 (50%)",
"functions": "1/1 (100%)",
"lines": "2/3 (66.66%)",
"statements": "2/3 (66.66%)",
}
`)
}
// v8-to-istanbul cannot exclude whole if-block
if (isV8Provider()) {
return
}
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "2/4 (50%)",
"functions": "1/1 (100%)",
"lines": "2/3 (66.66%)",
"statements": "2/3 (66.66%)",
}
`)
// The "customNamedTestFunction" should be excluded by auto-generated ignore hints
expect(functions).toMatchInlineSnapshot(`

View File

@ -1,6 +1,6 @@
import type { TestSpecification } from 'vitest/node'
import { expect, test } from 'vitest'
import { formatSummary, isV8Provider, readCoverageMap, runVitest } from '../utils'
import { formatSummary, readCoverageMap, runVitest } from '../utils'
const pools = ['forks']
@ -39,39 +39,20 @@ for (const isolate of [true, false]) {
[math.path]: formatSummary(math.toSummary()),
}
if (isV8Provider()) {
expect(summary).toStrictEqual({
'<process-cwd>/fixtures/src/branch.ts': {
branches: '3/3 (100%)',
functions: '1/1 (100%)',
lines: '6/6 (100%)',
statements: '6/6 (100%)',
},
'<process-cwd>/fixtures/src/math.ts': {
branches: '4/4 (100%)',
functions: '4/4 (100%)',
lines: '12/12 (100%)',
statements: '12/12 (100%)',
},
})
}
else {
expect(summary).toStrictEqual({
'<process-cwd>/fixtures/src/branch.ts': {
branches: '2/2 (100%)',
functions: '1/1 (100%)',
lines: '4/4 (100%)',
statements: '4/4 (100%)',
},
'<process-cwd>/fixtures/src/math.ts': {
branches: '0/0 (100%)',
functions: '4/4 (100%)',
lines: '4/4 (100%)',
statements: '4/4 (100%)',
},
expect(summary).toStrictEqual({
'<process-cwd>/fixtures/src/branch.ts': {
branches: '2/2 (100%)',
functions: '1/1 (100%)',
lines: '4/4 (100%)',
statements: '4/4 (100%)',
},
)
}
'<process-cwd>/fixtures/src/math.ts': {
branches: '0/0 (100%)',
functions: '4/4 (100%)',
lines: '4/4 (100%)',
statements: '4/4 (100%)',
},
})
})
}
}

View File

@ -1,6 +1,6 @@
import { expect } from 'vitest'
import { sum } from '../fixtures/src/math'
import { captureStdout, coverageTest, isV8Provider, normalizeURL, runVitest, test } from '../utils'
import { captureStdout, coverageTest, normalizeURL, runVitest, test } from '../utils'
test('report is not generated when tests fail', async () => {
const stdout = captureStdout()
@ -29,28 +29,15 @@ test('report is generated when tests fail and { reportOnFailure: true }', async
},
}, { throwOnError: false })
if (isV8Provider()) {
expect(stdout()).toMatchInlineSnapshot(`
"----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 50 | 100 | 25 | 50 |
math.ts | 50 | 100 | 25 | 50 | 6-7,10-11,14-15
----------|---------|----------|---------|---------|-------------------
"
`)
}
else {
expect(stdout()).toMatchInlineSnapshot(`
"----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 25 | 100 | 25 | 25 |
math.ts | 25 | 100 | 25 | 25 | 6-14
----------|---------|----------|---------|---------|-------------------
"
`)
}
expect(stdout()).toMatchInlineSnapshot(`
"----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 25 | 100 | 25 | 25 |
math.ts | 25 | 100 | 25 | 25 | 6-14
----------|---------|----------|---------|---------|-------------------
"
`)
expect(exitCode).toBe(1)
})

View File

@ -1,6 +1,6 @@
import libCoverage from 'istanbul-lib-coverage'
import { expect } from 'vitest'
import { isV8Provider, readCoverageJson, runVitest, test } from '../utils'
import { readCoverageJson, runVitest, test } from '../utils'
test('pre-transpiled code with source maps to original (#5341)', async () => {
await runVitest({
@ -22,24 +22,12 @@ test('pre-transpiled code with source maps to original (#5341)', async () => {
const fileCoverage = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/pre-transpiled/original.ts')
if (isV8Provider()) {
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "2/4 (50%)",
"functions": "2/2 (100%)",
"lines": "11/17 (64.7%)",
"statements": "11/17 (64.7%)",
}
`)
}
else {
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "3/6 (50%)",
"functions": "2/2 (100%)",
"lines": "6/8 (75%)",
"statements": "6/8 (75%)",
}
`)
}
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "3/6 (50%)",
"functions": "2/2 (100%)",
"lines": "6/8 (75%)",
"statements": "6/8 (75%)",
}
`)
})

View File

@ -1,5 +1,5 @@
import { expect } from 'vitest'
import { isV8Provider, readCoverageMap, runVitest, test } from '../utils'
import { readCoverageMap, runVitest, test } from '../utils'
test('coverage results matches snapshot', async () => {
await runVitest({
@ -17,52 +17,26 @@ test('coverage results matches snapshot', async () => {
const coverageMap = await readCoverageMap()
const fileCoverages = coverageMap.files().map(file => coverageMap.fileCoverageFor(file))
if (isV8Provider()) {
expect(fileCoverages).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/even.ts": {
"branches": "1/1 (100%)",
"functions": "1/2 (50%)",
"lines": "4/6 (66.66%)",
"statements": "4/6 (66.66%)",
},
"<process-cwd>/fixtures/src/math.ts": {
"branches": "1/1 (100%)",
"functions": "1/4 (25%)",
"lines": "6/12 (50%)",
"statements": "6/12 (50%)",
},
"<process-cwd>/fixtures/src/untested-file.ts": {
"branches": "1/1 (100%)",
"functions": "1/1 (100%)",
"lines": "0/15 (0%)",
"statements": "0/15 (0%)",
},
}
`)
}
else {
expect(fileCoverages).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/even.ts": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/src/math.ts": {
"branches": "0/0 (100%)",
"functions": "1/4 (25%)",
"lines": "1/4 (25%)",
"statements": "1/4 (25%)",
},
"<process-cwd>/fixtures/src/untested-file.ts": {
"branches": "0/4 (0%)",
"functions": "0/4 (0%)",
"lines": "0/8 (0%)",
"statements": "0/8 (0%)",
},
}
`)
}
expect(fileCoverages).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/even.ts": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/src/math.ts": {
"branches": "0/0 (100%)",
"functions": "1/4 (25%)",
"lines": "1/4 (25%)",
"statements": "1/4 (25%)",
},
"<process-cwd>/fixtures/src/untested-file.ts": {
"branches": "0/4 (0%)",
"functions": "0/4 (0%)",
"lines": "0/8 (0%)",
"statements": "0/8 (0%)",
},
}
`)
})

View File

@ -1,6 +1,6 @@
import { resolve } from 'node:path'
import { expect } from 'vitest'
import { isV8Provider, readCoverageMap, runVitest, test } from '../utils'
import { readCoverageMap, runVitest, test } from '../utils'
test('tests with multiple suites are covered (#3514)', async () => {
const { stdout } = await runVitest({
@ -29,24 +29,12 @@ test('tests with multiple suites are covered (#3514)', async () => {
// Some valid coverage should be reported
const fileCoverage = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/math.ts')
if (isV8Provider()) {
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "1/1 (100%)",
"functions": "1/4 (25%)",
"lines": "6/12 (50%)",
"statements": "6/12 (50%)",
}
`)
}
else {
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "0/0 (100%)",
"functions": "1/4 (25%)",
"lines": "1/4 (25%)",
"statements": "1/4 (25%)",
}
`)
}
expect(fileCoverage).toMatchInlineSnapshot(`
{
"branches": "0/0 (100%)",
"functions": "1/4 (25%)",
"lines": "1/4 (25%)",
"statements": "1/4 (25%)",
}
`)
})

View File

@ -1,6 +1,6 @@
import { readFileSync, writeFileSync } from 'node:fs'
import { expect, onTestFinished } from 'vitest'
import { isV8Provider, runVitest, test } from '../utils'
import { runVitest, test } from '../utils'
const config = 'fixtures/configs/vitest.config.thresholds-auto-update.ts'
@ -41,62 +41,32 @@ test('thresholds.autoUpdate updates thresholds', async () => {
config,
}, { throwOnError: false })
if (isV8Provider()) {
expect(readConfig()).toMatchInlineSnapshot(`
"import { defineConfig } from 'vitest/config'
expect(readConfig()).toMatchInlineSnapshot(`
"import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
thresholds: {
autoUpdate: true,
export default defineConfig({
test: {
coverage: {
thresholds: {
autoUpdate: true,
// Global ones
lines: 55.55,
functions: 33.33,
// Global ones
lines: 33.33,
functions: 33.33,
branches: 100,
statements: -4,
'**/src/math.ts': {
branches: 100,
statements: -8,
'**/src/math.ts': {
branches: 100,
functions: 25,
lines: -6,
statements: -6,
}
functions: 25,
lines: -3,
statements: -3,
}
}
},
})"
`)
}
else {
expect(readConfig()).toMatchInlineSnapshot(`
"import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
thresholds: {
autoUpdate: true,
// Global ones
lines: 33.33,
functions: 33.33,
branches: 100,
statements: -4,
'**/src/math.ts': {
branches: 100,
functions: 25,
lines: -3,
statements: -3,
}
}
}
},
})"
`)
}
}
},
})"
`)
})
function readConfig() {

View File

@ -1,6 +1,6 @@
import { expect } from 'vitest'
import { sum } from '../fixtures/src/math'
import { coverageTest, isV8Provider, normalizeURL, runVitest, test } from '../utils'
import { coverageTest, normalizeURL, runVitest, test } from '../utils'
test('failing percentage thresholds', async () => {
const { exitCode, stderr } = await runVitest({
@ -18,12 +18,9 @@ test('failing percentage thresholds', async () => {
},
}, { throwOnError: false })
const lines = isV8Provider() ? '50%' : '25%'
const statements = isV8Provider() ? '50%' : '25%'
expect(exitCode).toBe(1)
expect(stderr).toContain(`ERROR: Coverage for lines (${lines}) does not meet "**/fixtures/src/math.ts" threshold (100%)`)
expect(stderr).toContain(`ERROR: Coverage for statements (${statements}) does not meet "**/fixtures/src/math.ts" threshold (100%)`)
expect(stderr).toContain('ERROR: Coverage for lines (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)')
expect(stderr).toContain('ERROR: Coverage for statements (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)')
expect(stderr).toContain('ERROR: Coverage for functions (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)')
})
@ -36,7 +33,7 @@ test('failing absolute thresholds', async () => {
'**/fixtures/src/math.ts': {
branches: -1,
functions: -2,
lines: -5,
lines: -2,
statements: -1,
},
},
@ -45,15 +42,9 @@ test('failing absolute thresholds', async () => {
expect(exitCode).toBe(1)
if (isV8Provider()) {
expect(stderr).toContain('ERROR: Uncovered lines (6) exceed "**/fixtures/src/math.ts" threshold (5)')
expect(stderr).toContain('ERROR: Uncovered functions (3) exceed "**/fixtures/src/math.ts" threshold (2)')
expect(stderr).toContain('ERROR: Uncovered statements (6) exceed "**/fixtures/src/math.ts" threshold (1)')
}
else {
expect(stderr).toContain('ERROR: Uncovered functions (3) exceed "**/fixtures/src/math.ts" threshold (2)')
expect(stderr).toContain('ERROR: Uncovered statements (3) exceed "**/fixtures/src/math.ts" threshold (1)')
}
expect(stderr).toContain('ERROR: Uncovered functions (3) exceed "**/fixtures/src/math.ts" threshold (2)')
expect(stderr).toContain('ERROR: Uncovered statements (3) exceed "**/fixtures/src/math.ts" threshold (1)')
expect(stderr).toContain('ERROR: Uncovered lines (3) exceed "**/fixtures/src/math.ts" threshold (2)')
})
coverageTest('cover some lines, but not too much', () => {

View File

@ -1,7 +1,7 @@
import { expect } from 'vitest'
import { isEven, isOdd } from '../fixtures/src/even'
import { sum } from '../fixtures/src/math'
import { coverageTest, isV8Provider, normalizeURL, runVitest, test } from '../utils'
import { coverageTest, normalizeURL, runVitest, test } from '../utils'
test('threshold glob patterns count in global coverage', async () => {
await runVitest({
@ -14,8 +14,8 @@ test('threshold glob patterns count in global coverage', async () => {
thresholds: {
'branches': 100,
'functions': 50,
'lines': isV8Provider() ? 66 : 50,
'statements': isV8Provider() ? 66 : 50,
'lines': 50,
'statements': 50,
'**/fixtures/src/even.ts': {
branches: 100,
@ -49,22 +49,12 @@ test('{ thresholds: { 100: true } } on glob pattern', async () => {
expect(exitCode).toBe(1)
if (isV8Provider()) {
expect(stderr).toMatchInlineSnapshot(`
"ERROR: Coverage for lines (50%) does not meet "**/fixtures/src/math.ts" threshold (100%)
ERROR: Coverage for functions (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)
ERROR: Coverage for statements (50%) does not meet "**/fixtures/src/math.ts" threshold (100%)
"
`)
}
else {
expect(stderr).toMatchInlineSnapshot(`
"ERROR: Coverage for lines (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)
ERROR: Coverage for functions (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)
ERROR: Coverage for statements (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)
"
`)
}
expect(stderr).toMatchInlineSnapshot(`
"ERROR: Coverage for lines (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)
ERROR: Coverage for functions (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)
ERROR: Coverage for statements (25%) does not meet "**/fixtures/src/math.ts" threshold (100%)
"
`)
})
coverageTest('cover some lines, but not too much', () => {

View File

@ -1,8 +1,7 @@
import { readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { beforeAll, expect } from 'vitest'
import { rolldownVersion } from 'vitest/node'
import { isV8Provider, readCoverageMap, runVitest, test } from '../utils'
import { readCoverageMap, runVitest, test } from '../utils'
beforeAll(async () => {
await runVitest({
@ -24,34 +23,12 @@ test('files should not contain query parameters', () => {
test('coverage results matches snapshot', async () => {
const coverageMap = await readCoverageMap()
if (isV8Provider() && !rolldownVersion) {
expect(coverageMap).toMatchInlineSnapshot(`
{
"branches": "5/7 (71.42%)",
"functions": "3/5 (60%)",
"lines": "36/45 (80%)",
"statements": "36/45 (80%)",
}
`)
}
else if (isV8Provider() && rolldownVersion) {
expect(coverageMap).toMatchInlineSnapshot(`
{
"branches": "7/9 (77.77%)",
"functions": "4/6 (66.66%)",
"lines": "36/45 (80%)",
"statements": "36/45 (80%)",
}
`)
}
else {
expect(coverageMap).toMatchInlineSnapshot(`
{
"branches": "6/8 (75%)",
"functions": "5/7 (71.42%)",
"lines": "13/16 (81.25%)",
"statements": "14/17 (82.35%)",
}
`)
}
expect(coverageMap).toMatchInlineSnapshot(`
{
"branches": "6/8 (75%)",
"functions": "5/7 (71.42%)",
"lines": "13/16 (81.25%)",
"statements": "14/17 (82.35%)",
}
`)
})

View File

@ -1,5 +1,5 @@
import { expect } from 'vitest'
import { formatSummary, isV8Provider, readCoverageMap, runVitest, test } from '../utils'
import { formatSummary, readCoverageMap, runVitest, test } from '../utils'
test('web worker coverage is correct', async () => {
await runVitest({
@ -28,40 +28,20 @@ test('web worker coverage is correct', async () => {
}
// Check HTML report if these change unexpectedly
if (isV8Provider()) {
expect(summary).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/worker-wrapper.ts": {
"branches": "3/3 (100%)",
"functions": "2/4 (50%)",
"lines": "18/22 (81.81%)",
"statements": "18/22 (81.81%)",
},
"<process-cwd>/fixtures/src/worker.ts": {
"branches": "2/4 (50%)",
"functions": "2/3 (66.66%)",
"lines": "11/19 (57.89%)",
"statements": "11/19 (57.89%)",
},
}
`)
}
else {
expect(summary).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/worker-wrapper.ts": {
"branches": "0/0 (100%)",
"functions": "3/5 (60%)",
"lines": "9/11 (81.81%)",
"statements": "9/11 (81.81%)",
},
"<process-cwd>/fixtures/src/worker.ts": {
"branches": "2/4 (50%)",
"functions": "2/3 (66.66%)",
"lines": "7/12 (58.33%)",
"statements": "7/12 (58.33%)",
},
}
`)
}
expect(summary).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/worker-wrapper.ts": {
"branches": "0/0 (100%)",
"functions": "3/5 (60%)",
"lines": "9/11 (81.81%)",
"statements": "9/11 (81.81%)",
},
"<process-cwd>/fixtures/src/worker.ts": {
"branches": "2/4 (50%)",
"functions": "2/3 (66.66%)",
"lines": "7/12 (58.33%)",
"statements": "7/12 (58.33%)",
},
}
`)
})

View File

@ -1,5 +1,5 @@
import { expect } from 'vitest'
import { isV8Provider, readCoverageMap, runVitest, test } from '../utils'
import { readCoverageMap, runVitest, test } from '../utils'
test('uncovered files that require custom transform', async () => {
await runVitest({
@ -26,76 +26,38 @@ test('uncovered files that require custom transform', async () => {
const fileCoverages = coverageMap.files().map(file => coverageMap.fileCoverageFor(file))
if (isV8Provider()) {
expect(fileCoverages).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/covered.custom-1": {
"branches": "1/1 (100%)",
"functions": "1/2 (50%)",
"lines": "2/3 (66.66%)",
"statements": "2/3 (66.66%)",
},
"<process-cwd>/fixtures/src/math.ts": {
"branches": "1/1 (100%)",
"functions": "1/4 (25%)",
"lines": "6/12 (50%)",
"statements": "6/12 (50%)",
},
"<process-cwd>/fixtures/src/uncovered.custom-1": {
"branches": "0/1 (0%)",
"functions": "0/1 (0%)",
"lines": "0/2 (0%)",
"statements": "0/2 (0%)",
},
"<process-cwd>/fixtures/workspaces/custom-2/src/covered.custom-2": {
"branches": "1/1 (100%)",
"functions": "1/2 (50%)",
"lines": "2/3 (66.66%)",
"statements": "2/3 (66.66%)",
},
"<process-cwd>/fixtures/workspaces/custom-2/src/uncovered.custom-2": {
"branches": "0/1 (0%)",
"functions": "0/1 (0%)",
"lines": "0/2 (0%)",
"statements": "0/2 (0%)",
},
}
`)
}
else {
expect(fileCoverages).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/covered.custom-1": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/src/math.ts": {
"branches": "0/0 (100%)",
"functions": "1/4 (25%)",
"lines": "1/4 (25%)",
"statements": "1/4 (25%)",
},
"<process-cwd>/fixtures/src/uncovered.custom-1": {
"branches": "0/0 (100%)",
"functions": "0/1 (0%)",
"lines": "0/1 (0%)",
"statements": "0/1 (0%)",
},
"<process-cwd>/fixtures/workspaces/custom-2/src/covered.custom-2": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/workspaces/custom-2/src/uncovered.custom-2": {
"branches": "0/0 (100%)",
"functions": "0/1 (0%)",
"lines": "0/1 (0%)",
"statements": "0/1 (0%)",
},
}
`)
}
expect(fileCoverages).toMatchInlineSnapshot(`
{
"<process-cwd>/fixtures/src/covered.custom-1": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/src/math.ts": {
"branches": "0/0 (100%)",
"functions": "1/4 (25%)",
"lines": "1/4 (25%)",
"statements": "1/4 (25%)",
},
"<process-cwd>/fixtures/src/uncovered.custom-1": {
"branches": "0/0 (100%)",
"functions": "0/1 (0%)",
"lines": "0/1 (0%)",
"statements": "0/1 (0%)",
},
"<process-cwd>/fixtures/workspaces/custom-2/src/covered.custom-2": {
"branches": "0/0 (100%)",
"functions": "1/2 (50%)",
"lines": "1/2 (50%)",
"statements": "1/2 (50%)",
},
"<process-cwd>/fixtures/workspaces/custom-2/src/uncovered.custom-2": {
"branches": "0/0 (100%)",
"functions": "0/1 (0%)",
"lines": "0/1 (0%)",
"statements": "0/1 (0%)",
},
}
`)
})

View File

@ -46,8 +46,7 @@ export async function runVitest(config: UserConfig, options = { throwOnError: tr
enabled: true,
reporter: [],
...config.coverage,
provider: provider === 'v8-ast-aware' ? 'v8' : provider,
experimentalAstAwareRemapping: provider === 'v8-ast-aware',
provider,
customProviderModule: provider === 'custom' ? 'fixtures/custom-provider' : undefined,
},
browser: {
@ -110,10 +109,6 @@ export function isV8Provider() {
return process.env.COVERAGE_PROVIDER === 'v8'
}
export function isExperimentalV8Provider() {
return process.env.COVERAGE_PROVIDER === 'v8-ast-aware'
}
export function isBrowser() {
return process.env.COVERAGE_BROWSER === 'true'
}

View File

@ -31,25 +31,6 @@ export default defineWorkspace([
},
},
// Test cases for experimental AST aware v8-provider
{
test: {
...config.test,
name: 'v8-ast-aware',
env: { COVERAGE_PROVIDER: 'v8-ast-aware' },
// Intentionally run Istanbul tests too
include: [GENERIC_TESTS, ISTANBUL_TESTS, V8_TESTS],
exclude: [
UNIT_TESTS,
CUSTOM_TESTS,
BROWSER_TESTS,
// Not using original v8-to-istanbul that has patch applied: github.com/istanbuljs/v8-to-istanbul/pull/244
'test/empty-lines.v8.test.ts',
],
},
},
// Test cases for istanbul-provider
{
test: {
@ -108,7 +89,7 @@ export default defineWorkspace([
test: {
...config.test,
name: { label: 'v8-browser', color: 'red' },
env: { COVERAGE_PROVIDER: 'v8-ast-aware', COVERAGE_BROWSER: 'true' },
env: { COVERAGE_PROVIDER: 'v8', COVERAGE_BROWSER: 'true' },
include: [
BROWSER_TESTS,