Skip to content
Open
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
38 changes: 38 additions & 0 deletions vscode/src/test/suite/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import os from "os";
import fs from "fs";

import * as vscode from "vscode";
import sinon from "sinon";

import { MAJOR, MINOR, RUBY_VERSION } from "../rubyVersion";

Expand Down Expand Up @@ -57,6 +58,43 @@ export const LSP_WORKSPACE_FOLDER: vscode.WorkspaceFolder = {

export type FakeContext = vscode.ExtensionContext & { dispose: () => void };

// Stubs `vscode.workspace.getConfiguration` so that requested keys return stubbed values, while any unspecified keys
// (or unspecified sections) fall through to the real configuration
export function stubWorkspaceConfiguration(
sandbox: sinon.SinonSandbox,
stubs: Record<string, Record<string, unknown>>,
): sinon.SinonStub {
const original = vscode.workspace.getConfiguration.bind(vscode.workspace);

return sandbox
.stub(vscode.workspace, "getConfiguration")
.callsFake((section?: string, scope?: vscode.ConfigurationScope | null) => {
const real = original(section, scope);
const sectionStubs = stubs[section ?? ""];

if (!sectionStubs) {
return real;
}

// Can't Proxy a WorkspaceConfiguration: VS Code defines `get` as non-configurable + non-writable, which
// violates Proxy invariants. Instead, build a delegating object that overrides `get` and forwards everything
// else to the real configuration (preserving `this` via bind so private state access still works).
const wrapper: vscode.WorkspaceConfiguration = {
get(key: string, defaultValue?: unknown) {
if (key in sectionStubs) {
return sectionStubs[key];
}
return defaultValue === undefined ? real.get(key) : real.get(key, defaultValue);
},
has: real.has.bind(real),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we want to make sure has behave the same way get does to prevent mismatching results in the future?

inspect: real.inspect.bind(real),
update: real.update.bind(real),
};

return wrapper;
});
}

export function createContext() {
const subscriptions: vscode.Disposable[] = [];

Expand Down
162 changes: 76 additions & 86 deletions vscode/src/test/suite/ruby.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Shadowenv, UntrustedWorkspaceError } from "../../ruby/shadowenv";
import { Chruby } from "../../ruby/chruby";
import { ACTIVATION_SEPARATOR, FIELD_SEPARATOR, MissingRubyError, VALUE_SEPARATOR } from "../../ruby/versionManager";

import { createContext, FakeContext } from "./helpers";
import { createContext, FakeContext, stubWorkspaceConfiguration } from "./helpers";
import { FAKE_TELEMETRY } from "./fakeTelemetry";

suite("Ruby environment activation", () => {
Expand All @@ -39,70 +39,70 @@ suite("Ruby environment activation", () => {
context.dispose();
});

test("Activate fetches Ruby information when outside of Ruby LSP", async () => {
const manager = process.env.CI ? ManagerIdentifier.None : ManagerIdentifier.Chruby;
test("Populates Ruby version and YJIT support from the activation script", async () => {
stubWorkspaceConfiguration(sandbox, {
rubyLsp: {
rubyVersionManager: { identifier: ManagerIdentifier.None },
bundleGemfile: "",
},
});

sandbox.stub(vscode.workspace, "getConfiguration").returns({
get: (name: string) => {
if (name === "rubyVersionManager") {
return { identifier: manager };
} else if (name === "bundleGemfile") {
return "";
}
const envStub = [
"3.3.5",
"~/.gem/ruby/3.3.5,/opt/rubies/3.3.5/lib/ruby/gems/3.3.0",
"true",
`ANY${VALUE_SEPARATOR}true`,
].join(FIELD_SEPARATOR);

return undefined;
},
} as unknown as vscode.WorkspaceConfiguration);
sandbox.stub(common, "asyncExec").resolves({
stdout: "",
stderr: `${ACTIVATION_SEPARATOR}${envStub}${ACTIVATION_SEPARATOR}`,
});

const ruby = new Ruby(context, workspaceFolder, outputChannel, FAKE_TELEMETRY);
await ruby.activateRuby();

assert.ok(ruby.rubyVersion, "Expected Ruby version to be set");
assert.notStrictEqual(ruby.yjitEnabled, undefined, "Expected YJIT support to be set to true or false");
}).timeout(10000);
assert.strictEqual(ruby.rubyVersion, "3.3.5");
assert.strictEqual(ruby.yjitEnabled, true);
});

test("Deletes verbose and GC settings from activated environment", async () => {
const manager = process.env.CI ? ManagerIdentifier.None : ManagerIdentifier.Chruby;
stubWorkspaceConfiguration(sandbox, {
rubyLsp: {
rubyVersionManager: { identifier: ManagerIdentifier.None },
bundleGemfile: "",
},
});

sandbox.stub(vscode.workspace, "getConfiguration").returns({
get: (name: string) => {
if (name === "rubyVersionManager") {
return { identifier: manager };
} else if (name === "bundleGemfile") {
return "";
}
const envStub = [
"3.3.5",
"~/.gem/ruby/3.3.5,/opt/rubies/3.3.5/lib/ruby/gems/3.3.0",
"true",
`VERBOSE${VALUE_SEPARATOR}1`,
`DEBUG${VALUE_SEPARATOR}WARN`,
`RUBY_GC_HEAP_GROWTH_FACTOR${VALUE_SEPARATOR}1.7`,
].join(FIELD_SEPARATOR);

return undefined;
},
} as unknown as vscode.WorkspaceConfiguration);
sandbox.stub(common, "asyncExec").resolves({
stdout: "",
stderr: `${ACTIVATION_SEPARATOR}${envStub}${ACTIVATION_SEPARATOR}`,
});

const ruby = new Ruby(context, workspaceFolder, outputChannel, FAKE_TELEMETRY);

process.env.VERBOSE = "1";
process.env.DEBUG = "WARN";
process.env.RUBY_GC_HEAP_GROWTH_FACTOR = "1.7";
await ruby.activateRuby();

assert.strictEqual(ruby.env.VERBOSE, undefined);
assert.strictEqual(ruby.env.DEBUG, undefined);
assert.strictEqual(ruby.env.RUBY_GC_HEAP_GROWTH_FACTOR, undefined);
delete process.env.VERBOSE;
delete process.env.DEBUG;
delete process.env.RUBY_GC_HEAP_GROWTH_FACTOR;
});

test("Sets gem path for version managers based on shims", async () => {
sandbox.stub(vscode.workspace, "getConfiguration").returns({
get: (name: string) => {
if (name === "rubyVersionManager") {
return { identifier: ManagerIdentifier.Rbenv };
} else if (name === "bundleGemfile") {
return "";
}

return undefined;
stubWorkspaceConfiguration(sandbox, {
rubyLsp: {
rubyVersionManager: { identifier: ManagerIdentifier.Rbenv },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this stub out the rbenvExecutablePath config test runner may have?

bundleGemfile: "",
},
} as unknown as vscode.WorkspaceConfiguration);
});

const envStub = [
"3.3.5",
Expand Down Expand Up @@ -163,19 +163,24 @@ suite("Ruby environment activation", () => {
});

test("Clears outdated workspace Ruby path caches", async () => {
const manager = process.env.CI ? ManagerIdentifier.None : ManagerIdentifier.Chruby;
stubWorkspaceConfiguration(sandbox, {
rubyLsp: {
rubyVersionManager: { identifier: ManagerIdentifier.None },
bundleGemfile: "",
},
});

sandbox.stub(vscode.workspace, "getConfiguration").returns({
get: (name: string) => {
if (name === "rubyVersionManager") {
return { identifier: manager };
} else if (name === "bundleGemfile") {
return "";
}
const envStub = [
"3.3.5",
"~/.gem/ruby/3.3.5,/opt/rubies/3.3.5/lib/ruby/gems/3.3.0",
Comment on lines +174 to +175
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I noticed some of the values are repeated, could they be constants? Or defaults as part of a helper function?

"true",
`ANY${VALUE_SEPARATOR}true`,
].join(FIELD_SEPARATOR);

return undefined;
},
} as unknown as vscode.WorkspaceConfiguration);
sandbox.stub(common, "asyncExec").resolves({
stdout: "",
stderr: `${ACTIVATION_SEPARATOR}${envStub}${ACTIVATION_SEPARATOR}`,
});

await context.workspaceState.update(
`rubyLsp.workspaceRubyPath.${workspaceFolder.name}`,
Expand All @@ -202,18 +207,13 @@ suite("Ruby environment activation", () => {
index: 0,
};

sandbox.stub(vscode.workspace, "getConfiguration").returns({
get: (name: string) => {
if (name === "rubyVersionManager") {
return { identifier: ManagerIdentifier.None };
} else if (name === "bundleGemfile") {
// eslint-disable-next-line no-template-curly-in-string
return "${workspaceFolder}/Gemfile";
}

return undefined;
stubWorkspaceConfiguration(sandbox, {
rubyLsp: {
rubyVersionManager: { identifier: ManagerIdentifier.None },
// eslint-disable-next-line no-template-curly-in-string
bundleGemfile: "${workspaceFolder}/Gemfile",
},
} as unknown as vscode.WorkspaceConfiguration);
});

const envStub = [
"3.3.5",
Expand All @@ -235,17 +235,12 @@ suite("Ruby environment activation", () => {
});

test("Appends YJIT flag to existing RUBYOPT for Ruby 3.2", async () => {
sandbox.stub(vscode.workspace, "getConfiguration").returns({
get: (name: string) => {
if (name === "rubyVersionManager") {
return { identifier: ManagerIdentifier.None };
} else if (name === "bundleGemfile") {
return "";
}

return undefined;
stubWorkspaceConfiguration(sandbox, {
rubyLsp: {
rubyVersionManager: { identifier: ManagerIdentifier.None },
bundleGemfile: "",
},
} as unknown as vscode.WorkspaceConfiguration);
});

const envStub = [
"3.2.0",
Expand Down Expand Up @@ -275,17 +270,12 @@ suite("Ruby environment activation", () => {

const nonExistentGemfile = path.join(tmpPath, "nonexistent", "Gemfile");

sandbox.stub(vscode.workspace, "getConfiguration").returns({
get: (name: string) => {
if (name === "bundleGemfile") {
return nonExistentGemfile;
} else if (name === "rubyVersionManager") {
return { identifier: ManagerIdentifier.None };
}

return undefined;
stubWorkspaceConfiguration(sandbox, {
rubyLsp: {
rubyVersionManager: { identifier: ManagerIdentifier.None },
bundleGemfile: nonExistentGemfile,
},
} as unknown as vscode.WorkspaceConfiguration);
});

const ruby = new Ruby(context, tmpWorkspaceFolder, outputChannel, FAKE_TELEMETRY);

Expand Down
Loading