Skip to content

Commit 00c168b

Browse files
authored
Decouple Ruby tests from specific machine setup (#4088)
1 parent ac6feb6 commit 00c168b

2 files changed

Lines changed: 130 additions & 110 deletions

File tree

vscode/src/test/suite/helpers.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import os from "os";
33
import fs from "fs";
44

55
import * as vscode from "vscode";
6+
import sinon from "sinon";
67

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

@@ -57,6 +58,45 @@ export const LSP_WORKSPACE_FOLDER: vscode.WorkspaceFolder = {
5758

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

61+
// Stubs `vscode.workspace.getConfiguration` so that requested keys return stubbed values, while any unspecified keys
62+
// (or unspecified sections) fall through to the real configuration
63+
export function stubWorkspaceConfiguration(
64+
sandbox: sinon.SinonSandbox,
65+
stubs: Record<string, Record<string, unknown>>,
66+
): sinon.SinonStub {
67+
const original = vscode.workspace.getConfiguration.bind(vscode.workspace);
68+
69+
return sandbox
70+
.stub(vscode.workspace, "getConfiguration")
71+
.callsFake((section?: string, scope?: vscode.ConfigurationScope | null) => {
72+
const real = original(section, scope);
73+
const sectionStubs = stubs[section ?? ""];
74+
75+
if (!sectionStubs) {
76+
return real;
77+
}
78+
79+
// Can't Proxy a WorkspaceConfiguration: VS Code defines `get` as non-configurable + non-writable, which
80+
// violates Proxy invariants. Instead, build a delegating object that overrides `get` and forwards everything
81+
// else to the real configuration (preserving `this` via bind so private state access still works).
82+
const wrapper: vscode.WorkspaceConfiguration = {
83+
get(key: string, defaultValue?: unknown) {
84+
if (key in sectionStubs) {
85+
return sectionStubs[key];
86+
}
87+
return defaultValue === undefined ? real.get(key) : real.get(key, defaultValue);
88+
},
89+
has(key: string) {
90+
return key in sectionStubs || real.has(key);
91+
},
92+
inspect: real.inspect.bind(real),
93+
update: real.update.bind(real),
94+
};
95+
96+
return wrapper;
97+
});
98+
}
99+
60100
export function createContext() {
61101
const subscriptions: vscode.Disposable[] = [];
62102

vscode/src/test/suite/ruby.test.ts

Lines changed: 90 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,27 @@ import { Shadowenv, UntrustedWorkspaceError } from "../../ruby/shadowenv";
1515
import { Chruby } from "../../ruby/chruby";
1616
import { ACTIVATION_SEPARATOR, FIELD_SEPARATOR, MissingRubyError, VALUE_SEPARATOR } from "../../ruby/versionManager";
1717

18-
import { createContext, FakeContext } from "./helpers";
18+
import { createContext, FakeContext, stubWorkspaceConfiguration } from "./helpers";
1919
import { FAKE_TELEMETRY } from "./fakeTelemetry";
2020

21+
interface EnvStub {
22+
rubyVersion: string;
23+
gemPath: string;
24+
yjitEnabled: string;
25+
envVars: string[];
26+
}
27+
28+
const DEFAULT_ENV_STUB: EnvStub = {
29+
rubyVersion: "3.3.5",
30+
gemPath: "~/.gem/ruby/3.3.5,/opt/rubies/3.3.5/lib/ruby/gems/3.3.0",
31+
yjitEnabled: "true",
32+
envVars: [`ANY${VALUE_SEPARATOR}true`],
33+
};
34+
35+
function buildEnvStub(stub: EnvStub = DEFAULT_ENV_STUB): string {
36+
return [stub.rubyVersion, stub.gemPath, stub.yjitEnabled, ...stub.envVars].join(FIELD_SEPARATOR);
37+
}
38+
2139
suite("Ruby environment activation", () => {
2240
const workspacePath = path.dirname(path.dirname(path.dirname(path.dirname(__dirname))));
2341
const workspaceFolder: vscode.WorkspaceFolder = {
@@ -39,81 +57,67 @@ suite("Ruby environment activation", () => {
3957
context.dispose();
4058
});
4159

42-
test("Activate fetches Ruby information when outside of Ruby LSP", async () => {
43-
const manager = process.env.CI ? ManagerIdentifier.None : ManagerIdentifier.Chruby;
44-
45-
sandbox.stub(vscode.workspace, "getConfiguration").returns({
46-
get: (name: string) => {
47-
if (name === "rubyVersionManager") {
48-
return { identifier: manager };
49-
} else if (name === "bundleGemfile") {
50-
return "";
51-
}
52-
53-
return undefined;
60+
test("Populates Ruby version and YJIT support from the activation script", async () => {
61+
stubWorkspaceConfiguration(sandbox, {
62+
rubyLsp: {
63+
rubyVersionManager: { identifier: ManagerIdentifier.None },
64+
bundleGemfile: "",
5465
},
55-
} as unknown as vscode.WorkspaceConfiguration);
66+
});
67+
68+
sandbox.stub(common, "asyncExec").resolves({
69+
stdout: "",
70+
stderr: `${ACTIVATION_SEPARATOR}${buildEnvStub()}${ACTIVATION_SEPARATOR}`,
71+
});
5672

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

60-
assert.ok(ruby.rubyVersion, "Expected Ruby version to be set");
61-
assert.notStrictEqual(ruby.yjitEnabled, undefined, "Expected YJIT support to be set to true or false");
62-
}).timeout(10000);
76+
assert.strictEqual(ruby.rubyVersion, "3.3.5");
77+
assert.strictEqual(ruby.yjitEnabled, true);
78+
});
6379

6480
test("Deletes verbose and GC settings from activated environment", async () => {
65-
const manager = process.env.CI ? ManagerIdentifier.None : ManagerIdentifier.Chruby;
81+
stubWorkspaceConfiguration(sandbox, {
82+
rubyLsp: {
83+
rubyVersionManager: { identifier: ManagerIdentifier.None },
84+
bundleGemfile: "",
85+
},
86+
});
6687

67-
sandbox.stub(vscode.workspace, "getConfiguration").returns({
68-
get: (name: string) => {
69-
if (name === "rubyVersionManager") {
70-
return { identifier: manager };
71-
} else if (name === "bundleGemfile") {
72-
return "";
73-
}
88+
const envStub = buildEnvStub({
89+
...DEFAULT_ENV_STUB,
90+
envVars: [
91+
`VERBOSE${VALUE_SEPARATOR}1`,
92+
`DEBUG${VALUE_SEPARATOR}WARN`,
93+
`RUBY_GC_HEAP_GROWTH_FACTOR${VALUE_SEPARATOR}1.7`,
94+
],
95+
});
7496

75-
return undefined;
76-
},
77-
} as unknown as vscode.WorkspaceConfiguration);
97+
sandbox.stub(common, "asyncExec").resolves({
98+
stdout: "",
99+
stderr: `${ACTIVATION_SEPARATOR}${envStub}${ACTIVATION_SEPARATOR}`,
100+
});
78101

79102
const ruby = new Ruby(context, workspaceFolder, outputChannel, FAKE_TELEMETRY);
80-
81-
process.env.VERBOSE = "1";
82-
process.env.DEBUG = "WARN";
83-
process.env.RUBY_GC_HEAP_GROWTH_FACTOR = "1.7";
84103
await ruby.activateRuby();
85104

86105
assert.strictEqual(ruby.env.VERBOSE, undefined);
87106
assert.strictEqual(ruby.env.DEBUG, undefined);
88107
assert.strictEqual(ruby.env.RUBY_GC_HEAP_GROWTH_FACTOR, undefined);
89-
delete process.env.VERBOSE;
90-
delete process.env.DEBUG;
91-
delete process.env.RUBY_GC_HEAP_GROWTH_FACTOR;
92108
});
93109

94110
test("Sets gem path for version managers based on shims", async () => {
95-
sandbox.stub(vscode.workspace, "getConfiguration").returns({
96-
get: (name: string) => {
97-
if (name === "rubyVersionManager") {
98-
return { identifier: ManagerIdentifier.Rbenv };
99-
} else if (name === "bundleGemfile") {
100-
return "";
101-
}
102-
103-
return undefined;
111+
stubWorkspaceConfiguration(sandbox, {
112+
rubyLsp: {
113+
rubyVersionManager: { identifier: ManagerIdentifier.Rbenv },
114+
bundleGemfile: "",
104115
},
105-
} as unknown as vscode.WorkspaceConfiguration);
106-
107-
const envStub = [
108-
"3.3.5",
109-
"~/.gem/ruby/3.3.5,/opt/rubies/3.3.5/lib/ruby/gems/3.3.0",
110-
"true",
111-
`ANY${VALUE_SEPARATOR}true`,
112-
].join(FIELD_SEPARATOR);
116+
});
113117

114118
sandbox.stub(common, "asyncExec").resolves({
115119
stdout: "",
116-
stderr: `${ACTIVATION_SEPARATOR}${envStub}${ACTIVATION_SEPARATOR}`,
120+
stderr: `${ACTIVATION_SEPARATOR}${buildEnvStub()}${ACTIVATION_SEPARATOR}`,
117121
});
118122

119123
const ruby = new Ruby(context, workspaceFolder, outputChannel, FAKE_TELEMETRY);
@@ -163,19 +167,17 @@ suite("Ruby environment activation", () => {
163167
});
164168

165169
test("Clears outdated workspace Ruby path caches", async () => {
166-
const manager = process.env.CI ? ManagerIdentifier.None : ManagerIdentifier.Chruby;
167-
168-
sandbox.stub(vscode.workspace, "getConfiguration").returns({
169-
get: (name: string) => {
170-
if (name === "rubyVersionManager") {
171-
return { identifier: manager };
172-
} else if (name === "bundleGemfile") {
173-
return "";
174-
}
175-
176-
return undefined;
170+
stubWorkspaceConfiguration(sandbox, {
171+
rubyLsp: {
172+
rubyVersionManager: { identifier: ManagerIdentifier.None },
173+
bundleGemfile: "",
177174
},
178-
} as unknown as vscode.WorkspaceConfiguration);
175+
});
176+
177+
sandbox.stub(common, "asyncExec").resolves({
178+
stdout: "",
179+
stderr: `${ACTIVATION_SEPARATOR}${buildEnvStub()}${ACTIVATION_SEPARATOR}`,
180+
});
179181

180182
await context.workspaceState.update(
181183
`rubyLsp.workspaceRubyPath.${workspaceFolder.name}`,
@@ -202,29 +204,17 @@ suite("Ruby environment activation", () => {
202204
index: 0,
203205
};
204206

205-
sandbox.stub(vscode.workspace, "getConfiguration").returns({
206-
get: (name: string) => {
207-
if (name === "rubyVersionManager") {
208-
return { identifier: ManagerIdentifier.None };
209-
} else if (name === "bundleGemfile") {
210-
// eslint-disable-next-line no-template-curly-in-string
211-
return "${workspaceFolder}/Gemfile";
212-
}
213-
214-
return undefined;
207+
stubWorkspaceConfiguration(sandbox, {
208+
rubyLsp: {
209+
rubyVersionManager: { identifier: ManagerIdentifier.None },
210+
// eslint-disable-next-line no-template-curly-in-string
211+
bundleGemfile: "${workspaceFolder}/Gemfile",
215212
},
216-
} as unknown as vscode.WorkspaceConfiguration);
217-
218-
const envStub = [
219-
"3.3.5",
220-
"~/.gem/ruby/3.3.5,/opt/rubies/3.3.5/lib/ruby/gems/3.3.0",
221-
"true",
222-
`ANY${VALUE_SEPARATOR}true`,
223-
].join(FIELD_SEPARATOR);
213+
});
224214

225215
sandbox.stub(common, "asyncExec").resolves({
226216
stdout: "",
227-
stderr: `${ACTIVATION_SEPARATOR}${envStub}${ACTIVATION_SEPARATOR}`,
217+
stderr: `${ACTIVATION_SEPARATOR}${buildEnvStub()}${ACTIVATION_SEPARATOR}`,
228218
});
229219

230220
const ruby = new Ruby(context, tmpWorkspaceFolder, outputChannel, FAKE_TELEMETRY);
@@ -235,24 +225,19 @@ suite("Ruby environment activation", () => {
235225
});
236226

237227
test("Appends YJIT flag to existing RUBYOPT for Ruby 3.2", async () => {
238-
sandbox.stub(vscode.workspace, "getConfiguration").returns({
239-
get: (name: string) => {
240-
if (name === "rubyVersionManager") {
241-
return { identifier: ManagerIdentifier.None };
242-
} else if (name === "bundleGemfile") {
243-
return "";
244-
}
245-
246-
return undefined;
228+
stubWorkspaceConfiguration(sandbox, {
229+
rubyLsp: {
230+
rubyVersionManager: { identifier: ManagerIdentifier.None },
231+
bundleGemfile: "",
247232
},
248-
} as unknown as vscode.WorkspaceConfiguration);
233+
});
249234

250-
const envStub = [
251-
"3.2.0",
252-
"~/.gem/ruby/3.2.0,/opt/rubies/3.2.0/lib/ruby/gems/3.2.0",
253-
"true",
254-
`RUBYOPT${VALUE_SEPARATOR}-rbundler/setup`,
255-
].join(FIELD_SEPARATOR);
235+
const envStub = buildEnvStub({
236+
...DEFAULT_ENV_STUB,
237+
rubyVersion: "3.2.0",
238+
gemPath: "~/.gem/ruby/3.2.0,/opt/rubies/3.2.0/lib/ruby/gems/3.2.0",
239+
envVars: [`RUBYOPT${VALUE_SEPARATOR}-rbundler/setup`],
240+
});
256241

257242
sandbox.stub(common, "asyncExec").resolves({
258243
stdout: "",
@@ -275,17 +260,12 @@ suite("Ruby environment activation", () => {
275260

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

278-
sandbox.stub(vscode.workspace, "getConfiguration").returns({
279-
get: (name: string) => {
280-
if (name === "bundleGemfile") {
281-
return nonExistentGemfile;
282-
} else if (name === "rubyVersionManager") {
283-
return { identifier: ManagerIdentifier.None };
284-
}
285-
286-
return undefined;
263+
stubWorkspaceConfiguration(sandbox, {
264+
rubyLsp: {
265+
rubyVersionManager: { identifier: ManagerIdentifier.None },
266+
bundleGemfile: nonExistentGemfile,
287267
},
288-
} as unknown as vscode.WorkspaceConfiguration);
268+
});
289269

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

0 commit comments

Comments
 (0)