Skip to content

Commit

Permalink
fix: --inspect-brk to pause before execution (#5355)
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Mar 14, 2024
1 parent d627e20 commit e77c553
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 5 deletions.
5 changes: 5 additions & 0 deletions packages/vite-node/src/source-map.ts
Expand Up @@ -48,6 +48,11 @@ export function withInlineSourcemap(result: TransformResult, options: {
while (OTHER_SOURCE_MAP_REGEXP.test(code))
code = code.replace(OTHER_SOURCE_MAP_REGEXP, '')

// If the first line is not present on source maps, add simple 1:1 mapping ([0,0,0,0], [1,0,0,0])
// so that debuggers can be set to break on first line
if (map.mappings.startsWith(';'))
map.mappings = `AAAA,CAAA${map.mappings}`

const sourceMap = Buffer.from(JSON.stringify(map), 'utf-8').toString('base64')
result.code = `${code.trimEnd()}\n\n${VITE_NODE_SOURCEMAPPING_SOURCE}\n//# ${VITE_NODE_SOURCEMAPPING_URL};base64,${sourceMap}\n`

Expand Down
26 changes: 22 additions & 4 deletions packages/vitest/src/runtime/inspector.ts
@@ -1,15 +1,17 @@
import { createRequire } from 'node:module'

import type { ResolvedConfig } from '../types'
import type { ContextRPC } from '../types'

const __require = createRequire(import.meta.url)
let inspector: typeof import('node:inspector')
let session: InstanceType<typeof inspector.Session>

/**
* Enables debugging inside `worker_threads` and `child_process`.
* Should be called as early as possible when worker/process has been set up.
*/
export function setupInspect(config: ResolvedConfig) {
export function setupInspect(ctx: ContextRPC) {
const config = ctx.config
const isEnabled = config.inspect || config.inspectBrk

if (isEnabled) {
Expand All @@ -20,8 +22,22 @@ export function setupInspect(config: ResolvedConfig) {
if (!isOpen) {
inspector.open()

if (config.inspectBrk)
if (config.inspectBrk) {
inspector.waitForDebugger()
const firstTestFile = ctx.files[0]

// Stop at first test file
if (firstTestFile) {
session = new inspector.Session()
session.connect()

session.post('Debugger.enable')
session.post('Debugger.setBreakpointByUrl', {
lineNumber: 0,
url: new URL(firstTestFile, import.meta.url).href,
})
}
}
}
}

Expand All @@ -32,7 +48,9 @@ export function setupInspect(config: ResolvedConfig) {
const keepOpen = config.watch && (isIsolatedSingleFork || isIsolatedSingleThread)

return function cleanup() {
if (isEnabled && !keepOpen && inspector)
if (isEnabled && !keepOpen && inspector) {
inspector.close()
session?.disconnect()
}
}
}
2 changes: 1 addition & 1 deletion packages/vitest/src/runtime/worker.ts
Expand Up @@ -15,7 +15,7 @@ if (isChildProcess())
export async function run(ctx: ContextRPC) {
const prepareStart = performance.now()

const inspectorCleanup = setupInspect(ctx.config)
const inspectorCleanup = setupInspect(ctx)

process.env.VITEST_WORKER_ID = String(ctx.workerId)
process.env.VITEST_POOL_ID = String(poolId)
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

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

5 changes: 5 additions & 0 deletions test/inspect/fixtures/math.test.ts
@@ -0,0 +1,5 @@
import { expect, test } from "vitest";

test("sum", () => {
expect(1 + 1).toBe(2)
})
8 changes: 8 additions & 0 deletions test/inspect/fixtures/vitest.config.ts
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
include: ['./**.test.ts'],
watch: false,
},
})
13 changes: 13 additions & 0 deletions test/inspect/package.json
@@ -0,0 +1,13 @@
{
"name": "@vitest/test-inspect",
"type": "module",
"private": true,
"scripts": {
"test": "vitest"
},
"devDependencies": {
"vite": "latest",
"vitest": "workspace:*",
"ws": "^8.14.2"
}
}
86 changes: 86 additions & 0 deletions test/inspect/test/inspect.test.ts
@@ -0,0 +1,86 @@
import type { InspectorNotification } from 'node:inspector'
import { expect, test } from 'vitest'
import WebSocket from 'ws'

import { isWindows } from '../../../packages/vite-node/src/utils'
import { runVitestCli } from '../../test-utils'

type Message = Partial<InspectorNotification<any>>

test.skipIf(isWindows)('--inspect-brk stops at test file', async () => {
const vitest = await runVitestCli('--root', 'fixtures', '--inspect-brk', '--no-file-parallelism')

await vitest.waitForStderr('Debugger listening on ')
const url = vitest.stderr.split('\n')[0].replace('Debugger listening on ', '')

const { receive, send } = await createChannel(url)

send({ method: 'Debugger.enable' })
send({ method: 'Runtime.enable' })
await receive('Runtime.executionContextCreated')

const paused = receive('Debugger.paused')
send({ method: 'Runtime.runIfWaitingForDebugger' })

const { params } = await paused
const scriptId = params.callFrames[0].functionLocation.scriptId

// Verify that debugger paused on test file
const response = receive()
send({ method: 'Debugger.getScriptSource', params: { scriptId } })
const { result } = await response as any

expect(result.scriptSource).toContain('test("sum", () => {')
expect(result.scriptSource).toContain('expect(1 + 1).toBe(2)')

send({ method: 'Debugger.resume' })

await vitest.waitForStdout('Test Files 1 passed (1)')
await vitest.isDone
})

async function createChannel(url: string) {
const ws = new WebSocket(url)

let id = 1
let receiver = defer()

ws.onerror = receiver.reject
ws.onmessage = (message) => {
const response = JSON.parse(message.data.toString())
receiver.resolve(response)
}

async function receive(filter?: string) {
const message = await receiver.promise
receiver = defer()

if (filter && message.method !== filter)
return receive(filter)

return message
}

function send(message: Message) {
ws.send(JSON.stringify({ ...message, id: id++ }))
}

await new Promise(r => ws.on('open', r))

return { receive, send }
}

function defer(): {
promise: Promise<Message>
resolve: (response: Message) => void
reject: (error: unknown) => void
} {
const pr = {} as ReturnType<typeof defer>

pr.promise = new Promise((resolve, reject) => {
pr.resolve = resolve
pr.reject = reject
})

return pr
}
7 changes: 7 additions & 0 deletions test/inspect/vitest.config.ts
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
include: ['./test/**'],
},
})
3 changes: 3 additions & 0 deletions test/test-utils/index.ts
Expand Up @@ -188,6 +188,9 @@ export async function runCli(command: string, _options?: Options | string, ...ar
await cli.isDone
})

if (args.includes('--inspect') || args.includes('--inspect-brk'))
return cli

if (args.includes('--watch')) {
if (command === 'vitest') // Wait for initial test run to complete
await cli.waitForStdout('Waiting for file changes')
Expand Down

0 comments on commit e77c553

Please sign in to comment.