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 to work inside workers #2983

Merged
merged 1 commit into from
Mar 15, 2023
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
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.
AriPerkkio marked this conversation as resolved.
Show resolved Hide resolved

```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 @@ -541,6 +541,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