From de6ec1389e7660937e4f212974e32f45e298a911 Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Wed, 22 Jan 2025 10:01:41 -0500 Subject: [PATCH] Fix launcher file paths on Windows --- exe/ruby-lsp | 5 +- vscode/src/ruby.ts | 7 +- vscode/src/test/suite/client.test.ts | 93 +++--------------- vscode/src/test/suite/debugger.test.ts | 53 ++++++----- vscode/src/test/suite/fakeTelemetry.ts | 32 +++++++ vscode/src/test/suite/helpers.ts | 43 +++++++++ vscode/src/test/suite/launch.test.ts | 125 +++++++++++++++++++++++++ 7 files changed, 250 insertions(+), 108 deletions(-) create mode 100644 vscode/src/test/suite/helpers.ts create mode 100644 vscode/src/test/suite/launch.test.ts diff --git a/exe/ruby-lsp b/exe/ruby-lsp index 98049f100..59b83ad9c 100755 --- a/exe/ruby-lsp +++ b/exe/ruby-lsp @@ -64,7 +64,10 @@ if ENV["BUNDLE_GEMFILE"].nil? # which gives us the opportunity to control which specs are activated and enter degraded mode if any gems failed to # install rather than failing to boot the server completely if options[:launcher] - command = +"#{Gem.ruby} #{File.expand_path("ruby-lsp-launcher", __dir__)}" + # Run `/path/to/ruby /path/to/exe/ruby-lsp-launcher` and ensuring that the Windows long format path is normalized + # for exec + command = +"#{Gem.ruby.delete_prefix("//?/")} " + command << File.expand_path("ruby-lsp-launcher", __dir__).delete_prefix("//?/") command << " --debug" if options[:debug] exit exec(command) end diff --git a/vscode/src/ruby.ts b/vscode/src/ruby.ts index 870082fbf..a8d41682a 100644 --- a/vscode/src/ruby.ts +++ b/vscode/src/ruby.ts @@ -260,8 +260,11 @@ export class Ruby implements RubyInterface { this.sanitizeEnvironment(env); - // We need to set the process environment too to make other extensions such as Sorbet find the right Ruby paths - process.env = env; + if (this.context.extensionMode !== vscode.ExtensionMode.Test) { + // We need to set the process environment too to make other extensions such as Sorbet find the right Ruby paths + process.env = env; + } + this._env = env; this.rubyVersion = version; this.yjitEnabled = (yjit && major > 3) || (major === 3 && minor >= 2); diff --git a/vscode/src/test/suite/client.test.ts b/vscode/src/test/suite/client.test.ts index 6dcc762c1..c9a0866b1 100644 --- a/vscode/src/test/suite/client.test.ts +++ b/vscode/src/test/suite/client.test.ts @@ -31,41 +31,10 @@ import { after, afterEach, before } from "mocha"; import { Ruby, ManagerIdentifier } from "../../ruby"; import Client from "../../client"; import { WorkspaceChannel } from "../../workspaceChannel"; -import { RUBY_VERSION, MAJOR, MINOR } from "../rubyVersion"; +import { MAJOR, MINOR } from "../rubyVersion"; -import { FAKE_TELEMETRY } from "./fakeTelemetry"; - -class FakeLogger { - receivedMessages = ""; - - trace(message: string, ..._args: any[]): void { - this.receivedMessages += message; - } - - debug(message: string, ..._args: any[]): void { - this.receivedMessages += message; - } - - info(message: string, ..._args: any[]): void { - this.receivedMessages += message; - } - - warn(message: string, ..._args: any[]): void { - this.receivedMessages += message; - } - - error(error: string | Error, ..._args: any[]): void { - this.receivedMessages += error.toString(); - } - - append(value: string): void { - this.receivedMessages += value; - } - - appendLine(value: string): void { - this.receivedMessages += value; - } -} +import { FAKE_TELEMETRY, FakeLogger } from "./fakeTelemetry"; +import { createRubySymlinks } from "./helpers"; async function launchClient(workspaceUri: vscode.Uri) { const workspaceFolder: vscode.WorkspaceFolder = { @@ -85,6 +54,8 @@ async function launchClient(workspaceUri: vscode.Uri) { const fakeLogger = new FakeLogger(); const outputChannel = new WorkspaceChannel("fake", fakeLogger as any); + let managerConfig; + // Ensure that we're activating the correct Ruby version on CI if (process.env.CI) { await vscode.workspace @@ -94,54 +65,12 @@ async function launchClient(workspaceUri: vscode.Uri) { .getConfiguration("rubyLsp") .update("linters", ["rubocop_internal"], true); - 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), - ); + createRubySymlinks(); + + if (os.platform() === "win32") { + managerConfig = { identifier: ManagerIdentifier.RubyInstaller }; } 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()}`), - ); + managerConfig = { identifier: ManagerIdentifier.Chruby }; } } @@ -151,7 +80,7 @@ async function launchClient(workspaceUri: vscode.Uri) { outputChannel, FAKE_TELEMETRY, ); - await ruby.activateRuby(); + await ruby.activateRuby(managerConfig); ruby.env.RUBY_LSP_BYPASS_TYPECHECKER = "true"; const virtualDocuments = new Map(); diff --git a/vscode/src/test/suite/debugger.test.ts b/vscode/src/test/suite/debugger.test.ts index ac22c0c8a..5e7cb6b4a 100644 --- a/vscode/src/test/suite/debugger.test.ts +++ b/vscode/src/test/suite/debugger.test.ts @@ -4,7 +4,7 @@ import * as path from "path"; import * as os from "os"; import * as vscode from "vscode"; -import sinon from "sinon"; +import { afterEach, beforeEach } from "mocha"; import { Debugger } from "../../debugger"; import { Ruby, ManagerIdentifier } from "../../ruby"; @@ -14,8 +14,25 @@ import { LOG_CHANNEL, asyncExec } from "../../common"; import { RUBY_VERSION } from "../rubyVersion"; import { FAKE_TELEMETRY } from "./fakeTelemetry"; +import { createRubySymlinks } from "./helpers"; suite("Debugger", () => { + const original = vscode.workspace + .getConfiguration("debug") + .get("saveBeforeStart"); + + beforeEach(async () => { + await vscode.workspace + .getConfiguration("debug") + .update("saveBeforeStart", "none", true); + }); + + afterEach(async () => { + await vscode.workspace + .getConfiguration("debug") + .update("saveBeforeStart", original, true); + }); + test("Provide debug configurations returns the default configs", () => { const context = { subscriptions: [] } as unknown as vscode.ExtensionContext; const debug = new Debugger(context, () => { @@ -161,26 +178,15 @@ suite("Debugger", () => { }); test("Launching the debugger", async () => { - // eslint-disable-next-line no-process-env - const manager = process.env.CI - ? ManagerIdentifier.None - : ManagerIdentifier.Chruby; + const manager = + os.platform() === "win32" + ? { identifier: ManagerIdentifier.RubyInstaller } + : { identifier: ManagerIdentifier.Chruby }; - const configStub = sinon - .stub(vscode.workspace, "getConfiguration") - .returns({ - get: (name: string) => { - if (name === "rubyVersionManager") { - return { identifier: manager }; - } else if (name === "bundleGemfile") { - return ""; - } else if (name === "saveBeforeStart") { - return "none"; - } - - return undefined; - }, - } as unknown as vscode.WorkspaceConfiguration); + // eslint-disable-next-line no-process-env + if (process.env.CI) { + createRubySymlinks(); + } const tmpPath = fs.mkdtempSync( path.join(os.tmpdir(), "ruby-lsp-test-debugger"), @@ -205,13 +211,14 @@ suite("Debugger", () => { name: path.basename(tmpPath), index: 0, }; + const ruby = new Ruby( context, workspaceFolder, outputChannel, FAKE_TELEMETRY, ); - await ruby.activateRuby(); + await ruby.activateRuby(manager); try { await asyncExec("bundle install", { env: ruby.env, cwd: tmpPath }); @@ -247,11 +254,11 @@ suite("Debugger", () => { // 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) => { - configStub.restore(); + const callback = vscode.debug.onDidTerminateDebugSession((_session) => { debug.dispose(); context.subscriptions.forEach((subscription) => subscription.dispose()); fs.rmSync(tmpPath, { recursive: true, force: true }); + callback.dispose(); resolve(); }); }); diff --git a/vscode/src/test/suite/fakeTelemetry.ts b/vscode/src/test/suite/fakeTelemetry.ts index f2b130dcf..9354d4ab4 100644 --- a/vscode/src/test/suite/fakeTelemetry.ts +++ b/vscode/src/test/suite/fakeTelemetry.ts @@ -27,3 +27,35 @@ export const FAKE_TELEMETRY = vscode.env.createTelemetryLogger( ignoreUnhandledErrors: true, }, ); + +export class FakeLogger { + receivedMessages = ""; + + trace(message: string, ..._args: any[]): void { + this.receivedMessages += message; + } + + debug(message: string, ..._args: any[]): void { + this.receivedMessages += message; + } + + info(message: string, ..._args: any[]): void { + this.receivedMessages += message; + } + + warn(message: string, ..._args: any[]): void { + this.receivedMessages += message; + } + + error(error: string | Error, ..._args: any[]): void { + this.receivedMessages += error.toString(); + } + + append(value: string): void { + this.receivedMessages += value; + } + + appendLine(value: string): void { + this.receivedMessages += value; + } +} diff --git a/vscode/src/test/suite/helpers.ts b/vscode/src/test/suite/helpers.ts new file mode 100644 index 000000000..ade371649 --- /dev/null +++ b/vscode/src/test/suite/helpers.ts @@ -0,0 +1,43 @@ +/* eslint-disable no-process-env */ +import path from "path"; +import os from "os"; +import fs from "fs"; + +import { MAJOR, MINOR, RUBY_VERSION } from "../rubyVersion"; + +export function createRubySymlinks() { + if (os.platform() === "linux") { + const linkPath = path.join(os.homedir(), ".rubies", RUBY_VERSION); + + if (!fs.existsSync(linkPath)) { + fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true }); + fs.symlinkSync(`/opt/hostedtoolcache/Ruby/${RUBY_VERSION}/x64`, linkPath); + } + } else if (os.platform() === "darwin") { + const linkPath = path.join(os.homedir(), ".rubies", RUBY_VERSION); + + if (!fs.existsSync(linkPath)) { + fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true }); + fs.symlinkSync( + `/Users/runner/hostedtoolcache/Ruby/${RUBY_VERSION}/arm64`, + linkPath, + ); + } + } else { + const linkPath = path.join("C:", `Ruby${MAJOR}${MINOR}-${os.arch()}`); + + if (!fs.existsSync(linkPath)) { + fs.symlinkSync( + path.join( + "C:", + "hostedtoolcache", + "windows", + "Ruby", + RUBY_VERSION, + "x64", + ), + linkPath, + ); + } + } +} diff --git a/vscode/src/test/suite/launch.test.ts b/vscode/src/test/suite/launch.test.ts new file mode 100644 index 000000000..1fc28f85f --- /dev/null +++ b/vscode/src/test/suite/launch.test.ts @@ -0,0 +1,125 @@ +/* eslint-disable no-process-env */ +import assert from "assert"; +import path from "path"; +import os from "os"; + +import * as vscode from "vscode"; +import { State, WorkDoneProgress } from "vscode-languageclient/node"; +import sinon from "sinon"; +import { beforeEach } from "mocha"; + +import { ManagerIdentifier, Ruby } from "../../ruby"; +import Client from "../../client"; +import { WorkspaceChannel } from "../../workspaceChannel"; +import * as common from "../../common"; + +import { FAKE_TELEMETRY, FakeLogger } from "./fakeTelemetry"; +import { createRubySymlinks } from "./helpers"; + +suite("Launch integrations", () => { + const workspacePath = path.dirname( + path.dirname(path.dirname(path.dirname(__dirname))), + ); + const workspaceUri = vscode.Uri.file(workspacePath); + const workspaceFolder: vscode.WorkspaceFolder = { + uri: workspaceUri, + name: path.basename(workspaceUri.fsPath), + index: 0, + }; + + const context = { + extensionMode: vscode.ExtensionMode.Test, + subscriptions: [], + workspaceState: { + get: (_name: string) => undefined, + update: (_name: string, _value: any) => Promise.resolve(), + }, + extensionUri: vscode.Uri.joinPath(workspaceUri, "vscode"), + } as unknown as vscode.ExtensionContext; + const fakeLogger = new FakeLogger(); + const outputChannel = new WorkspaceChannel("fake", fakeLogger as any); + + async function createClient() { + const ruby = new Ruby( + context, + workspaceFolder, + outputChannel, + FAKE_TELEMETRY, + ); + + if (process.env.CI && os.platform() === "win32") { + await ruby.activateRuby({ identifier: ManagerIdentifier.RubyInstaller }); + } else if (process.env.CI) { + await ruby.activateRuby({ identifier: ManagerIdentifier.Chruby }); + } else { + await ruby.activateRuby(); + } + + const client = new Client( + context, + FAKE_TELEMETRY, + ruby, + () => {}, + workspaceFolder, + outputChannel, + new Map(), + ); + + client.clientOptions.initializationFailedHandler = (error) => { + assert.fail( + `Failed to start server ${error.message}\n${fakeLogger.receivedMessages}`, + ); + }; + + return client; + } + + async function startClient(client: Client) { + try { + await client.start(); + } catch (error: any) { + assert.fail( + `Failed to start server ${error.message}\n${fakeLogger.receivedMessages}`, + ); + } + assert.strictEqual(client.state, State.Running); + + // Wait for composing the bundle and indexing to finish. We don't _need_ the codebase to be indexed for these tests, + // but trying to stop the server in the middle of composing the bundle may timeout, so this makes the tests more + // robust + return new Promise((resolve) => { + client.onProgress( + WorkDoneProgress.type, + "indexing-progress", + (value: any) => { + if (value.kind === "end") { + resolve(client); + } + }, + ); + }); + } + + beforeEach(() => { + if (process.env.CI) { + createRubySymlinks(); + } + }); + + test("with launcher mode enabled", async () => { + const featureStub = sinon.stub(common, "featureEnabled").returns(true); + const client = await createClient(); + featureStub.restore(); + + await startClient(client); + + try { + await client.stop(); + await client.dispose(); + } catch (error: any) { + assert.fail( + `Failed to stop server: ${error.message}\n${fakeLogger.receivedMessages}`, + ); + } + }).timeout(120000); +});