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

[macOS] Properly handle cases where lldb-dap cannot be found with xcrun #1119

Merged
merged 10 commits into from
Oct 4, 2024
Merged
39 changes: 39 additions & 0 deletions docs/contributor/writing-tests-for-vscode-swift.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A brief description of each framework can be found below:

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

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.

## Mocking the File System

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.
matthewbastien marked this conversation as resolved.
Show resolved Hide resolved

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:

```typescript
import * as chai from "chai";
import * as mockFS from "mock-fs";
import * as fs from "fs/promises";
matthewbastien marked this conversation as resolved.
Show resolved Hide resolved

suite("mock-fs example", () => {
// This teardown step is also important to make sure your tests clean up the
// mocked file system when they complete!
teardown(() => {
mockFS.restore();
matthewbastien marked this conversation as resolved.
Show resolved Hide resolved
});

test("mock out a file on disk", async () => {
// A single function call can be used to configure the file system
mockFS({
"/path/to/some/file": "Some really cool file contents",
});
await expect(fs.readFile("/path/to/some/file", "utf-8"))
.to.eventually.equal("Some really cool file contents");
});
});
```

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:

```typescript
test("file is not readable by the current user", async () => {
mockFS({ "/path/to/file": mockFS.file({ mode: 0o000 }) });
await expect(fs.readFile("/path/to/file", "utf-8")).to.eventually.be.rejected;
});
```

## Mocking Utilities

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.
Expand Down
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,7 @@
"@types/glob": "^7.1.6",
"@types/lcov-parse": "^1.0.2",
"@types/mocha": "^10.0.7",
"@types/mock-fs": "^4.13.4",
"@types/node": "^18.19.39",
"@types/plist": "^3.0.5",
"@types/sinon": "^17.0.3",
Expand All @@ -1316,6 +1317,7 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"mocha": "^10.6.0",
"mock-fs": "^5.3.0",
"node-pty": "^1.0.0",
"prettier": "3.3.2",
"sinon": "^19.0.2",
Expand Down
4 changes: 2 additions & 2 deletions src/WorkspaceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import configuration from "./configuration";
import contextKeys from "./contextKeys";
import { setSnippetContextKey } from "./SwiftSnippets";
import { CommentCompletionProviders } from "./editor/CommentCompletion";
import { DebugAdapter } from "./debugger/debugAdapter";
import { DebugAdapter, LaunchConfigType } from "./debugger/debugAdapter";
import { SwiftBuildStatus } from "./ui/SwiftBuildStatus";
import { SwiftToolchain } from "./toolchain/toolchain";
import { DiagnosticsManager } from "./DiagnosticsManager";
Expand Down Expand Up @@ -421,7 +421,7 @@ export class WorkspaceContext implements vscode.Disposable {
/** find LLDB version and setup path in CodeLLDB */
async setLLDBVersion() {
// check we are using CodeLLDB
if (DebugAdapter.getDebugAdapterType(this.swiftVersion) !== "lldb-vscode") {
if (DebugAdapter.getLaunchConfigType(this.swiftVersion) !== LaunchConfigType.CODE_LLDB) {
return;
}
const libPathResult = await getLLDBLibPath(this.toolchain);
Expand Down
3 changes: 2 additions & 1 deletion src/commands/attachDebugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import * as vscode from "vscode";
import { WorkspaceContext } from "../WorkspaceContext";
import { getLldbProcess } from "../debugger/lldb";
import { LaunchConfigType } from "../debugger/debugAdapter";

/**
* Attaches the LLDB debugger to a running process selected by the user.
Expand All @@ -36,7 +37,7 @@ export async function attachDebugger(ctx: WorkspaceContext) {
});
if (picked) {
const debugConfig: vscode.DebugConfiguration = {
type: "swift-lldb",
type: LaunchConfigType.SWIFT_EXTENSION,
request: "attach",
name: "Attach",
pid: picked.pid,
Expand Down
4 changes: 2 additions & 2 deletions src/debugger/buildConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ export class TestingConfigurationFactory {
}).map(([key, value]) => `settings set target.env-vars ${key}="${value}"`);

return {
type: DebugAdapter.adapterName,
type: DebugAdapter.getLaunchConfigType(this.ctx.workspaceContext.swiftVersion),
request: "custom",
name: `Test ${this.ctx.swiftPackage.name}`,
targetCreateCommands: [`file -a ${arch} ${xctestPath}/xctest`],
Expand Down Expand Up @@ -638,7 +638,7 @@ export class TestingConfigurationFactory {
function getBaseConfig(ctx: FolderContext, expandEnvVariables: boolean) {
const { folder, nameSuffix } = getFolderAndNameSuffix(ctx, expandEnvVariables);
return {
type: DebugAdapter.adapterName,
type: DebugAdapter.getLaunchConfigType(ctx.workspaceContext.swiftVersion),
request: "launch",
sourceLanguages: ["swift"],
name: `Test ${ctx.swiftPackage.name}`,
Expand Down
70 changes: 43 additions & 27 deletions src/debugger/debugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,30 @@ import { Version } from "../utilities/version";
import { WorkspaceContext } from "../WorkspaceContext";
import { SwiftToolchain } from "../toolchain/toolchain";

/**
* The supported {@link vscode.DebugConfiguration.type Debug Configuration Type} for auto-generation of launch configurations
*/
export const enum LaunchConfigType {
SWIFT_EXTENSION = "swift-lldb",
CODE_LLDB = "lldb",
}

/**
* Class managing which debug adapter we are using. Will only setup lldb-vscode/lldb-dap if it is available.
*/
export class DebugAdapter {
private static debugAdapaterExists = false;

/** Debug adapter name */
public static get adapterName(): string {
return configuration.debugger.useDebugAdapterFromToolchain && this.debugAdapaterExists
? "swift-lldb"
: "lldb";
}

/** Return debug adapter for toolchain */
public static getDebugAdapterType(swiftVersion: Version): "lldb-vscode" | "lldb-dap" {
/**
* Return the launch configuration type for the given Swift version. This also takes
* into account user settings when determining which launch configuration to use.
*
* @param swiftVersion the version of the Swift toolchain
* @returns the type of launch configuration used by the given Swift toolchain version
*/
public static getLaunchConfigType(swiftVersion: Version): LaunchConfigType {
return swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) &&
configuration.debugger.useDebugAdapterFromToolchain
? "lldb-dap"
: "lldb-vscode";
? LaunchConfigType.SWIFT_EXTENSION
: LaunchConfigType.CODE_LLDB;
}

/** Return the path to the debug adapter */
Expand All @@ -48,42 +53,53 @@ export class DebugAdapter {
return customDebugAdapterPath;
}

const debugAdapter = this.getDebugAdapterType(toolchain.swiftVersion);
if (process.platform === "darwin" && debugAdapter === "lldb-dap") {
return await toolchain.getLLDBDebugAdapter();
} else {
return toolchain.getToolchainExecutable(debugAdapter);
const debugAdapter = this.getLaunchConfigType(toolchain.swiftVersion);
switch (debugAdapter) {
case LaunchConfigType.SWIFT_EXTENSION:
return toolchain.getLLDBDebugAdapter();
case LaunchConfigType.CODE_LLDB:
return toolchain.getLLDB();
}
}

/**
* Verify that the toolchain debug adapter exists
* Verify that the toolchain debug adapter exists and display an error message to the user
* if it doesn't.
*
* Has the side effect of setting the `swift.lldbVSCodeAvailable` context key depending
* on the result.
*
* @param workspace WorkspaceContext
* @param quiet Should dialog be displayed
* @returns Is debugger available
* @param quiet Whether or not the dialog should be displayed if the adapter does not exist
* @returns Whether or not the debug adapter exists
*/
public static async verifyDebugAdapterExists(
workspace: WorkspaceContext,
quiet = false
): Promise<boolean> {
const lldbDebugAdapterPath = await this.debugAdapterPath(workspace.toolchain);
const lldbDebugAdapterPath = await this.debugAdapterPath(workspace.toolchain).catch(
error => {
workspace.outputChannel.log(error);
return undefined;
}
);

if (!(await fileExists(lldbDebugAdapterPath))) {
if (!lldbDebugAdapterPath || !(await fileExists(lldbDebugAdapterPath))) {
if (!quiet) {
const debugAdapterName = this.getDebugAdapterType(workspace.toolchain.swiftVersion);
const debugAdapterName = this.getLaunchConfigType(workspace.toolchain.swiftVersion);
vscode.window.showErrorMessage(
configuration.debugger.customDebugAdapterPath.length > 0
? `Cannot find ${debugAdapterName} debug adapter specified in setting Swift.Debugger.Path.`
: `Cannot find ${debugAdapterName} debug adapter in your Swift toolchain.`
);
}
workspace.outputChannel.log(`Failed to find ${lldbDebugAdapterPath}`);
this.debugAdapaterExists = false;
if (lldbDebugAdapterPath) {
workspace.outputChannel.log(`Failed to find ${lldbDebugAdapterPath}`);
}
contextKeys.lldbVSCodeAvailable = false;
return false;
}

this.debugAdapaterExists = true;
contextKeys.lldbVSCodeAvailable = true;
return true;
}
Expand Down
10 changes: 5 additions & 5 deletions src/debugger/debugAdapterFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
import * as vscode from "vscode";
import * as path from "path";
import { WorkspaceContext } from "../WorkspaceContext";
import { DebugAdapter } from "./debugAdapter";
import { DebugAdapter, LaunchConfigType } from "./debugAdapter";
import { Version } from "../utilities/version";

export function registerLLDBDebugAdapter(workspaceContext: WorkspaceContext): vscode.Disposable {
const debugAdpaterFactory = vscode.debug.registerDebugAdapterDescriptorFactory(
"swift-lldb",
LaunchConfigType.SWIFT_EXTENSION,
new LLDBDebugAdapterExecutableFactory(workspaceContext)
);
const debugConfigProvider = vscode.debug.registerDebugConfigurationProvider(
"swift-lldb",
LaunchConfigType.SWIFT_EXTENSION,
new LLDBDebugConfigurationProvider(
process.platform,
workspaceContext.toolchain.swiftVersion
Expand Down Expand Up @@ -102,8 +102,8 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration
}

// Delegate to CodeLLDB if that's the debug adapter we have selected
if (DebugAdapter.getDebugAdapterType(this.swiftVersion) === "lldb-vscode") {
launchConfig.type = "lldb";
if (DebugAdapter.getLaunchConfigType(this.swiftVersion) === LaunchConfigType.CODE_LLDB) {
launchConfig.type = LaunchConfigType.CODE_LLDB;
launchConfig.sourceLanguages = ["swift"];
}
return launchConfig;
Expand Down
4 changes: 2 additions & 2 deletions src/debugger/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function createExecutableConfigurations(ctx: FolderContext): vscode.DebugConfigu
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true, "posix");
return executableProducts.flatMap(product => {
const baseConfig = {
type: DebugAdapter.adapterName,
type: DebugAdapter.getLaunchConfigType(ctx.workspaceContext.swiftVersion),
request: "launch",
args: [],
cwd: folder,
Expand Down Expand Up @@ -155,7 +155,7 @@ export function createSnippetConfiguration(
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true);

return {
type: DebugAdapter.adapterName,
type: DebugAdapter.getLaunchConfigType(ctx.workspaceContext.swiftVersion),
request: "launch",
name: `Run ${snippetName}`,
program: path.posix.join(buildDirectory, "debug", snippetName),
Expand Down
5 changes: 3 additions & 2 deletions src/debugger/logTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import * as vscode from "vscode";
import { DebugAdapter } from "./debugAdapter";
import { WorkspaceContext } from "../WorkspaceContext";

/**
* Factory class for building LoggingDebugAdapterTracker
Expand All @@ -38,9 +39,9 @@ interface DebugMessage {
body: OutputEventBody;
}

export function registerLoggingDebugAdapterTracker(): vscode.Disposable {
export function registerLoggingDebugAdapterTracker(ctx: WorkspaceContext): vscode.Disposable {
return vscode.debug.registerDebugAdapterTrackerFactory(
DebugAdapter.adapterName,
DebugAdapter.getLaunchConfigType(ctx.swiftVersion),
new LoggingDebugAdapterTrackerFactory()
);
}
Expand Down
Loading