Skip to content

Commit

Permalink
fix(coverage): vite-node to pass correct execution wrapper offset (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio authored Feb 13, 2025
1 parent cb48e64 commit 1f2e555
Show file tree
Hide file tree
Showing 21 changed files with 193 additions and 94 deletions.
13 changes: 9 additions & 4 deletions packages/coverage-v8/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CoverageProviderModule } from 'vitest/node'
import type { V8CoverageProvider } from './provider'
import type { ScriptCoverageWithOffset, V8CoverageProvider } from './provider'
import inspector, { type Profiler } from 'node:inspector'
import { fileURLToPath } from 'node:url'
import { provider } from 'std-env'
import { loadProvider } from './load-provider'

Expand All @@ -23,15 +24,19 @@ export default {
})
},

takeCoverage(): Promise<{ result: Profiler.ScriptCoverage[] }> {
takeCoverage(options): Promise<{ result: ScriptCoverageWithOffset[] }> {
return new Promise((resolve, reject) => {
session.post('Profiler.takePreciseCoverage', async (error, coverage) => {
if (error) {
return reject(error)
}

// Reduce amount of data sent over rpc by doing some early result filtering
const result = coverage.result.filter(filterResult)
const result = coverage.result
.filter(filterResult)
.map(res => ({
...res,
startOffset: options?.moduleExecutionInfo?.get(fileURLToPath(res.url))?.startOffset || 0,
}))

resolve({ result })
})
Expand Down
29 changes: 15 additions & 14 deletions packages/coverage-v8/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ import { cleanUrl } from 'vite-node/utils'
import { BaseCoverageProvider } from 'vitest/coverage'
import { version } from '../package.json' with { type: 'json' }

type TransformResults = Map<string, FetchResult>
type RawCoverage = Profiler.TakePreciseCoverageReturnType
export interface ScriptCoverageWithOffset extends Profiler.ScriptCoverage {
startOffset: number
}

// TODO: vite-node should export this
const WRAPPER_LENGTH = 185
type TransformResults = Map<string, FetchResult>
interface RawCoverage { result: ScriptCoverageWithOffset[] }

// Note that this needs to match the line ending as well
const VITE_EXPORTS_LINE_PATTERN
Expand Down Expand Up @@ -69,6 +70,14 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
await this.readCoverageFiles<RawCoverage>({
onFileRead(coverage) {
merged = mergeProcessCovs([merged, coverage])

// mergeProcessCovs sometimes loses startOffset, e.g. in vue
merged.result.forEach((result) => {
if (!result.startOffset) {
const original = coverage.result.find(r => r.url === result.url)
result.startOffset = original?.startOffset || 0
}
})
},
onFinished: async (project, transformMode) => {
const converted = await this.convertCoverage(
Expand Down Expand Up @@ -230,15 +239,12 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
source: string
originalSource: string
sourceMap?: { sourcemap: EncodedSourceMap }
isExecuted: boolean
}> {
const filePath = normalize(fileURLToPath(url))

let isExecuted = true
let transformResult: FetchResult | TransformResult | undefined = transformResults.get(filePath)

if (!transformResult) {
isExecuted = false
transformResult = await onTransform(removeStartsWith(url, FILE_PROTOCOL)).catch(() => undefined)
}

Expand All @@ -258,7 +264,6 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
// These can be uncovered files included by "all: true" or files that are loaded outside vite-node
if (!map) {
return {
isExecuted,
source: code || sourcesContent[0],
originalSource: sourcesContent[0],
}
Expand All @@ -273,7 +278,6 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
}

return {
isExecuted,
originalSource: sourcesContent[0],
source: code || sourcesContent[0],
sourceMap: {
Expand Down Expand Up @@ -338,20 +342,17 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
}

await Promise.all(
chunk.map(async ({ url, functions }) => {
chunk.map(async ({ url, functions, startOffset }) => {
const sources = await this.getSources(
url,
transformResults,
onTransform,
functions,
)

// If file was executed by vite-node we'll need to add its wrapper
const wrapperLength = sources.isExecuted ? WRAPPER_LENGTH : 0

const converter = v8ToIstanbul(
url,
wrapperLength,
startOffset,
sources,
undefined,
this.options.ignoreEmptyLines,
Expand Down
4 changes: 4 additions & 0 deletions packages/vite-node/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ export class ModuleCacheMap extends Map<string, ModuleCache> {
}
}

export type ModuleExecutionInfo = Map<string, { startOffset: number }>

export class ViteNodeRunner {
root: string

Expand Down Expand Up @@ -505,6 +507,8 @@ export class ViteNodeRunner {
columnOffset: -codeDefinition.length,
}

this.options.moduleExecutionInfo?.set(options.filename, { startOffset: codeDefinition.length })

const fn = vm.runInThisContext(code, options)
await fn(...Object.values(context))
}
Expand Down
5 changes: 3 additions & 2 deletions packages/vite-node/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { EncodedSourceMap } from '@jridgewell/trace-mapping'
import type { ViteHotContext } from 'vite/types/hot.js'
import type { ModuleCacheMap, ViteNodeRunner } from './client'
import type { ModuleCacheMap, ModuleExecutionInfo, ViteNodeRunner } from './client'

export type Nullable<T> = T | null | undefined
export type Arrayable<T> = T | Array<T>
Expand Down Expand Up @@ -87,6 +87,7 @@ export interface ViteNodeRunnerOptions {
createHotContext?: CreateHotContextFunction
base?: string
moduleCache?: ModuleCacheMap
moduleExecutionInfo?: ModuleExecutionInfo
interopDefault?: boolean
requestStubs?: Record<string, any>
debug?: boolean
Expand Down Expand Up @@ -140,4 +141,4 @@ export interface DebuggerOptions {
loadDumppedModules?: boolean
}

export type { ModuleCacheMap }
export type { ModuleCacheMap, ModuleExecutionInfo }
19 changes: 7 additions & 12 deletions packages/vitest/src/integrations/coverage.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import type {
CoverageModuleLoader,
CoverageProvider,
CoverageProviderModule,
} from '../node/types/coverage'
import type { SerializedCoverageConfig } from '../runtime/config'

interface Loader {
executeId: (id: string) => Promise<{ default: CoverageProviderModule }>
isBrowser?: boolean
}

export const CoverageProviderMap: Record<string, string> = {
v8: '@vitest/coverage-v8',
istanbul: '@vitest/coverage-istanbul',
}

async function resolveCoverageProviderModule(
options: SerializedCoverageConfig | undefined,
loader: Loader,
loader: CoverageModuleLoader,
) {
if (!options?.enabled || !options.provider) {
return null
Expand Down Expand Up @@ -65,7 +60,7 @@ async function resolveCoverageProviderModule(

export async function getCoverageProvider(
options: SerializedCoverageConfig | undefined,
loader: Loader,
loader: CoverageModuleLoader,
): Promise<CoverageProvider | null> {
const coverageModule = await resolveCoverageProviderModule(options, loader)

Expand All @@ -78,7 +73,7 @@ export async function getCoverageProvider(

export async function startCoverageInsideWorker(
options: SerializedCoverageConfig | undefined,
loader: Loader,
loader: CoverageModuleLoader,
runtimeOptions: { isolate: boolean },
) {
const coverageModule = await resolveCoverageProviderModule(options, loader)
Expand All @@ -92,20 +87,20 @@ export async function startCoverageInsideWorker(

export async function takeCoverageInsideWorker(
options: SerializedCoverageConfig | undefined,
loader: Loader,
loader: CoverageModuleLoader,
) {
const coverageModule = await resolveCoverageProviderModule(options, loader)

if (coverageModule) {
return coverageModule.takeCoverage?.()
return coverageModule.takeCoverage?.({ moduleExecutionInfo: loader.moduleExecutionInfo })
}

return null
}

export async function stopCoverageInsideWorker(
options: SerializedCoverageConfig | undefined,
loader: Loader,
loader: CoverageModuleLoader,
runtimeOptions: { isolate: boolean },
) {
const coverageModule = await resolveCoverageProviderModule(options, loader)
Expand Down
9 changes: 8 additions & 1 deletion packages/vitest/src/node/types/coverage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReportOptions } from 'istanbul-reports'
import type { TransformResult as ViteTransformResult } from 'vite'
import type { ModuleExecutionInfo } from 'vite-node'
import type { AfterSuiteRunMeta, Arrayable } from '../../types/general'
import type { Vitest } from '../core'

Expand Down Expand Up @@ -57,6 +58,12 @@ export interface ReportContext {
allTestsRun?: boolean
}

export interface CoverageModuleLoader {
executeId: (id: string) => Promise<{ default: CoverageProviderModule }>
isBrowser?: boolean
moduleExecutionInfo?: ModuleExecutionInfo
}

export interface CoverageProviderModule {
/**
* Factory for creating a new coverage provider
Expand All @@ -71,7 +78,7 @@ export interface CoverageProviderModule {
/**
* Executed on after each run in the worker thread. Possible to return a payload passed to the provider
*/
takeCoverage?: () => unknown | Promise<unknown>
takeCoverage?: (runtimeOptions?: { moduleExecutionInfo?: ModuleExecutionInfo }) => unknown | Promise<unknown>

/**
* Executed after all tests have been run in the worker thread.
Expand Down
9 changes: 9 additions & 0 deletions packages/vitest/src/runtime/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ export async function startVitestExecutor(options: ContextExecutorOptions) {
get moduleCache() {
return state().moduleCache
},
get moduleExecutionInfo() {
return state().moduleExecutionInfo
},
get interopDefault() {
return state().config.deps.interopDefault
},
Expand Down Expand Up @@ -255,6 +258,10 @@ export class VitestExecutor extends ViteNodeRunner {
return globalThis.__vitest_worker__ || this.options.state
}

get moduleExecutionInfo() {
return this.options.moduleExecutionInfo
}

shouldResolveId(id: string, _importee?: string | undefined): boolean {
if (isInternalRequest(id) || id.startsWith('data:')) {
return false
Expand Down Expand Up @@ -313,6 +320,8 @@ export class VitestExecutor extends ViteNodeRunner {
columnOffset: -codeDefinition.length,
}

this.options.moduleExecutionInfo?.set(options.filename, { startOffset: codeDefinition.length })

const fn = vm.runInContext(code, vmContext, {
...options,
// if we encountered an import, it's not inlined
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/runtime/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ async function execute(method: 'run' | 'collect', ctx: ContextRPC) {
ctx,
// here we create a new one, workers can reassign this if they need to keep it non-isolated
moduleCache: new ModuleCacheMap(),
moduleExecutionInfo: new Map(),
config: ctx.config,
onCancel,
environment,
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/types/worker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CancelReason, FileSpecification, Task } from '@vitest/runner'
import type { BirpcReturn } from 'birpc'
import type { ModuleCacheMap, ViteNodeResolveId } from 'vite-node'
import type { ModuleCacheMap, ModuleExecutionInfo, ViteNodeResolveId } from 'vite-node'
import type { SerializedConfig } from '../runtime/config'
import type { Environment } from './environment'
import type { TransformMode } from './general'
Expand Down Expand Up @@ -42,6 +42,7 @@ export interface WorkerGlobalState {
environmentTeardownRun?: boolean
onCancel: Promise<CancelReason>
moduleCache: ModuleCacheMap
moduleExecutionInfo?: ModuleExecutionInfo
providedContext: Record<string, any>
durations: {
environment: number
Expand Down
3 changes: 2 additions & 1 deletion packages/web-worker/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function createMessageEvent(

export function getRunnerOptions(): any {
const state = getWorkerState()
const { config, rpc, moduleCache } = state
const { config, rpc, moduleCache, moduleExecutionInfo } = state

return {
async fetchModule(id: string) {
Expand All @@ -96,6 +96,7 @@ export function getRunnerOptions(): any {
return rpc.resolveId(id, importer, 'web')
},
moduleCache,
moduleExecutionInfo,
interopDefault: config.deps.interopDefault ?? true,
moduleDirectories: config.deps.moduleDirectories,
root: config.root,
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.

2 changes: 0 additions & 2 deletions test/coverage-test/fixtures/src/cjs-package/entry.js

This file was deleted.

5 changes: 0 additions & 5 deletions test/coverage-test/fixtures/src/cjs-package/package.json

This file was deleted.

9 changes: 0 additions & 9 deletions test/coverage-test/fixtures/src/cjs-package/target.js

This file was deleted.

29 changes: 29 additions & 0 deletions test/coverage-test/fixtures/src/worker-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export async function sumInBackground(a: number, b: number) {
const worker = new Worker(new URL("./worker?with-some-query=123", import.meta.url), {
type: "module",
});

const promise = new Promise<MessageEvent>(resolve => {
worker.onmessage = resolve;
});

function uncovered() {
return "This is uncovered"
}

worker.postMessage({ a, b });

const result = await promise;
covered();
worker.terminate();

return result.data;
}

function covered() {
return "This is covered"
}

export function uncovered() {
return "This is uncovered"
}
Loading

0 comments on commit 1f2e555

Please sign in to comment.