From c046f31aba81ce06c7526b2812ec9b0d75525dc4 Mon Sep 17 00:00:00 2001
From: Vinicius Stock <vinicius.stock@shopify.com>
Date: Thu, 7 Nov 2024 17:19:13 -0500
Subject: [PATCH] Add debugger integration test for project with local Bundler
 settings

---
 vscode/src/rubyLsp.ts                        |   8 +-
 vscode/src/test/suite/client.test.ts         |  56 +-------
 vscode/src/test/suite/rubyLsp.test.ts        | 131 +++++++++++++++++++
 vscode/src/test/suite/testController.test.ts |   5 +
 vscode/src/test/suite/testHelpers.ts         |  66 ++++++++++
 5 files changed, 209 insertions(+), 57 deletions(-)
 create mode 100644 vscode/src/test/suite/rubyLsp.test.ts
 create mode 100644 vscode/src/test/suite/testHelpers.ts

diff --git a/vscode/src/rubyLsp.ts b/vscode/src/rubyLsp.ts
index 4a055f7439..670d91f0ec 100644
--- a/vscode/src/rubyLsp.ts
+++ b/vscode/src/rubyLsp.ts
@@ -119,10 +119,10 @@ export class RubyLsp {
 
   // Activate the extension. This method should perform all actions necessary to start the extension, such as booting
   // all language servers for each existing workspace
-  async activate() {
-    await vscode.commands.executeCommand("testing.clearTestResults");
-
-    const firstWorkspace = vscode.workspace.workspaceFolders?.[0];
+  async activate(firstWorkspace = vscode.workspace.workspaceFolders?.[0]) {
+    if (this.context.extensionMode !== vscode.ExtensionMode.Test) {
+      await vscode.commands.executeCommand("testing.clearTestResults");
+    }
 
     // We only activate the first workspace eagerly to avoid running into performance and memory issues. Having too many
     // workspaces spawning the Ruby LSP server and indexing can grind the editor to a halt. All other workspaces are
diff --git a/vscode/src/test/suite/client.test.ts b/vscode/src/test/suite/client.test.ts
index 15dd1317de..086e7a2b42 100644
--- a/vscode/src/test/suite/client.test.ts
+++ b/vscode/src/test/suite/client.test.ts
@@ -26,12 +26,13 @@ import {
 } from "vscode-languageclient/node";
 import { after, afterEach, before } from "mocha";
 
-import { Ruby, ManagerIdentifier } from "../../ruby";
+import { Ruby } from "../../ruby";
 import Client from "../../client";
 import { WorkspaceChannel } from "../../workspaceChannel";
 import { RUBY_VERSION } from "../rubyVersion";
 
 import { FAKE_TELEMETRY } from "./fakeTelemetry";
+import { ensureRubyInstallationPaths } from "./testHelpers";
 
 const [major, minor, _patch] = RUBY_VERSION.split(".");
 
@@ -85,58 +86,7 @@ async function launchClient(workspaceUri: vscode.Uri) {
   const fakeLogger = new FakeLogger();
   const outputChannel = new WorkspaceChannel("fake", fakeLogger as any);
 
-  // Ensure that we're activating the correct Ruby version on CI
-  if (process.env.CI) {
-    if (os.platform() === "linux") {
-      await vscode.workspace
-        .getConfiguration("rubyLsp")
-        .update(
-          "rubyVersionManager",
-          { identifier: ManagerIdentifier.Chruby },
-          true,
-        );
-
-      fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true });
-      fs.symlinkSync(
-        `/opt/hostedtoolcache/Ruby/${RUBY_VERSION}/x64`,
-        path.join(os.homedir(), ".rubies", RUBY_VERSION),
-      );
-    } else if (os.platform() === "darwin") {
-      await vscode.workspace
-        .getConfiguration("rubyLsp")
-        .update(
-          "rubyVersionManager",
-          { identifier: ManagerIdentifier.Chruby },
-          true,
-        );
-
-      fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true });
-      fs.symlinkSync(
-        `/Users/runner/hostedtoolcache/Ruby/${RUBY_VERSION}/arm64`,
-        path.join(os.homedir(), ".rubies", RUBY_VERSION),
-      );
-    } else {
-      await vscode.workspace
-        .getConfiguration("rubyLsp")
-        .update(
-          "rubyVersionManager",
-          { identifier: ManagerIdentifier.RubyInstaller },
-          true,
-        );
-
-      fs.symlinkSync(
-        path.join(
-          "C:",
-          "hostedtoolcache",
-          "windows",
-          "Ruby",
-          RUBY_VERSION,
-          "x64",
-        ),
-        path.join("C:", `Ruby${major}${minor}-${os.arch()}`),
-      );
-    }
-  }
+  await ensureRubyInstallationPaths();
 
   const ruby = new Ruby(
     context,
diff --git a/vscode/src/test/suite/rubyLsp.test.ts b/vscode/src/test/suite/rubyLsp.test.ts
new file mode 100644
index 0000000000..1af8e048a2
--- /dev/null
+++ b/vscode/src/test/suite/rubyLsp.test.ts
@@ -0,0 +1,131 @@
+import path from "path";
+import assert from "assert";
+import fs from "fs";
+import os from "os";
+
+import sinon from "sinon";
+import * as vscode from "vscode";
+import { beforeEach, afterEach, before, after } from "mocha";
+
+import { RubyLsp } from "../../rubyLsp";
+import { RUBY_VERSION } from "../rubyVersion";
+
+import { FAKE_TELEMETRY } from "./fakeTelemetry";
+import { ensureRubyInstallationPaths } from "./testHelpers";
+
+suite("Ruby LSP", () => {
+  const context = {
+    extensionMode: vscode.ExtensionMode.Test,
+    subscriptions: [],
+    workspaceState: {
+      get: (_name: string) => undefined,
+      update: (_name: string, _value: any) => Promise.resolve(),
+    },
+    extensionUri: vscode.Uri.file(
+      path.dirname(path.dirname(path.dirname(__dirname))),
+    ),
+  } as unknown as vscode.ExtensionContext;
+  let workspacePath: string;
+  let workspaceUri: vscode.Uri;
+  let workspaceFolder: vscode.WorkspaceFolder;
+  const originalSaveBeforeStart = vscode.workspace
+    .getConfiguration("debug")
+    .get("saveBeforeStart");
+
+  before(async () => {
+    await vscode.workspace
+      .getConfiguration("debug")
+      .update("saveBeforeStart", "none", true);
+  });
+
+  after(async () => {
+    await vscode.workspace
+      .getConfiguration("debug")
+      .update("saveBeforeStart", originalSaveBeforeStart, true);
+  });
+
+  beforeEach(() => {
+    workspacePath = fs.mkdtempSync(path.join(os.tmpdir(), "ruby-lsp-test-"));
+    workspaceUri = vscode.Uri.file(workspacePath);
+    workspaceFolder = {
+      uri: workspaceUri,
+      name: path.basename(workspacePath),
+      index: 0,
+    };
+  });
+
+  afterEach(() => {
+    fs.rmSync(workspacePath, { recursive: true, force: true });
+  });
+
+  test("launching debugger in a project with local Bundler settings and composed bundle", async () => {
+    fs.writeFileSync(path.join(workspacePath, "test.rb"), "1 + 1");
+    fs.writeFileSync(path.join(workspacePath, ".ruby-version"), RUBY_VERSION);
+    fs.writeFileSync(
+      path.join(workspacePath, "Gemfile"),
+      'source "https://rubygems.org"\n',
+    );
+    fs.writeFileSync(
+      path.join(workspacePath, "Gemfile.lock"),
+      [
+        "GEM",
+        "  remote: https://rubygems.org/",
+        "  specs:",
+        "",
+        "PLATFORMS",
+        "  arm64-darwin-23",
+        "  ruby",
+        "",
+        "DEPENDENCIES",
+        "",
+        "BUNDLED WITH",
+        "  2.5.16",
+      ].join("\n"),
+    );
+    fs.mkdirSync(path.join(workspacePath, ".bundle"));
+    fs.writeFileSync(
+      path.join(workspacePath, ".bundle", "config"),
+      "BUNDLE_PATH: vendor/bundle",
+    );
+
+    await ensureRubyInstallationPaths();
+
+    const rubyLsp = new RubyLsp(context, FAKE_TELEMETRY);
+    await rubyLsp.activate(workspaceFolder);
+
+    const stub = sinon.stub(vscode.window, "activeTextEditor").get(() => {
+      return {
+        document: {
+          uri: vscode.Uri.file(path.join(workspacePath, "test.rb")),
+        },
+      } as vscode.TextEditor;
+    });
+
+    const getWorkspaceStub = sinon
+      .stub(vscode.workspace, "getWorkspaceFolder")
+      .returns(workspaceFolder);
+
+    try {
+      await vscode.debug.startDebugging(workspaceFolder, {
+        type: "ruby_lsp",
+        name: "Debug",
+        request: "launch",
+        program: `ruby ${path.join(workspacePath, "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, but we need to wait for that
+    // so that we can clean up stubs otherwise they leak into other tests.
+    await new Promise<void>((resolve) => {
+      vscode.debug.onDidTerminateDebugSession((_session) => {
+        stub.restore();
+        getWorkspaceStub.restore();
+        context.subscriptions.forEach((subscription) => subscription.dispose());
+        resolve();
+      });
+    });
+  }).timeout(90000);
+});
diff --git a/vscode/src/test/suite/testController.test.ts b/vscode/src/test/suite/testController.test.ts
index 29b1987688..8d1002590f 100644
--- a/vscode/src/test/suite/testController.test.ts
+++ b/vscode/src/test/suite/testController.test.ts
@@ -2,6 +2,7 @@ import * as assert from "assert";
 
 import * as vscode from "vscode";
 import { CodeLens } from "vscode-languageclient/node";
+import { afterEach } from "mocha";
 
 import { TestController } from "../../testController";
 import { Command } from "../../common";
@@ -18,6 +19,10 @@ suite("TestController", () => {
     },
   } as unknown as vscode.ExtensionContext;
 
+  afterEach(() => {
+    context.subscriptions.forEach((subscription) => subscription.dispose());
+  });
+
   test("createTestItems doesn't break when there's a missing group", () => {
     const controller = new TestController(
       context,
diff --git a/vscode/src/test/suite/testHelpers.ts b/vscode/src/test/suite/testHelpers.ts
new file mode 100644
index 0000000000..eefcb7eb53
--- /dev/null
+++ b/vscode/src/test/suite/testHelpers.ts
@@ -0,0 +1,66 @@
+/* eslint-disable no-process-env */
+
+import os from "os";
+import fs from "fs";
+import path from "path";
+
+import * as vscode from "vscode";
+
+import { ManagerIdentifier } from "../../ruby";
+import { RUBY_VERSION } from "../rubyVersion";
+
+export async function ensureRubyInstallationPaths() {
+  const [major, minor, _patch] = RUBY_VERSION.split(".");
+  // Ensure that we're activating the correct Ruby version on CI
+  if (process.env.CI) {
+    if (os.platform() === "linux") {
+      await vscode.workspace
+        .getConfiguration("rubyLsp")
+        .update(
+          "rubyVersionManager",
+          { identifier: ManagerIdentifier.Chruby },
+          true,
+        );
+
+      fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true });
+      fs.symlinkSync(
+        `/opt/hostedtoolcache/Ruby/${RUBY_VERSION}/x64`,
+        path.join(os.homedir(), ".rubies", RUBY_VERSION),
+      );
+    } else if (os.platform() === "darwin") {
+      await vscode.workspace
+        .getConfiguration("rubyLsp")
+        .update(
+          "rubyVersionManager",
+          { identifier: ManagerIdentifier.Chruby },
+          true,
+        );
+
+      fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true });
+      fs.symlinkSync(
+        `/Users/runner/hostedtoolcache/Ruby/${RUBY_VERSION}/arm64`,
+        path.join(os.homedir(), ".rubies", RUBY_VERSION),
+      );
+    } else {
+      await vscode.workspace
+        .getConfiguration("rubyLsp")
+        .update(
+          "rubyVersionManager",
+          { identifier: ManagerIdentifier.RubyInstaller },
+          true,
+        );
+
+      fs.symlinkSync(
+        path.join(
+          "C:",
+          "hostedtoolcache",
+          "windows",
+          "Ruby",
+          RUBY_VERSION,
+          "x64",
+        ),
+        path.join("C:", `Ruby${major}${minor}-${os.arch()}`),
+      );
+    }
+  }
+}