Skip to content

Commit

Permalink
Fix launcher file paths on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Jan 22, 2025
1 parent 0650829 commit 751ad9e
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 90 deletions.
5 changes: 4 additions & 1 deletion exe/ruby-lsp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 11 additions & 82 deletions vscode/src/test/suite/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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
Expand All @@ -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 };
}
}

Expand All @@ -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<string, string>();
Expand Down
18 changes: 11 additions & 7 deletions vscode/src/test/suite/debugger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { LOG_CHANNEL, asyncExec } from "../../common";
import { RUBY_VERSION } from "../rubyVersion";

import { FAKE_TELEMETRY } from "./fakeTelemetry";
import { createRubySymlinks } from "./helpers";

suite("Debugger", () => {
test("Provide debug configurations returns the default configs", () => {
Expand Down Expand Up @@ -161,18 +162,21 @@ suite("Debugger", () => {
});

test("Launching the debugger", async () => {
const manager =
os.platform() === "win32"
? { identifier: ManagerIdentifier.RubyInstaller }
: { identifier: ManagerIdentifier.Chruby };

// eslint-disable-next-line no-process-env
const manager = process.env.CI
? ManagerIdentifier.None
: ManagerIdentifier.Chruby;
if (process.env.CI) {
createRubySymlinks();
}

const configStub = sinon
.stub(vscode.workspace, "getConfiguration")
.returns({
get: (name: string) => {
if (name === "rubyVersionManager") {
return { identifier: manager };
} else if (name === "bundleGemfile") {
if (name === "bundleGemfile") {
return "";
} else if (name === "saveBeforeStart") {
return "none";
Expand Down Expand Up @@ -211,7 +215,7 @@ suite("Debugger", () => {
outputChannel,
FAKE_TELEMETRY,
);
await ruby.activateRuby();
await ruby.activateRuby(manager);

try {
await asyncExec("bundle install", { env: ruby.env, cwd: tmpPath });
Expand Down
32 changes: 32 additions & 0 deletions vscode/src/test/suite/fakeTelemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
43 changes: 43 additions & 0 deletions vscode/src/test/suite/helpers.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
}
125 changes: 125 additions & 0 deletions vscode/src/test/suite/launch.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>(),
);

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<Client>((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);
});

0 comments on commit 751ad9e

Please sign in to comment.