Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix incorect coverage reporting (fix #375) #655

Merged
merged 16 commits into from Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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:110 (vm.runInThisContext)
// TODO: Include our transformations in soucemaps
const offset = 190
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Later we should calculate this number based on the length of the arguments, but I am fine to have the magic number for now.


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('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