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

feat: better debug support #334

Merged
merged 23 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
98716e1
feat: better debug support
sheremet-va Apr 6, 2024
c974ed1
refactor: cleanup
sheremet-va Apr 6, 2024
0486fc1
refactor: allow debugExclude as a folder option
sheremet-va Apr 6, 2024
eab82d0
refactor: organize worker
sheremet-va Apr 6, 2024
ebf35b4
chore: cleanup
sheremet-va Apr 6, 2024
a51c569
refactor: improve types
sheremet-va Apr 9, 2024
b59ffc9
fix: always report ocverage if exists
sheremet-va Apr 9, 2024
271345d
chore: print where coverage is reported to
sheremet-va Apr 9, 2024
d5c042f
chore: cleanup
sheremet-va Apr 9, 2024
823afdd
chore: privatize properties
sheremet-va Apr 9, 2024
03c910b
feat: add vitest.debuggerAddress option
sheremet-va Apr 9, 2024
ec6c2b2
fix: don't use empty string if debuggerAddress is not provided
sheremet-va Apr 9, 2024
b895d16
chore: comment for proxy
sheremet-va Apr 9, 2024
56eb39d
chore: remove unused isTestFile
sheremet-va Apr 9, 2024
8fc65e9
feat: support debug restart
sheremet-va Apr 9, 2024
354c83c
fix: stop debugger when request is cancelled
sheremet-va Apr 9, 2024
fab687b
fix: add minimum debug version
sheremet-va Apr 9, 2024
033bc9f
chore: add requirements for debugger to readme
sheremet-va Apr 9, 2024
2e4f497
chore: cleanup
sheremet-va Apr 10, 2024
4604f36
fix: stop tests so the extension doesn't hang on long-running tests
sheremet-va Apr 10, 2024
9d88f01
chore: improve test run reporting during the debug
sheremet-va Apr 10, 2024
ec6e04d
fix: use the same test run for tests in a folder
sheremet-va Apr 10, 2024
e8ac77f
docs: more info
sheremet-va Apr 10, 2024
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ These options are resolved relative to the [workspace file](https://code.visuals
- `vitest.configSearchPatternExclude`: [Glob pattern](https://code.visualstudio.com/docs/editor/glob-patterns) that should be ignored when this extension looks for config files. Note that this is applied to _config_ files, not test files inside configs. Default: `**/{node_modules,.*}/**`If the extension cannot find Vitest, please open an issue.
- `vitest.nodeExecutable`: This extension spawns another process and will use this value as `execPath` argument.
- `vitest.debuggerPort`: Port that the debugger will be attached to. By default uses 9229 or tries to find a free port if it's not available.
- `vitest.debuggerAddress`: TCP/IP address of process to be debugged. Default: localhost

### Other Options

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@
"default": "**/{node_modules,.*}/**",
"scope": "window"
},
"vitest.debuggerAddress": {
"description": "TCP/IP address of process to be debugged. Default: localhost",
"type": "string",
"scope": "window"
},
"vitest.debuggerPort": {
"description": "Port that the debugger will be attached to. By default uses 9229 or tries to find a free port if it's not available.",
"type": "string",
Expand Down
10 changes: 3 additions & 7 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,6 @@ export class VitestFolderAPI extends VitestReporter {
await this.meta.rpc.cancelRun(this.id)
}

getCoverageConfig() {
return this.meta.rpc.getCoverageConfig(this.id)
}

waitForCoverageReport() {
return this.meta.rpc.waitForCoverageReport(this.id)
}
Expand All @@ -198,11 +194,11 @@ export class VitestFolderAPI extends VitestReporter {
}

stopInspect() {
return this.meta.rpc.stopInspect()
return this.meta.rpc.stopInspect(this.id)
}

startInspect(port: number) {
return this.meta.rpc.startInspect(port)
startInspect(port: number, address?: string) {
return this.meta.rpc.startInspect(this.id, port, address)
}
}

Expand Down
38 changes: 22 additions & 16 deletions src/api/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import v8 from 'node:v8'
import type { ChildProcess } from 'node:child_process'
import { type BirpcReturn, createBirpc } from 'birpc'
import type { File, ResolvedCoverageOptions, TaskResultPack, UserConsoleLog } from 'vitest'
import type { File, TaskResultPack, UserConsoleLog } from 'vitest'

export interface BirpcMethods {
getFiles: (id: string) => Promise<[project: string, file: string][]>
collectTests: (id: string, testFile: string[]) => Promise<void>
cancelRun: (id: string) => Promise<void>
runTests: (id: string, files?: string[], testNamePattern?: string) => Promise<void>
isTestFile: (file: string) => Promise<boolean>
export interface VitestMethods {
getFiles: () => Promise<[project: string, file: string][]>
collectTests: (testFile: string[]) => Promise<void>
cancelRun: () => Promise<void>
runTests: (files?: string[], testNamePattern?: string) => Promise<void>

watchTests: (id: string, files?: string[], testNamePattern?: string) => Promise<void>
unwatchTests: (id: string) => Promise<void>
watchTests: (files?: string[], testNamePattern?: string) => void
unwatchTests: () => void

enableCoverage: (id: string) => void
disableCoverage: (id: string) => void
getCoverageConfig: (id: string) => Promise<ResolvedCoverageOptions>
waitForCoverageReport: (id: string) => Promise<string | null>
enableCoverage: () => void
disableCoverage: () => void
waitForCoverageReport: () => Promise<string | null>

startInspect: (port: number) => void
startInspect: (port: number, address?: string) => void
stopInspect: () => void
}

type VitestPoolMethods = {
[K in keyof VitestMethods]: (id: string, ...args: Parameters<VitestMethods[K]>) => ReturnType<VitestMethods[K]>
}

export interface VitestPool extends VitestPoolMethods {
close: () => void
isTestFile: (file: string) => boolean
}

export interface VitestEvents {
Expand All @@ -36,7 +42,7 @@ export type BirpcEvents = {
[K in keyof VitestEvents]: (folder: string, ...args: Parameters<VitestEvents[K]>) => void
}

export type VitestRPC = BirpcReturn<BirpcMethods, BirpcEvents>
export type VitestRPC = BirpcReturn<VitestPool, BirpcEvents>

function createHandler<T extends (...args: any) => any>() {
const handlers: T[] = []
Expand Down Expand Up @@ -95,7 +101,7 @@ function createRpcOptions() {
export function createVitestRpc(vitest: ChildProcess) {
const { events, handlers } = createRpcOptions()

const api = createBirpc<BirpcMethods, BirpcEvents>(
const api = createBirpc<VitestPool, BirpcEvents>(
events,
{
timeout: -1,
Expand Down
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) {
configSearchPatternExclude,
nodeExecutable: resolvePath(nodeExecutable),
disableWorkspaceWarning: get<boolean>('disableWorkspaceWarning', false),
debuggerPort: get<number>('debuggerPort'),
debuggerPort: get<number>('debuggerPort') || undefined,
debuggerAddress: get<string>('debuggerAddress', undefined) || undefined,
}
}

Expand Down
110 changes: 61 additions & 49 deletions src/debug/debugManager.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import * as vscode from 'vscode'
import type { TestRunner } from '../runner/runner'
import getPort from 'get-port'
import { getConfig } from '../config'
import { log } from '../log'
import type { VitestFolderAPI } from '../api'
import { showVitestError } from '../utils'

interface VitestDebugConfig {
request: vscode.TestRunRequest
token: vscode.CancellationToken
runner: TestRunner
api: VitestFolderAPI
}

export class TestDebugManager extends vscode.Disposable {
private disposables: vscode.Disposable[] = []
private sessions = new Set<vscode.DebugSession>()
private configurations = new Map<string, VitestDebugConfig>()
private port: number | undefined
private address: string | undefined
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved

private static DEBUG_DEFAULT_PORT = 9229

constructor() {
super(() => {
Expand All @@ -22,59 +19,74 @@ export class TestDebugManager extends vscode.Disposable {

this.disposables.push(
vscode.debug.onDidStartDebugSession((session) => {
// the main attach session is called "Remote Process [0]"
// https://github.com/microsoft/vscode-js-debug/blob/dfceaf103ce0cb83b53f1c3d88c06b8b63cb17da/src/targets/node/nodeAttacher.ts#L89
// there are also other sessions that are spawned from the main session
// but they have different names (like workers are named [worker #id])
// I wonder how we could make it easier to make custom debug configurations here?
// All this logic exists only because when "retry" is clicked, the main debug session is
// not recreated, - instead it reattaches itself and this session is created again,
// so we need to track that to correctly restart the tests
if (!session.configuration.name.startsWith('Remote Process'))
return
const baseSession = this.getVitestSession(session)
if (!baseSession)
return
const sym = baseSession.configuration.__vitest as string
const config = this.configurations.get(sym)
if (!config)
if (!session.configuration.__vitest)
return
this.sessions.add(baseSession)
const { request, runner, token } = config
runner.runTests(request, token).catch((err: any) => {
showVitestError('Failed to debug tests', err)
})
this.sessions.add(session)
}),
vscode.debug.onDidTerminateDebugSession((session) => {
if (!session.configuration.__vitest)
return
this.sessions.delete(session)
const sym = session.configuration.__vitest as string
const config = this.configurations.get(sym)
if (!config)
return
const { api } = config
api.cancelRun()
api.stopInspect()
}),
)
}

public configure(id: string, config: VitestDebugConfig) {
this.configurations.set(id, config)
public async enable(api: VitestFolderAPI) {
await this.stop()

const config = getConfig()
this.port ??= config.debuggerPort || await getPort({ port: TestDebugManager.DEBUG_DEFAULT_PORT })
this.address ??= config.debuggerAddress
api.startInspect(this.port)
}

public async stop() {
await Promise.allSettled([...this.sessions].map(s => vscode.debug.stopDebugging(s)))
this.configurations.clear()
public async disable(api: VitestFolderAPI) {
await this.stop()
this.port = undefined
this.address = undefined
api.stopInspect()
}

private getVitestSession(session: vscode.DebugSession): vscode.DebugSession | undefined {
if (session.configuration.__vitest)
return session
if (session.parentSession)
return this.getVitestSession(session.parentSession)
public start(folder: vscode.WorkspaceFolder) {
const config = getConfig(folder)

const debugConfig = {
type: 'pwa-node',
request: 'attach',
name: 'Debug Tests',
port: this.port,
address: this.address,
autoAttachChildProcesses: true,
skipFiles: config.debugExclude,
smartStep: true,
__vitest: true,
env: {
...process.env,
VITEST_VSCODE: 'true',
},
}

log.info(`[DEBUG] Starting debugging on ${debugConfig.address || 'localhost'}:${debugConfig.port}`)

vscode.debug.startDebugging(
folder,
debugConfig,
{ suppressDebugView: true },
).then(
(fulfilled) => {
if (fulfilled)
log.info('[DEBUG] Debugging started')
else
log.error('[DEBUG] Debugging failed')
},
(err) => {
log.error('[DEBUG] Start debugging failed')
log.error(err.toString())
},
)
}

return undefined
public async stop() {
await Promise.allSettled([...this.sessions].map(s => vscode.debug.stopDebugging(s)))
}
}
60 changes: 0 additions & 60 deletions src/debug/startSession.ts

This file was deleted.

4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class VitestExtension {

const configFiles = vitest.filter(x => x.configFile && !x.workspaceFile)

// TODO: hard limit on the number of config files

if (configFiles.length > 3 && configFiles.every(c => getConfig(c.folder).disableWorkspaceWarning !== true)) {
vscode.window.showWarningMessage(
`Vitest found ${configFiles.length} config files. For better performance, consider using a workspace configuration.`,
Expand Down Expand Up @@ -146,7 +148,7 @@ class VitestExtension {
() => {},
false,
undefined,
true,
false, // disable continues debugging
)
}
debugProfile.tag = api.tag
Expand Down