Skip to content

Commit

Permalink
fix(coverage): use transformMode based source maps
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Oct 15, 2023
1 parent dcf1626 commit 6f83002
Show file tree
Hide file tree
Showing 14 changed files with 1,363 additions and 56 deletions.
2 changes: 1 addition & 1 deletion packages/browser/src/client/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
69 changes: 50 additions & 19 deletions packages/coverage-istanbul/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<AfterSuiteRunMeta['transformMode'], CoverageMapData[]> = { ssr: [], web: [] }

initialize(ctx: Vitest) {
const config: CoverageIstanbulOptions = ctx.config.coverage
Expand Down Expand Up @@ -106,36 +106,47 @@ 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) {
// Try to guess which transform mode should be used to uncovered files
const transformMode = this.coverages.ssr.length > this.coverages.web.length ? 'ssr' : 'web'

if (this.options.all && allTestsRun)
await this.includeUntestedFiles(mergedCoverage)
const coveredFiles = coverageMaps.map(map => map.files()).flat()
const uncoveredCoverage = await this.getCoverageMapForUncoveredFiles(coveredFiles, transformMode)

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,
})

Expand Down Expand Up @@ -181,19 +192,21 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
}
}

async includeUntestedFiles(coverageMap: CoverageMap) {
async getCoverageMapForUncoveredFiles(coveredFiles: string[], transformMode: AfterSuiteRunMeta['transformMode']) {
// 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)
const transformResult = await this.ctx.vitenode.transformRequest(filename, filename, transformMode)
return { transformResult, filename }
}))

const coverageMap = libCoverage.createCoverageMap({})

for (const { transformResult, filename } of transformResults) {
const sourceMap = transformResult?.map

Expand All @@ -209,9 +222,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<CoverageMap>((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`
Expand Down
96 changes: 62 additions & 34 deletions packages/coverage-v8/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -39,6 +39,7 @@ interface TestExclude {

type Options = ResolvedCoverageOptions<'v8'>
type TransformResults = Map<string, FetchResult>
type RawCoverage = Profiler.TakePreciseCoverageReturnType

// TODO: vite-node should export this
const WRAPPER_LENGTH = 185
Expand All @@ -52,7 +53,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
ctx!: Vitest
options!: Options
testExclude!: InstanceType<TestExclude>
coverages: Profiler.TakePreciseCoverageReturnType[] = []
coverages: Record<AfterSuiteRunMeta['transformMode'], RawCoverage[]> = { ssr: [], web: [] }

initialize(ctx: Vitest) {
const config: CoverageV8Options = ctx.config.coverage
Expand Down Expand Up @@ -92,54 +93,47 @@ 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)

scriptCoverages.push(...untestedFiles)
}
// Try to guess which transform mode should be used to uncovered files
const transformMode = this.coverages.ssr.length > this.coverages.web.length ? 'ssr' : 'web'

const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => {
const sources = await this.getSources(url, transformResults, functions)
const coveredFiles = coverageMaps.map(map => map.files()).flat()
const untestedCoverage = await this.getUntestedFiles(coveredFiles, transformMode)
const untestedCoverageResults = untestedCoverage.map(files => ({ result: [files] }))

// 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({}))
coverageMaps.push(await this.mergeAndTransformCoverage(untestedCoverageResults, transformMode))
}

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,
})

Expand Down Expand Up @@ -185,7 +179,9 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
}
}

private async getUntestedFiles(testedFiles: string[], transformResults: TransformResults): Promise<Profiler.ScriptCoverage[]> {
private async getUntestedFiles(testedFiles: string[], transformMode: AfterSuiteRunMeta['transformMode']): Promise<Profiler.ScriptCoverage[]> {
const transformResults = normalizeTransformResults(this.ctx.projects.map(project => project.vitenode.fetchCaches[transformMode]))

const includedFiles = await this.testExclude.glob(this.ctx.config.root)
const uncoveredFiles = includedFiles
.map(file => pathToFileURL(resolve(this.ctx.config.root, file)))
Expand Down Expand Up @@ -247,6 +243,38 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
},
}
}

private async mergeAndTransformCoverage(coverages: RawCoverage[], transformMode: 'web' | 'ssr') {
const transformResults = normalizeTransformResults(this.ctx.projects.map(project => project.vitenode.fetchCaches[transformMode]))
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<CoverageMap>((coverage, previousCoverageMap) => {
const map = libCoverage.createCoverageMap(coverage)
map.merge(previousCoverageMap)
return map
}, libCoverage.createCoverageMap({}))
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/runtime/runners/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/types/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type ResolveIdFunction = (id: string, importer?: string) => Promise<ViteN

export interface AfterSuiteRunMeta {
coverage?: unknown
transformMode: Environment['transformMode']
}

export type WorkerRPC = BirpcReturn<RuntimeRPC, RunnerRPC>
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ 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 with {"coverage":{"customCoverage":"Coverage report passed from workers to main thread"},"transformMode":"ssr"}",
"onAfterSuiteRun with {"coverage":{"customCoverage":"Coverage report passed from workers to main thread"},"transformMode":"web"}",
"reportCoverage with {"allTestsRun":true}",
],
"transformedFiles": [
Expand All @@ -22,6 +23,7 @@ exports[`custom json report 1`] = `
"<process-cwd>/src/implicitElse.ts",
"<process-cwd>/src/importEnv.ts",
"<process-cwd>/src/index.mts",
"<process-cwd>/src/multi-environment.ts",
"<process-cwd>/src/multi-suite.ts",
"<process-cwd>/src/utils.ts",
],
Expand Down
Loading

0 comments on commit 6f83002

Please sign in to comment.