Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions packages/coverage-istanbul/src/provider.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -26,6 +26,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
instrumenter!: Instrumenter

private transformedModuleIds = new Set<string>()
private uncoveredFileCoverages = new Map<string, FileCoverageData>()

initialize(ctx: Vitest): void {
this._initialize(ctx)
Expand Down Expand Up @@ -75,6 +76,11 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
return true
}

async clean(clean = true): Promise<void> {
await super.clean(clean)
this.uncoveredFileCoverages.clear()
}

onFileTransform(
sourceCode: string,
id: string,
Expand All @@ -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) => {
Expand Down Expand Up @@ -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<typeof setTimeout> | undefined
let start: number | undefined
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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(`
[
"<process-cwd>/fixtures/src/even.ts",
"<process-cwd>/fixtures/src/math.ts",
"<process-cwd>/fixtures/src/untested-file.ts",
]
`)

const untested = coverageMap.fileCoverageFor('<process-cwd>/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('<process-cwd>/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`,
)
}
},
},
}
}
Loading