Skip to content

Commit 40f18a0

Browse files
authoredJun 26, 2023
fix(coverage): v8 to prevent crash on dynamic CJS files (#3657)
1 parent fbb56ad commit 40f18a0

File tree

9 files changed

+2427
-3
lines changed

9 files changed

+2427
-3
lines changed
 

‎packages/coverage-v8/src/provider.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
110110
}
111111

112112
const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => {
113-
const sources = await this.getSources(url)
113+
const sources = await this.getSources(url, functions)
114114

115115
// If no source map was found from vite-node we can assume this file was not run in the wrapper
116116
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0
@@ -204,7 +204,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
204204
}))
205205
}
206206

207-
private async getSources(url: string): Promise<{
207+
private async getSources(url: string, functions: Profiler.FunctionCoverage[] = []): Promise<{
208208
source: string
209209
originalSource?: string
210210
sourceMap?: { sourcemap: EncodedSourceMap }
@@ -217,7 +217,12 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
217217

218218
const map = transformResult?.map
219219
const code = transformResult?.code
220-
const sourcesContent = map?.sourcesContent?.[0] || await fs.readFile(filePath, 'utf-8')
220+
const sourcesContent = map?.sourcesContent?.[0] || await fs.readFile(filePath, 'utf-8').catch(() => {
221+
// If file does not exist construct a dummy source for it.
222+
// These can be files that were generated dynamically during the test run and were removed after it.
223+
const length = findLongestFunctionLength(functions)
224+
return '.'.repeat(length)
225+
})
221226

222227
// These can be uncovered files included by "all: true" or files that are loaded outside vite-node
223228
if (!map)
@@ -261,3 +266,14 @@ function removeViteHelpersFromSourceMaps(source: string | undefined, map: Encode
261266

262267
return combinedMap as EncodedSourceMap
263268
}
269+
270+
/**
271+
* Find the function with highest `endOffset` to determine the length of the file
272+
*/
273+
function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) {
274+
return functions.reduce((previous, current) => {
275+
const maxEndOffset = current.ranges.reduce((endOffset, range) => Math.max(endOffset, range.endOffset), 0)
276+
277+
return Math.max(previous, maxEndOffset)
278+
}, 0)
279+
}

‎test/coverage-test/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/dynamic-file.ignore.*

‎test/coverage-test/coverage-report-tests/__snapshots__/c8.report.test.ts.snap

+907
Large diffs are not rendered by default.

‎test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ exports[`custom json report 1`] = `
1616
"<process-cwd>/src/Defined.vue",
1717
"<process-cwd>/src/Hello.vue",
1818
"<process-cwd>/src/another-setup.ts",
19+
"<process-cwd>/src/dynamic-file-esm.ignore.mjs",
20+
"<process-cwd>/src/dynamic-files.ts",
1921
"<process-cwd>/src/function-count.ts",
2022
"<process-cwd>/src/implicitElse.ts",
2123
"<process-cwd>/src/importEnv.ts",

‎test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap

+395
Large diffs are not rendered by default.

‎test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap

+1,042
Large diffs are not rendered by default.
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { existsSync, rmSync, writeFileSync } from 'node:fs'
2+
import { createRequire } from 'node:module'
3+
import { fileURLToPath } from 'node:url'
4+
5+
export async function runDynamicFileESM() {
6+
const filename = fileURLToPath(new URL('./dynamic-file-esm.ignore.mjs', import.meta.url))
7+
8+
if (existsSync(filename))
9+
rmSync(filename)
10+
11+
writeFileSync(filename, `
12+
// File created by coverage-test/src/dynamic-files.ts
13+
export function run() {
14+
return "Import works"
15+
}
16+
function uncovered() {}
17+
`.trim(), 'utf-8')
18+
19+
const { run } = await import(filename)
20+
21+
if (run() !== 'Import works')
22+
throw new Error(`Failed to run ${filename}`)
23+
24+
rmSync(filename)
25+
}
26+
27+
export async function runDynamicFileCJS() {
28+
const filename = fileURLToPath(new URL('./dynamic-file-cjs.ignore.js', import.meta.url))
29+
30+
if (existsSync(filename))
31+
rmSync(filename)
32+
33+
writeFileSync(filename, `
34+
// File created by coverage-test/src/dynamic-files.ts
35+
module.exports.run = function run() {
36+
return "Import works"
37+
}
38+
function uncovered() {}
39+
`.trim(), 'utf-8')
40+
41+
const { run } = createRequire(import.meta.url)(filename)
42+
43+
if (run() !== 'Import works')
44+
throw new Error(`Failed to run ${filename}`)
45+
46+
rmSync(filename)
47+
}

‎test/coverage-test/test/coverage.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { expect, test } from 'vitest'
22
import { implicitElse } from '../src/implicitElse'
33
import { useImportEnv } from '../src/importEnv'
44
import { second } from '../src/function-count'
5+
import { runDynamicFileCJS, runDynamicFileESM } from '../src/dynamic-files'
6+
7+
// Browser mode crashes with dynamic files. Enable this when browser mode works.
8+
// To keep istanbul report consistent between browser and node, skip dynamic tests when istanbul is used.
9+
const skipDynamicFiles = process.env.COVERAGE_PROVIDER === 'istanbul' || !process.env.COVERAGE_PROVIDER
510

611
const { pythagoras } = await (() => {
712
if ('__vitest_browser__' in globalThis)
@@ -27,3 +32,11 @@ test('import meta env', () => {
2732
test('cover function counts', () => {
2833
expect(second()).toBe(2)
2934
})
35+
36+
test.skipIf(skipDynamicFiles)('run dynamic ESM file', async () => {
37+
await runDynamicFileESM()
38+
})
39+
40+
test.skipIf(skipDynamicFiles)('run dynamic CJS file', async () => {
41+
await runDynamicFileCJS()
42+
})

‎test/coverage-test/testing.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const UPDATE_SNAPSHOTS = false
55

66
const provider = process.argv[1 + process.argv.indexOf('--provider')]
77
const isBrowser = process.argv.includes('--browser')
8+
process.env.COVERAGE_PROVIDER = provider
89

910
const threadsConfig = [{ threads: true }, { threads: false }, { singleThread: true }]
1011

0 commit comments

Comments
 (0)
Please sign in to comment.