Skip to content
This repository has been archived by the owner on May 30, 2024. It is now read-only.

Commit

Permalink
Add debugger launch test
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Jan 26, 2024
1 parent 147f6d4 commit 72cd955
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
ruby-version: "3.3"

- name: 📦 Install dependencies
run: yarn --frozen-lockfile
Expand Down
43 changes: 29 additions & 14 deletions src/debugger.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
import path from "path";
import fs from "fs";
import { ChildProcessWithoutNullStreams, spawn, execSync } from "child_process";
import { ChildProcessWithoutNullStreams, spawn } from "child_process";

import * as vscode from "vscode";

import { LOG_CHANNEL } from "./common";
import { LOG_CHANNEL, asyncExec } from "./common";
import { Workspace } from "./workspace";

class TerminalLogger {
append(message: string) {
// eslint-disable-next-line no-console
console.log(message);
}

appendLine(value: string): void {
// eslint-disable-next-line no-console
console.log(value);
}
}

export class Debugger
implements
vscode.DebugAdapterDescriptorFactory,
vscode.DebugConfigurationProvider
{
private debugProcess?: ChildProcessWithoutNullStreams;
private readonly console = vscode.debug.activeDebugConsole;
// eslint-disable-next-line no-process-env
private readonly console = process.env.CI
? new TerminalLogger()
: vscode.debug.activeDebugConsole;

private readonly workspaceResolver: (
uri: vscode.Uri | undefined,
) => Workspace | undefined;
Expand Down Expand Up @@ -132,19 +148,17 @@ export class Debugger
}
}

private getSockets(session: vscode.DebugSession): string[] {
const cmd = "bundle exec rdbg --util=list-socks";
const workspaceFolder = session.workspaceFolder;
if (!workspaceFolder) {
throw new Error("Debugging requires a workspace folder to be opened");
}
private async getSockets(session: vscode.DebugSession) {
const configuration = session.configuration;
let sockets: string[] = [];

try {
sockets = execSync(cmd, {
cwd: workspaceFolder.uri.fsPath,
const result = await asyncExec("bundle exec rdbg --util=list-socks", {
cwd: session.workspaceFolder?.uri.fsPath,
env: configuration.env,
})
});

sockets = result.stdout
.toString()
.split("\n")
.filter((socket) => socket.length > 0);
Expand All @@ -159,7 +173,8 @@ export class Debugger
): Promise<vscode.DebugAdapterDescriptor> {
// When using attach, a process will be launched using Ruby debug and it will create a socket automatically. We have
// to find the available sockets and ask the user which one they want to attach to
const sockets = this.getSockets(session);
const sockets = await this.getSockets(session);

if (sockets.length === 0) {
throw new Error(`No debuggee processes found. Is the process running?`);
}
Expand Down Expand Up @@ -253,7 +268,7 @@ export class Debugger
// If the Ruby debug exits with an exit code > 1, then an error might've occurred. The reason we don't use only
// code zero here is because debug actually exits with 1 if the user cancels the debug session, which is not
// actually an error
this.debugProcess.on("exit", (code) => {
this.debugProcess.on("close", (code) => {
if (code) {
const message = `Debugger exited with status ${code}. Check the output channel for more information.`;
this.console.append(message);
Expand Down
2 changes: 1 addition & 1 deletion src/test/suite/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ suite("Client", () => {
name: path.basename(tmpPath),
index: 0,
};
fs.writeFileSync(path.join(tmpPath, ".ruby-version"), "3.2.2");
fs.writeFileSync(path.join(tmpPath, ".ruby-version"), "3.3.0");

const context = {
extensionMode: vscode.ExtensionMode.Test,
Expand Down
101 changes: 100 additions & 1 deletion src/test/suite/debugger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import * as os from "os";
import * as vscode from "vscode";

import { Debugger } from "../../debugger";
import { Ruby } from "../../ruby";
import { Ruby, VersionManager } from "../../ruby";
import { Workspace } from "../../workspace";
import { WorkspaceChannel } from "../../workspaceChannel";
import { LOG_CHANNEL, asyncExec } from "../../common";

suite("Debugger", () => {
test("Provide debug configurations returns the default configs", () => {
Expand Down Expand Up @@ -134,4 +136,101 @@ suite("Debugger", () => {
context.subscriptions.forEach((subscription) => subscription.dispose());
fs.rmSync(tmpPath, { recursive: true, force: true });
});

test("Launching the debugger", async () => {
// eslint-disable-next-line no-process-env
if (process.env.CI) {
await vscode.workspace
.getConfiguration("rubyLsp")
.update("rubyVersionManager", VersionManager.None, true, true);
}

// By default, VS Code always saves all open files when launching a debugging session. This is a problem for tests
// because it attempts to save an untitled test file and then we get stuck in the save file dialog with no way of
// closing it. We have to disable that before running this test
const currentSaveBeforeStart = await vscode.workspace
.getConfiguration("debug")
.get("saveBeforeStart");
await vscode.workspace
.getConfiguration("debug")
.update("saveBeforeStart", "none", true, true);

const tmpPath = fs.mkdtempSync(
path.join(os.tmpdir(), "ruby-lsp-test-debugger"),
);
fs.writeFileSync(path.join(tmpPath, "test.rb"), "1 + 1");
fs.writeFileSync(path.join(tmpPath, ".ruby-version"), "3.3.0");
fs.writeFileSync(
path.join(tmpPath, "Gemfile"),
'source "https://rubygems.org"\ngem "debug"',
);

const context = { subscriptions: [] } as unknown as vscode.ExtensionContext;
const outputChannel = new WorkspaceChannel("fake", LOG_CHANNEL);
const workspaceFolder: vscode.WorkspaceFolder = {
uri: vscode.Uri.from({ scheme: "file", path: tmpPath }),
name: path.basename(tmpPath),
index: 0,
};
const ruby = new Ruby(context, workspaceFolder, outputChannel);
await ruby.activateRuby();

try {
await asyncExec("bundle install", { env: ruby.env, cwd: tmpPath });
} catch (error: any) {
assert.fail(`Failed to bundle install: ${error.message}`);
}

assert.ok(fs.existsSync(path.join(tmpPath, "Gemfile.lock")));
assert.match(
fs.readFileSync(path.join(tmpPath, "Gemfile.lock")).toString(),
/debug/,
);

const result = await asyncExec("gem env path", {
env: ruby.env,
cwd: tmpPath,
});

const gemPath = result.stdout.toString().trim();

// eslint-disable-next-line require-atomic-updates
ruby.env.GEM_PATH = gemPath;

const separator = process.platform === "win32" ? ";" : ":";
const pathAdditions = gemPath
.split(separator)
.map((part) => path.join(part, "bin"));

ruby.env.PATH = `${pathAdditions.join(separator)}${separator}${ruby.env.PATH}`;

const debug = new Debugger(context, () => {
return {
ruby,
workspaceFolder,
} as Workspace;
});

try {
await vscode.debug.startDebugging(workspaceFolder, {
type: "ruby_lsp",
name: "Debug",
request: "launch",
program: `ruby ${path.join(tmpPath, "test.rb")}`,
});
} catch (error: any) {
assert.fail(`Failed to launch debugger: ${error.message}`);
}

// The debugger might take a bit of time to disconnect from the editor. We need to perform cleanup when we receive
// the termination callback or else we try to dispose of the debugger client too early
vscode.debug.onDidTerminateDebugSession(async (_session) => {
debug.dispose();
context.subscriptions.forEach((subscription) => subscription.dispose());
fs.rmSync(tmpPath, { recursive: true, force: true });
await vscode.workspace
.getConfiguration("debug")
.update("saveBeforeStart", currentSaveBeforeStart, true, true);
});
}).timeout(30000);
});
2 changes: 1 addition & 1 deletion src/test/suite/ruby.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ suite("Ruby environment activation", () => {
}

const tmpPath = fs.mkdtempSync(path.join(os.tmpdir(), "ruby-lsp-test-"));
fs.writeFileSync(path.join(tmpPath, ".ruby-version"), "3.2.2");
fs.writeFileSync(path.join(tmpPath, ".ruby-version"), "3.3.0");

const context = {
extensionMode: vscode.ExtensionMode.Test,
Expand Down

0 comments on commit 72cd955

Please sign in to comment.