diff --git a/vscode/src/rubyLsp.ts b/vscode/src/rubyLsp.ts index 4a055f743..670d91f0e 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 15dd1317d..086e7a2b4 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 000000000..1af8e048a --- /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((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 29b198768..8d1002590 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 000000000..eefcb7eb5 --- /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()}`), + ); + } + } +}