Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(browser): support v8 coverage #6273

Merged
merged 8 commits into from
Aug 12, 2024
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
19 changes: 6 additions & 13 deletions packages/browser/src/client/tester/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } fro
import { TraceMap, originalPositionFor } from 'vitest/utils'
import { page } from '@vitest/browser/context'
import { globalChannel } from '@vitest/browser/client'
import { importFs, importId } from '../utils'
import { executor } from '../utils'
import { VitestBrowserSnapshotEnvironment } from './snapshot'
import { rpc } from './rpc'
import type { VitestBrowserClientMocker } from './mocker'
Expand Down Expand Up @@ -91,7 +91,7 @@ export function createBrowserRunner(
if (coverage) {
await rpc().onAfterSuiteRun({
coverage,
transformMode: 'web',
transformMode: 'browser',
projectName: this.config.name,
})
}
Expand Down Expand Up @@ -148,27 +148,20 @@ export async function initiateRunner(
const runnerClass
= config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner

const executeId = (id: string) => {
if (id[0] === '/' || id[1] === ':') {
return importFs(id)
}
return importId(id)
}

const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, {
takeCoverage: () =>
takeCoverageInsideWorker(config.coverage, { executeId }),
takeCoverageInsideWorker(config.coverage, executor),
})
if (!config.snapshotOptions.snapshotEnvironment) {
config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment()
}
const runner = new BrowserRunner({
config,
})
const executor = { executeId } as VitestExecutor

const [diffOptions] = await Promise.all([
loadDiffConfig(config, executor),
loadSnapshotSerializers(config, executor),
loadDiffConfig(config, executor as unknown as VitestExecutor),
loadSnapshotSerializers(config, executor as unknown as VitestExecutor),
])
runner.config.diffOptions = diffOptions
cachedRunner = runner
Expand Down
8 changes: 6 additions & 2 deletions packages/browser/src/client/tester/tester.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser'
import { SpyModule, collectTests, setupCommonEnv, startCoverageInsideWorker, startTests, stopCoverageInsideWorker } from 'vitest/browser'
import { page } from '@vitest/browser/context'
import { channel, client, onCancel } from '@vitest/browser/client'
import { getBrowserState, getConfig, getWorkerState } from '../utils'
import { executor, getBrowserState, getConfig, getWorkerState } from '../utils'
import { setupDialogsSpy } from './dialog'
import { setupConsoleLogSpy } from './logger'
import { createSafeRpc } from './rpc'
Expand Down Expand Up @@ -114,6 +114,8 @@ async function executeTests(method: 'run' | 'collect', files: string[]) {

try {
await setupCommonEnv(config)
await startCoverageInsideWorker(config.coverage, executor)

for (const file of files) {
state.filepath = file

Expand All @@ -139,6 +141,8 @@ async function executeTests(method: 'run' | 'collect', files: string[]) {
}, 'Cleanup Error')
}
state.environmentTeardownRun = true
await stopCoverageInsideWorker(config.coverage, executor)

debug('finished running tests')
done(files)
}
Expand Down
11 changes: 11 additions & 0 deletions packages/browser/src/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ export async function importFs(id: string) {
return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name))
}

export const executor = {
isBrowser: true,

executeId: (id: string) => {
if (id[0] === '/' || id[1] === ':') {
return importFs(id)
}
return importId(id)
},
}

export function getConfig(): SerializedConfig {
return getBrowserState().config
}
Expand Down
5 changes: 3 additions & 2 deletions packages/coverage-istanbul/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,14 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
return
}

if (transformMode !== 'web' && transformMode !== 'ssr') {
if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') {
throw new Error(`Invalid transform mode: ${transformMode}`)
}

let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT)

if (!entry) {
entry = { web: [], ssr: [] }
entry = { web: [], ssr: [], browser: [] }
this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry)
}

Expand Down Expand Up @@ -251,6 +251,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
for (const filenames of [
coveragePerProject.ssr,
coveragePerProject.web,
coveragePerProject.browser,
]) {
const coverageMapByTransformMode = libCoverage.createCoverageMap({})

Expand Down
11 changes: 11 additions & 0 deletions packages/coverage-v8/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./browser": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.js"
},
"./*": "./*"
},
"main": "./dist/index.js",
Expand All @@ -41,8 +45,14 @@
"dev": "rollup -c --watch --watch.include 'src/**'"
},
"peerDependencies": {
"@vitest/browser": "workspace:*",
"vitest": "workspace:*"
},
"peerDependenciesMeta": {
"@vitest/browser": {
"optional": true
}
},
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@bcoe/v8-coverage": "^0.2.3",
Expand All @@ -63,6 +73,7 @@
"@types/istanbul-lib-report": "^3.0.3",
"@types/istanbul-lib-source-maps": "^4.0.4",
"@types/istanbul-reports": "^3.0.4",
"@vitest/browser": "workspace:*",
"pathe": "^1.1.2",
"v8-to-istanbul": "^9.3.0",
"vite-node": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions packages/coverage-v8/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const pkg = require('./package.json')

const entries = {
index: 'src/index.ts',
browser: 'src/browser.ts',
provider: 'src/provider.ts',
}

Expand Down
63 changes: 63 additions & 0 deletions packages/coverage-v8/src/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { cdp } from '@vitest/browser/context'
import type { V8CoverageProvider } from './provider'
import { loadProvider } from './load-provider'

const session = cdp()

type ScriptCoverage = Awaited<ReturnType<typeof session.send<'Profiler.takePreciseCoverage'>>>

export default {
async startCoverage() {
await session.send('Profiler.enable')
await session.send('Profiler.startPreciseCoverage', {
callCount: true,
detailed: true,
})
},

async takeCoverage(): Promise<{ result: any[] }> {
const coverage = await session.send('Profiler.takePreciseCoverage')
const result: typeof coverage.result = []

// Reduce amount of data sent over rpc by doing some early result filtering
for (const entry of coverage.result) {
if (filterResult(entry)) {
result.push({
...entry,
url: decodeURIComponent(entry.url.replace(window.location.origin, '')),
})
}
}

return { result }
},

async stopCoverage() {
await session.send('Profiler.stopPreciseCoverage')
await session.send('Profiler.disable')
},

async getProvider(): Promise<V8CoverageProvider> {
return loadProvider()
},
}

function filterResult(coverage: ScriptCoverage['result'][number]): boolean {
if (!coverage.url.startsWith(window.location.origin)) {
return false
}

if (coverage.url.includes('/node_modules/')) {
return false
}

if (coverage.url.includes('__vitest_browser__')) {
return false
}

if (coverage.url.includes('__vitest__/assets')) {
return false
}

return true
}
57 changes: 46 additions & 11 deletions packages/coverage-v8/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,58 @@
import type { Profiler } from 'node:inspector'
import * as coverage from './takeCoverage'
import inspector, { type Profiler } from 'node:inspector'
import { provider } from 'std-env'
import type { V8CoverageProvider } from './provider'
import { loadProvider } from './load-provider'

const session = new inspector.Session()

export default {
startCoverage(): void {
return coverage.startCoverage()
session.connect()
session.post('Profiler.enable')
session.post('Profiler.startPreciseCoverage', {
callCount: true,
detailed: true,
})
},

takeCoverage(): Promise<{ result: Profiler.ScriptCoverage[] }> {
return coverage.takeCoverage()
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)

resolve({ result })
})

if (provider === 'stackblitz') {
resolve({ result: [] })
}
})
},

stopCoverage(): void {
return coverage.stopCoverage()
session.post('Profiler.stopPreciseCoverage')
session.post('Profiler.disable')
session.disconnect()
},

async getProvider(): Promise<V8CoverageProvider> {
// to not bundle the provider
const name = './provider.js'
const { V8CoverageProvider } = (await import(
name
)) as typeof import('./provider')
return new V8CoverageProvider()
return loadProvider()
},
}

function filterResult(coverage: Profiler.ScriptCoverage): boolean {
if (!coverage.url.startsWith('file://')) {
return false
}

if (coverage.url.includes('/node_modules/')) {
return false
}

return true
}
8 changes: 8 additions & 0 deletions packages/coverage-v8/src/load-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// to not bundle the provider
const name = './provider.js'

export async function loadProvider() {
const { V8CoverageProvider } = (await import(/* @vite-ignore */ name)) as typeof import('./provider')

return new V8CoverageProvider()
}
Loading