Skip to content

Commit

Permalink
fix: --inspect to work inside workers (#2983)
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Mar 15, 2023
1 parent c2e25bb commit 36087d1
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 11 deletions.
16 changes: 16 additions & 0 deletions docs/guide/debugging.md
Expand Up @@ -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.
2 changes: 0 additions & 2 deletions packages/vitest/src/node/cli-wrapper.ts
Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions packages/vitest/src/node/config.ts
Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions packages/vitest/src/runtime/child.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions 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()
}
}
16 changes: 12 additions & 4 deletions packages/vitest/src/runtime/worker.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
}
}
14 changes: 14 additions & 0 deletions packages/vitest/src/types/config.ts
Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions test/config/test/failures.test.ts
Expand Up @@ -19,3 +19,21 @@ test('shard index must be smaller than count', async () => {

expect(error).toMatch('Error: --shard <index> must be a positive number less then <count>')
})

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"')
})
5 changes: 4 additions & 1 deletion test/config/test/utils.ts
Expand Up @@ -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))
Expand Down

0 comments on commit 36087d1

Please sign in to comment.