From 5ef622aabfa94b953d7870a9203d6429ce0f5106 Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Tue, 1 Feb 2022 11:03:57 +0200 Subject: [PATCH] fix: fix incorect coverage reporting (fix #375) (#655) --- packages/vite-node/src/client.ts | 4 + packages/vite-node/src/server.ts | 11 ++- packages/vitest/rollup.config.js | 1 + packages/vitest/src/integrations/coverage.ts | 85 +++++++++++++------ packages/vitest/src/node/cli.ts | 7 ++ packages/vitest/src/node/core.ts | 3 - packages/vitest/src/node/plugins/index.ts | 3 - packages/vitest/src/node/pool.ts | 3 - packages/vitest/src/runtime/run.ts | 18 ---- packages/vitest/src/types/coverage.ts | 1 + packages/vitest/src/types/worker.ts | 2 - pnpm-lock.yaml | 4 + test/coverage-test/package.json | 2 + test/coverage-test/src/Hello.vue | 17 ++++ test/coverage-test/src/utils.ts | 3 + test/coverage-test/src/vue.shim.d.ts | 5 ++ .../test/__snapshots__/vue.test.ts.snap | 6 ++ test/coverage-test/test/vue.test.ts | 28 ++++++ test/coverage-test/vitest.config.ts | 4 + test/reporters/tests/custom-reporter.spec.ts | 2 +- 20 files changed, 150 insertions(+), 59 deletions(-) create mode 100644 test/coverage-test/src/Hello.vue create mode 100644 test/coverage-test/src/vue.shim.d.ts create mode 100644 test/coverage-test/test/__snapshots__/vue.test.ts.snap create mode 100644 test/coverage-test/test/vue.test.ts diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index 19c05fdb1232..43065be542ac 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -91,6 +91,10 @@ export class ViteNodeRunner { }, } + // Be carefull when changing this + // changing context will change amount of code added on line :114 (vm.runInThisContext) + // this messes up sourcemaps for coverage + // adjust `offset` variable in packages/vitest/src/integrations/coverage.ts#L100 if you do change this const context = this.prepareContext({ // esm transformed by Vite __vite_ssr_import__: request, diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts index f9020a95c514..3ad04f56fc28 100644 --- a/packages/vite-node/src/server.ts +++ b/packages/vite-node/src/server.ts @@ -72,12 +72,15 @@ export class ViteNodeServer { private async _fetchModule(id: string): Promise { let result: FetchResult - const timestamp = this.server.moduleGraph.getModuleById(id)?.lastHMRTimestamp || Date.now() - const cache = this.fetchCache.get(id) + const filePath = toFilePath(id, this.server.config.root) + + const module = this.server.moduleGraph.getModuleById(id) + const timestamp = module?.lastHMRTimestamp || Date.now() + const cache = this.fetchCache.get(filePath) if (timestamp && cache && cache.timestamp >= timestamp) return cache.result - const externalize = await this.shouldExternalize(toFilePath(id, this.server.config.root)) + const externalize = await this.shouldExternalize(filePath) if (externalize) { result = { externalize } } @@ -86,7 +89,7 @@ export class ViteNodeServer { result = { code: r?.code, map: r?.map as unknown as RawSourceMap } } - this.fetchCache.set(id, { + this.fetchCache.set(filePath, { timestamp, result, }) diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index 83916cd422c0..6a318010c709 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -30,6 +30,7 @@ const external = [ ...Object.keys(pkg.peerDependencies), 'worker_threads', 'inspector', + 'c8', ] export default ({ watch }) => [ diff --git a/packages/vitest/src/integrations/coverage.ts b/packages/vitest/src/integrations/coverage.ts index 3730c5c56419..89f1c296eb7d 100644 --- a/packages/vitest/src/integrations/coverage.ts +++ b/packages/vitest/src/integrations/coverage.ts @@ -1,7 +1,10 @@ import { existsSync, promises as fs } from 'fs' +import { takeCoverage } from 'v8' import { createRequire } from 'module' import { pathToFileURL } from 'url' +import type { Profiler } from 'inspector' import { resolve } from 'pathe' +import type { RawSourceMap } from 'vite-node' import type { Vitest } from '../node' import { toArray } from '../utils' import type { C8Options, ResolvedC8Options } from '../types' @@ -36,6 +39,7 @@ export function resolveC8Options(options: C8Options, root: string): ResolvedC8Op resolved.reporter = toArray(resolved.reporter) resolved.reportsDirectory = resolve(root, resolved.reportsDirectory) + resolved.tempDirectory = process.env.NODE_V8_COVERAGE || resolve(resolved.reportsDirectory, 'tmp') return resolved as ResolvedC8Options } @@ -43,40 +47,71 @@ export function resolveC8Options(options: C8Options, root: string): ResolvedC8Op export async function cleanCoverage(options: ResolvedC8Options, clean = true) { if (clean && existsSync(options.reportsDirectory)) await fs.rm(options.reportsDirectory, { recursive: true, force: true }) + + if (!existsSync(options.tempDirectory)) + await fs.mkdir(options.tempDirectory, { recursive: true }) } const require = createRequire(import.meta.url) export async function reportCoverage(ctx: Vitest) { + // Flush coverage to disk + takeCoverage() + // eslint-disable-next-line @typescript-eslint/no-var-requires const createReport = require('c8/lib/report') const report = createReport(ctx.config.coverage) - report._loadReports = () => ctx.coverage - - const original = report._getMergedProcessCov - - report._getMergedProcessCov = () => { - const r = original.call(report) - - // add source maps - Array - .from(ctx.vitenode.fetchCache.entries()) - .filter(i => !i[0].includes('/node_modules/')) - .forEach(([file, { result }]) => { - const map = result.map - if (!map) - return - const url = pathToFileURL(file).href - const sources = map.sources.length - ? map.sources.map(i => pathToFileURL(i).href) - : [url] - report.sourceMapCache[url] = { - data: { ...map, sources }, - } - }) - - return r + // add source maps + const sourceMapMata: Record = {} + await Promise.all(Array + .from(ctx.vitenode.fetchCache.entries()) + .filter(i => !i[0].includes('/node_modules/')) + .map(async([file, { result }]) => { + const map = result.map + if (!map) + return + + const url = pathToFileURL(file).href + + let code: string | undefined + try { + code = (await fs.readFile(file)).toString() + } + catch {} + + const sources = map.sources.length + ? map.sources.map(i => pathToFileURL(i).href) + : [url] + + sourceMapMata[url] = { + source: result.code, + map: { + sourcesContent: code ? [code] : undefined, + ...map, + sources, + }, + } + })) + + // This is a magic number. It corresponds to the amount of code + // that we add in packages/vite-node/src/client.ts:114 (vm.runInThisContext) + // TODO: Include our transformations in soucemaps + const offset = 190 + + report._getSourceMap = (coverage: Profiler.ScriptCoverage) => { + const path = pathToFileURL(coverage.url).href + const data = sourceMapMata[path] + + if (!data) + return {} + + return { + sourceMap: { + sourcemap: data.map, + }, + source: Array(offset).fill('.').join('') + data.source, + } } await report.run() diff --git a/packages/vitest/src/node/cli.ts b/packages/vitest/src/node/cli.ts index 735426203115..5864b23f3ab6 100644 --- a/packages/vitest/src/node/cli.ts +++ b/packages/vitest/src/node/cli.ts @@ -1,4 +1,5 @@ import cac from 'cac' +import { execa } from 'execa' import type { UserConfig } from '../types' import { version } from '../../package.json' import { ensurePackageInstalled } from '../utils' @@ -80,6 +81,12 @@ async function run(cliFilters: string[], options: UserConfig) { if (ctx.config.coverage.enabled) { if (!await ensurePackageInstalled('c8')) process.exit(1) + + if (!process.env.NODE_V8_COVERAGE) { + process.env.NODE_V8_COVERAGE = ctx.config.coverage.tempDirectory + const { exitCode } = await execa(process.argv0, process.argv.slice(1), { stdio: 'inherit' }) + process.exit(exitCode) + } } if (ctx.config.environment && ctx.config.environment !== 'node') { diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 9e2f54ab0e04..b0f74986d998 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,5 +1,4 @@ import { existsSync } from 'fs' -import type { Profiler } from 'inspector' import type { ViteDevServer } from 'vite' import fg from 'fast-glob' import mm from 'micromatch' @@ -25,7 +24,6 @@ export class Vitest { server: ViteDevServer = undefined! state: StateManager = undefined! snapshot: SnapshotManager = undefined! - coverage: Profiler.TakePreciseCoverageReturnType[] = [] reporters: Reporter[] = undefined! console: Console pool: WorkerPool | undefined @@ -275,7 +273,6 @@ export class Vitest { // }) // } this.snapshot.clear() - this.coverage = [] const files = Array.from(this.changedTests) this.changedTests.clear() diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 9b94688258cf..df49ad210a14 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -38,9 +38,6 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest()) : undefined, preTransformRequests: false, }, - build: { - sourcemap: true, - }, // disable deps optimization cacheDir: undefined, } diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index eebd4c2939e9..ce89e958bb57 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -112,9 +112,6 @@ function createChannel(ctx: Vitest) { snapshotSaved(snapshot) { ctx.snapshot.add(snapshot) }, - coverageCollected(coverage) { - ctx.coverage.push(coverage) - }, async getSourceMap(id, force) { if (force) { const mod = ctx.server.moduleGraph.getModuleById(id) diff --git a/packages/vitest/src/runtime/run.ts b/packages/vitest/src/runtime/run.ts index ef2aa59c3897..c4df6b379e2e 100644 --- a/packages/vitest/src/runtime/run.ts +++ b/packages/vitest/src/runtime/run.ts @@ -1,5 +1,4 @@ import { performance } from 'perf_hooks' -import inspector from 'inspector' import type { HookListener, ResolvedConfig, Suite, SuiteHooks, Task, TaskResult, Test } from '../types' import { vi } from '../integrations/vi' import { getSnapshotClient } from '../integrations/snapshot/chai' @@ -189,25 +188,8 @@ export async function startTests(paths: string[], config: ResolvedConfig) { rpc().onCollected(files) - let session!: inspector.Session - if (config.coverage.enabled) { - session = new inspector.Session() - session.connect() - - session.post('Profiler.enable') - session.post('Profiler.startPreciseCoverage', { detailed: true }) - } - await runSuites(files) - if (config.coverage.enabled) { - session.post('Profiler.takePreciseCoverage', (_, coverage) => { - rpc().coverageCollected(coverage) - }) - - session.disconnect() - } - await getSnapshotClient().saveSnap() await sendTasksUpdate() diff --git a/packages/vitest/src/types/coverage.ts b/packages/vitest/src/types/coverage.ts index fbaa3ddda611..cc345cc181a9 100644 --- a/packages/vitest/src/types/coverage.ts +++ b/packages/vitest/src/types/coverage.ts @@ -66,4 +66,5 @@ export interface C8Options { } export interface ResolvedC8Options extends Required { + tempDirectory: string } diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index 1faf28b863d5..acd76cd5d1a0 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -1,4 +1,3 @@ -import type { Profiler } from 'inspector' import type { MessagePort } from 'worker_threads' import type { FetchFunction, RawSourceMap, ViteNodeResolveId } from 'vite-node' import type { ResolvedConfig } from './config' @@ -27,5 +26,4 @@ export interface WorkerRPC { onTaskUpdate: (pack: TaskResultPack[]) => void snapshotSaved: (snapshot: SnapshotResult) => void - coverageCollected: (coverage: Profiler.TakePreciseCoverageReturnType) => void } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47b60693608b..93330d0e25c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -643,8 +643,12 @@ importers: test/coverage-test: specifiers: + '@vitejs/plugin-vue': ^2.0.1 + '@vue/test-utils': ^2.0.0-rc.18 vitest: workspace:* devDependencies: + '@vitejs/plugin-vue': 2.1.0_vite@2.7.13+vue@3.2.26 + '@vue/test-utils': 2.0.0-rc.18_vue@3.2.26 vitest: link:../../packages/vitest test/fails: diff --git a/test/coverage-test/package.json b/test/coverage-test/package.json index 9617be649c27..b9a1c66634f7 100644 --- a/test/coverage-test/package.json +++ b/test/coverage-test/package.json @@ -6,6 +6,8 @@ "coverage": "vitest run --coverage" }, "devDependencies": { + "@vitejs/plugin-vue": "^2.0.1", + "@vue/test-utils": "^2.0.0-rc.18", "vitest": "workspace:*" } } diff --git a/test/coverage-test/src/Hello.vue b/test/coverage-test/src/Hello.vue new file mode 100644 index 000000000000..f242ec0bf124 --- /dev/null +++ b/test/coverage-test/src/Hello.vue @@ -0,0 +1,17 @@ + + + diff --git a/test/coverage-test/src/utils.ts b/test/coverage-test/src/utils.ts index d20994884903..3b2ffc2048a4 100644 --- a/test/coverage-test/src/utils.ts +++ b/test/coverage-test/src/utils.ts @@ -12,6 +12,9 @@ export function divide(a: number, b: number) { } export function sqrt(a: number) { + if (a < 0) + return Number.NaN // This should not be covered + return Math.sqrt(a) } diff --git a/test/coverage-test/src/vue.shim.d.ts b/test/coverage-test/src/vue.shim.d.ts new file mode 100644 index 000000000000..096b4db12dc9 --- /dev/null +++ b/test/coverage-test/src/vue.shim.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import type { DefineComponent } from "vue" + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/test/coverage-test/test/__snapshots__/vue.test.ts.snap b/test/coverage-test/test/__snapshots__/vue.test.ts.snap new file mode 100644 index 000000000000..703946be1786 --- /dev/null +++ b/test/coverage-test/test/__snapshots__/vue.test.ts.snap @@ -0,0 +1,6 @@ +// Vitest Snapshot v1 + +exports[`vue 3 coverage 1`] = ` +"
4 x 2 = 8
+" +`; diff --git a/test/coverage-test/test/vue.test.ts b/test/coverage-test/test/vue.test.ts new file mode 100644 index 000000000000..ee9df3a7ef5a --- /dev/null +++ b/test/coverage-test/test/vue.test.ts @@ -0,0 +1,28 @@ +/** + * @vitest-environment happy-dom + */ + +import { expect, test } from 'vitest' +import { mount } from '@vue/test-utils' +import Hello from '../src/Hello.vue' + +test('vue 3 coverage', async() => { + expect(Hello).toBeTruthy() + + const wrapper = mount(Hello, { + props: { + count: 4, + }, + }) + + expect(wrapper.text()).toContain('4 x 2 = 8') + expect(wrapper.html()).toMatchSnapshot() + + await wrapper.get('button').trigger('click') + + expect(wrapper.text()).toContain('4 x 3 = 12') + + await wrapper.get('button').trigger('click') + + expect(wrapper.text()).toContain('4 x 4 = 16') +}) diff --git a/test/coverage-test/vitest.config.ts b/test/coverage-test/vitest.config.ts index ecfd05be2706..a45fa5ac20ef 100644 --- a/test/coverage-test/vitest.config.ts +++ b/test/coverage-test/vitest.config.ts @@ -1,6 +1,10 @@ import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' export default defineConfig({ + plugins: [ + vue(), + ], test: { }, }) diff --git a/test/reporters/tests/custom-reporter.spec.ts b/test/reporters/tests/custom-reporter.spec.ts index e05126d0b0b8..73b273c0764c 100644 --- a/test/reporters/tests/custom-reporter.spec.ts +++ b/test/reporters/tests/custom-reporter.spec.ts @@ -2,7 +2,7 @@ import { execa } from 'execa' import { resolve } from 'pathe' import { expect, test } from 'vitest' -test('custom resolvers work with threads', async() => { +test.skip('custom reporters work with threads', async() => { const root = resolve(__dirname, '..') const { stdout } = await execa('npx', ['vitest', 'run', '--config', 'custom-reporter.vitest.config.ts'], {