Skip to content

Commit

Permalink
feat(coverage): v8 to ignore empty lines, comments, types (#5457)
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Apr 5, 2024
1 parent d400388 commit 10b8971
Show file tree
Hide file tree
Showing 17 changed files with 1,227 additions and 156 deletions.
46 changes: 46 additions & 0 deletions docs/config/index.md
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
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
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
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
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
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
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
@@ -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
}
17 changes: 12 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 10b8971

Please sign in to comment.