Skip to content

Commit

Permalink
fix: fix incorect coverage reporting (fix #375) (#655)
Browse files Browse the repository at this point in the history
  • Loading branch information
Demivan committed Feb 1, 2022
1 parent 022f561 commit 5ef622a
Show file tree
Hide file tree
Showing 20 changed files with 150 additions and 59 deletions.
4 changes: 4 additions & 0 deletions packages/vite-node/src/client.ts
Expand Up @@ -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,
Expand Down
11 changes: 7 additions & 4 deletions packages/vite-node/src/server.ts
Expand Up @@ -72,12 +72,15 @@ export class ViteNodeServer {
private async _fetchModule(id: string): Promise<FetchResult> {
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 }
}
Expand All @@ -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,
})
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/rollup.config.js
Expand Up @@ -30,6 +30,7 @@ const external = [
...Object.keys(pkg.peerDependencies),
'worker_threads',
'inspector',
'c8',
]

export default ({ watch }) => [
Expand Down
85 changes: 60 additions & 25 deletions 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'
Expand Down Expand Up @@ -36,47 +39,79 @@ 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
}

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<string, { map: RawSourceMap; source: string | undefined }> = {}
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()
Expand Down
7 changes: 7 additions & 0 deletions 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'
Expand Down Expand Up @@ -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') {
Expand Down
3 changes: 0 additions & 3 deletions 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'
Expand All @@ -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
Expand Down Expand Up @@ -275,7 +273,6 @@ export class Vitest {
// })
// }
this.snapshot.clear()
this.coverage = []
const files = Array.from(this.changedTests)
this.changedTests.clear()

Expand Down
3 changes: 0 additions & 3 deletions packages/vitest/src/node/plugins/index.ts
Expand Up @@ -38,9 +38,6 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest())
: undefined,
preTransformRequests: false,
},
build: {
sourcemap: true,
},
// disable deps optimization
cacheDir: undefined,
}
Expand Down
3 changes: 0 additions & 3 deletions packages/vitest/src/node/pool.ts
Expand Up @@ -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)
Expand Down
18 changes: 0 additions & 18 deletions 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'
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/types/coverage.ts
Expand Up @@ -66,4 +66,5 @@ export interface C8Options {
}

export interface ResolvedC8Options extends Required<C8Options> {
tempDirectory: string
}
2 changes: 0 additions & 2 deletions 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'
Expand Down Expand Up @@ -27,5 +26,4 @@ export interface WorkerRPC {
onTaskUpdate: (pack: TaskResultPack[]) => void

snapshotSaved: (snapshot: SnapshotResult) => void
coverageCollected: (coverage: Profiler.TakePreciseCoverageReturnType) => void
}
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

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

2 changes: 2 additions & 0 deletions test/coverage-test/package.json
Expand Up @@ -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:*"
}
}
17 changes: 17 additions & 0 deletions test/coverage-test/src/Hello.vue
@@ -0,0 +1,17 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
const times = ref(2)
const props = defineProps<{ count: number }>()
const result = computed(() => props.count * times.value)
defineExpose(props)
</script>

<template>
<div>{{ count }} x {{ times }} = {{ result }}</div>
<button @click="times += 1">
x1
</button>
</template>
3 changes: 3 additions & 0 deletions test/coverage-test/src/utils.ts
Expand Up @@ -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)
}

Expand Down
5 changes: 5 additions & 0 deletions 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
}
6 changes: 6 additions & 0 deletions test/coverage-test/test/__snapshots__/vue.test.ts.snap
@@ -0,0 +1,6 @@
// Vitest Snapshot v1

exports[`vue 3 coverage 1`] = `
"<div>4 x 2 = 8</div>
<button> x1 </button>"
`;
28 changes: 28 additions & 0 deletions 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')
})
4 changes: 4 additions & 0 deletions 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: {
},
})
2 changes: 1 addition & 1 deletion test/reporters/tests/custom-reporter.spec.ts
Expand Up @@ -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'], {
Expand Down

0 comments on commit 5ef622a

Please sign in to comment.