From 36087d1edf412735ca850ebb6613fbc2044fa2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Wed, 15 Mar 2023 21:44:15 +0200 Subject: [PATCH] fix: --inspect to work inside workers (#2983) --- docs/guide/debugging.md | 16 ++++++++++++ packages/vitest/src/node/cli-wrapper.ts | 2 -- packages/vitest/src/node/config.ts | 7 ++++++ packages/vitest/src/runtime/child.ts | 16 +++++++++--- packages/vitest/src/runtime/inspector.ts | 31 ++++++++++++++++++++++++ packages/vitest/src/runtime/worker.ts | 16 +++++++++--- packages/vitest/src/types/config.ts | 14 +++++++++++ test/config/test/failures.test.ts | 18 ++++++++++++++ test/config/test/utils.ts | 5 +++- 9 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 packages/vitest/src/runtime/inspector.ts diff --git a/docs/guide/debugging.md b/docs/guide/debugging.md index 77fd4d9f49cf..1653d09da758 100644 --- a/docs/guide/debugging.md +++ b/docs/guide/debugging.md @@ -45,3 +45,19 @@ JavaScript file | ./node_modules/vitest/vitest.mjs Application parameters | run --threads false Then run this configuration in debug mode. The IDE will stop at JS/TS breakpoints set in the editor. + +## Node Inspector, e.g. Chrome DevTools + +Vitest also supports debugging tests without IDEs. However this requires that tests are not run parallel. Use one of the following commands to launch Vitest. + +```sh +# To run in a single worker +vitest --inspect-brk --single-thread + +# To run in a child process +vitest --inspect-brk --no-threads +``` + +Once Vitest starts it will stop execution and waits for you to open developer tools that can connect to [NodeJS inspector](https://nodejs.org/en/docs/guides/debugging-getting-started/). You can use Chrome DevTools for this by opening `chrome://inspect` on browser. + +In watch mode you can keep the debugger open during test re-runs by using the `--single-thread --isolate false` options. diff --git a/packages/vitest/src/node/cli-wrapper.ts b/packages/vitest/src/node/cli-wrapper.ts index c8ff746a093f..791fbac859c1 100644 --- a/packages/vitest/src/node/cli-wrapper.ts +++ b/packages/vitest/src/node/cli-wrapper.ts @@ -11,8 +11,6 @@ const ENTRY = new URL('./cli.js', import.meta.url) /** Arguments passed to Node before the script */ const NODE_ARGS = [ - '--inspect', - '--inspect-brk', '--trace-deprecation', '--experimental-wasm-threads', '--wasm-atomics-on-non-shared-memory', diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index 5548b5dcbde8..34dac6bb1af2 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -106,6 +106,13 @@ export function resolveConfig( resolved.shard = { index, count } } + if (resolved.inspect || resolved.inspectBrk) { + if (resolved.threads !== false && resolved.singleThread !== true) { + const inspectOption = `--inspect${resolved.inspectBrk ? '-brk' : ''}` + throw new Error(`You cannot use ${inspectOption} without "threads: false" or "singleThread: true"`) + } + } + resolved.deps = resolved.deps || {} // vitenode will try to import such file with native node, // but then our mocker will not work properly diff --git a/packages/vitest/src/runtime/child.ts b/packages/vitest/src/runtime/child.ts index b4509434ed77..b16bf03bee41 100644 --- a/packages/vitest/src/runtime/child.ts +++ b/packages/vitest/src/runtime/child.ts @@ -6,6 +6,7 @@ import type { RuntimeRPC } from '../types/rpc' import type { ChildContext } from '../types/child' import { mockMap, moduleCache, startViteNode } from './execute' import { rpcDone } from './rpc' +import { setupInspect } from './inspector' function init(ctx: ChildContext) { const { config } = ctx @@ -58,10 +59,17 @@ function unwrapConfig(config: ResolvedConfig) { } export async function run(ctx: ChildContext) { - init(ctx) - const { run, executor } = await startViteNode(ctx) - await run(ctx.files, ctx.config, ctx.environment, executor) - await rpcDone() + const inspectorCleanup = setupInspect(ctx.config) + + try { + init(ctx) + const { run, executor } = await startViteNode(ctx) + await run(ctx.files, ctx.config, ctx.environment, executor) + await rpcDone() + } + finally { + inspectorCleanup() + } } const procesExit = process.exit diff --git a/packages/vitest/src/runtime/inspector.ts b/packages/vitest/src/runtime/inspector.ts new file mode 100644 index 000000000000..e494f19439b1 --- /dev/null +++ b/packages/vitest/src/runtime/inspector.ts @@ -0,0 +1,31 @@ +import inspector from 'node:inspector' + +import type { ResolvedConfig } from '../types' + +/** + * 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) { + const isEnabled = config.inspect || config.inspectBrk + + if (isEnabled) { + // Inspector may be open already if "isolate: false" is used + const isOpen = inspector.url() !== undefined + + if (!isOpen) { + inspector.open() + + if (config.inspectBrk) + inspector.waitForDebugger() + } + } + + // In watch mode the inspector can persist re-runs if "isolate: false, singleThread: true" is used + const keepOpen = config.watch && !config.isolate && config.singleThread + + return function cleanup() { + if (isEnabled && !keepOpen) + inspector.close() + } +} diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index 46cacf264826..305eca7fb220 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -3,6 +3,7 @@ import { workerId as poolId } from 'tinypool' import type { RuntimeRPC, WorkerContext } from '../types' import { getWorkerState } from '../utils/global' import { mockMap, moduleCache, startViteNode } from './execute' +import { setupInspect } from './inspector' import { rpcDone } from './rpc' function init(ctx: WorkerContext) { @@ -43,8 +44,15 @@ function init(ctx: WorkerContext) { } export async function run(ctx: WorkerContext) { - init(ctx) - const { run, executor } = await startViteNode(ctx) - await run(ctx.files, ctx.config, ctx.environment, executor) - await rpcDone() + const inspectorCleanup = setupInspect(ctx.config) + + try { + init(ctx) + const { run, executor } = await startViteNode(ctx) + await run(ctx.files, ctx.config, ctx.environment, executor) + await rpcDone() + } + finally { + inspectorCleanup() + } } diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 7bd231f47f71..2deb42366892 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -550,6 +550,20 @@ export interface InlineConfig { * Path to a custom test runner. */ runner?: string + + /** + * Debug tests by opening `node:inspector` in worker / child process. + * Provides similar experience as `--inspect` Node CLI argument. + * Requires `singleThread: true` OR `threads: false`. + */ + inspect?: boolean + + /** + * Debug tests by opening `node:inspector` in worker / child process and wait for debugger to connect. + * Provides similar experience as `--inspect-brk` Node CLI argument. + * Requires `singleThread: true` OR `threads: false`. + */ + inspectBrk?: boolean } export interface TypecheckConfig { diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index 0abed5a48c3f..7931e167e738 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -19,3 +19,21 @@ test('shard index must be smaller than count', async () => { expect(error).toMatch('Error: --shard must be a positive number less then ') }) + +test('inspect requires changing threads or singleThread', async () => { + const { error } = await runVitest('run', ['--inspect']) + + expect(error).toMatch('Error: You cannot use --inspect without "threads: false" or "singleThread: true"') +}) + +test('inspect cannot be used with threads', async () => { + const { error } = await runVitest('run', ['--inspect', '--threads', 'true']) + + expect(error).toMatch('Error: You cannot use --inspect without "threads: false" or "singleThread: true"') +}) + +test('inspect-brk cannot be used with threads', async () => { + const { error } = await runVitest('run', ['--inspect-brk', '--threads', 'true']) + + expect(error).toMatch('Error: You cannot use --inspect-brk without "threads: false" or "singleThread: true"') +}) diff --git a/test/config/test/utils.ts b/test/config/test/utils.ts index acda2c401b5b..574cd02fd8d5 100644 --- a/test/config/test/utils.ts +++ b/test/config/test/utils.ts @@ -6,7 +6,10 @@ export async function runVitest(mode: 'run' | 'watch', cliArguments: string[]) { let error = '' subprocess.stderr?.on('data', (data) => { - error += stripAnsi(data.toString()) + error = stripAnsi(data.toString()) + + // Sometimes on Windows CI execa doesn't exit properly. Force exit when stderr is caught. + subprocess.kill() }) await new Promise(resolve => subprocess.on('exit', resolve))