Skip to content

Commit

Permalink
Add debugger integration test for project with local Bundler settings
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Jan 7, 2025
1 parent 460743e commit a38fdb4
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 62 deletions.
11 changes: 8 additions & 3 deletions vscode/src/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ export class Debugger
uri: vscode.Uri | undefined,
) => Workspace | undefined;

private readonly context: vscode.ExtensionContext;

constructor(
context: vscode.ExtensionContext,
workspaceResolver: (uri: vscode.Uri | undefined) => Workspace | undefined,
) {
this.workspaceResolver = workspaceResolver;

this.context = context;
context.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider("ruby_lsp", this),
vscode.debug.registerDebugAdapterDescriptorFactory("ruby_lsp", this),
Expand Down Expand Up @@ -258,9 +261,6 @@ export class Debugger

this.logDebuggerMessage(`Spawning debugger in directory ${cwd}`);
this.logDebuggerMessage(` Command bundle ${args.join(" ")}`);
this.logDebuggerMessage(
` Environment ${JSON.stringify(configuration.env)}`,
);

this.debugProcess = spawn("bundle", args, {
shell: true,
Expand Down Expand Up @@ -354,5 +354,10 @@ export class Debugger
// Log to Debug Console: Unlike Output panel, this needs explicit newlines
// so we preserve the original message format including any newlines
this.console.append(message);

if (this.context.extensionMode === vscode.ExtensionMode.Test) {
// eslint-disable-next-line no-console
console.log(message);
}
}
}
11 changes: 6 additions & 5 deletions vscode/src/rubyLsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import { collectRubyLspInfo } from "./infoCollector";
// activation event. One instance of this class controls all of the existing workspaces, telemetry and handles all
// commands
export class RubyLsp {
private readonly workspaces: Map<string, Workspace> = new Map();
// Only public for testing
public readonly workspaces: Map<string, Workspace> = new Map();
private readonly context: vscode.ExtensionContext;
private readonly statusItems: StatusItems;
private readonly testController: TestController;
Expand Down Expand Up @@ -119,10 +120,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
Expand Down
58 changes: 4 additions & 54 deletions vscode/src/test/suite/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,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, MAJOR, MINOR } from "../rubyVersion";
import { MAJOR, MINOR } from "../rubyVersion";

import { FAKE_TELEMETRY } from "./fakeTelemetry";
import { ensureRubyInstallationPaths } from "./testHelpers";

class FakeLogger {
receivedMessages = "";
Expand Down Expand Up @@ -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,
Expand Down
162 changes: 162 additions & 0 deletions vscode/src/test/suite/rubyLsp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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 { State } from "vscode-languageclient";

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-integration-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: ${path.join("vendor", "bundle")}`,
);

await ensureRubyInstallationPaths();

const rubyLsp = new RubyLsp(context, FAKE_TELEMETRY);

try {
await rubyLsp.activate(workspaceFolder);

const client = rubyLsp.workspaces.get(
workspaceFolder.uri.toString(),
)!.lspClient!;

if (client.state !== State.Running) {
await new Promise<void>((resolve) => {
const callback = client.onDidChangeState(() => {
if (client.state === State.Running) {
callback.dispose();
resolve();
}
});
});
}
} catch (error: any) {
assert.fail(
`Failed to activate Ruby LSP: ${error.message}\n\n${error.stack}`,
);
}

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) => {
if (!("logLevel" in subscription)) {
subscription.dispose();
}
});

resolve();
});
});
}).timeout(90000);
});
5 changes: 5 additions & 0 deletions vscode/src/test/suite/testController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down
66 changes: 66 additions & 0 deletions vscode/src/test/suite/testHelpers.ts
Original file line number Diff line number Diff line change
@@ -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()}`),
);
}
}
}

0 comments on commit a38fdb4

Please sign in to comment.