diff --git a/packages/vite-node/src/source-map.ts b/packages/vite-node/src/source-map.ts index fa4fc280f3d2..85bb26216a11 100644 --- a/packages/vite-node/src/source-map.ts +++ b/packages/vite-node/src/source-map.ts @@ -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` diff --git a/packages/vitest/src/runtime/inspector.ts b/packages/vitest/src/runtime/inspector.ts index a43f976454e8..fdc8f31ab25d 100644 --- a/packages/vitest/src/runtime/inspector.ts +++ b/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 /** * 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) { @@ -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, + }) + } + } } } @@ -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() + } } } diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 41a3cb9cd4a1..d31467382ddb 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -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) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b2111674c5f..1ee182d27ca4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1854,6 +1854,18 @@ importers: specifier: workspace:* version: link:../../packages/vitest + test/inspect: + devDependencies: + vite: + specifier: ^5.0.12 + version: 5.0.12(@types/node@20.11.5)(less@4.1.3) + vitest: + specifier: workspace:* + version: link:../../packages/vitest + ws: + specifier: ^8.14.2 + version: 8.14.2 + test/mixed-pools: devDependencies: vitest: diff --git a/test/inspect/fixtures/math.test.ts b/test/inspect/fixtures/math.test.ts new file mode 100644 index 000000000000..54b50210a8f8 --- /dev/null +++ b/test/inspect/fixtures/math.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from "vitest"; + +test("sum", () => { + expect(1 + 1).toBe(2) +}) \ No newline at end of file diff --git a/test/inspect/fixtures/vitest.config.ts b/test/inspect/fixtures/vitest.config.ts new file mode 100644 index 000000000000..800411ae369f --- /dev/null +++ b/test/inspect/fixtures/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['./**.test.ts'], + watch: false, + }, +}) diff --git a/test/inspect/package.json b/test/inspect/package.json new file mode 100644 index 000000000000..ed5e2c5d23c7 --- /dev/null +++ b/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" + } +} diff --git a/test/inspect/test/inspect.test.ts b/test/inspect/test/inspect.test.ts new file mode 100644 index 000000000000..bc634436727d --- /dev/null +++ b/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> + +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 + resolve: (response: Message) => void + reject: (error: unknown) => void +} { + const pr = {} as ReturnType + + pr.promise = new Promise((resolve, reject) => { + pr.resolve = resolve + pr.reject = reject + }) + + return pr +} diff --git a/test/inspect/vitest.config.ts b/test/inspect/vitest.config.ts new file mode 100644 index 000000000000..654ae2aa887a --- /dev/null +++ b/test/inspect/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['./test/**'], + }, +}) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index f0312d64b201..a58edba0084d 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -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')