From 8a43ecf851450434e63a1a86d4831c30178c223c Mon Sep 17 00:00:00 2001 From: AriPerkkio Date: Fri, 20 Oct 2023 20:35:46 +0300 Subject: [PATCH] fix(coverage): use workspace project based source maps --- packages/browser/src/client/runner.ts | 10 +++- packages/coverage-istanbul/src/provider.ts | 30 +++++++---- packages/coverage-v8/src/provider.ts | 53 +++++++++++-------- packages/vitest/src/node/pools/child.ts | 1 + packages/vitest/src/node/pools/threads.ts | 1 + packages/vitest/src/node/pools/vm-threads.ts | 1 + packages/vitest/src/runtime/runners/index.ts | 7 ++- packages/vitest/src/types/rpc.ts | 1 + packages/vitest/src/types/worker.ts | 1 + .../__snapshots__/custom.report.test.ts.snap | 4 +- test/coverage-test/custom-provider.ts | 7 ++- 11 files changed, 78 insertions(+), 38 deletions(-) diff --git a/packages/browser/src/client/runner.ts b/packages/browser/src/client/runner.ts index 3ea8a84f52b2..faf74af717bc 100644 --- a/packages/browser/src/client/runner.ts +++ b/packages/browser/src/client/runner.ts @@ -42,8 +42,14 @@ export function createBrowserRunner(original: any, coverageModule: CoverageHandl async onAfterRunFiles() { await super.onAfterRun?.() const coverage = await coverageModule?.takeCoverage?.() - if (coverage) - await rpc().onAfterSuiteRun({ coverage, transformMode: 'web' }) + + if (coverage) { + await rpc().onAfterSuiteRun({ + coverage, + transformMode: 'web', + projectName: this.config.name, + }) + } } onCollected(files: File[]): unknown { diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index dd92d83bfceb..b0443dd6a4ee 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -16,6 +16,8 @@ import _TestExclude from 'test-exclude' import { COVERAGE_STORE_KEY } from './constants' type Options = ResolvedCoverageOptions<'istanbul'> +type CoverageByTransformMode = Record +type ProjectName = NonNullable | typeof DEFAULT_PROJECT interface TestExclude { new(opts: { @@ -31,6 +33,8 @@ interface TestExclude { } } +const DEFAULT_PROJECT = Symbol.for('default-project') + export class IstanbulCoverageProvider extends BaseCoverageProvider implements CoverageProvider { name = 'istanbul' @@ -45,7 +49,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co * If storing in memory causes issues, we can simply write these into fs in `onAfterSuiteRun` * and read them back when merging coverage objects in `onAfterAllFilesRun`. */ - coverages: Record = { ssr: [], web: [] } + coverages = new Map() initialize(ctx: Vitest) { const config: CoverageIstanbulOptions = ctx.config.coverage @@ -111,25 +115,34 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co * Note that adding new entries here and requiring on those without * backwards compatibility is a breaking change. */ - onAfterSuiteRun({ coverage, transformMode }: AfterSuiteRunMeta) { + onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta) { if (transformMode !== 'web' && transformMode !== 'ssr') throw new Error(`Invalid transform mode: ${transformMode}`) - this.coverages[transformMode].push(coverage as CoverageMapData) + let entry = this.coverages.get(projectName || DEFAULT_PROJECT) + + if (!entry) { + entry = { web: [], ssr: [] } + this.coverages.set(projectName || DEFAULT_PROJECT, entry) + } + + entry[transformMode].push(coverage as CoverageMapData) } async clean(clean = true) { if (clean && existsSync(this.options.reportsDirectory)) await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }) - this.coverages = { ssr: [], web: [] } + this.coverages = new Map() } async reportCoverage({ allTestsRun }: ReportContext = {}) { - const coverageMaps = await Promise.all([ - mergeAndTransformCoverage(this.coverages.ssr), - mergeAndTransformCoverage(this.coverages.web), - ]) + const coverageMaps = await Promise.all( + Array.from(this.coverages.values()).map(coverages => [ + mergeAndTransformCoverage(coverages.ssr), + mergeAndTransformCoverage(coverages.web), + ]).flat(), + ) if (this.options.all && allTestsRun) { const coveredFiles = coverageMaps.map(map => map.files()).flat() @@ -143,7 +156,6 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, - sourceFinder: libSourceMaps.createSourceMapStore().sourceFinder, watermarks: this.options.watermarks, }) diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index 539a88d55d76..172733093a1f 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -40,12 +40,15 @@ interface TestExclude { type Options = ResolvedCoverageOptions<'v8'> type TransformResults = Map type RawCoverage = Profiler.TakePreciseCoverageReturnType +type CoverageByTransformMode = Record +type ProjectName = NonNullable | typeof DEFAULT_PROJECT // TODO: vite-node should export this const WRAPPER_LENGTH = 185 // Note that this needs to match the line ending as well const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g +const DEFAULT_PROJECT = Symbol.for('default-project') export class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider { name = 'v8' @@ -53,7 +56,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage ctx!: Vitest options!: Options testExclude!: InstanceType - coverages: Record = { ssr: [], web: [] } + coverages = new Map() initialize(ctx: Vitest) { const config: CoverageV8Options = ctx.config.coverage @@ -93,7 +96,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage if (clean && existsSync(this.options.reportsDirectory)) await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }) - this.coverages = { ssr: [], web: [] } + this.coverages = new Map() } /* @@ -101,21 +104,30 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage * Note that adding new entries here and requiring on those without * backwards compatibility is a breaking change. */ - onAfterSuiteRun({ coverage, transformMode }: AfterSuiteRunMeta) { + onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta) { if (transformMode !== 'web' && transformMode !== 'ssr') throw new Error(`Invalid transform mode: ${transformMode}`) - this.coverages[transformMode].push(coverage as RawCoverage) + let entry = this.coverages.get(projectName || DEFAULT_PROJECT) + + if (!entry) { + entry = { web: [], ssr: [] } + this.coverages.set(projectName || DEFAULT_PROJECT, entry) + } + + entry[transformMode].push(coverage as RawCoverage) } async reportCoverage({ allTestsRun }: ReportContext = {}) { if (provider === 'stackblitz') this.ctx.logger.log(c.blue(' % ') + c.yellow('@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.')) - const coverageMaps = await Promise.all([ - this.mergeAndTransformCoverage(this.coverages.ssr, 'ssr'), - this.mergeAndTransformCoverage(this.coverages.web, 'web'), - ]) + const coverageMaps = await Promise.all( + Array.from(this.coverages.entries()).map(([projectName, coverages]) => [ + this.mergeAndTransformCoverage(coverages.ssr, projectName, 'ssr'), + this.mergeAndTransformCoverage(coverages.web, projectName, 'web'), + ]).flat(), + ) if (this.options.all && allTestsRun) { const coveredFiles = coverageMaps.map(map => map.files()).flat() @@ -130,7 +142,6 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, - sourceFinder: libSourceMaps.createSourceMapStore().sourceFinder, watermarks: this.options.watermarks, }) @@ -177,7 +188,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } private async getUntestedFiles(testedFiles: string[]): Promise { - const transformResults = normalizeTransformResults([this.ctx.vitenode.fetchCache]) + const transformResults = normalizeTransformResults(this.ctx.vitenode.fetchCache) const includedFiles = await this.testExclude.glob(this.ctx.config.root) const uncoveredFiles = includedFiles @@ -241,12 +252,10 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } } - private async mergeAndTransformCoverage(coverages: RawCoverage[], transformMode?: 'web' | 'ssr') { - const fetchCaches = transformMode - ? this.ctx.projects.map(project => project.vitenode.fetchCaches[transformMode]) - : [this.ctx.vitenode.fetchCache] - - const transformResults = normalizeTransformResults(fetchCaches) + private async mergeAndTransformCoverage(coverages: RawCoverage[], projectName?: ProjectName, transformMode?: 'web' | 'ssr') { + const viteNode = this.ctx.projects.find(project => project.getName() === projectName)?.vitenode || this.ctx.vitenode + const fetchCache = transformMode ? viteNode.fetchCaches[transformMode] : viteNode.fetchCache + const transformResults = normalizeTransformResults(fetchCache) const merged = mergeProcessCovs(coverages) const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url))) @@ -314,16 +323,14 @@ function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) { }, 0) } -function normalizeTransformResults(fetchCaches: Map[]) { +function normalizeTransformResults(fetchCache: Map) { const normalized: TransformResults = new Map() - for (const fetchCache of fetchCaches) { - for (const [key, value] of fetchCache.entries()) { - const cleanEntry = cleanUrl(key) + for (const [key, value] of fetchCache.entries()) { + const cleanEntry = cleanUrl(key) - if (!normalized.has(cleanEntry)) - normalized.set(cleanEntry, value.result) - } + if (!normalized.has(cleanEntry)) + normalized.set(cleanEntry, value.result) } return normalized diff --git a/packages/vitest/src/node/pools/child.ts b/packages/vitest/src/node/pools/child.ts index 3a341b6d9f22..6f6cc7351c2b 100644 --- a/packages/vitest/src/node/pools/child.ts +++ b/packages/vitest/src/node/pools/child.ts @@ -100,6 +100,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env, forksPath } invalidates, environment, workerId, + projectName: project.getName(), } try { await pool.run(data, { name, channel }) diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 60d9a85ad337..9e8023c8656a 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -88,6 +88,7 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env, workerPath }: Po invalidates, environment, workerId, + projectName: project.getName(), } try { await pool.run(data, { transferList: [workerPort], name }) diff --git a/packages/vitest/src/node/pools/vm-threads.ts b/packages/vitest/src/node/pools/vm-threads.ts index 32e63189c574..35a1c6ae5f7d 100644 --- a/packages/vitest/src/node/pools/vm-threads.ts +++ b/packages/vitest/src/node/pools/vm-threads.ts @@ -95,6 +95,7 @@ export function createVmThreadsPool(ctx: Vitest, { execArgv, env, vmPath }: Pool invalidates, environment, workerId, + projectName: project.getName(), } try { await pool.run(data, { transferList: [workerPort], name }) diff --git a/packages/vitest/src/runtime/runners/index.ts b/packages/vitest/src/runtime/runners/index.ts index 4110817d7915..16e1a6925f22 100644 --- a/packages/vitest/src/runtime/runners/index.ts +++ b/packages/vitest/src/runtime/runners/index.ts @@ -66,7 +66,12 @@ export async function resolveTestRunner(config: ResolvedConfig, executor: Vitest testRunner.onAfterRunFiles = async (files) => { const state = getWorkerState() const coverage = await takeCoverageInsideWorker(config.coverage, executor) - rpc().onAfterSuiteRun({ coverage, transformMode: state.environment.transformMode }) + rpc().onAfterSuiteRun({ + coverage, + transformMode: state.environment.transformMode, + projectName: state.ctx.projectName, + }) + await originalOnAfterRun?.call(testRunner, files) } diff --git a/packages/vitest/src/types/rpc.ts b/packages/vitest/src/types/rpc.ts index bd93be04c432..faad4cfe5107 100644 --- a/packages/vitest/src/types/rpc.ts +++ b/packages/vitest/src/types/rpc.ts @@ -47,6 +47,7 @@ export interface ResolvedTestEnvironment { export interface ContextRPC { config: ResolvedConfig + projectName: string files: string[] invalidates?: string[] environment: ContextTestEnvironment diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index 98a6068e45a6..23e88a5eb21f 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -17,6 +17,7 @@ export type ResolveIdFunction = (id: string, importer?: string) => Promise diff --git a/test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap b/test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap index 6b35b36d792a..1a271a091b87 100644 --- a/test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap +++ b/test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap @@ -10,8 +10,8 @@ exports[`custom json report 1`] = ` "reportCoverage with {"allTestsRun":true}", ], "coverageReports": [ - "{"coverage":{"customCoverage":"Coverage report passed from workers to main thread"},"transformMode":"ssr"}", - "{"coverage":{"customCoverage":"Coverage report passed from workers to main thread"},"transformMode":"web"}", + "{"coverage":{"customCoverage":"Coverage report passed from workers to main thread"},"transformMode":"ssr","projectName":true}", + "{"coverage":{"customCoverage":"Coverage report passed from workers to main thread"},"transformMode":"web","projectName":true}", ], "transformedFiles": [ "/src/Counter/Counter.component.ts", diff --git a/test/coverage-test/custom-provider.ts b/test/coverage-test/custom-provider.ts index 34bc48fce6c0..2a8f7d6e60a7 100644 --- a/test/coverage-test/custom-provider.ts +++ b/test/coverage-test/custom-provider.ts @@ -61,7 +61,12 @@ class CustomCoverageProvider implements CoverageProvider { this.calls.add('onAfterSuiteRun') // Keep coverage info separate from calls and ignore its order - this.coverageReports.add(JSON.stringify(meta)) + this.coverageReports.add(JSON.stringify({ + ...meta, + + // Project name keeps changing so let's simply check that its present + projectName: meta.projectName && typeof meta.projectName === 'string', + })) } reportCoverage(reportContext?: ReportContext) {