diff --git a/exe/ruby-lsp b/exe/ruby-lsp index 98049f100..1c2833c2d 100755 --- a/exe/ruby-lsp +++ b/exe/ruby-lsp @@ -64,9 +64,9 @@ 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__)}" - command << " --debug" if options[:debug] - exit exec(command) + flags = [] + flags << "--debug" if options[:debug] + exit exec(Gem.ruby, File.expand_path("ruby-lsp-launcher", __dir__), *flags) end require_relative "../lib/ruby_lsp/setup_bundler" diff --git a/vscode/src/client.ts b/vscode/src/client.ts index 186370c74..9f1665701 100644 --- a/vscode/src/client.ts +++ b/vscode/src/client.ts @@ -178,10 +178,14 @@ function collectClientOptions( ruby.gemPath.forEach((gemPath) => { supportedSchemes.forEach((scheme) => { + // On Windows, gem paths may be using backslashes, but those are not valid as a glob pattern. We need to ensure + // that we're using forward slashes for the document selectors + const pathAsGlobPattern = gemPath.replace(/\\/g, "/"); + documentSelector.push({ scheme, language: "ruby", - pattern: `${gemPath}/**/*`, + pattern: `${pathAsGlobPattern}/**/*`, }); // Because of how default gems are installed, the gemPath location is actually not exactly where the files are @@ -193,11 +197,11 @@ function collectClientOptions( // // Notice that we still need to add the regular path to the selector because some version managers will install // gems under the non-corrected path - if (/lib\/ruby\/gems\/(?=\d)/.test(gemPath)) { + if (/lib\/ruby\/gems\/(?=\d)/.test(pathAsGlobPattern)) { documentSelector.push({ scheme, language: "ruby", - pattern: `${gemPath.replace(/lib\/ruby\/gems\/(?=\d)/, "lib/ruby/")}/**/*`, + pattern: `${pathAsGlobPattern.replace(/lib\/ruby\/gems\/(?=\d)/, "lib/ruby/")}/**/*`, }); } }); 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/ruby/chruby.ts b/vscode/src/ruby/chruby.ts index cfad8ae47..ddc709207 100644 --- a/vscode/src/ruby/chruby.ts +++ b/vscode/src/ruby/chruby.ts @@ -169,6 +169,45 @@ export class Chruby extends VersionManager { return undefined; } + // Run the activation script using the Ruby installation we found so that we can discover gem paths + protected async runActivationScript( + rubyExecutableUri: vscode.Uri, + rubyVersion: RubyVersion, + ): Promise<{ + defaultGems: string; + gemHome: string; + yjit: boolean; + version: string; + }> { + // Typically, GEM_HOME points to $HOME/.gem/ruby/version_without_patch. For example, for Ruby 3.2.2, it would be + // $HOME/.gem/ruby/3.2.0. However, chruby overrides GEM_HOME to use the patch part of the version, resulting in + // $HOME/.gem/ruby/3.2.2. In our activation script, we check if a directory using the patch exists and then prefer + // that over the default one. + const script = [ + "user_dir = Gem.user_dir", + "paths = Gem.path", + "if paths.length > 2", + " paths.delete(Gem.default_dir)", + " paths.delete(Gem.user_dir)", + " if paths[0]", + " user_dir = paths[0] if Dir.exist?(paths[0])", + " end", + "end", + `newer_gem_home = File.join(File.dirname(user_dir), "${rubyVersion.version}")`, + "gems = (Dir.exist?(newer_gem_home) ? newer_gem_home : user_dir)", + `STDERR.print([Gem.default_dir, gems, !!defined?(RubyVM::YJIT), RUBY_VERSION].join("${ACTIVATION_SEPARATOR}"))`, + ].join(";"); + + const result = await this.runScript( + `${rubyExecutableUri.fsPath} -W0 -e '${script}'`, + ); + + const [defaultGems, gemHome, yjit, version] = + result.stderr.split(ACTIVATION_SEPARATOR); + + return { defaultGems, gemHome, yjit: yjit === "true", version }; + } + private async findClosestRubyInstallation(rubyVersion: RubyVersion): Promise<{ uri: vscode.Uri; rubyVersion: RubyVersion; @@ -443,45 +482,6 @@ export class Chruby extends VersionManager { throw new Error("Cannot find any Ruby installations"); } - // Run the activation script using the Ruby installation we found so that we can discover gem paths - private async runActivationScript( - rubyExecutableUri: vscode.Uri, - rubyVersion: RubyVersion, - ): Promise<{ - defaultGems: string; - gemHome: string; - yjit: boolean; - version: string; - }> { - // Typically, GEM_HOME points to $HOME/.gem/ruby/version_without_patch. For example, for Ruby 3.2.2, it would be - // $HOME/.gem/ruby/3.2.0. However, chruby overrides GEM_HOME to use the patch part of the version, resulting in - // $HOME/.gem/ruby/3.2.2. In our activation script, we check if a directory using the patch exists and then prefer - // that over the default one. - const script = [ - "user_dir = Gem.user_dir", - "paths = Gem.path", - "if paths.length > 2", - " paths.delete(Gem.default_dir)", - " paths.delete(Gem.user_dir)", - " if paths[0]", - " user_dir = paths[0] if Dir.exist?(paths[0])", - " end", - "end", - `newer_gem_home = File.join(File.dirname(user_dir), "${rubyVersion.version}")`, - "gems = (Dir.exist?(newer_gem_home) ? newer_gem_home : user_dir)", - `STDERR.print([Gem.default_dir, gems, !!defined?(RubyVM::YJIT), RUBY_VERSION].join("${ACTIVATION_SEPARATOR}"))`, - ].join(";"); - - const result = await this.runScript( - `${rubyExecutableUri.fsPath} -W0 -e '${script}'`, - ); - - const [defaultGems, gemHome, yjit, version] = - result.stderr.split(ACTIVATION_SEPARATOR); - - return { defaultGems, gemHome, yjit: yjit === "true", version }; - } - private missingRubyError(version: string) { return new Error(`Cannot find Ruby installation for version ${version}`); } diff --git a/vscode/src/ruby/rubyInstaller.ts b/vscode/src/ruby/rubyInstaller.ts index 33f4912ab..324977c29 100644 --- a/vscode/src/ruby/rubyInstaller.ts +++ b/vscode/src/ruby/rubyInstaller.ts @@ -55,6 +55,29 @@ export class RubyInstaller extends Chruby { ); } + protected async runActivationScript( + rubyExecutableUri: vscode.Uri, + rubyVersion: RubyVersion, + ): Promise<{ + defaultGems: string; + gemHome: string; + yjit: boolean; + version: string; + }> { + const activationResult = await super.runActivationScript( + rubyExecutableUri, + rubyVersion, + ); + + activationResult.gemHome = activationResult.gemHome.replace(/\//g, "\\"); + activationResult.defaultGems = activationResult.defaultGems.replace( + /\//g, + "\\", + ); + + return activationResult; + } + // Override the `runScript` method to ensure that we do not pass any `shell` to `asyncExec`. The activation script is // only compatible with `cmd.exe`, and not Powershell, due to escaping of quotes. We need to ensure to always run the // script on `cmd.exe`. 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..b16df6727 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.None } + : { 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,13 +254,13 @@ 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(); }); }); - }).timeout(45000); + }).timeout(90000); }); 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..1ea5a0199 --- /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 time out, 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); +}); diff --git a/vscode/src/test/suite/ruby/rubyInstaller.test.ts b/vscode/src/test/suite/ruby/rubyInstaller.test.ts index 31c056f77..2655fa8aa 100644 --- a/vscode/src/test/suite/ruby/rubyInstaller.test.ts +++ b/vscode/src/test/suite/ruby/rubyInstaller.test.ts @@ -4,7 +4,7 @@ import path from "path"; import os from "os"; import sinon from "sinon"; -import { before, after } from "mocha"; +import { before, after, beforeEach } from "mocha"; import * as vscode from "vscode"; import * as common from "../../../common"; @@ -13,6 +13,7 @@ import { WorkspaceChannel } from "../../../workspaceChannel"; import { LOG_CHANNEL } from "../../../common"; import { RUBY_VERSION, VERSION_REGEX } from "../../rubyVersion"; import { ACTIVATION_SEPARATOR } from "../../../ruby/versionManager"; +import { createRubySymlinks } from "../helpers"; suite("RubyInstaller", () => { if (os.platform() !== "win32") { @@ -26,6 +27,13 @@ suite("RubyInstaller", () => { let workspaceFolder: vscode.WorkspaceFolder; let outputChannel: WorkspaceChannel; + beforeEach(() => { + // eslint-disable-next-line no-process-env + if (process.env.CI) { + createRubySymlinks(); + } + }); + before(() => { rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "ruby-lsp-test-")); @@ -45,19 +53,6 @@ suite("RubyInstaller", () => { }); test("Finds Ruby when under C:/RubyXY-arch", async () => { - const [major, minor, _patch] = RUBY_VERSION.split(".").map(Number); - fs.symlinkSync( - path.join( - "C:", - "hostedtoolcache", - "windows", - "Ruby", - RUBY_VERSION, - "x64", - ), - path.join("C:", `Ruby${major}${minor}-${os.arch()}`), - ); - fs.writeFileSync(path.join(workspacePath, ".ruby-version"), RUBY_VERSION); const windows = new RubyInstaller( @@ -67,31 +62,16 @@ suite("RubyInstaller", () => { ); const { env, version, yjit } = await windows.activate(); - assert.match(env.GEM_PATH!, new RegExp(`ruby/${VERSION_REGEX}`)); - assert.match(env.GEM_PATH!, new RegExp(`lib/ruby/gems/${VERSION_REGEX}`)); + assert.match(env.GEM_PATH!, new RegExp(`ruby\\\\${VERSION_REGEX}`)); + assert.match( + env.GEM_PATH!, + new RegExp(`lib\\\\ruby\\\\gems\\\\${VERSION_REGEX}`), + ); assert.strictEqual(version, RUBY_VERSION); assert.notStrictEqual(yjit, undefined); - - fs.rmSync(path.join("C:", `Ruby${major}${minor}-${os.arch()}`), { - recursive: true, - force: true, - }); }); test("Finds Ruby when under C:/Users/Username/RubyXY-arch", async () => { - const [major, minor, _patch] = RUBY_VERSION.split(".").map(Number); - fs.symlinkSync( - path.join( - "C:", - "hostedtoolcache", - "windows", - "Ruby", - RUBY_VERSION, - "x64", - ), - path.join(os.homedir(), `Ruby${major}${minor}-${os.arch()}`), - ); - fs.writeFileSync(path.join(workspacePath, ".ruby-version"), RUBY_VERSION); const windows = new RubyInstaller( @@ -101,31 +81,16 @@ suite("RubyInstaller", () => { ); const { env, version, yjit } = await windows.activate(); - assert.match(env.GEM_PATH!, new RegExp(`ruby/${VERSION_REGEX}`)); - assert.match(env.GEM_PATH!, new RegExp(`lib/ruby/gems/${VERSION_REGEX}`)); + assert.match(env.GEM_PATH!, new RegExp(`ruby\\\\${VERSION_REGEX}`)); + assert.match( + env.GEM_PATH!, + new RegExp(`lib\\\\ruby\\\\gems\\\\${VERSION_REGEX}`), + ); assert.strictEqual(version, RUBY_VERSION); assert.notStrictEqual(yjit, undefined); - - fs.rmSync(path.join(os.homedir(), `Ruby${major}${minor}-${os.arch()}`), { - recursive: true, - force: true, - }); }); test("Doesn't set the shell when invoking activation script", async () => { - const [major, minor, _patch] = RUBY_VERSION.split(".").map(Number); - fs.symlinkSync( - path.join( - "C:", - "hostedtoolcache", - "windows", - "Ruby", - RUBY_VERSION, - "x64", - ), - path.join(os.homedir(), `Ruby${major}${minor}-${os.arch()}`), - ); - fs.writeFileSync(path.join(workspacePath, ".ruby-version"), RUBY_VERSION); const windows = new RubyInstaller( @@ -147,10 +112,33 @@ suite("RubyInstaller", () => { assert.strictEqual(execStub.callCount, 1); const callArgs = execStub.getCall(0).args; assert.strictEqual(callArgs[1]?.shell, undefined); + }); + + test("Normalizes long file formats to back slashes", async () => { + fs.writeFileSync(path.join(workspacePath, ".ruby-version"), RUBY_VERSION); - fs.rmSync(path.join(os.homedir(), `Ruby${major}${minor}-${os.arch()}`), { - recursive: true, - force: true, + const windows = new RubyInstaller( + workspaceFolder, + outputChannel, + async () => {}, + ); + const result = [ + "//?/C:/Ruby32/gems", + "//?/C:/Ruby32/default_gems", + true, + RUBY_VERSION, + ].join(ACTIVATION_SEPARATOR); + const execStub = sinon.stub(common, "asyncExec").resolves({ + stdout: "", + stderr: result, }); + + const { gemPath } = await windows.activate(); + execStub.restore(); + + assert.deepStrictEqual(gemPath, [ + "\\\\?\\C:\\Ruby32\\default_gems", + "\\\\?\\C:\\Ruby32\\gems", + ]); }); });