Skip to content

Commit

Permalink
refactor: create interface for coverage logic
Browse files Browse the repository at this point in the history
- Enables vitest-dev#1252
  • Loading branch information
AriPerkkio authored Jul 17, 2022
1 parent a9dbbfc commit 480666f
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 118 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 @@ -21,6 +21,7 @@ const defaultCoverageExcludes = [
]

const coverageConfigDefaults = {
provider: 'c8',
enabled: false,
clean: true,
cleanOnRerun: false,
Expand All @@ -32,7 +33,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
13 changes: 13 additions & 0 deletions packages/vitest/src/integrations/coverage/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Vitest } from '../../node'
import { ResolvedCoverageOptions } from '../../types';

export interface BaseCoverageReporter {
resolveOptions(): ResolvedCoverageOptions

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

clean(clean?: boolean): Promise<void> | void;

report(): Promise<void> | void;
}
195 changes: 106 additions & 89 deletions packages/vitest/src/integrations/coverage/c8.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,132 @@
import { existsSync, promises as fs } from 'fs'
import { createRequire } from 'module'
import { pathToFileURL } from 'url'
import type { Profiler } from 'inspector'
import { resolve } from 'pathe'
import type { Profiler } from 'inspector'
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'

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 })

if (!existsSync(options.tempDirectory))
await fs.mkdir(options.tempDirectory, { recursive: true })
}
import type { BaseCoverageReporter } from './base'
import type { C8Options, ResolvedCoverageOptions } from '../../types'
import type { Vitest } from '../../node'

const require = createRequire(import.meta.url)

// 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()
}
export class C8Reporter implements BaseCoverageReporter {
ctx!: Vitest;
options!: ResolvedCoverageOptions & { provider: "c8" }

export async function reportCoverage(ctx: Vitest) {
takeCoverage()
initialize(ctx: Vitest) {
this.ctx = ctx;
this.options = resolveC8Options(ctx.config.coverage, ctx.config.root)
}

const createReport = require('c8/lib/report')
const report = createReport(ctx.config.coverage)
resolveOptions() {
return this.options;
}

// 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
async clean(clean = true) {
if (clean && existsSync(this.options.reportsDirectory))
await fs.rm(this.options.reportsDirectory, { recursive: true, force: true })

const url = pathToFileURL(file).href
if (!existsSync(this.options.tempDirectory))
await fs.mkdir(this.options.tempDirectory, { recursive: true })
}

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 report() {
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 = 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 = 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 = 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 @@ -31,9 +31,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')) {
process.exitCode = 1
return false
const requiredPackages = ctx.config.coverage.provider === 'c8'
? ['c8']
: []

for (const pkg of requiredPackages) {
if (!await ensurePackageInstalled(pkg)) {
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 @@ -31,6 +32,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 @@ -84,12 +86,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 @@ -139,7 +146,7 @@ export class Vitest {
await this.runFiles(files)

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

if (this.config.watch)
await this.report('onWatcherStart')
Expand Down Expand Up @@ -320,14 +327,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.report()

await this.report('onWatcherStart')
}, WATCHER_DEBOUNCE)
Expand Down
3 changes: 1 addition & 2 deletions packages/vitest/src/runtime/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { File, HookCleanupCallback, HookListener, ResolvedConfig, Suite, Su
import { vi } from '../integrations/vi'
import { getSnapshotClient } from '../integrations/snapshot/chai'
import { clearTimeout, getFullName, getWorkerState, hasFailed, hasTests, partitionSuiteChildren, setTimeout, shuffle } from '../utils'
import { takeCoverage } from '../integrations/coverage/c8'
import { getState, setState } from '../integrations/chai/jest-expect'
import { GLOBAL_EXPECT } from '../integrations/chai/constants'
import { getFn, getHooks } from './map'
Expand Down Expand Up @@ -288,7 +287,7 @@ export async function startTests(paths: string[], config: ResolvedConfig) {

await runFiles(files, config)

takeCoverage()
// TODO: Why was v8.takeCoverage() called here?

await getSnapshotClient().saveCurrent()

Expand Down
Loading

0 comments on commit 480666f

Please sign in to comment.