From ef2aaf1bb1c6fa272508b2f72c2b8cd6ed7c14a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Sun, 17 Jul 2022 11:17:25 +0000 Subject: [PATCH] refactor: create interface for coverage logic --- packages/vite-node/src/client.ts | 2 +- packages/vitest/src/defaults.ts | 5 +- .../vitest/src/integrations/coverage/base.ts | 14 ++ .../vitest/src/integrations/coverage/c8.ts | 196 ++++++++++-------- packages/vitest/src/node/cli-api.ts | 12 +- packages/vitest/src/node/config.ts | 3 - packages/vitest/src/node/core.ts | 19 +- packages/vitest/src/node/pool.ts | 5 +- packages/vitest/src/runtime/run.ts | 5 +- packages/vitest/src/types/config.ts | 6 +- packages/vitest/src/types/coverage.ts | 29 ++- packages/vitest/src/types/worker.ts | 1 + 12 files changed, 179 insertions(+), 118 deletions(-) create mode 100644 packages/vitest/src/integrations/coverage/base.ts diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index 6e03349b5ac6..ae11a210e9b2 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -199,7 +199,7 @@ export class ViteNodeRunner { // Be careful 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/c8.ts#L100 if you do change this + // adjust `offset` variable in packages/vitest/src/integrations/coverage/c8.ts#86 if you do change this const context = this.prepareContext({ // esm transformed by Vite __vite_ssr_import__: request, diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts index 706f1a0b5104..6874f76de84c 100644 --- a/packages/vitest/src/defaults.ts +++ b/packages/vitest/src/defaults.ts @@ -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}/**'] @@ -22,6 +22,7 @@ const defaultCoverageExcludes = [ ] const coverageConfigDefaults = { + provider: 'c8', enabled: false, clean: true, cleanOnRerun: false, @@ -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, diff --git a/packages/vitest/src/integrations/coverage/base.ts b/packages/vitest/src/integrations/coverage/base.ts new file mode 100644 index 000000000000..a380b741c64b --- /dev/null +++ b/packages/vitest/src/integrations/coverage/base.ts @@ -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 + + resolveOptions(): ResolvedCoverageOptions + clean(clean?: boolean): void | Promise + + onBeforeFilesRun?(): void | Promise + onAfterAllFilesRun(): void | Promise + onAfterSuiteRun(): void | Promise +} diff --git a/packages/vitest/src/integrations/coverage/c8.ts b/packages/vitest/src/integrations/coverage/c8.ts index bf17a410c4b2..b0b927366f87 100644 --- a/packages/vitest/src/integrations/coverage/c8.ts +++ b/packages/vitest/src/integrations/coverage/c8.ts @@ -4,112 +4,134 @@ 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 = {} - 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 = {} + 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 + await report.run() - report._getSourceMap = (coverage: Profiler.ScriptCoverage) => { - const path = _url.pathToFileURL(coverage.url).href - const data = sourceMapMeta[path] + const { checkCoverages } = require('c8/lib/commands/check-coverage') + await checkCoverages(this.options, report) + } +} - if (!data) - return {} +function resolveC8Options(options: C8Options, root: string) { + const resolved = { + ...configDefaults.coverage, + ...options as any, + } - return { - sourceMap: { - sourcemap: data.map, - }, - source: Array(offset).fill('.').join('') + data.source, - } + if (options['100']) { + resolved.lines = 100 + resolved.functions = 100 + resolved.branches = 100 + resolved.statements = 100 } - await report.run() + resolved.reporter = toArray(resolved.reporter) + resolved.reportsDirectory = resolve(root, resolved.reportsDirectory) + resolved.tempDirectory = process.env.NODE_V8_COVERAGE || resolve(resolved.reportsDirectory, 'tmp') - 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 - } + return resolved +} - const { checkCoverages } = require('c8/lib/commands/check-coverage') - await checkCoverages(ctx.config.coverage, report) - } +// 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() } diff --git a/packages/vitest/src/node/cli-api.ts b/packages/vitest/src/node/cli-api.ts index 25f84e1128cf..5527c99767c1 100644 --- a/packages/vitest/src/node/cli-api.ts +++ b/packages/vitest/src/node/cli-api.ts @@ -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 + } } } diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index ae243e4424b8..d3bf25b23037 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -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' @@ -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') diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 387404691e81..438ac8656466 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -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' @@ -30,6 +31,7 @@ export class Vitest { snapshot: SnapshotManager = undefined! cache: VitestCache = undefined! reporters: Reporter[] = undefined! + coverageReporter: BaseCoverageReporter = undefined! logger: Logger pool: WorkerPool | undefined @@ -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 { @@ -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') @@ -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') diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index d5c59b129b30..d14ea76b9cb9 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -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', @@ -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) diff --git a/packages/vitest/src/runtime/run.ts b/packages/vitest/src/runtime/run.ts index def1124e9e54..e610fd032dfe 100644 --- a/packages/vitest/src/runtime/run.ts +++ b/packages/vitest/src/runtime/run.ts @@ -2,7 +2,6 @@ import limit from 'p-limit' import type { File, HookCleanupCallback, HookListener, ResolvedConfig, Suite, SuiteHooks, Task, TaskResult, TaskState, Test } from '../types' import { vi } from '../integrations/vi' import { clearTimeout, getFullName, getWorkerState, hasFailed, hasTests, isBrowser, isNode, 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' @@ -316,7 +315,9 @@ async function startTestsNode(paths: string[], config: ResolvedConfig) { await runFiles(files, config) - takeCoverage() + // TODO: Not sure if this will work. Previously v8.takeCoverage() was called + // here inside the worker. Now, this will simply inform the main process to call it. + rpc().onFilesRun() await getSnapshotClient().saveCurrent() diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 169153ba79ec..1b73348a9106 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -3,7 +3,7 @@ import type { PrettyFormatOptions } from 'pretty-format' import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' import type { BuiltinReporters } from '../node/reporters' import type { TestSequencerConstructor } from '../node/sequencers/types' -import type { C8Options, ResolvedC8Options } from './coverage' +import type { CoverageOptions, ResolvedCoverageOptions } from './coverage' import type { JSDOMOptions } from './jsdom-options' import type { Reporter } from './reporter' import type { SnapshotStateOptions } from './snapshot' @@ -229,7 +229,7 @@ export interface InlineConfig { /** * Coverage options */ - coverage?: C8Options + coverage?: CoverageOptions /** * run test names with the specified pattern @@ -460,7 +460,7 @@ export interface ResolvedConfig extends Omit, 'config' | 'f testNamePattern?: RegExp related?: string[] - coverage: ResolvedC8Options + coverage: ResolvedCoverageOptions snapshotOptions: SnapshotStateOptions reporters: (Reporter | BuiltinReporters)[] diff --git a/packages/vitest/src/types/coverage.ts b/packages/vitest/src/types/coverage.ts index dd1c7652871f..342cf39c3ea9 100644 --- a/packages/vitest/src/types/coverage.ts +++ b/packages/vitest/src/types/coverage.ts @@ -15,29 +15,38 @@ export type CoverageReporter = | 'text-summary' | 'text' -export interface C8Options { +export type CoverageProviders = 'c8' + +export type CoverageOptions = C8Options & { provider?: 'c8' } + +interface BaseCoverageOptions { /** * Enable coverage, pass `--coverage` to enable * * @default false */ enabled?: boolean + + /** + * Clean coverage report on watch rerun + * + * @default false + */ + cleanOnRerun?: boolean + /** * Directory to write coverage report to */ reportsDirectory?: string +} + +export interface C8Options extends BaseCoverageOptions { /** * Clean coverage before running tests * * @default true */ clean?: boolean - /** - * Clean coverage report on watch rerun - * - * @default false - */ - cleanOnRerun?: boolean /** * Allow files from outside of your cwd. * @@ -71,6 +80,6 @@ export interface C8Options { statements?: number } -export interface ResolvedC8Options extends Required { - tempDirectory: string -} +export type ResolvedCoverageOptions = + & { tempDirectory: string } + & Required diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index 2e49a77c1866..68eff9c88bfb 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -28,6 +28,7 @@ export interface WorkerRPC { onUserConsoleLog: (log: UserConsoleLog) => void onUnhandledRejection: (err: unknown) => void onCollected: (files: File[]) => void + onFilesRun: () => void onTaskUpdate: (pack: TaskResultPack[]) => void snapshotSaved: (snapshot: SnapshotResult) => void