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

Warn users if shadownev is not installed #2498

Merged
merged 1 commit into from
Aug 28, 2024
Merged
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: 27 additions & 11 deletions vscode/src/ruby/shadowenv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,34 @@ export class Shadowenv extends VersionManager {
version: parsedResult.version,
};
} catch (error: any) {
// If running `shadowev exec` fails, it's typically because the workspace has not been trusted yet. Here we offer
// to trust it and fail it the user decides to not the trust the workspace (since in that case, we are unable to
// activate the Ruby environment).
const answer = await vscode.window.showErrorMessage(
`Failed to run shadowenv exec. Is ${this.bundleUri.fsPath} trusted? Run 'shadowenv trust --help' to know more`,
"Trust workspace",
"Cancel",
);
const { stdout } = await this.runScript("command -v shadowenv");

if (stdout.trim().length === 0) {
const answer = await vscode.window.showErrorMessage(
`Couldn't find shadowenv executable. Double-check that it's installed and that it's in your PATH.`,
"Reload window",
"Cancel",
);

if (answer === "Reload window") {
return vscode.commands.executeCommand(
"workbench.action.reloadWindow",
);
}
} else {
// If running `shadowev exec` fails, it's typically because the workspace has not been trusted yet. Here we
// offer to trust it and fail it the user decides to not the trust the workspace (since in that case, we are
// unable to activate the Ruby environment).
const answer = await vscode.window.showErrorMessage(
`Failed to run shadowenv. Is ${this.bundleUri.fsPath} trusted? Run 'shadowenv trust --help' to know more`,
"Trust workspace",
"Cancel",
);

if (answer === "Trust workspace") {
await asyncExec("shadowenv trust", { cwd: this.bundleUri.fsPath });
return this.activate();
if (answer === "Trust workspace") {
await asyncExec("shadowenv trust", { cwd: this.bundleUri.fsPath });
return this.activate();
}
}

throw new Error(
Expand Down
149 changes: 83 additions & 66 deletions vscode/src/test/suite/ruby/shadowenv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Shadowenv } from "../../../ruby/shadowenv";
import { WorkspaceChannel } from "../../../workspaceChannel";
import { LOG_CHANNEL, asyncExec } from "../../../common";
import { RUBY_VERSION } from "../../rubyVersion";
import * as common from "../../../common";

suite("Shadowenv", () => {
if (os.platform() === "win32") {
Expand All @@ -24,8 +25,8 @@ suite("Shadowenv", () => {
let workspacePath: string;
let workspaceFolder: vscode.WorkspaceFolder;
let outputChannel: WorkspaceChannel;
let bundleGemfileStub: sinon.SinonStub;
let rubyBinPath: string;
const [major, minor, patch] = RUBY_VERSION.split(".");

if (process.env.CI && os.platform() === "linux") {
rubyBinPath = path.join(
Expand Down Expand Up @@ -57,17 +58,49 @@ suite("Shadowenv", () => {
`Ruby bin path does not exist ${rubyBinPath}`,
);

const shadowLispFile = `
(provide "ruby" "${RUBY_VERSION}")

(when-let ((ruby-root (env/get "RUBY_ROOT")))
(env/remove-from-pathlist "PATH" (path-concat ruby-root "bin"))
(when-let ((gem-root (env/get "GEM_ROOT")))
(env/remove-from-pathlist "PATH" (path-concat gem-root "bin")))
(when-let ((gem-home (env/get "GEM_HOME")))
(env/remove-from-pathlist "PATH" (path-concat gem-home "bin"))))

(env/set "BUNDLE_PATH" ())
(env/set "GEM_PATH" ())
(env/set "GEM_HOME" ())
(env/set "RUBYOPT" ())
(env/set "RUBYLIB" ())

(env/set "RUBY_ROOT" "${path.dirname(rubyBinPath)}")
(env/prepend-to-pathlist "PATH" "${rubyBinPath}")
(env/set "RUBY_ENGINE" "ruby")
(env/set "RUBY_VERSION" "${RUBY_VERSION}")
(env/set "GEM_ROOT" "${path.dirname(rubyBinPath)}/lib/ruby/gems/${major}.${minor}.0")

(when-let ((gem-root (env/get "GEM_ROOT")))
(env/prepend-to-pathlist "GEM_PATH" gem-root)
(env/prepend-to-pathlist "PATH" (path-concat gem-root "bin")))

(let ((gem-home
(path-concat (env/get "HOME") ".gem" (env/get "RUBY_ENGINE") "${RUBY_VERSION}")))
(do
(env/set "GEM_HOME" gem-home)
(env/prepend-to-pathlist "GEM_PATH" gem-home)
(env/prepend-to-pathlist "PATH" (path-concat gem-home "bin"))))
`;

beforeEach(() => {
rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "ruby-lsp-test-chruby-"));
rootPath = fs.mkdtempSync(
path.join(os.tmpdir(), "ruby-lsp-test-shadowenv-"),
);
workspacePath = path.join(rootPath, "workspace");

fs.mkdirSync(workspacePath);
fs.mkdirSync(path.join(workspacePath, ".shadowenv.d"));

bundleGemfileStub = sinon
.stub(vscode.workspace, "getConfiguration")
.returns({ get: () => path.join(workspacePath, "Gemfile") } as any)!;

workspaceFolder = {
uri: vscode.Uri.from({ scheme: "file", path: workspacePath }),
name: path.basename(workspacePath),
Expand All @@ -78,7 +111,6 @@ suite("Shadowenv", () => {

afterEach(() => {
fs.rmSync(rootPath, { recursive: true, force: true });
bundleGemfileStub.restore();
});

test("Finds Ruby only binary path is appended to PATH", async () => {
Expand All @@ -102,26 +134,7 @@ suite("Shadowenv", () => {

fs.writeFileSync(
path.join(workspacePath, ".shadowenv.d", "500_ruby.lisp"),
`(provide "ruby" "${RUBY_VERSION}")
(when-let ((ruby-root (env/get "RUBY_ROOT")))
(env/remove-from-pathlist "PATH" (path-concat ruby-root "bin"))
(when-let ((gem-root (env/get "GEM_ROOT")))
(env/remove-from-pathlist "PATH" (path-concat gem-root "bin")))
(when-let ((gem-home (env/get "GEM_HOME")))
(env/remove-from-pathlist "PATH" (path-concat gem-home "bin"))))

(env/set "BUNDLE_PATH" ())
(env/set "GEM_PATH" ())
(env/set "GEM_HOME" ())
(env/set "RUBYOPT" ())
(env/set "RUBYLIB" ())

(env/set "RUBY_ROOT" "${path.dirname(rubyBinPath)}")
(env/prepend-to-pathlist "PATH" "${rubyBinPath}")
(env/set "RUBY_ENGINE" "ruby")
(env/set "RUBY_VERSION" "${RUBY_VERSION}")
(env/set "GEM_ROOT" "${path.dirname(rubyBinPath)}/lib/ruby/gems/${RUBY_VERSION}")
`,
shadowLispFile,
);

const shadowenv = new Shadowenv(workspaceFolder, outputChannel);
Expand All @@ -130,45 +143,16 @@ suite("Shadowenv", () => {
assert.match(env.PATH!, new RegExp(rubyBinPath));
assert.strictEqual(
env.GEM_ROOT,
`${path.dirname(rubyBinPath)}/lib/ruby/gems/${RUBY_VERSION}`,
`${path.dirname(rubyBinPath)}/lib/ruby/gems/${major}.${minor}.0`,
);
assert.strictEqual(version, RUBY_VERSION);
assert.notStrictEqual(yjit, undefined);
});

test("Overrides GEM_HOME and GEM_PATH if necessary", async () => {
await asyncExec("shadowenv trust", { cwd: workspacePath });

fs.writeFileSync(
path.join(workspacePath, ".shadowenv.d", "500_ruby.lisp"),
`(env/set "RUBY_ENGINE" "ruby")
(env/set "RUBY_VERSION" "${RUBY_VERSION}")

(env/set "GEM_HOME" "/fake/.bundle/project/${RUBY_VERSION}")
(env/set "GEM_PATH" "/fake/.bundle/project/${RUBY_VERSION}:")
(env/prepend-to-pathlist "PATH" "/fake/.bundle/project/${RUBY_VERSION}/bin")
(env/prepend-to-pathlist "PATH" "${rubyBinPath}")`,
);

const shadowenv = new Shadowenv(workspaceFolder, outputChannel);
const { env, version, yjit } = await shadowenv.activate();

assert.match(env.PATH!, new RegExp(rubyBinPath));
assert.strictEqual(env.GEM_HOME, `/fake/.bundle/project/${RUBY_VERSION}`);
assert.strictEqual(version, RUBY_VERSION);
assert.notStrictEqual(yjit, undefined);
});

test("Untrusted workspace offers to trust it", async () => {
fs.writeFileSync(
path.join(workspacePath, ".shadowenv.d", "500_ruby.lisp"),
`(env/set "RUBY_ENGINE" "ruby")
(env/set "RUBY_VERSION" "${RUBY_VERSION}")

(env/set "GEM_HOME" "/fake/.bundle/project/${RUBY_VERSION}")
(env/set "GEM_PATH" "/fake/.bundle/project/${RUBY_VERSION}:")
(env/prepend-to-pathlist "PATH" "/fake/.bundle/project/${RUBY_VERSION}/bin")
(env/prepend-to-pathlist "PATH" "${rubyBinPath}")`,
shadowLispFile,
);

const stub = sinon
Expand All @@ -179,7 +163,10 @@ suite("Shadowenv", () => {
const { env, version, yjit } = await shadowenv.activate();

assert.match(env.PATH!, new RegExp(rubyBinPath));
assert.strictEqual(env.GEM_HOME, `/fake/.bundle/project/${RUBY_VERSION}`);
assert.match(
env.GEM_HOME!,
new RegExp(`\\.gem\\/ruby\\/${major}\\.${minor}\\.${patch}`),
);
assert.strictEqual(version, RUBY_VERSION);
assert.notStrictEqual(yjit, undefined);

Expand All @@ -191,13 +178,7 @@ suite("Shadowenv", () => {
test("Deciding not to trust the workspace fails activation", async () => {
fs.writeFileSync(
path.join(workspacePath, ".shadowenv.d", "500_ruby.lisp"),
`(env/set "RUBY_ENGINE" "ruby")
(env/set "RUBY_VERSION" "${RUBY_VERSION}")

(env/set "GEM_HOME" "/fake/.bundle/project/${RUBY_VERSION}")
(env/set "GEM_PATH" "/fake/.bundle/project/${RUBY_VERSION}:")
(env/prepend-to-pathlist "PATH" "/fake/.bundle/project/${RUBY_VERSION}/bin")
(env/prepend-to-pathlist "PATH" "${rubyBinPath}")`,
shadowLispFile,
);

const stub = sinon
Expand All @@ -214,4 +195,40 @@ suite("Shadowenv", () => {

stub.restore();
});

test("Warns user is shadowenv executable can't be found", async () => {
await asyncExec("shadowenv trust", { cwd: workspacePath });

fs.writeFileSync(
path.join(workspacePath, ".shadowenv.d", "500_ruby.lisp"),
shadowLispFile,
);

const shadowenv = new Shadowenv(workspaceFolder, outputChannel);

// First, reject the call to `shadowenv exec`. Then resolve the call to `which shadowenv` to return nothing
const execStub = sinon
.stub(common, "asyncExec")
.onFirstCall()
.rejects(new Error("shadowenv: command not found"))
.onSecondCall()
.resolves({ stdout: "", stderr: "" });

const windowStub = sinon
.stub(vscode.window, "showErrorMessage")
.resolves("Cancel" as any);

await assert.rejects(async () => {
await shadowenv.activate();
});

assert.ok(
windowStub.calledOnceWith(
"Couldn't find shadowenv executable. Double-check that it's installed and that it's in your PATH.",
),
);

execStub.restore();
windowStub.restore();
});
});
Loading