Skip to content

Commit

Permalink
Add debug support in Test Explorer (#29)
Browse files Browse the repository at this point in the history
Add debug support to the test explorer.

When this feature is enabled (via `bazelbsp.debug.enabled` setting), an
additional debug run option will appear on each test item.

Overall flow:
- When a debug button for a test case is clicked, run is trigged as
usual, with added `additionalBazelParams` (now supported by bazel-bsp
server). These flags are configurable based on
`bazelbsp.debug.bazelFlags` and should include any flags needed to get
Bazel to build and launch the target in debug mode.
- As the build runs, messages are watched for a debug ready message. The
pattern to watch for is configurable in `bazelbsp.debug.readyPattern`.
- Finally, once the pattern is matched, the VS Code launch configuration
is triggered. This allows use of an existing launch configuration in the
workspace (which can be set via settings, workspace, launch.json, etc),
configurable via `bazelbsp.debug.launchConfigName`.

If any of the settings are not properly set, the run will still attempt
to proceed but print a warning in the output.
  • Loading branch information
mnoah1 authored Dec 26, 2024
1 parent 8c6eda0 commit 0c2fd00
Show file tree
Hide file tree
Showing 8 changed files with 412 additions and 1 deletion.
17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@
"type": "boolean",
"default": "true",
"description": "Find all tests within open files, without waiting for the file's target to be expanded in the Test Explorer."
},
"bazelbsp.debug.enabled": {
"type": "boolean",
"default": "false",
"markdownDescription": "Enable debugging integration in the Test Explorer. This adds an additional Debug run profile for each test item.\nSet the bazelFlags, profileName, and readyPattern settings in this section to match your repo's required behavior."
},
"bazelbsp.debug.bazelFlags": {
"type": "array",
"description": "Flags to be added when debugging a target. Include any flags needed to ensure Bazel builds and runs the target in debug mode."
},
"bazelbsp.debug.readyPattern": {
"type": "string",
"description": "Regex pattern in the console output that signals that the target is ready for a debugger to connect. Once this is seen, the configured launch configuration will be triggered."
},
"bazelbsp.debug.launchConfigName": {
"type": "string",
"description": "Name of launch configuration that will be executed to begin the DAP debugging session. This must be a valid launch configuration in the launch.json file, workspace, or contributed by another extension."
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/bsp/bsp-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export namespace TestParamsDataKind {
export interface BazelTestParamsData {
coverage?: boolean
testFilter?: string
additionalBazelParams?: string
}

export namespace OnBuildPublishOutput {
Expand Down
11 changes: 11 additions & 0 deletions src/test-info/test-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {BuildTarget, TestParams, TestResult, StatusCode} from '../bsp/bsp'
import {TestParamsDataKind, BazelTestParamsData} from '../bsp/bsp-ext'
import {TestCaseStatus, TestRunTracker} from '../test-runner/run-tracker'
import {DocumentTestItem, LanguageToolManager} from '../language-tools/manager'
import {getExtensionSetting, SettingName} from '../utils/settings'

export enum TestItemType {
Root,
Expand Down Expand Up @@ -113,6 +114,16 @@ export class BuildTargetTestCaseInfo extends TestCaseInfo {
coverage:
currentRun.getRunProfileKind() === vscode.TestRunProfileKind.Coverage,
}

// Includes additional debug-specific flags when necessary.
if (currentRun.getRunProfileKind() === vscode.TestRunProfileKind.Debug) {
const configuredFlags = currentRun.getDebugBazelFlags()
if (configuredFlags && configuredFlags.length > 0) {
// Bazel BSP accepts whitespace separated list of flags.
bazelParams.additionalBazelParams = configuredFlags.join(' ')
}
}

const params = {
targets: [this.target.id],
originId: currentRun.originName,
Expand Down
94 changes: 94 additions & 0 deletions src/test-runner/run-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {CoverageTracker} from '../coverage-utils/coverage-tracker'
import {LanguageToolManager} from '../language-tools/manager'
import {TaskEventTracker} from './task-events'
import {ANSI_CODES} from '../utils/utils'
import {getExtensionSetting, SettingName} from '../utils/settings'

export enum TestCaseStatus {
Pending,
Expand All @@ -44,6 +45,12 @@ export interface RunTrackerParams {
languageToolManager: LanguageToolManager
}

type DebugInfo = {
debugFlags?: string[]
launchConfig?: vscode.DebugConfiguration
readyPattern?: RegExp
}

export class TestRunTracker implements TaskOriginHandlers {
// All tests that are included in this run. See iterator definition below.
private allTests: Map<TestItemType, TestCaseInfo[]>
Expand All @@ -63,6 +70,7 @@ export class TestRunTracker implements TaskOriginHandlers {
private languageToolManager: LanguageToolManager
private pending: Thenable<void>[] = []
private buildTaskTracker: TaskEventTracker = new TaskEventTracker()
private debugInfo: DebugInfo | undefined

constructor(params: RunTrackerParams) {
this.allTests = new Map<TestItemType, TestCaseInfo[]>()
Expand All @@ -76,6 +84,7 @@ export class TestRunTracker implements TaskOriginHandlers {
this.languageToolManager = params.languageToolManager

this.prepareCurrentRun()
this.prepareDebugInfo()
}

public get originName(): string {
Expand Down Expand Up @@ -228,6 +237,21 @@ export class TestRunTracker implements TaskOriginHandlers {
this.run.appendOutput(params.message)
this.run.appendOutput('\n\r')
}

// During debug runs, watch each message for indication of debug readiness.
// If the message matches the configured pattern, start the debug session.
if (
this.debugInfo?.launchConfig &&
this.debugInfo.readyPattern?.test(params.message)
) {
this.run.appendOutput(
`Starting remote debug session [Launch config: '${this.debugInfo.launchConfig.name}']\r\n`
)
vscode.debug.startDebugging(
vscode.workspace.workspaceFolders?.[0],
this.debugInfo.launchConfig
)
}
}

/**
Expand All @@ -254,6 +278,10 @@ export class TestRunTracker implements TaskOriginHandlers {
return this.request.profile?.kind
}

public getDebugBazelFlags(): string[] | undefined {
return this.debugInfo?.debugFlags
}

/**
* Collects and stores the parents and all children to be included in this test run.
* Populates maps to group the test items by their TestItemType and current status.
Expand Down Expand Up @@ -284,6 +312,72 @@ export class TestRunTracker implements TaskOriginHandlers {
}
}

/**
* During debug runs, this collects and stores the necessary settings that will be applied through this run.
* In the event that a setting is not found, information will be printed with the test output, but the run will still attempt to proceed.
*/
private prepareDebugInfo() {
if (this.getRunProfileKind() !== vscode.TestRunProfileKind.Debug) {
return
}

// Determine configured launch configuration name.
const configName = getExtensionSetting(SettingName.LAUNCH_CONFIG_NAME)
if (!configName) {
this.run.appendOutput(
'No launch configuration name is configured. Debugger will not connect automatically for this run.\r\n'
)
this.run.appendOutput(
'Check the `bazelbsp.debug.profileName` VS Code setting to ensure it corresponds to a valid launch configuration.\r\n'
)
return
}

// Store the selected launch configuration.
const launchConfigurations = vscode.workspace.getConfiguration('launch')
const configurations =
launchConfigurations.get<any[]>('configurations') || []
const selectedConfig = configurations.find(
config => config.name !== undefined && config.name === configName
)
if (!selectedConfig) {
this.run.appendOutput(
`Unable to find debug profile ${configName}. Debugger will not connect automatically for this run.\r\n`
)
this.run.appendOutput(
'Check the `bazelbsp.debug.profileName` VS Code setting to ensure it corresponds to a valid launch configuration.\r\n'
)
}

// Ensure that matcher pattern is set for the output.
const readyPattern = getExtensionSetting(SettingName.DEBUG_READY_PATTERN)
if (!readyPattern) {
this.run.appendOutput(
'No matcher pattern is set. Debugger will not connect automatically for this run.\r\n'
)
this.run.appendOutput(
'Check the `bazelbsp.debug.readyPattern` VS Code setting to ensure that a pattern is set.\r\n'
)
}

// Ensure that matcher pattern is set for the output.
let debugFlags = getExtensionSetting(SettingName.DEBUG_BAZEL_FLAGS)
if (!debugFlags) {
this.run.appendOutput(
'No additional debug-specific Bazel flags have been found for this run.\r\n'
)
this.run.appendOutput(
'Check the `bazelbsp.debug.bazelFlags` VS Code setting to ensure that necessary flags are set.\r\n'
)
}

this.debugInfo = {
debugFlags: debugFlags,
launchConfig: selectedConfig,
readyPattern: readyPattern ? new RegExp(readyPattern) : undefined,
}
}

/**
* Iterate recursively through all children of the given test item, and collect them in the destination map.
* @param destination Map to be populated with the collected test items, grouped by TestItemType.
Expand Down
12 changes: 12 additions & 0 deletions src/test-runner/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {MessageConnection} from 'vscode-jsonrpc'
import {TestRunTracker} from './run-tracker'
import {RunTrackerFactory} from './run-factory'
import {CoverageTracker} from '../coverage-utils/coverage-tracker'
import {getExtensionSetting, SettingName} from '../utils/settings'

@Injectable()
export class TestRunner implements OnModuleInit, vscode.Disposable {
Expand Down Expand Up @@ -53,6 +54,17 @@ export class TestRunner implements OnModuleInit, vscode.Disposable {
this.runProfiles.set(vscode.TestRunProfileKind.Coverage, coverageRunProfile)
coverageRunProfile.loadDetailedCoverage =
this.coverageTracker.loadDetailedCoverage.bind(this.coverageTracker)

// Debug run profile, added only when enabled.
if (getExtensionSetting(SettingName.DEBUG_ENABLED)) {
const debugRunProfile =
this.testCaseStore.testController.createRunProfile(
'Run with Debug',
vscode.TestRunProfileKind.Debug,
this.runHandler.bind(this)
)
this.runProfiles.set(vscode.TestRunProfileKind.Debug, debugRunProfile)
}
}

private async runHandler(
Expand Down
Loading

0 comments on commit 0c2fd00

Please sign in to comment.