From 7be75b79115c7ffe03204bbfe7a0403b05167ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Thu, 8 Oct 2026 14:23:01 +0300 Subject: [PATCH 1/2] test: failing test for #11433 part 2 --- ...ered-concurrent-transform.istanbul.test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 test/coverage-test/test/uncovered-concurrent-transform.istanbul.test.ts diff --git a/test/coverage-test/test/uncovered-concurrent-transform.istanbul.test.ts b/test/coverage-test/test/uncovered-concurrent-transform.istanbul.test.ts new file mode 100644 index 000000000000..113c4d41bbcb --- /dev/null +++ b/test/coverage-test/test/uncovered-concurrent-transform.istanbul.test.ts @@ -0,0 +1,66 @@ +import type { DevEnvironment, Plugin } from 'vite' +import { resolve } from 'node:path' +import { expect } from 'vitest' +import { formatSummary, readCoverageMap, runVitest, test } from '../utils' + +test('uncovered file coverage is not replaced by concurrent transforms (#11433)', async () => { + await runVitest( + { + include: ['fixtures/test/math.test.ts'], + coverage: { + reporter: 'json', + include: ['fixtures/src/math.ts', 'fixtures/src/untested-file.ts', 'fixtures/src/even.ts'], + }, + }, + undefined, + { plugins: [transformAnotherFileDuringUncoveredTransform()] }, + ) + + const coverageMap = await readCoverageMap() + expect(coverageMap.files()).toMatchInlineSnapshot(` + [ + "/fixtures/src/even.ts", + "/fixtures/src/math.ts", + "/fixtures/src/untested-file.ts", + ] + `) + + const untested = coverageMap.fileCoverageFor('/fixtures/src/untested-file.ts') + expect(formatSummary(untested.toSummary())).toMatchInlineSnapshot(` + { + "branches": "0/2 (0%)", + "functions": "0/4 (0%)", + "lines": "0/6 (0%)", + "statements": "0/6 (0%)", + } + `) + + const even = coverageMap.fileCoverageFor('/fixtures/src/even.ts') + expect(formatSummary(even.toSummary())).toMatchInlineSnapshot(` + { + "branches": "0/0 (100%)", + "functions": "0/2 (0%)", + "lines": "0/2 (0%)", + "statements": "0/2 (0%)", + } + `) +}) + +// Simulates import warm-up finishing while the uncovered file's transform is still pending +function transformAnotherFileDuringUncoveredTransform(): Plugin { + const anotherFile = resolve(import.meta.dirname, '../fixtures/src/even.ts') + + return { + name: 'test:transform-another-file-during-uncovered-transform', + transform: { + order: 'post', + async handler(_, id) { + if (id.includes('untested-file.ts') && id.includes('vitest-uncovered-coverage=true')) { + await (this.environment as DevEnvironment).transformRequest( + `${anotherFile}?concurrent-transform`, + ) + } + }, + }, + } +} From 2dfb5b0f2656a15da0cb2e82318338e8e50fade1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Thu, 8 Oct 2026 14:40:04 +0300 Subject: [PATCH 2/2] fix(coverage): istanbul uncovered files race with transforms --- packages/coverage-istanbul/src/provider.ts | 36 +++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 2899dbc01490..0a5c5b1f5d0f 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -1,4 +1,4 @@ -import type { CoverageMap } from '@vitest/istanbul-lib-coverage' +import type { CoverageMap, FileCoverageData } from '@vitest/istanbul-lib-coverage' import type { Instrumenter } from '@vitest/istanbul-lib-instrument' import type { ProxifiedModule } from 'magicast' import type { CoverageProvider, ReportContext, Vite, Vitest } from 'vitest/node' @@ -26,6 +26,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co instrumenter!: Instrumenter private transformedModuleIds = new Set() + private uncoveredFileCoverages = new Map() initialize(ctx: Vitest): void { this._initialize(ctx) @@ -75,6 +76,11 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co return true } + async clean(clean = true): Promise { + await super.clean(clean) + this.uncoveredFileCoverages.clear() + } + onFileTransform( sourceCode: string, id: string, @@ -95,8 +101,11 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co .replaceAll(/(if +\(import\.meta\.vitest\))/g, '/* istanbul ignore next */ $1') const code = this.instrumenter.instrumentSync(sourceCode, id, sourceMap as any) + const uncoveredKey = getUncoveredKey(id) - if (!id.includes('vitest-uncovered-coverage=true')) { + if (uncoveredKey) { + this.uncoveredFileCoverages.set(uncoveredKey, this.instrumenter.lastFileCoverage()) + } else { const transformMap = new GenMapping(sourceMap) eachMapping(new TraceMap(sourceMap as any), (mapping) => { @@ -223,8 +232,6 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co const transform = this.createUncoveredFileTransformer(this.ctx) - // Note that these cannot be run parallel as synchronous instrumenter.lastFileCoverage - // returns the coverage of the last transformed file for (const [index, filename] of uncoveredFiles.entries()) { let timeout: ReturnType | undefined let start: number | undefined @@ -239,10 +246,15 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co debug('Uncovered file %d/%d', index, uncoveredFiles.length) } - // Make sure file is not served from cache so that instrumenter loads up requested file coverage - await transform(`${filename}?cache=${cacheKey}&vitest-uncovered-coverage=true`) - const lastCoverage = this.instrumenter.lastFileCoverage() - coverageMap.addFileCoverage(lastCoverage) + // Unique key makes sure file is not served from cache + const uncoveredKey = `${cacheKey}-${index}` + await transform(`${filename}?cache=${uncoveredKey}&vitest-uncovered-coverage=true`) + + const fileCoverage = + this.uncoveredFileCoverages.get(uncoveredKey) ?? this.instrumenter.lastFileCoverage() + this.uncoveredFileCoverages.delete(uncoveredKey) + + coverageMap.addFileCoverage(fileCoverage) if (debug.enabled) { clearTimeout(timeout) @@ -304,3 +316,11 @@ async function transformCoverage(coverageMap: CoverageMap) { function removeQueryParameters(filename: string) { return filename.split('?')[0] } + +function getUncoveredKey(id: string) { + if (!id.includes('vitest-uncovered-coverage=true')) { + return + } + + return new URLSearchParams(id.split('?')[1]).get('cache') ?? undefined +}