From f3ca6475a49519e5bdd32d6b5656f3a3f67794bf Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Wed, 17 Jan 2024 12:04:16 -0300 Subject: [PATCH] Add debugger launch test --- .github/workflows/ci.yml | 2 +- src/debugger.ts | 29 ++++++++++--- src/test/suite/client.test.ts | 2 +- src/test/suite/debugger.test.ts | 74 ++++++++++++++++++++++++++++++++- src/test/suite/ruby.test.ts | 2 +- 5 files changed, 99 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d4b5973..2637e51e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/src/debugger.ts b/src/debugger.ts index 45d1c8a7..2647db7e 100644 --- a/src/debugger.ts +++ b/src/debugger.ts @@ -1,5 +1,6 @@ import path from "path"; import fs from "fs"; +import os from "os"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import * as vscode from "vscode"; @@ -7,13 +8,29 @@ import * as vscode from "vscode"; import { LOG_CHANNEL } 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; @@ -135,7 +152,7 @@ export class Debugger private async attachDebuggee(): Promise { // 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 socketsDir = path.join("/", "tmp", "ruby-lsp-debug-sockets"); + const socketsDir = path.join(os.tmpdir(), "rlspdebug"); const sockets = fs .readdirSync(socketsDir) .map((file) => file) @@ -227,7 +244,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); @@ -241,13 +258,13 @@ export class Debugger // Generate a socket path so that Ruby debug doesn't have to create one for us. This makes coordination easier since // we always know the path to the socket private socketPath(workspaceName: string) { - const socketsDir = path.join("/", "tmp", "ruby-lsp-debug-sockets"); + const socketsDir = path.join(os.tmpdir(), "rlspdebug"); if (!fs.existsSync(socketsDir)) { fs.mkdirSync(socketsDir); } let socketIndex = 0; - const prefix = `ruby-debug-${workspaceName}`; + const prefix = `rd-${workspaceName}`; const existingSockets = fs .readdirSync(socketsDir) .map((file) => file) @@ -261,6 +278,6 @@ export class Debugger ) + 1; } - return `${socketsDir}/${prefix}-${socketIndex}.sock`; + return path.join(socketsDir, `${prefix}-${socketIndex}.sock`); } } diff --git a/src/test/suite/client.test.ts b/src/test/suite/client.test.ts index f285a3d4..d45d3c99 100644 --- a/src/test/suite/client.test.ts +++ b/src/test/suite/client.test.ts @@ -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, diff --git a/src/test/suite/debugger.test.ts b/src/test/suite/debugger.test.ts index 72343ec1..1430480a 100644 --- a/src/test/suite/debugger.test.ts +++ b/src/test/suite/debugger.test.ts @@ -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", () => { @@ -134,4 +136,74 @@ 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(); + await asyncExec("bundle install", { env: ruby.env, cwd: tmpPath }); + + 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")}`, + env: ruby.env, + }); + } 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); }); diff --git a/src/test/suite/ruby.test.ts b/src/test/suite/ruby.test.ts index e231f93d..09c87860 100644 --- a/src/test/suite/ruby.test.ts +++ b/src/test/suite/ruby.test.ts @@ -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,