Skip to content

Commit

Permalink
refactor: create interface for coverage logic
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Jul 20, 2022
1 parent eb64c3d commit 0c16d01
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 117 deletions.
5 changes: 3 additions & 2 deletions packages/vitest/src/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// rollup dts building will external vitest
// so output dts entry using vitest to import internal types
// eslint-disable-next-line no-restricted-imports
import type { ResolvedC8Options, UserConfig } from 'vitest'
import type { ResolvedCoverageOptions, UserConfig } from 'vitest'

export const defaultInclude = ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']
export const defaultExclude = ['**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**']
Expand All @@ -22,6 +22,7 @@ const defaultCoverageExcludes = [
]

const coverageConfigDefaults = {
provider: 'c8',
enabled: false,
clean: true,
cleanOnRerun: false,
Expand All @@ -33,7 +34,7 @@ const coverageConfigDefaults = {
// default extensions used by c8, plus '.vue' and '.svelte'
// see https://github.com/istanbuljs/schema/blob/master/default-extension.js
extension: ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.vue', '.svelte'],
} as ResolvedC8Options
} as ResolvedCoverageOptions

export const fakeTimersDefaults = {
loopLimit: 10_000,
Expand Down
14 changes: 14 additions & 0 deletions packages/vitest/src/integrations/coverage/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Vitest } from '../../node'
import type { ResolvedCoverageOptions } from '../../types'

export interface BaseCoverageReporter {
// TODO: Maybe this could be just a constructor?
initialize(ctx: Vitest): Promise<void> | void

resolveOptions(): ResolvedCoverageOptions
clean(clean?: boolean): void | Promise<void>

onBeforeFilesRun?(): void | Promise<void>
onAfterAllFilesRun(): void | Promise<void>
onAfterSuiteRun(): void | Promise<void>
}
198 changes: 111 additions & 87 deletions packages/vitest/src/integrations/coverage/c8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,112 +4,136 @@ import _url 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'
import { configDefaults } from '../../defaults'
import type { C8Options, ResolvedCoverageOptions } from '../../types'
import type { Vitest } from '../../node'
import type { BaseCoverageReporter } from './base'

export function resolveC8Options(options: C8Options, root: string): ResolvedC8Options {
const resolved: ResolvedC8Options = {
...configDefaults.coverage,
...options as any,
}

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 })
const require = createRequire(import.meta.url)

if (!existsSync(options.tempDirectory))
await fs.mkdir(options.tempDirectory, { recursive: true })
}
export class C8Reporter implements BaseCoverageReporter {
ctx!: Vitest
options!: ResolvedCoverageOptions & { provider: 'c8' }

const require = createRequire(import.meta.url)
initialize(ctx: Vitest) {
this.ctx = ctx
this.options = resolveC8Options(ctx.config.coverage, ctx.config.root)
}

// Flush coverage to disk
export function takeCoverage() {
const v8 = require('v8')
if (v8.takeCoverage == null)
console.warn('[Vitest] takeCoverage is not available in this NodeJs version.\nCoverage could be incomplete. Update to NodeJs 14.18.')
else
v8.takeCoverage()
}
resolveOptions() {
return this.options
}

export async function reportCoverage(ctx: Vitest) {
takeCoverage()
onBeforeFilesRun() {
process.env.NODE_V8_COVERAGE ||= this.options.tempDirectory
}

const createReport = require('c8/lib/report')
const report = createReport(ctx.config.coverage)
async clean(clean = true) {
if (clean && existsSync(this.options.reportsDirectory))
await fs.rm(this.options.reportsDirectory, { recursive: true, force: true })

// add source maps
const sourceMapMeta: 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
if (!existsSync(this.options.tempDirectory))
await fs.mkdir(this.options.tempDirectory, { recursive: true })
}

const url = _url.pathToFileURL(file).href
onAfterSuiteRun() {
takeCoverage()
}

let code: string | undefined
try {
code = (await fs.readFile(file)).toString()
}
catch {}

// Vite does not report full path in sourcemap sources
// so use an actual file path
const sources = [url]

sourceMapMeta[url] = {
source: result.code,
map: {
sourcesContent: code ? [code] : undefined,
...map,
sources,
async onAfterAllFilesRun() {
takeCoverage()

const createReport = require('c8/lib/report')
const report = createReport(this.ctx.config.coverage)

// add source maps
const sourceMapMeta: Record<string, { map: RawSourceMap; source: string | undefined }> = {}
await Promise.all(Array
.from(this.ctx.vitenode.fetchCache.entries())
.filter(i => !i[0].includes('/node_modules/'))
.map(async ([file, { result }]) => {
const map = result.map
if (!map)
return

const url = _url.pathToFileURL(file).href

let code: string | undefined
try {
code = (await fs.readFile(file)).toString()
}
catch {}

// Vite does not report full path in sourcemap sources
// so use an actual file path
const sources = [url]

sourceMapMeta[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 sourcemaps
const offset = 224

report._getSourceMap = (coverage: Profiler.ScriptCoverage) => {
const path = _url.pathToFileURL(coverage.url).href
const data = sourceMapMeta[path]

if (!data)
return {}

return {
sourceMap: {
sourcemap: data.map,
},
source: Array(offset).fill('.').join('') + data.source,
}
}))

// 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 sourcemaps
const offset = 224
}

report._getSourceMap = (coverage: Profiler.ScriptCoverage) => {
const path = _url.pathToFileURL(coverage.url).href
const data = sourceMapMeta[path]
await report.run()

if (!data)
return {}
if (this.ctx.config.coverage.enabled && this.ctx.config.coverage.provider === 'c8') {
if (this.ctx.config.coverage['100']) {
this.ctx.config.coverage.lines = 100
this.ctx.config.coverage.functions = 100
this.ctx.config.coverage.branches = 100
this.ctx.config.coverage.statements = 100
}

return {
sourceMap: {
sourcemap: data.map,
},
source: Array(offset).fill('.').join('') + data.source,
const { checkCoverages } = require('c8/lib/commands/check-coverage')
await checkCoverages(this.ctx.config.coverage, report)
}
}
}

await report.run()
function resolveC8Options(options: C8Options, root: string) {
const resolved = {
...configDefaults.coverage,
...options as any,
}

if (ctx.config.coverage.enabled) {
if (ctx.config.coverage['100']) {
ctx.config.coverage.lines = 100
ctx.config.coverage.functions = 100
ctx.config.coverage.branches = 100
ctx.config.coverage.statements = 100
}
resolved.reporter = toArray(resolved.reporter)
resolved.reportsDirectory = resolve(root, resolved.reportsDirectory)
resolved.tempDirectory = process.env.NODE_V8_COVERAGE || resolve(resolved.reportsDirectory, 'tmp')

const { checkCoverages } = require('c8/lib/commands/check-coverage')
await checkCoverages(ctx.config.coverage, report)
}
return resolved
}

// Flush coverage to disk
function takeCoverage() {
const v8 = require('v8')
if (v8.takeCoverage == null)
console.warn('[Vitest] takeCoverage is not available in this NodeJs version.\nCoverage could be incomplete. Update to NodeJs 14.18.')
else
v8.takeCoverage()
}
12 changes: 9 additions & 3 deletions packages/vitest/src/node/cli-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,15 @@ export async function startVitest(cliFilters: string[], options: CliOptions, vit
const ctx = await createVitest(options, viteOverrides)

if (ctx.config.coverage.enabled) {
if (!await ensurePackageInstalled('c8', root)) {
process.exitCode = 1
return false
const requiredPackages = ctx.config.coverage.provider === 'c8'
? ['c8']
: []

for (const pkg of requiredPackages) {
if (!await ensurePackageInstalled(pkg, root)) {
process.exitCode = 1
return false
}
}
}

Expand Down
3 changes: 0 additions & 3 deletions packages/vitest/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type { ResolvedConfig as ResolvedViteConfig } from 'vite'
import type { ApiConfig, ResolvedConfig, UserConfig } from '../types'
import { defaultPort } from '../constants'
import { configDefaults } from '../defaults'
import { resolveC8Options } from '../integrations/coverage/c8'
import { toArray } from '../utils'
import { VitestCache } from './cache'
import { BaseSequencer } from './sequencers/BaseSequencer'
Expand Down Expand Up @@ -93,8 +92,6 @@ export function resolveConfig(
if (viteConfig.base !== '/')
resolved.base = viteConfig.base

resolved.coverage = resolveC8Options(options.coverage || {}, resolved.root)

if (options.shard) {
if (resolved.watch)
throw new Error('You cannot use --shard option with enabled watch')
Expand Down
19 changes: 13 additions & 6 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { ViteNodeServer } from 'vite-node/server'
import type { ArgumentsType, Reporter, ResolvedConfig, UserConfig } from '../types'
import { SnapshotManager } from '../integrations/snapshot/manager'
import { clearTimeout, deepMerge, hasFailed, noop, setTimeout, slash } from '../utils'
import { cleanCoverage, reportCoverage } from '../integrations/coverage/c8'
import type { BaseCoverageReporter } from '../integrations/coverage/base'
import { C8Reporter } from '../integrations/coverage/c8'
import { createPool } from './pool'
import type { WorkerPool } from './pool'
import { createReporters } from './reporters/utils'
Expand All @@ -30,6 +31,7 @@ export class Vitest {
snapshot: SnapshotManager = undefined!
cache: VitestCache = undefined!
reporters: Reporter[] = undefined!
coverageReporter: BaseCoverageReporter = undefined!
logger: Logger
pool: WorkerPool | undefined

Expand Down Expand Up @@ -83,12 +85,17 @@ export class Vitest {

this.reporters = await createReporters(resolved.reporters, this.runner)

this.coverageReporter = new C8Reporter()
this.coverageReporter.initialize(this)

this.config.coverage = this.coverageReporter.resolveOptions()

this.runningPromise = undefined

this._onRestartListeners.forEach(fn => fn())

if (resolved.coverage.enabled)
await cleanCoverage(resolved.coverage, resolved.coverage.clean)
if (this.config.coverage.enabled)
await this.coverageReporter.clean(this.config.coverage.clean)

this.cache.results.setConfig(resolved.root, resolved.cache)
try {
Expand Down Expand Up @@ -138,7 +145,7 @@ export class Vitest {
await this.runFiles(files)

if (this.config.coverage.enabled)
await reportCoverage(this)
await this.coverageReporter.onAfterAllFilesRun()

if (this.config.watch && !this.config.browser)
await this.report('onWatcherStart')
Expand Down Expand Up @@ -321,14 +328,14 @@ export class Vitest {
this.changedTests.clear()

if (this.config.coverage.enabled && this.config.coverage.cleanOnRerun)
await cleanCoverage(this.config.coverage)
await this.coverageReporter.clean()

await this.report('onWatcherRerun', files, triggerId)

await this.runFiles(files)

if (this.config.coverage.enabled)
await reportCoverage(this)
await this.coverageReporter.onAfterAllFilesRun()

if (!this.config.browser)
await this.report('onWatcherStart')
Expand Down
5 changes: 4 additions & 1 deletion packages/vitest/src/node/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function createPool(ctx: Vitest): WorkerPool {
}

if (ctx.config.coverage.enabled)
process.env.NODE_V8_COVERAGE ||= ctx.config.coverage.tempDirectory
ctx.coverageReporter.onBeforeFilesRun?.()

options.env = {
TEST: 'true',
Expand Down Expand Up @@ -156,6 +156,9 @@ function createChannel(ctx: Vitest) {
ctx.state.collectFiles(files)
ctx.report('onCollected', files)
},
onFilesRun() {
ctx.coverageReporter.onAfterSuiteRun()
},
onTaskUpdate(packs) {
ctx.state.updateTasks(packs)
ctx.report('onTaskUpdate', packs)
Expand Down

0 comments on commit 0c16d01

Please sign in to comment.