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

fix: --inspect-brk to pause before execution #5355

Merged
merged 3 commits into from Mar 14, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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}`
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved

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