From 480666fbf1257422992cec190cccc627ef02f624 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 - Enables vitest-dev/vitest#1252 --- packages/vitest/src/defaults.ts | 5 +- .../vitest/src/integrations/coverage/base.ts | 13 ++ .../vitest/src/integrations/coverage/c8.ts | 195 ++++++++++-------- 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/runtime/run.ts | 3 +- packages/vitest/src/types/config.ts | 6 +- packages/vitest/src/types/coverage.ts | 29 ++- 9 files changed, 167 insertions(+), 118 deletions(-) create mode 100644 packages/vitest/src/integrations/coverage/base.ts diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts index 94688fc69788e..0169a5bf56749 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}/**'] @@ -21,6 +21,7 @@ const defaultCoverageExcludes = [ ] const coverageConfigDefaults = { + provider: 'c8', enabled: false, clean: true, cleanOnRerun: false, @@ -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, diff --git a/packages/vitest/src/integrations/coverage/base.ts b/packages/vitest/src/integrations/coverage/base.ts new file mode 100644 index 0000000000000..7b344813de330 --- /dev/null +++ b/packages/vitest/src/integrations/coverage/base.ts @@ -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; + + clean(clean?: boolean): Promise | void; + + report(): Promise | void; +} \ No newline at end of file diff --git a/packages/vitest/src/integrations/coverage/c8.ts b/packages/vitest/src/integrations/coverage/c8.ts index 6a59bdf311e17..77230bc3d8ba2 100644 --- a/packages/vitest/src/integrations/coverage/c8.ts +++ b/packages/vitest/src/integrations/coverage/c8.ts @@ -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 = {} - 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 = {} + 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() +} \ No newline at end of file diff --git a/packages/vitest/src/node/cli-api.ts b/packages/vitest/src/node/cli-api.ts index 29ba2b5d17764..decad25973715 100644 --- a/packages/vitest/src/node/cli-api.ts +++ b/packages/vitest/src/node/cli-api.ts @@ -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 + } } } diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index b2b26aff3773a..00cbd2003ec66 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 176745194c01b..85e8be7dea399 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' @@ -31,6 +32,7 @@ export class Vitest { snapshot: SnapshotManager = undefined! cache: VitestCache = undefined! reporters: Reporter[] = undefined! + coverageReporter: BaseCoverageReporter = undefined! logger: Logger pool: WorkerPool | undefined @@ -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 { @@ -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') @@ -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) diff --git a/packages/vitest/src/runtime/run.ts b/packages/vitest/src/runtime/run.ts index f5b30143a429e..63cc80abdf01e 100644 --- a/packages/vitest/src/runtime/run.ts +++ b/packages/vitest/src/runtime/run.ts @@ -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' @@ -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() diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 446de846febda..8ac1626e9fdef 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 @@ -447,7 +447,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 dd1c7652871f2..342cf39c3ea96 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