Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(coverage): v8 to ignore empty lines, comments, types #5457

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,20 @@ List of files included in coverage as glob patterns

List of files excluded from coverage as glob patterns.

This option overrides all default options. Extend the default options when adding new patterns to ignore:

```ts
import { coverageConfigDefaults, defineConfig } from 'vitest/config'

export default defineConfig({
test: {
coverage: {
exclude: ['**/custom-pattern/**', ...coverageConfigDefaults.exclude]
},
},
})
```

#### coverage.all

- **Type:** `boolean`
Expand Down Expand Up @@ -1320,6 +1334,38 @@ Sets thresholds for files matching the glob pattern.
}
```

#### coverage.ignoreEmptyLines

- **Type:** `boolean`
- **Default:** `false`
- **Available for providers:** `'v8'`
- **CLI:** `--coverage.ignoreEmptyLines=<boolean>`

Ignore empty lines, comments and other non-runtime code, e.g. Typescript types.

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.ignoreClassMethods

- **Type:** `string[]`
Expand Down
5 changes: 5 additions & 0 deletions docs/guide/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ npm i -D @vitest/coverage-istanbul

## Coverage Setup

:::tip
It's recommended to always define [`coverage.include`](https://vitest.dev/config/#coverage-include) in your configuration file.
This helps Vitest to reduce the amount of files picked by [`coverage.all`](https://vitest.dev/config/#coverage-all).
:::

To test with coverage enabled, you can pass the `--coverage` flag in CLI.
By default, reporter `['text', 'html', 'clover', 'json']` will be used.

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
"@types/chai@4.3.6": "patches/@types__chai@4.3.6.patch",
"@sinonjs/fake-timers@11.1.0": "patches/@sinonjs__fake-timers@11.1.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"
"@types/sinonjs__fake-timers@8.1.5": "patches/@types__sinonjs__fake-timers@8.1.5.patch",
"v8-to-istanbul@9.2.0": "patches/v8-to-istanbul@9.2.0.patch"
}
},
"simple-git-hooks": {
Expand Down
4 changes: 2 additions & 2 deletions packages/coverage-v8/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@
"picocolors": "^1.0.0",
"std-env": "^3.5.0",
"strip-literal": "^2.0.0",
"test-exclude": "^6.0.0",
"v8-to-istanbul": "^9.2.0"
"test-exclude": "^6.0.0"
},
"devDependencies": {
"@types/debug": "^4.1.12",
Expand All @@ -66,6 +65,7 @@
"@types/istanbul-lib-source-maps": "^4.0.4",
"@types/istanbul-reports": "^3.0.4",
"pathe": "^1.1.1",
"v8-to-istanbul": "^9.2.0",
"vite-node": "workspace:*",
"vitest": "workspace:*"
}
Expand Down
12 changes: 5 additions & 7 deletions packages/coverage-v8/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,14 +266,12 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
}

const coverages = await Promise.all(chunk.map(async (filename) => {
const transformResult = await this.ctx.vitenode.transformRequest(filename.pathname).catch(() => {})
const { originalSource, source } = await this.getSources(filename.href, transformResults)

// Ignore empty files, e.g. files that contain only typescript types and no runtime code
if (transformResult && stripLiteral(transformResult.code).trim() === '')
if (source && stripLiteral(source).trim() === '')
return null

const { originalSource } = await this.getSources(filename.href, transformResults)

const coverage = {
url: filename.href,
scriptId: '0',
Expand Down Expand Up @@ -309,9 +307,9 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
}> {
const filePath = normalize(fileURLToPath(url))

const transformResult = transformResults.get(filePath)
const transformResult = transformResults.get(filePath) || await this.ctx.vitenode.transformRequest(filePath).catch(() => {})

const map = transformResult?.map
const map = transformResult?.map as (EncodedSourceMap | undefined)
const code = transformResult?.code
const sourcesContent = map?.sourcesContent?.[0] || await fs.readFile(filePath, 'utf-8').catch(() => {
// If file does not exist construct a dummy source for it.
Expand Down Expand Up @@ -367,7 +365,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
// If no source map was found from vite-node we can assume this file was not run in the wrapper
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0

const converter = v8ToIstanbul(url, wrapperLength, sources)
const converter = v8ToIstanbul(url, wrapperLength, sources, undefined, this.options.ignoreEmptyLines)
await converter.load()

converter.applyCoverage(functions)
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const coverageConfigDefaults: ResolvedCoverageOptions = {
reporter: [['text', {}], ['html', {}], ['clover', {}], ['json', {}]],
extension: ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts', '.tsx', '.jsx', '.vue', '.svelte', '.marko'],
allowExternal: false,
ignoreEmptyLines: false,
processingConcurrency: Math.min(20, os.availableParallelism?.() ?? os.cpus().length),
}

Expand Down
7 changes: 6 additions & 1 deletion packages/vitest/src/types/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,12 @@ export interface CoverageIstanbulOptions extends BaseCoverageOptions {
ignoreClassMethods?: string[]
}

export interface CoverageV8Options extends BaseCoverageOptions {}
export interface CoverageV8Options extends BaseCoverageOptions {
/**
* Ignore empty lines, comments and other non-runtime code, e.g. Typescript types
*/
ignoreEmptyLines?: boolean
}

export interface CustomProviderOptions extends Pick<BaseCoverageOptions, FieldsWithDefaultValues> {
/** Name of the module or path to a file to load the custom provider from */
Expand Down
156 changes: 156 additions & 0 deletions patches/v8-to-istanbul@9.2.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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..c1e0c0ae19984480e408713d1691fa174a7c4c1f 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')
@@ -25,12 +26,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 +60,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 +84,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 +283,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
}
Loading
Loading