Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add debugger integration test for project with local Bundler settings #2882

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
// 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 @@

// 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 Expand Up @@ -626,7 +627,7 @@
delete newConfig.useBundler;

const command = (newConfig.command || "").replace(
"${workspaceRoot}/",

Check warning on line 630 in vscode/src/rubyLsp.ts

View workflow job for this annotation

GitHub Actions / lint_node

Unexpected template string expression

Check warning on line 630 in vscode/src/rubyLsp.ts

View workflow job for this annotation

GitHub Actions / lint_node

Unexpected template string expression
"",
);
const script = newConfig.script || "";
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: [],
vinistock marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there two as?

let workspacePath: string;
let workspaceUri: vscode.Uri;
let workspaceFolder: vscode.WorkspaceFolder;
const originalSaveBeforeStart = vscode.workspace
vinistock marked this conversation as resolved.
Show resolved Hide resolved
.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 });
vinistock marked this conversation as resolved.
Show resolved Hide resolved
});

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);
vinistock marked this conversation as resolved.
Show resolved Hide resolved
fs.writeFileSync(
vinistock marked this conversation as resolved.
Show resolved Hide resolved
path.join(workspacePath, "Gemfile"),
'source "https://rubygems.org"\n',
);
fs.writeFileSync(
path.join(workspacePath, "Gemfile.lock"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be read from a fixture?

[
"GEM",
" remote: https://rubygems.org/",
" specs:",
"",
"PLATFORMS",
" arm64-darwin-23",
" ruby",
"",
"DEPENDENCIES",
"",
"BUNDLED WITH",
" 2.5.16",
vinistock marked this conversation as resolved.
Show resolved Hide resolved
].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(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const stub = sinon.stub(vscode.window, "activeTextEditor").get(() => {
const windowStub = 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()}`),
);
}
}
}
Loading