Skip to content

Commit b69ee94

Browse files
[macOS] Properly handle cases where lldb-dap cannot be found with xcrun (#1119)
1 parent f4d6bb0 commit b69ee94

16 files changed

+632
-123
lines changed

docs/contributor/writing-tests-for-vscode-swift.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ A brief description of each framework can be found below:
1212

1313
- [Organizing Tests](#organizing-tests)
1414
- [Writing Unit Tests](#writing-unit-tests)
15+
- [Mocking the File System](#mocking-the-file-system)
1516
- [Mocking Utilities](#mocking-utilities)
1617
- [Mocking interfaces, classes, and functions](#mocking-interfaces-classes-and-functions)
1718
- [Mocking VS Code events](#mocking-vs-code-events)
@@ -113,6 +114,44 @@ suite("ReloadExtension Unit Test Suite", () => {
113114

114115
You may have also noticed that we needed to cast the `"Reload Extensions"` string to `any` when resolving `showWarningMessage()`. Unforunately, this may be necessary for methods that have incompatible overloaded signatures due to a TypeScript issue that remains unfixed.
115116

117+
## Mocking the File System
118+
119+
Mocking file system access can be a challenging endeavor that is prone to fail when implementation details of the unit under test change. This is because there are many different ways of accessing and manipulating files, making it almost impossible to catch all possible failure paths. For example, you could check for file existence using `fs.stat()` or simply call `fs.readFile()` and catch errors with a single function call. Using the real file system is slow and requires extra setup code in test cases to configure.
120+
121+
The [`mock-fs`](https://github.com/tschaub/mock-fs) module is a well-maintained library that can be used to mitigate these issues by temporarily replacing Node's built-in `fs` module with an in-memory file system. This can be useful for testing logic that uses the `fs` module without actually reaching out to the file system. Just a single function call can be used to configure what the fake file system will contain:
122+
123+
```typescript
124+
import * as chai from "chai";
125+
import * as mockFS from "mock-fs";
126+
import * as fs from "fs/promises";
127+
128+
suite("mock-fs example", () => {
129+
// This teardown step is also important to make sure your tests clean up the
130+
// mocked file system when they complete!
131+
teardown(() => {
132+
mockFS.restore();
133+
});
134+
135+
test("mock out a file on disk", async () => {
136+
// A single function call can be used to configure the file system
137+
mockFS({
138+
"/path/to/some/file": "Some really cool file contents",
139+
});
140+
await expect(fs.readFile("/path/to/some/file", "utf-8"))
141+
.to.eventually.equal("Some really cool file contents");
142+
});
143+
});
144+
```
145+
146+
In order to test failure paths, you can either create an empty file system or use `mockFS.file()` to set the mode to make a file that is not accessible to the current user:
147+
148+
```typescript
149+
test("file is not readable by the current user", async () => {
150+
mockFS({ "/path/to/file": mockFS.file({ mode: 0o000 }) });
151+
await expect(fs.readFile("/path/to/file", "utf-8")).to.eventually.be.rejected;
152+
});
153+
```
154+
116155
## Mocking Utilities
117156

118157
This section outlines the various utilities that can be used to improve the readability of your tests. The [MockUtils](../../test/MockUtils.ts) module can be used to perform more advanced mocking than what Sinon provides out of the box. This module has its [own set of tests](../../test/unit-tests/MockUtils.test.ts) that you can use to get a feel for how it works.

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,7 @@
12971297
"@types/glob": "^7.1.6",
12981298
"@types/lcov-parse": "^1.0.2",
12991299
"@types/mocha": "^10.0.7",
1300+
"@types/mock-fs": "^4.13.4",
13001301
"@types/node": "^18.19.39",
13011302
"@types/plist": "^3.0.5",
13021303
"@types/sinon": "^17.0.3",
@@ -1316,6 +1317,7 @@
13161317
"eslint": "^8.57.0",
13171318
"eslint-config-prettier": "^9.1.0",
13181319
"mocha": "^10.6.0",
1320+
"mock-fs": "^5.3.0",
13191321
"node-pty": "^1.0.0",
13201322
"prettier": "3.3.2",
13211323
"sinon": "^19.0.2",

src/WorkspaceContext.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import configuration from "./configuration";
2929
import contextKeys from "./contextKeys";
3030
import { setSnippetContextKey } from "./SwiftSnippets";
3131
import { CommentCompletionProviders } from "./editor/CommentCompletion";
32-
import { DebugAdapter } from "./debugger/debugAdapter";
32+
import { DebugAdapter, LaunchConfigType } from "./debugger/debugAdapter";
3333
import { SwiftBuildStatus } from "./ui/SwiftBuildStatus";
3434
import { SwiftToolchain } from "./toolchain/toolchain";
3535
import { DiagnosticsManager } from "./DiagnosticsManager";
@@ -421,7 +421,7 @@ export class WorkspaceContext implements vscode.Disposable {
421421
/** find LLDB version and setup path in CodeLLDB */
422422
async setLLDBVersion() {
423423
// check we are using CodeLLDB
424-
if (DebugAdapter.getDebugAdapterType(this.swiftVersion) !== "lldb-vscode") {
424+
if (DebugAdapter.getLaunchConfigType(this.swiftVersion) !== LaunchConfigType.CODE_LLDB) {
425425
return;
426426
}
427427
const libPathResult = await getLLDBLibPath(this.toolchain);

src/commands/attachDebugger.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import * as vscode from "vscode";
1616
import { WorkspaceContext } from "../WorkspaceContext";
1717
import { getLldbProcess } from "../debugger/lldb";
18+
import { LaunchConfigType } from "../debugger/debugAdapter";
1819

1920
/**
2021
* Attaches the LLDB debugger to a running process selected by the user.
@@ -36,7 +37,7 @@ export async function attachDebugger(ctx: WorkspaceContext) {
3637
});
3738
if (picked) {
3839
const debugConfig: vscode.DebugConfiguration = {
39-
type: "swift-lldb",
40+
type: LaunchConfigType.SWIFT_EXTENSION,
4041
request: "attach",
4142
name: "Attach",
4243
pid: picked.pid,

src/debugger/buildConfig.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ export class TestingConfigurationFactory {
430430
}).map(([key, value]) => `settings set target.env-vars ${key}="${value}"`);
431431

432432
return {
433-
type: DebugAdapter.adapterName,
433+
type: DebugAdapter.getLaunchConfigType(this.ctx.workspaceContext.swiftVersion),
434434
request: "custom",
435435
name: `Test ${this.ctx.swiftPackage.name}`,
436436
targetCreateCommands: [`file -a ${arch} ${xctestPath}/xctest`],
@@ -638,7 +638,7 @@ export class TestingConfigurationFactory {
638638
function getBaseConfig(ctx: FolderContext, expandEnvVariables: boolean) {
639639
const { folder, nameSuffix } = getFolderAndNameSuffix(ctx, expandEnvVariables);
640640
return {
641-
type: DebugAdapter.adapterName,
641+
type: DebugAdapter.getLaunchConfigType(ctx.workspaceContext.swiftVersion),
642642
request: "launch",
643643
sourceLanguages: ["swift"],
644644
name: `Test ${ctx.swiftPackage.name}`,

src/debugger/debugAdapter.ts

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,30 @@ import { Version } from "../utilities/version";
2020
import { WorkspaceContext } from "../WorkspaceContext";
2121
import { SwiftToolchain } from "../toolchain/toolchain";
2222

23+
/**
24+
* The supported {@link vscode.DebugConfiguration.type Debug Configuration Type} for auto-generation of launch configurations
25+
*/
26+
export const enum LaunchConfigType {
27+
SWIFT_EXTENSION = "swift-lldb",
28+
CODE_LLDB = "lldb",
29+
}
30+
2331
/**
2432
* Class managing which debug adapter we are using. Will only setup lldb-vscode/lldb-dap if it is available.
2533
*/
2634
export class DebugAdapter {
27-
private static debugAdapaterExists = false;
28-
29-
/** Debug adapter name */
30-
public static get adapterName(): string {
31-
return configuration.debugger.useDebugAdapterFromToolchain && this.debugAdapaterExists
32-
? "swift-lldb"
33-
: "lldb";
34-
}
35-
36-
/** Return debug adapter for toolchain */
37-
public static getDebugAdapterType(swiftVersion: Version): "lldb-vscode" | "lldb-dap" {
35+
/**
36+
* Return the launch configuration type for the given Swift version. This also takes
37+
* into account user settings when determining which launch configuration to use.
38+
*
39+
* @param swiftVersion the version of the Swift toolchain
40+
* @returns the type of launch configuration used by the given Swift toolchain version
41+
*/
42+
public static getLaunchConfigType(swiftVersion: Version): LaunchConfigType {
3843
return swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) &&
3944
configuration.debugger.useDebugAdapterFromToolchain
40-
? "lldb-dap"
41-
: "lldb-vscode";
45+
? LaunchConfigType.SWIFT_EXTENSION
46+
: LaunchConfigType.CODE_LLDB;
4247
}
4348

4449
/** Return the path to the debug adapter */
@@ -48,42 +53,53 @@ export class DebugAdapter {
4853
return customDebugAdapterPath;
4954
}
5055

51-
const debugAdapter = this.getDebugAdapterType(toolchain.swiftVersion);
52-
if (process.platform === "darwin" && debugAdapter === "lldb-dap") {
53-
return await toolchain.getLLDBDebugAdapter();
54-
} else {
55-
return toolchain.getToolchainExecutable(debugAdapter);
56+
const debugAdapter = this.getLaunchConfigType(toolchain.swiftVersion);
57+
switch (debugAdapter) {
58+
case LaunchConfigType.SWIFT_EXTENSION:
59+
return toolchain.getLLDBDebugAdapter();
60+
case LaunchConfigType.CODE_LLDB:
61+
return toolchain.getLLDB();
5662
}
5763
}
5864

5965
/**
60-
* Verify that the toolchain debug adapter exists
66+
* Verify that the toolchain debug adapter exists and display an error message to the user
67+
* if it doesn't.
68+
*
69+
* Has the side effect of setting the `swift.lldbVSCodeAvailable` context key depending
70+
* on the result.
71+
*
6172
* @param workspace WorkspaceContext
62-
* @param quiet Should dialog be displayed
63-
* @returns Is debugger available
73+
* @param quiet Whether or not the dialog should be displayed if the adapter does not exist
74+
* @returns Whether or not the debug adapter exists
6475
*/
6576
public static async verifyDebugAdapterExists(
6677
workspace: WorkspaceContext,
6778
quiet = false
6879
): Promise<boolean> {
69-
const lldbDebugAdapterPath = await this.debugAdapterPath(workspace.toolchain);
80+
const lldbDebugAdapterPath = await this.debugAdapterPath(workspace.toolchain).catch(
81+
error => {
82+
workspace.outputChannel.log(error);
83+
return undefined;
84+
}
85+
);
7086

71-
if (!(await fileExists(lldbDebugAdapterPath))) {
87+
if (!lldbDebugAdapterPath || !(await fileExists(lldbDebugAdapterPath))) {
7288
if (!quiet) {
73-
const debugAdapterName = this.getDebugAdapterType(workspace.toolchain.swiftVersion);
89+
const debugAdapterName = this.getLaunchConfigType(workspace.toolchain.swiftVersion);
7490
vscode.window.showErrorMessage(
7591
configuration.debugger.customDebugAdapterPath.length > 0
7692
? `Cannot find ${debugAdapterName} debug adapter specified in setting Swift.Debugger.Path.`
7793
: `Cannot find ${debugAdapterName} debug adapter in your Swift toolchain.`
7894
);
7995
}
80-
workspace.outputChannel.log(`Failed to find ${lldbDebugAdapterPath}`);
81-
this.debugAdapaterExists = false;
96+
if (lldbDebugAdapterPath) {
97+
workspace.outputChannel.log(`Failed to find ${lldbDebugAdapterPath}`);
98+
}
8299
contextKeys.lldbVSCodeAvailable = false;
83100
return false;
84101
}
85102

86-
this.debugAdapaterExists = true;
87103
contextKeys.lldbVSCodeAvailable = true;
88104
return true;
89105
}

src/debugger/debugAdapterFactory.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@
1515
import * as vscode from "vscode";
1616
import * as path from "path";
1717
import { WorkspaceContext } from "../WorkspaceContext";
18-
import { DebugAdapter } from "./debugAdapter";
18+
import { DebugAdapter, LaunchConfigType } from "./debugAdapter";
1919
import { Version } from "../utilities/version";
2020

2121
export function registerLLDBDebugAdapter(workspaceContext: WorkspaceContext): vscode.Disposable {
2222
const debugAdpaterFactory = vscode.debug.registerDebugAdapterDescriptorFactory(
23-
"swift-lldb",
23+
LaunchConfigType.SWIFT_EXTENSION,
2424
new LLDBDebugAdapterExecutableFactory(workspaceContext)
2525
);
2626
const debugConfigProvider = vscode.debug.registerDebugConfigurationProvider(
27-
"swift-lldb",
27+
LaunchConfigType.SWIFT_EXTENSION,
2828
new LLDBDebugConfigurationProvider(
2929
process.platform,
3030
workspaceContext.toolchain.swiftVersion
@@ -102,8 +102,8 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration
102102
}
103103

104104
// Delegate to CodeLLDB if that's the debug adapter we have selected
105-
if (DebugAdapter.getDebugAdapterType(this.swiftVersion) === "lldb-vscode") {
106-
launchConfig.type = "lldb";
105+
if (DebugAdapter.getLaunchConfigType(this.swiftVersion) === LaunchConfigType.CODE_LLDB) {
106+
launchConfig.type = LaunchConfigType.CODE_LLDB;
107107
launchConfig.sourceLanguages = ["swift"];
108108
}
109109
return launchConfig;

src/debugger/launch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ function createExecutableConfigurations(ctx: FolderContext): vscode.DebugConfigu
118118
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true, "posix");
119119
return executableProducts.flatMap(product => {
120120
const baseConfig = {
121-
type: DebugAdapter.adapterName,
121+
type: DebugAdapter.getLaunchConfigType(ctx.workspaceContext.swiftVersion),
122122
request: "launch",
123123
args: [],
124124
cwd: folder,
@@ -155,7 +155,7 @@ export function createSnippetConfiguration(
155155
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true);
156156

157157
return {
158-
type: DebugAdapter.adapterName,
158+
type: DebugAdapter.getLaunchConfigType(ctx.workspaceContext.swiftVersion),
159159
request: "launch",
160160
name: `Run ${snippetName}`,
161161
program: path.posix.join(buildDirectory, "debug", snippetName),

src/debugger/logTracker.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import * as vscode from "vscode";
1616
import { DebugAdapter } from "./debugAdapter";
17+
import { WorkspaceContext } from "../WorkspaceContext";
1718

1819
/**
1920
* Factory class for building LoggingDebugAdapterTracker
@@ -38,9 +39,9 @@ interface DebugMessage {
3839
body: OutputEventBody;
3940
}
4041

41-
export function registerLoggingDebugAdapterTracker(): vscode.Disposable {
42+
export function registerLoggingDebugAdapterTracker(ctx: WorkspaceContext): vscode.Disposable {
4243
return vscode.debug.registerDebugAdapterTrackerFactory(
43-
DebugAdapter.adapterName,
44+
DebugAdapter.getLaunchConfigType(ctx.swiftVersion),
4445
new LoggingDebugAdapterTrackerFactory()
4546
);
4647
}

0 commit comments

Comments
 (0)