Skip to content

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

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

Merged
merged 10 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 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,30 @@ 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

The [`mock-fs`](https://github.com/tschaub/mock-fs) module can be used to temporarily replace 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 make sure that you add a `teardown()` block that restores the `fs` module after each test:

```typescript
import * as chai from "chai";
import * as mockFS from "mock-fs";
import * as fs from "fs/promises";

suite("mock-fs example", () => {
teardown(() => {
mockFS.restore();
});

test("mock out a file on disk", async () => {
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");
});
});
```

## 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
2 changes: 1 addition & 1 deletion src/WorkspaceContext.ts
Original file line number Diff line number Diff line change
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.getDebugAdapterType(this.swiftVersion) !== "lldb") {
return;
}
const libPathResult = await getLLDBLibPath(this.toolchain);
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.getAdapterName(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.getAdapterName(ctx.workspaceContext.swiftVersion),
request: "launch",
sourceLanguages: ["swift"],
name: `Test ${ctx.swiftPackage.name}`,
Expand Down
42 changes: 25 additions & 17 deletions src/debugger/debugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,19 @@ import { SwiftToolchain } from "../toolchain/toolchain";
* 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
public static getAdapterName(swiftVersion: Version): "swift-lldb" | "lldb" {
return DebugAdapter.getDebugAdapterType(swiftVersion) === "lldb-dap"
? "swift-lldb"
: "lldb";
}

/** Return debug adapter for toolchain */
public static getDebugAdapterType(swiftVersion: Version): "lldb-vscode" | "lldb-dap" {
public static getDebugAdapterType(swiftVersion: Version): "lldb" | "lldb-dap" {
return swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) &&
configuration.debugger.useDebugAdapterFromToolchain
? "lldb-dap"
: "lldb-vscode";
: "lldb";
}

/** Return the path to the debug adapter */
Expand All @@ -49,26 +47,36 @@ export class DebugAdapter {
}

const debugAdapter = this.getDebugAdapterType(toolchain.swiftVersion);
if (process.platform === "darwin" && debugAdapter === "lldb-dap") {
return await toolchain.getLLDBDebugAdapter();
if (debugAdapter === "lldb-dap") {
return toolchain.getLLDBDebugAdapter();
} else {
return toolchain.getToolchainExecutable(debugAdapter);
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);
vscode.window.showErrorMessage(
Expand All @@ -77,13 +85,13 @@ export class DebugAdapter {
: `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
2 changes: 1 addition & 1 deletion src/debugger/debugAdapterFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ 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") {
if (DebugAdapter.getDebugAdapterType(this.swiftVersion) === "lldb") {
launchConfig.type = "lldb";
launchConfig.sourceLanguages = ["swift"];
}
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.getAdapterName(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.getAdapterName(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.getAdapterName(ctx.swiftVersion),
new LoggingDebugAdapterTrackerFactory()
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api |
context.subscriptions.push(workspaceContext);

// setup swift version of LLDB. Don't await on this as it can run in the background
await DebugAdapter.verifyDebugAdapterExists(workspaceContext, true);
DebugAdapter.verifyDebugAdapterExists(workspaceContext, true).catch(error => {
outputChannel.log(error);
});
workspaceContext.setLLDBVersion();

// listen for workspace folder changes and active text editor changes
Expand Down Expand Up @@ -230,7 +232,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api |
const lldbDebugAdapter = registerLLDBDebugAdapter(workspaceContext);
context.subscriptions.push(lldbDebugAdapter);

const loggingDebugAdapter = registerLoggingDebugAdapterTracker();
const loggingDebugAdapter = registerLoggingDebugAdapterTracker(workspaceContext);

// setup workspace context with initial workspace folders
workspaceContext.addWorkspaceFolders();
Expand Down
32 changes: 22 additions & 10 deletions src/toolchain/toolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,30 +397,42 @@ export class SwiftToolchain {
* search inside Xcode.
*/
private async findToolchainOrXcodeExecutable(executable: string): Promise<string> {
const toolchainExecutablePath = path.join(
this.swiftFolderPath,
process.platform === "win32" ? `${executable}.exe` : executable
);
if (process.platform === "win32") {
executable += ".exe";
}
const toolchainExecutablePath = path.join(this.swiftFolderPath, executable);

if (await pathExists(toolchainExecutablePath)) {
return toolchainExecutablePath;
}

if (process.platform !== "darwin") {
throw new Error(`Failed to find ${executable} in swift toolchain`);
throw new Error(
`Failed to find ${executable} within Swift toolchain '${this.toolchainPath}'`
);
}
return this.findXcodeExecutable(executable);
}

private async findXcodeExecutable(executable: string): Promise<string> {
const xcodeDirectory = SwiftToolchain.getXcodeDirectory(this.toolchainPath);
if (!xcodeDirectory) {
throw new Error(`Failed to find ${executable} in Swift toolchain`);
throw new Error(
`Failed to find ${executable} within Swift toolchain '${this.toolchainPath}'`
);
}
try {
const { stdout } = await execFile("xcrun", ["-find", executable], {
env: { ...process.env, DEVELOPER_DIR: xcodeDirectory },
});
return stdout.trimEnd();
} catch (error) {
let errorMessage = `Failed to find ${executable} within Xcode Swift toolchain '${xcodeDirectory}'`;
if (error instanceof Error) {
errorMessage += `:\n${error.message}`;
}
throw new Error(errorMessage);
}
const { stdout } = await execFile("xcrun", ["-find", executable], {
env: { ...process.env, DEVELOPER_DIR: xcodeDirectory },
});
return stdout.trimEnd();
}

private basePlatformDeveloperPath(): string | undefined {
Expand Down
12 changes: 11 additions & 1 deletion src/utilities/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ export function swiftLibraryPathKey(): string {
}
}

export class ExecFileError extends Error {
constructor(
public readonly causedBy: Error,
public readonly stdout: string,
public readonly stderr: string
) {
super(causedBy.message);
}
}

/**
* Asynchronous wrapper around {@link cp.execFile child_process.execFile}.
*
Expand Down Expand Up @@ -88,7 +98,7 @@ export async function execFile(
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) =>
cp.execFile(executable, args, options, (error, stdout, stderr) => {
if (error) {
reject({ error, stdout, stderr, toString: () => error.message });
reject(new ExecFileError(error, stdout, stderr));
}
resolve({ stdout, stderr });
})
Expand Down
Loading