Skip to content

Commit 5a4786f

Browse files
authored
Add docstring and unit tests for attachDebugger (#1056)
* Refactor to use showQuickPick to reduce test surface The onButtonPressed param was never utilized, so opt to use the simpler showQuickPick instead to keep things simple. Issue: #1023 * Add docstring and unit tests for attachDebugger We should hit 100% line coverage for this module now. Issue: #1023 * Fix comment that does not reflect what the test is doing
1 parent 66ca41d commit 5a4786f

File tree

7 files changed

+261
-99
lines changed

7 files changed

+261
-99
lines changed

src/commands/attachDebugger.ts

+18-25
Original file line numberDiff line numberDiff line change
@@ -14,41 +14,34 @@
1414

1515
import * as vscode from "vscode";
1616
import { WorkspaceContext } from "../WorkspaceContext";
17-
import { withQuickPick } from "../ui/QuickPick";
18-
import { execFile, getErrorDescription } from "../utilities/utilities";
17+
import { getLldbProcess } from "../debugger/lldb";
1918

2019
/**
21-
* Attach the debugger to a running process.
20+
* Attaches the LLDB debugger to a running process selected by the user.
21+
*
22+
* This function retrieves a list of processes using `getLldbProcess`, then presents
23+
* a process picker to the user. If the user selects a process, it configures LLDB
24+
* to attach to that process and starts the debugging session in VS Code.
25+
*
26+
* @param {WorkspaceContext} ctx - The workspace context, which provides access to toolchain and configuration details.
27+
* @returns {Promise<void>} - A promise that resolves when the debugger is successfully attached or the user cancels the operation.
28+
*
29+
* @throws Will display an error message if no processes are available, or if the debugger fails to attach to the selected process.
2230
*/
2331
export async function attachDebugger(ctx: WorkspaceContext) {
24-
// use LLDB to get list of processes
25-
const lldb = await ctx.toolchain.getLLDB();
26-
try {
27-
const { stdout } = await execFile(lldb, [
28-
"--batch",
29-
"--no-lldbinit",
30-
"--one-line",
31-
"platform process list --show-args --all-users",
32-
]);
33-
const entries = stdout.split("\n");
34-
const processPickItems = entries.flatMap(line => {
35-
const match = /^(\d+)\s+\d+\s+\S+\s+\S+\s+(.+)$/.exec(line);
36-
if (match) {
37-
return [{ pid: parseInt(match[1]), label: `${match[1]}: ${match[2]}` }];
38-
} else {
39-
return [];
40-
}
32+
const processPickItems = await getLldbProcess(ctx);
33+
if (processPickItems !== undefined) {
34+
const picked = await vscode.window.showQuickPick(processPickItems, {
35+
placeHolder: "Select Process",
4136
});
42-
await withQuickPick("Select Process", processPickItems, async selected => {
37+
if (picked) {
4338
const debugConfig: vscode.DebugConfiguration = {
4439
type: "swift-lldb",
4540
request: "attach",
4641
name: "Attach",
47-
pid: selected.pid,
42+
pid: picked.pid,
4843
};
4944
await vscode.debug.startDebugging(undefined, debugConfig);
50-
});
51-
} catch (error) {
52-
vscode.window.showErrorMessage(`Failed to run LLDB: ${getErrorDescription(error)}`);
45+
}
5346
}
5447
}

src/commands/switchPlatform.ts

+21-20
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,44 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import * as vscode from "vscode";
16-
import { withQuickPick } from "../ui/QuickPick";
1716
import { DarwinCompatibleTarget, SwiftToolchain } from "../toolchain/toolchain";
1817
import configuration from "../configuration";
1918

2019
/**
2120
* Switches the target SDK to the platform selected in a QuickPick UI.
2221
*/
2322
export async function switchPlatform() {
24-
await withQuickPick(
25-
"Select a new target",
23+
const picked = await vscode.window.showQuickPick(
2624
[
2725
{ value: undefined, label: "macOS" },
2826
{ value: DarwinCompatibleTarget.iOS, label: "iOS" },
2927
{ value: DarwinCompatibleTarget.tvOS, label: "tvOS" },
3028
{ value: DarwinCompatibleTarget.watchOS, label: "watchOS" },
3129
{ value: DarwinCompatibleTarget.visionOS, label: "visionOS" },
3230
],
33-
async picked => {
34-
try {
35-
const sdkForTarget = picked.value
36-
? await SwiftToolchain.getSDKForTarget(picked.value)
37-
: "";
38-
if (sdkForTarget !== undefined) {
39-
if (sdkForTarget !== "") {
40-
configuration.sdk = sdkForTarget;
41-
vscode.window.showWarningMessage(
42-
`Selecting the ${picked.label} SDK will provide code editing support, but compiling with this SDK will have undefined results.`
43-
);
44-
} else {
45-
configuration.sdk = undefined;
46-
}
31+
{
32+
placeHolder: "Select a new target",
33+
}
34+
);
35+
if (picked) {
36+
try {
37+
const sdkForTarget = picked.value
38+
? await SwiftToolchain.getSDKForTarget(picked.value)
39+
: "";
40+
if (sdkForTarget !== undefined) {
41+
if (sdkForTarget !== "") {
42+
configuration.sdk = sdkForTarget;
43+
vscode.window.showWarningMessage(
44+
`Selecting the ${picked.label} SDK will provide code editing support, but compiling with this SDK will have undefined results.`
45+
);
4746
} else {
48-
vscode.window.showErrorMessage("Unable to obtain requested SDK path");
47+
configuration.sdk = undefined;
4948
}
50-
} catch {
49+
} else {
5150
vscode.window.showErrorMessage("Unable to obtain requested SDK path");
5251
}
52+
} catch {
53+
vscode.window.showErrorMessage("Unable to obtain requested SDK path");
5354
}
54-
);
55+
}
5556
}

src/debugger/lldb.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import * as path from "path";
1919
import * as fs from "fs/promises";
20-
import { execFile } from "../utilities/utilities";
20+
import * as vscode from "vscode";
21+
import { WorkspaceContext } from "../WorkspaceContext";
22+
import { execFile, getErrorDescription } from "../utilities/utilities";
2123
import { Result } from "../utilities/result";
2224
import { SwiftToolchain } from "../toolchain/toolchain";
2325

@@ -103,3 +105,43 @@ async function findFileByPattern(path: string, pattern: RegExp): Promise<string
103105
}
104106
return null;
105107
}
108+
109+
/**
110+
* Retrieves a list of LLDB processes from the system using LLDB.
111+
*
112+
* This function executes an LLDB command to list all processes on the system,
113+
* including their arguments, and returns them in an array of objects where each
114+
* object contains the `pid` and a `label` describing the process.
115+
*
116+
* @param {WorkspaceContext} ctx - The workspace context, which includes the toolchain needed to run LLDB.
117+
* @returns {Promise<Array<{ pid: number; label: string }> | undefined>}
118+
* A promise that resolves to an array of processes, where each process is represented by an object with a `pid` and a `label`.
119+
* If an error occurs or no processes are found, it returns `undefined`.
120+
*
121+
* @throws Will display an error message in VS Code if the LLDB command fails.
122+
*/
123+
export async function getLldbProcess(
124+
ctx: WorkspaceContext
125+
): Promise<Array<{ pid: number; label: string }> | undefined> {
126+
try {
127+
// use LLDB to get list of processes
128+
const lldb = await ctx.toolchain.getLLDB();
129+
const { stdout } = await execFile(lldb, [
130+
"--batch",
131+
"--no-lldbinit",
132+
"--one-line",
133+
"platform process list --show-args --all-users",
134+
]);
135+
const entries = stdout.split("\n");
136+
return entries.flatMap(line => {
137+
const match = /^(\d+)\s+\d+\s+\S+\s+\S+\s+(.+)$/.exec(line);
138+
if (match) {
139+
return [{ pid: parseInt(match[1]), label: `${match[1]}: ${match[2]}` }];
140+
} else {
141+
return [];
142+
}
143+
});
144+
} catch (error) {
145+
vscode.window.showErrorMessage(`Failed to run LLDB: ${getErrorDescription(error)}`);
146+
}
147+
}

src/ui/QuickPick.ts

-53
This file was deleted.
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as assert from "assert";
2+
import { getLldbProcess } from "../../../src/debugger/lldb";
3+
import { SwiftToolchain } from "../../../src/toolchain/toolchain";
4+
import { WorkspaceContext } from "../../../src/WorkspaceContext";
5+
import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel";
6+
7+
suite("getLldbProcess Contract Test Suite", () => {
8+
test("happy path, make sure lldb call returns proper output", async () => {
9+
const toolchain = await SwiftToolchain.create();
10+
const workspaceContext = await WorkspaceContext.create(
11+
new SwiftOutputChannel("Swift"),
12+
toolchain
13+
);
14+
assert.notStrictEqual(await getLldbProcess(workspaceContext), []);
15+
});
16+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2021-2024 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import * as vscode from "vscode";
16+
import * as lldb from "../../../src/debugger/lldb";
17+
import { attachDebugger } from "../../../src/commands/attachDebugger";
18+
import { mockNamespace } from "../MockUtils";
19+
import {
20+
mock,
21+
instance,
22+
when,
23+
spy,
24+
verify,
25+
anything,
26+
deepEqual,
27+
objectContaining,
28+
} from "ts-mockito";
29+
import { SwiftToolchain } from "../../../src/toolchain/toolchain";
30+
import { WorkspaceContext } from "../../../src/WorkspaceContext";
31+
import { registerLLDBDebugAdapter } from "../../../src/debugger/debugAdapterFactory";
32+
33+
suite("attachDebugger Unit Test Suite", () => {
34+
const lldbSpy = spy(lldb);
35+
const mockContext = mock(WorkspaceContext);
36+
const mockToolchain = mock(SwiftToolchain);
37+
const windowMock = mockNamespace(vscode, "window");
38+
const debugMock = mockNamespace(vscode, "debug");
39+
40+
setup(() => {
41+
when(mockContext.toolchain).thenReturn(instance(mockToolchain));
42+
});
43+
44+
test("should call startDebugging with correct debugConfig", async () => {
45+
// Setup fake debug adapter
46+
registerLLDBDebugAdapter(instance(mockContext));
47+
48+
// Mock the list of processes returned by getLldbProcess
49+
const processPickItems = [
50+
{ pid: 1234, label: "1234: Process1" },
51+
{ pid: 2345, label: "2345: Process2" },
52+
];
53+
when(lldbSpy.getLldbProcess(anything())).thenResolve(processPickItems);
54+
55+
// Mock showQuickPick to return a selected process.
56+
// It's unfortunate that anthing will match the wrong function, so we have to hard code which makes the test more brittle.
57+
// So just change here when the test starts failing.
58+
when(
59+
windowMock.showQuickPick(
60+
deepEqual(processPickItems),
61+
deepEqual({ placeHolder: "Select Process" })
62+
) as Promise<(typeof processPickItems)[0]>
63+
).thenResolve(processPickItems[0]);
64+
65+
// Call attachDebugger
66+
await attachDebugger(instance(mockContext));
67+
68+
// Verify startDebugging was called with the right pid.
69+
// Integration level check needed: actual config return a fulfilled promise.
70+
verify(
71+
debugMock.startDebugging(undefined, objectContaining(processPickItems[0].pid))
72+
).once();
73+
});
74+
});

0 commit comments

Comments
 (0)