Skip to content

Commit

Permalink
feat(coverage): v8 to ignore empty lines, comments, types
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Mar 31, 2024
1 parent 59c71d2 commit 5ca624f
Show file tree
Hide file tree
Showing 16 changed files with 1,210 additions and 156 deletions.
32 changes: 32 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1334,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
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/[email protected]": "patches/@[email protected]",
"@sinonjs/[email protected]": "patches/@[email protected]",
"[email protected]": "patches/[email protected]",
"@types/[email protected]": "patches/@[email protected]"
"@types/[email protected]": "patches/@[email protected]",
"[email protected]": "patches/[email protected]"
}
},
"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/[email protected]
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
}
19 changes: 14 additions & 5 deletions pnpm-lock.yaml

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

Loading

0 comments on commit 5ca624f

Please sign in to comment.