From e39e1bbc0f10e06bb7f9e7acc136ca486afb2860 Mon Sep 17 00:00:00 2001 From: AriPerkkio Date: Sun, 15 Oct 2023 11:34:03 +0300 Subject: [PATCH] fix(coverage): use `transformMode` based source maps --- packages/browser/src/client/runner.ts | 2 +- packages/coverage-istanbul/src/provider.ts | 64 +- packages/coverage-v8/src/provider.ts | 100 ++- packages/vitest/src/runtime/runners/index.ts | 3 +- packages/vitest/src/types/worker.ts | 1 + pnpm-lock.yaml | 3 + .../__snapshots__/custom.report.test.ts.snap | 7 +- .../istanbul.report.test.ts.snap | 461 ++++++++++++ .../__snapshots__/v8.report.test.ts.snap | 701 ++++++++++++++++++ .../generic.report.test.ts | 22 + test/coverage-test/custom-provider.ts | 8 +- test/coverage-test/package.json | 1 + test/coverage-test/src/multi-environment.ts | 31 + test/coverage-test/test/ssr.test.ts | 9 + test/coverage-test/test/web.test.ts | 9 + test/coverage-test/vitest.config.ts | 29 + .../check-coverage.test.ts | 3 +- test/workspaces/vitest.config.ts | 1 + 18 files changed, 1396 insertions(+), 59 deletions(-) create mode 100644 test/coverage-test/src/multi-environment.ts create mode 100644 test/coverage-test/test/ssr.test.ts create mode 100644 test/coverage-test/test/web.test.ts diff --git a/packages/browser/src/client/runner.ts b/packages/browser/src/client/runner.ts index 0829e951c0f8..3ea8a84f52b2 100644 --- a/packages/browser/src/client/runner.ts +++ b/packages/browser/src/client/runner.ts @@ -43,7 +43,7 @@ export function createBrowserRunner(original: any, coverageModule: CoverageHandl await super.onAfterRun?.() const coverage = await coverageModule?.takeCoverage?.() if (coverage) - await rpc().onAfterSuiteRun({ coverage }) + await rpc().onAfterSuiteRun({ coverage, transformMode: 'web' }) } onCollected(files: File[]): unknown { diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 52013e0820e4..dd92d83bfceb 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -6,7 +6,7 @@ import { BaseCoverageProvider } from 'vitest/coverage' import c from 'picocolors' import libReport from 'istanbul-lib-report' import reports from 'istanbul-reports' -import type { CoverageMap } from 'istanbul-lib-coverage' +import type { CoverageMap, CoverageMapData } from 'istanbul-lib-coverage' import libCoverage from 'istanbul-lib-coverage' import libSourceMaps from 'istanbul-lib-source-maps' import { type Instrumenter, createInstrumenter } from 'istanbul-lib-instrument' @@ -45,7 +45,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: any[] = [] + coverages: Record = { ssr: [], web: [] } initialize(ctx: Vitest) { const config: CoverageIstanbulOptions = ctx.config.coverage @@ -106,36 +106,44 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co return { code, map } } - onAfterSuiteRun({ coverage }: AfterSuiteRunMeta) { - this.coverages.push(coverage) + /* + * Coverage and meta information passed from Vitest runners. + * Note that adding new entries here and requiring on those without + * backwards compatibility is a breaking change. + */ + onAfterSuiteRun({ coverage, transformMode }: AfterSuiteRunMeta) { + if (transformMode !== 'web' && transformMode !== 'ssr') + throw new Error(`Invalid transform mode: ${transformMode}`) + + this.coverages[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 = [] + this.coverages = { ssr: [], web: [] } } async reportCoverage({ allTestsRun }: ReportContext = {}) { - const mergedCoverage: CoverageMap = this.coverages.reduce((coverage, previousCoverageMap) => { - const map = libCoverage.createCoverageMap(coverage) - map.merge(previousCoverageMap) - return map - }, libCoverage.createCoverageMap({})) + const coverageMaps = await Promise.all([ + mergeAndTransformCoverage(this.coverages.ssr), + mergeAndTransformCoverage(this.coverages.web), + ]) - if (this.options.all && allTestsRun) - await this.includeUntestedFiles(mergedCoverage) + if (this.options.all && allTestsRun) { + const coveredFiles = coverageMaps.map(map => map.files()).flat() + const uncoveredCoverage = await this.getCoverageMapForUncoveredFiles(coveredFiles) - includeImplicitElseBranches(mergedCoverage) + coverageMaps.push(await mergeAndTransformCoverage([uncoveredCoverage])) + } - const sourceMapStore = libSourceMaps.createSourceMapStore() - const coverageMap: CoverageMap = await sourceMapStore.transformCoverage(mergedCoverage) + const coverageMap = mergeCoverageMaps(...coverageMaps) const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, - sourceFinder: sourceMapStore.sourceFinder, + sourceFinder: libSourceMaps.createSourceMapStore().sourceFinder, watermarks: this.options.watermarks, }) @@ -181,19 +189,21 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co } } - async includeUntestedFiles(coverageMap: CoverageMap) { + async getCoverageMapForUncoveredFiles(coveredFiles: string[]) { // Load, instrument and collect empty coverages from all files which // are not already in the coverage map const includedFiles = await this.testExclude.glob(this.ctx.config.root) const uncoveredFiles = includedFiles .map(file => resolve(this.ctx.config.root, file)) - .filter(file => !coverageMap.data[file]) + .filter(file => !coveredFiles.includes(file)) const transformResults = await Promise.all(uncoveredFiles.map(async (filename) => { const transformResult = await this.ctx.vitenode.transformRequest(filename) return { transformResult, filename } })) + const coverageMap = libCoverage.createCoverageMap({}) + for (const { transformResult, filename } of transformResults) { const sourceMap = transformResult?.map @@ -209,9 +219,27 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co coverageMap.addFileCoverage(lastCoverage) } } + + return coverageMap.data } } +async function mergeAndTransformCoverage(coverages: CoverageMapData[]) { + const mergedCoverage = mergeCoverageMaps(...coverages) + includeImplicitElseBranches(mergedCoverage) + + const sourceMapStore = libSourceMaps.createSourceMapStore() + return await sourceMapStore.transformCoverage(mergedCoverage) +} + +function mergeCoverageMaps(...coverageMaps: (CoverageMap | CoverageMapData)[]) { + return coverageMaps.reduce((coverage, previousCoverageMap) => { + const map = libCoverage.createCoverageMap(coverage) + map.merge(previousCoverageMap) + return map + }, libCoverage.createCoverageMap({})) +} + /** * Remove possible query parameters from filenames * - From `/src/components/Header.component.ts?vue&type=script&src=true&lang.ts` diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index bb22a7958ba2..6a0421fa6f98 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -5,7 +5,7 @@ import v8ToIstanbul from 'v8-to-istanbul' import { mergeProcessCovs } from '@bcoe/v8-coverage' import libReport from 'istanbul-lib-report' import reports from 'istanbul-reports' -import type { CoverageMap } from 'istanbul-lib-coverage' +import type { CoverageMap, CoverageMapData } from 'istanbul-lib-coverage' import libCoverage from 'istanbul-lib-coverage' import libSourceMaps from 'istanbul-lib-source-maps' import MagicString from 'magic-string' @@ -39,6 +39,7 @@ interface TestExclude { type Options = ResolvedCoverageOptions<'v8'> type TransformResults = Map +type RawCoverage = Profiler.TakePreciseCoverageReturnType // TODO: vite-node should export this const WRAPPER_LENGTH = 185 @@ -52,7 +53,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage ctx!: Vitest options!: Options testExclude!: InstanceType - coverages: Profiler.TakePreciseCoverageReturnType[] = [] + coverages: Record = { ssr: [], web: [] } initialize(ctx: Vitest) { const config: CoverageV8Options = ctx.config.coverage @@ -92,54 +93,44 @@ 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 = [] + this.coverages = { ssr: [], web: [] } } - onAfterSuiteRun({ coverage }: AfterSuiteRunMeta) { - this.coverages.push(coverage as Profiler.TakePreciseCoverageReturnType) + /* + * Coverage and meta information passed from Vitest runners. + * Note that adding new entries here and requiring on those without + * backwards compatibility is a breaking change. + */ + onAfterSuiteRun({ coverage, transformMode }: AfterSuiteRunMeta) { + if (transformMode !== 'web' && transformMode !== 'ssr') + throw new Error(`Invalid transform mode: ${transformMode}`) + + this.coverages[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 transformResults = normalizeTransformResults(this.ctx.projects.map(project => project.vitenode.fetchCache)) - const merged = mergeProcessCovs(this.coverages) - const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url))) + const coverageMaps = await Promise.all([ + this.mergeAndTransformCoverage(this.coverages.ssr, 'ssr'), + this.mergeAndTransformCoverage(this.coverages.web, 'web'), + ]) if (this.options.all && allTestsRun) { - const coveredFiles = Array.from(scriptCoverages.map(r => r.url)) - const untestedFiles = await this.getUntestedFiles(coveredFiles, transformResults) + const coveredFiles = coverageMaps.map(map => map.files()).flat() + const untestedCoverage = await this.getUntestedFiles(coveredFiles) + const untestedCoverageResults = untestedCoverage.map(files => ({ result: [files] })) - scriptCoverages.push(...untestedFiles) + coverageMaps.push(await this.mergeAndTransformCoverage(untestedCoverageResults)) } - const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => { - const sources = await this.getSources(url, transformResults, functions) - - // If no source map was found from vite-node we can assume this file was not run in the wrapper - const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0 - - const converter = v8ToIstanbul(url, wrapperLength, sources) - await converter.load() - - converter.applyCoverage(functions) - return converter.toIstanbul() - })) - - const mergedCoverage = converted.reduce((coverage, previousCoverageMap) => { - const map = libCoverage.createCoverageMap(coverage) - map.merge(previousCoverageMap) - return map - }, libCoverage.createCoverageMap({})) - - const sourceMapStore = libSourceMaps.createSourceMapStore() - const coverageMap: CoverageMap = await sourceMapStore.transformCoverage(mergedCoverage) + const coverageMap = mergeCoverageMaps(...coverageMaps) const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, - sourceFinder: sourceMapStore.sourceFinder, + sourceFinder: libSourceMaps.createSourceMapStore().sourceFinder, watermarks: this.options.watermarks, }) @@ -185,11 +176,13 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } } - private async getUntestedFiles(testedFiles: string[], transformResults: TransformResults): Promise { + private async getUntestedFiles(testedFiles: string[]): Promise { + const transformResults = normalizeTransformResults([this.ctx.vitenode.fetchCache]) + const includedFiles = await this.testExclude.glob(this.ctx.config.root) const uncoveredFiles = includedFiles .map(file => pathToFileURL(resolve(this.ctx.config.root, file))) - .filter(file => !testedFiles.includes(file.href)) + .filter(file => !testedFiles.includes(file.pathname)) return await Promise.all(uncoveredFiles.map(async (uncoveredFile) => { const { source } = await this.getSources(uncoveredFile.href, transformResults) @@ -247,6 +240,43 @@ 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) + + const merged = mergeProcessCovs(coverages) + const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url))) + + const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => { + const sources = await this.getSources(url, transformResults, functions) + + // If no source map was found from vite-node we can assume this file was not run in the wrapper + const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0 + + const converter = v8ToIstanbul(url, wrapperLength, sources) + await converter.load() + + converter.applyCoverage(functions) + return converter.toIstanbul() + })) + + const mergedCoverage = mergeCoverageMaps(...converted) + + const sourceMapStore = libSourceMaps.createSourceMapStore() + return sourceMapStore.transformCoverage(mergedCoverage) + } +} + +function mergeCoverageMaps(...coverageMaps: (CoverageMap | CoverageMapData)[]) { + return coverageMaps.reduce((coverage, previousCoverageMap) => { + const map = libCoverage.createCoverageMap(coverage) + map.merge(previousCoverageMap) + return map + }, libCoverage.createCoverageMap({})) } /** diff --git a/packages/vitest/src/runtime/runners/index.ts b/packages/vitest/src/runtime/runners/index.ts index a9ed72112d3d..4110817d7915 100644 --- a/packages/vitest/src/runtime/runners/index.ts +++ b/packages/vitest/src/runtime/runners/index.ts @@ -64,8 +64,9 @@ export async function resolveTestRunner(config: ResolvedConfig, executor: Vitest const originalOnAfterRun = testRunner.onAfterRunFiles testRunner.onAfterRunFiles = async (files) => { + const state = getWorkerState() const coverage = await takeCoverageInsideWorker(config.coverage, executor) - rpc().onAfterSuiteRun({ coverage }) + rpc().onAfterSuiteRun({ coverage, transformMode: state.environment.transformMode }) await originalOnAfterRun?.call(testRunner, files) } diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index afd77d668ed1..98a6068e45a6 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -16,6 +16,7 @@ export type ResolveIdFunction = (id: string, importer?: string) => Promise diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a34a79b86d58..dfff4de34fc9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1652,6 +1652,9 @@ importers: test/coverage-test: devDependencies: + '@ampproject/remapping': + specifier: ^2.2.1 + version: 2.2.1 '@types/istanbul-lib-coverage': specifier: ^2.0.4 version: 2.0.4 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 ed9e39ee813f..6b35b36d792a 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 @@ -6,9 +6,13 @@ exports[`custom json report 1`] = ` "initialized with context", "resolveOptions", "clean with force", - "onAfterSuiteRun with {"coverage":{"customCoverage":"Coverage report passed from workers to main thread"}}", + "onAfterSuiteRun", "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"}", + ], "transformedFiles": [ "/src/Counter/Counter.component.ts", "/src/Counter/Counter.vue", @@ -22,6 +26,7 @@ exports[`custom json report 1`] = ` "/src/implicitElse.ts", "/src/importEnv.ts", "/src/index.mts", + "/src/multi-environment.ts", "/src/multi-suite.ts", "/src/utils.ts", ], diff --git a/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap b/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap index 9af54d6026c4..852af788a740 100644 --- a/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap +++ b/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap @@ -1342,6 +1342,467 @@ exports[`istanbul json report 1`] = ` }, }, }, + "/src/multi-environment.ts": { + "b": { + "0": [ + 0, + 4, + ], + "1": [ + 4, + 0, + ], + "2": [ + 1, + 3, + ], + "3": [ + 4, + 1, + ], + "4": [ + 0, + 3, + ], + "5": [ + 3, + 0, + ], + "6": [ + 1, + 2, + ], + "7": [ + 3, + 1, + ], + }, + "branchMap": { + "0": { + "loc": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 11, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 11, + }, + }, + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 14, + }, + }, + ], + "type": "if", + }, + "1": { + "loc": { + "end": { + "column": 26, + "line": 11, + }, + "start": { + "column": 6, + "line": 11, + }, + }, + "locations": [ + { + "end": { + "column": 17, + "line": 11, + }, + "start": { + "column": 6, + "line": 11, + }, + }, + { + "end": { + "column": 26, + "line": 11, + }, + "start": { + "column": 17, + "line": 11, + }, + }, + ], + "type": "binary-expr", + }, + "2": { + "loc": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 14, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 14, + }, + }, + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 19, + }, + }, + ], + "type": "if", + }, + "3": { + "loc": { + "end": { + "column": 31, + "line": 16, + }, + "start": { + "column": 11, + "line": 16, + }, + }, + "locations": [ + { + "end": { + "column": 22, + "line": 16, + }, + "start": { + "column": 11, + "line": 16, + }, + }, + { + "end": { + "column": 31, + "line": 16, + }, + "start": { + "column": 22, + "line": 16, + }, + }, + ], + "type": "binary-expr", + }, + "4": { + "loc": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 19, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 19, + }, + }, + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + ], + "type": "if", + }, + "5": { + "loc": { + "end": { + "column": 33, + "line": 20, + }, + "start": { + "column": 11, + "line": 20, + }, + }, + "locations": [ + { + "end": { + "column": 23, + "line": 20, + }, + "start": { + "column": 11, + "line": 20, + }, + }, + { + "end": { + "column": 33, + "line": 20, + }, + "start": { + "column": 23, + "line": 20, + }, + }, + ], + "type": "binary-expr", + }, + "6": { + "loc": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + ], + "type": "if", + }, + "7": { + "loc": { + "end": { + "column": 33, + "line": 24, + }, + "start": { + "column": 11, + "line": 24, + }, + }, + "locations": [ + { + "end": { + "column": 23, + "line": 24, + }, + "start": { + "column": 11, + "line": 24, + }, + }, + { + "end": { + "column": 33, + "line": 24, + }, + "start": { + "column": 23, + "line": 24, + }, + }, + ], + "type": "binary-expr", + }, + }, + "f": { + "0": 4, + }, + "fnMap": { + "0": { + "decl": { + "end": { + "column": 20, + "line": 6, + }, + "start": { + "column": 16, + "line": 6, + }, + }, + "loc": { + "end": { + "column": null, + "line": 31, + }, + "start": { + "column": 42, + "line": 6, + }, + }, + "name": "sum", + }, + }, + "path": "/src/multi-environment.ts", + "s": { + "0": 2, + "1": 4, + "2": 0, + "3": 4, + "4": 1, + "5": 3, + "6": 0, + "7": 3, + "8": 1, + "9": 2, + }, + "statementMap": { + "0": { + "end": { + "column": null, + "line": 4, + }, + "start": { + "column": 23, + "line": 4, + }, + }, + "1": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 11, + }, + }, + "2": { + "end": { + "column": null, + "line": 13, + }, + "start": { + "column": 4, + "line": 13, + }, + }, + "3": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 14, + }, + }, + "4": { + "end": { + "column": null, + "line": 18, + }, + "start": { + "column": 4, + "line": 18, + }, + }, + "5": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 19, + }, + }, + "6": { + "end": { + "column": null, + "line": 22, + }, + "start": { + "column": 4, + "line": 22, + }, + }, + "7": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + "8": { + "end": { + "column": null, + "line": 26, + }, + "start": { + "column": 4, + "line": 26, + }, + }, + "9": { + "end": { + "column": null, + "line": 30, + }, + "start": { + "column": 2, + "line": 30, + }, + }, + }, + }, "/src/multi-suite.ts": { "b": {}, "branchMap": {}, diff --git a/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap b/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap index 08c1cc21d00e..402a1ab0d14f 100644 --- a/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap +++ b/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap @@ -3305,6 +3305,707 @@ exports[`v8 json report 1`] = ` }, }, }, + "/src/multi-environment.ts": { + "all": false, + "b": { + "0": [ + 4, + ], + "1": [ + 0, + ], + "10": [ + 1, + ], + "2": [ + 0, + ], + "3": [ + 1, + ], + "4": [ + 1, + ], + "5": [ + 0, + ], + "6": [ + 0, + ], + "7": [ + 1, + ], + "8": [ + 0, + ], + "9": [ + 0, + ], + }, + "branchMap": { + "0": { + "line": 6, + "loc": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 7, + "line": 6, + }, + }, + "locations": [ + { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 7, + "line": 6, + }, + }, + ], + "type": "branch", + }, + "1": { + "line": 11, + "loc": { + "end": { + "column": 26, + "line": 11, + }, + "start": { + "column": 12, + "line": 11, + }, + }, + "locations": [ + { + "end": { + "column": 26, + "line": 11, + }, + "start": { + "column": 12, + "line": 11, + }, + }, + ], + "type": "branch", + }, + "10": { + "line": 24, + "loc": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 33, + "line": 24, + }, + }, + "locations": [ + { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 33, + "line": 24, + }, + }, + ], + "type": "branch", + }, + "2": { + "line": 11, + "loc": { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 26, + "line": 11, + }, + }, + "locations": [ + { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 26, + "line": 11, + }, + }, + ], + "type": "branch", + }, + "3": { + "line": 16, + "loc": { + "end": { + "column": 31, + "line": 16, + }, + "start": { + "column": 17, + "line": 16, + }, + }, + "locations": [ + { + "end": { + "column": 31, + "line": 16, + }, + "start": { + "column": 17, + "line": 16, + }, + }, + ], + "type": "branch", + }, + "4": { + "line": 16, + "loc": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 31, + "line": 16, + }, + }, + "locations": [ + { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 31, + "line": 16, + }, + }, + ], + "type": "branch", + }, + "5": { + "line": 20, + "loc": { + "end": { + "column": 33, + "line": 20, + }, + "start": { + "column": 17, + "line": 20, + }, + }, + "locations": [ + { + "end": { + "column": 33, + "line": 20, + }, + "start": { + "column": 17, + "line": 20, + }, + }, + ], + "type": "branch", + }, + "6": { + "line": 20, + "loc": { + "end": { + "column": 3, + "line": 23, + }, + "start": { + "column": 33, + "line": 20, + }, + }, + "locations": [ + { + "end": { + "column": 3, + "line": 23, + }, + "start": { + "column": 33, + "line": 20, + }, + }, + ], + "type": "branch", + }, + "7": { + "line": 24, + "loc": { + "end": { + "column": 33, + "line": 24, + }, + "start": { + "column": 17, + "line": 24, + }, + }, + "locations": [ + { + "end": { + "column": 33, + "line": 24, + }, + "start": { + "column": 17, + "line": 24, + }, + }, + ], + "type": "branch", + }, + "8": { + "line": 24, + "loc": { + "end": { + "column": 3, + "line": 27, + }, + "start": { + "column": 33, + "line": 24, + }, + }, + "locations": [ + { + "end": { + "column": 3, + "line": 27, + }, + "start": { + "column": 33, + "line": 24, + }, + }, + ], + "type": "branch", + }, + "9": { + "line": 16, + "loc": { + "end": { + "column": 3, + "line": 19, + }, + "start": { + "column": 31, + "line": 16, + }, + }, + "locations": [ + { + "end": { + "column": 3, + "line": 19, + }, + "start": { + "column": 31, + "line": 16, + }, + }, + ], + "type": "branch", + }, + }, + "f": { + "0": 4, + }, + "fnMap": { + "0": { + "decl": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 7, + "line": 6, + }, + }, + "line": 6, + "loc": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 7, + "line": 6, + }, + }, + "name": "sum", + }, + }, + "path": "/src/multi-environment.ts", + "s": { + "0": 2, + "1": 2, + "10": 4, + "11": 0, + "12": 0, + "13": 0, + "14": 4, + "15": 4, + "16": 1, + "17": 1, + "18": 1, + "19": 3, + "2": 2, + "20": 0, + "21": 0, + "22": 0, + "23": 3, + "24": 1, + "25": 1, + "26": 1, + "27": 2, + "28": 2, + "29": 2, + "3": 2, + "30": 2, + "4": 2, + "5": 2, + "6": 4, + "7": 4, + "8": 4, + "9": 4, + }, + "statementMap": { + "0": { + "end": { + "column": 3, + "line": 1, + }, + "start": { + "column": 0, + "line": 1, + }, + }, + "1": { + "end": { + "column": 55, + "line": 2, + }, + "start": { + "column": 0, + "line": 2, + }, + }, + "10": { + "end": { + "column": 27, + "line": 11, + }, + "start": { + "column": 0, + "line": 11, + }, + }, + "11": { + "end": { + "column": 38, + "line": 12, + }, + "start": { + "column": 0, + "line": 12, + }, + }, + "12": { + "end": { + "column": 13, + "line": 13, + }, + "start": { + "column": 0, + "line": 13, + }, + }, + "13": { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 0, + "line": 14, + }, + }, + "14": { + "end": { + "column": 12, + "line": 15, + }, + "start": { + "column": 0, + "line": 15, + }, + }, + "15": { + "end": { + "column": 32, + "line": 16, + }, + "start": { + "column": 0, + "line": 16, + }, + }, + "16": { + "end": { + "column": 34, + "line": 17, + }, + "start": { + "column": 0, + "line": 17, + }, + }, + "17": { + "end": { + "column": 12, + "line": 18, + }, + "start": { + "column": 0, + "line": 18, + }, + }, + "18": { + "end": { + "column": 3, + "line": 19, + }, + "start": { + "column": 0, + "line": 19, + }, + }, + "19": { + "end": { + "column": 34, + "line": 20, + }, + "start": { + "column": 0, + "line": 20, + }, + }, + "2": { + "end": { + "column": 3, + "line": 3, + }, + "start": { + "column": 0, + "line": 3, + }, + }, + "20": { + "end": { + "column": 38, + "line": 21, + }, + "start": { + "column": 0, + "line": 21, + }, + }, + "21": { + "end": { + "column": 13, + "line": 22, + }, + "start": { + "column": 0, + "line": 22, + }, + }, + "22": { + "end": { + "column": 3, + "line": 23, + }, + "start": { + "column": 0, + "line": 23, + }, + }, + "23": { + "end": { + "column": 34, + "line": 24, + }, + "start": { + "column": 0, + "line": 24, + }, + }, + "24": { + "end": { + "column": 34, + "line": 25, + }, + "start": { + "column": 0, + "line": 25, + }, + }, + "25": { + "end": { + "column": 13, + "line": 26, + }, + "start": { + "column": 0, + "line": 26, + }, + }, + "26": { + "end": { + "column": 3, + "line": 27, + }, + "start": { + "column": 0, + "line": 27, + }, + }, + "27": { + "end": { + "column": 0, + "line": 28, + }, + "start": { + "column": 0, + "line": 28, + }, + }, + "28": { + "end": { + "column": 61, + "line": 29, + }, + "start": { + "column": 0, + "line": 29, + }, + }, + "29": { + "end": { + "column": 14, + "line": 30, + }, + "start": { + "column": 0, + "line": 30, + }, + }, + "3": { + "end": { + "column": 40, + "line": 4, + }, + "start": { + "column": 0, + "line": 4, + }, + }, + "30": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 0, + "line": 31, + }, + }, + "4": { + "end": { + "column": 0, + "line": 5, + }, + "start": { + "column": 0, + "line": 5, + }, + }, + "5": { + "end": { + "column": 43, + "line": 6, + }, + "start": { + "column": 0, + "line": 6, + }, + }, + "6": { + "end": { + "column": 4, + "line": 7, + }, + "start": { + "column": 0, + "line": 7, + }, + }, + "7": { + "end": { + "column": 64, + "line": 8, + }, + "start": { + "column": 0, + "line": 8, + }, + }, + "8": { + "end": { + "column": 35, + "line": 9, + }, + "start": { + "column": 0, + "line": 9, + }, + }, + "9": { + "end": { + "column": 5, + "line": 10, + }, + "start": { + "column": 0, + "line": 10, + }, + }, + }, + }, "/src/multi-suite.ts": { "all": false, "b": { diff --git a/test/coverage-test/coverage-report-tests/generic.report.test.ts b/test/coverage-test/coverage-report-tests/generic.report.test.ts index 350c23c3677e..fd2102756671 100644 --- a/test/coverage-test/coverage-report-tests/generic.report.test.ts +++ b/test/coverage-test/coverage-report-tests/generic.report.test.ts @@ -117,3 +117,25 @@ test('virtual files should be excluded', () => { expect(file).not.toContain('\x00') } }) + +test('multi environment coverage is merged correctly', async () => { + const coverageJson = await readCoverageJson() + const coverageMap = libCoverage.createCoverageMap(coverageJson as any) + const fileCoverage = coverageMap.fileCoverageFor('/src/multi-environment.ts') + const lineCoverage = fileCoverage.getLineCoverage() + + // Condition not covered by any test + expect(lineCoverage[13]).toBe(0) + + // Condition covered by SSR test but not by Web + expect(lineCoverage[18]).toBe(1) + + // Condition not covered by any test + expect(lineCoverage[22]).toBe(0) + + // Condition covered by Web test but not by SSR + expect(lineCoverage[26]).toBe(1) + + // Condition covered by both tests + expect(lineCoverage[30]).toBe(2) +}) diff --git a/test/coverage-test/custom-provider.ts b/test/coverage-test/custom-provider.ts index eae1bbcbe4ad..34bc48fce6c0 100644 --- a/test/coverage-test/custom-provider.ts +++ b/test/coverage-test/custom-provider.ts @@ -43,6 +43,7 @@ class CustomCoverageProvider implements CoverageProvider { options!: ResolvedCoverageOptions calls: Set = new Set() + coverageReports: Set = new Set() transformedFiles: Set = new Set() initialize(ctx: Vitest) { @@ -56,7 +57,11 @@ class CustomCoverageProvider implements CoverageProvider { } onAfterSuiteRun(meta: AfterSuiteRunMeta) { - this.calls.add(`onAfterSuiteRun with ${JSON.stringify(meta)}`) + // Do not include coverage info here, as order of tests is not guaranteed + this.calls.add('onAfterSuiteRun') + + // Keep coverage info separate from calls and ignore its order + this.coverageReports.add(JSON.stringify(meta)) } reportCoverage(reportContext?: ReportContext) { @@ -64,6 +69,7 @@ class CustomCoverageProvider implements CoverageProvider { const jsonReport = JSON.stringify({ calls: Array.from(this.calls.values()), + coverageReports: Array.from(this.coverageReports.values()).sort(), transformedFiles: Array.from(this.transformedFiles.values()).sort(), }, null, 2) diff --git a/test/coverage-test/package.json b/test/coverage-test/package.json index a2ab46d6e726..2a74f2524ee9 100644 --- a/test/coverage-test/package.json +++ b/test/coverage-test/package.json @@ -11,6 +11,7 @@ "test:types": "vitest --typecheck.only --run --reporter verbose" }, "devDependencies": { + "@ampproject/remapping": "^2.2.1", "@types/istanbul-lib-coverage": "^2.0.4", "@vitejs/plugin-vue": "latest", "@vitest/browser": "workspace:*", diff --git a/test/coverage-test/src/multi-environment.ts b/test/coverage-test/src/multi-environment.ts new file mode 100644 index 000000000000..e151f08d74cc --- /dev/null +++ b/test/coverage-test/src/multi-environment.ts @@ -0,0 +1,31 @@ +/** + * The variable below is modified by custom Vite plugin + */ +export const padding = 'default-padding' + +export function sum(a: number, b: number) { + /* + * These if-branches should show correctly on coverage report. + * Otherwise source maps are off. + */ + if (a === 8 && b === 9) { + // This is not covered by any test + return 17 + } + // Comment + else if (a === 2 && b === 2) { + // This is covered by SSR test + return 4 + } + else if (a === 11 && b === 22) { + // This is not covered by any test + return 33 + } + else if (a === 10 && b === 23) { + // This is covered by Web test + return 33 + } + + // This is covered by SSR and Web test, should show 2x hits + return a + b +} diff --git a/test/coverage-test/test/ssr.test.ts b/test/coverage-test/test/ssr.test.ts new file mode 100644 index 000000000000..039bf7c69a3d --- /dev/null +++ b/test/coverage-test/test/ssr.test.ts @@ -0,0 +1,9 @@ +// @vitest-environment node + +import { expect, test } from 'vitest' +import { sum } from '../src/multi-environment' + +test('runs on server', () => { + expect(sum(2, 2)).toBe(4) + expect(sum(100, 200)).toBe(300) +}) diff --git a/test/coverage-test/test/web.test.ts b/test/coverage-test/test/web.test.ts new file mode 100644 index 000000000000..6740d9bd0f89 --- /dev/null +++ b/test/coverage-test/test/web.test.ts @@ -0,0 +1,9 @@ +// @vitest-environment jsdom + +import { expect, test } from 'vitest' +import { sum } from '../src/multi-environment' + +test('runs on client', () => { + expect(sum(1, 2)).toBe(3) + expect(sum(10, 23)).toBe(33) +}) diff --git a/test/coverage-test/vitest.config.ts b/test/coverage-test/vitest.config.ts index 93d04fbfacce..abf3c1e21afd 100644 --- a/test/coverage-test/vitest.config.ts +++ b/test/coverage-test/vitest.config.ts @@ -1,12 +1,40 @@ import { resolve } from 'pathe' import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' +import MagicString from 'magic-string' +import remapping from '@ampproject/remapping' const provider = process.argv[1 + process.argv.indexOf('--provider')] export default defineConfig({ plugins: [ vue(), + /* + * Transforms `multi-environment.ts` differently based on test environment (JSDOM/Node) + * so that there are multiple different source maps for a single file. + * This causes a case where coverage report is incorrect if sourcemaps are not picked based on transform mode. + */ + { + name: 'vitest-custom-multi-transform', + enforce: 'pre', + transform(code, id, options) { + if (id.includes('src/multi-environment')) { + const ssr = options?.ssr || false + const transforMode = `transformMode is ${ssr ? 'ssr' : 'csr'}` + const padding = '\n*****'.repeat(ssr ? 0 : 15) + + const transformed = new MagicString(code) + transformed.replace('\'default-padding\'', `\`${transforMode} ${padding}\``) + + const map = remapping( + [transformed.generateMap({ hires: true }), this.getCombinedSourcemap() as any], + () => null, + ) as any + + return { code: transformed.toString(), map } + } + }, + }, { // Simulates Vite's virtual files: https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention name: 'vitest-custom-virtual-files', @@ -45,6 +73,7 @@ export default defineConfig({ customProviderModule: provider === 'custom' ? 'custom-provider' : undefined, include: ['src/**'], clean: true, + reportOnFailure: true, reporter: [ 'text', ['html'], diff --git a/test/workspaces/coverage-report-tests/check-coverage.test.ts b/test/workspaces/coverage-report-tests/check-coverage.test.ts index 2f089fbca30b..846cbaa5d7d7 100644 --- a/test/workspaces/coverage-report-tests/check-coverage.test.ts +++ b/test/workspaces/coverage-report-tests/check-coverage.test.ts @@ -1,5 +1,4 @@ import { existsSync, readFileSync } from 'node:fs' -import { normalize } from 'node:path' import { expect, test } from 'vitest' import libCoverage from 'istanbul-lib-coverage' import { resolve } from 'pathe' @@ -12,7 +11,7 @@ test('coverage exists', () => { test('file coverage summary matches', () => { const coverageJson = JSON.parse(readFileSync('./coverage/coverage-final.json', 'utf-8')) const coverageMap = libCoverage.createCoverageMap(coverageJson) - const fileCoverage = coverageMap.fileCoverageFor(normalize(resolve('./src/math.ts'))) + const fileCoverage = coverageMap.fileCoverageFor(resolve('./src/math.ts')) // There should be 1 uncovered branch and 1 uncovered function. See math.ts. const { branches, functions } = fileCoverage.toSummary() diff --git a/test/workspaces/vitest.config.ts b/test/workspaces/vitest.config.ts index 5a9cf2e2004e..7e7b028bf25d 100644 --- a/test/workspaces/vitest.config.ts +++ b/test/workspaces/vitest.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ test: { coverage: { enabled: true, + provider: 'istanbul', }, reporters: ['default', 'json'], outputFile: './results.json',