-
Notifications
You must be signed in to change notification settings - Fork 148
SSH: Check for Remote SSH extension in VS Code and Cursor #4676
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| package vscode | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "runtime" | ||
| "testing" | ||
|
|
||
| "github.com/databricks/cli/libs/cmdio" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
@@ -88,3 +90,208 @@ func TestCheckIDECommand_Found(t *testing.T) { | |
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestParseExtensionVersion(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| output string | ||
| extensionID string | ||
| wantVersion string | ||
| wantFound bool | ||
| minVersion string | ||
| wantAtLeast bool | ||
| }{ | ||
| { | ||
| name: "found and above minimum", | ||
| output: "ms-python.python@2024.1.1\nms-vscode-remote.remote-ssh@0.123.0\n", | ||
| extensionID: "ms-vscode-remote.remote-ssh", | ||
| wantVersion: "0.123.0", | ||
| wantFound: true, | ||
| minVersion: "0.120.0", | ||
| wantAtLeast: true, | ||
| }, | ||
| { | ||
| name: "found but below minimum", | ||
| output: "ms-vscode-remote.remote-ssh@0.100.0\n", | ||
| extensionID: "ms-vscode-remote.remote-ssh", | ||
| wantVersion: "0.100.0", | ||
| wantFound: true, | ||
| minVersion: "0.120.0", | ||
| wantAtLeast: false, | ||
| }, | ||
| { | ||
| name: "not found", | ||
| output: "ms-python.python@2024.1.1\n", | ||
| extensionID: "ms-vscode-remote.remote-ssh", | ||
| wantVersion: "", | ||
| wantFound: false, | ||
| }, | ||
| { | ||
| name: "empty output", | ||
| output: "", | ||
| extensionID: "ms-vscode-remote.remote-ssh", | ||
| wantVersion: "", | ||
| wantFound: false, | ||
| }, | ||
| { | ||
| name: "multiple extensions", | ||
| output: "ext.a@1.0.0\next.b@2.0.0\next.c@3.0.0\n", | ||
| extensionID: "ext.b", | ||
| wantVersion: "2.0.0", | ||
| wantFound: true, | ||
| minVersion: "1.0.0", | ||
| wantAtLeast: true, | ||
| }, | ||
| { | ||
| name: "prerelease is less than release", | ||
| output: "ms-vscode-remote.remote-ssh@0.120.0-beta.1\n", | ||
| extensionID: "ms-vscode-remote.remote-ssh", | ||
| wantVersion: "0.120.0-beta.1", | ||
| wantFound: true, | ||
| minVersion: "0.120.0", | ||
| wantAtLeast: false, | ||
| }, | ||
| { | ||
| name: "line with whitespace", | ||
| output: " ms-vscode-remote.remote-ssh@0.123.0 \n", | ||
| extensionID: "ms-vscode-remote.remote-ssh", | ||
| wantVersion: "0.123.0", | ||
| wantFound: true, | ||
| minVersion: "0.120.0", | ||
| wantAtLeast: true, | ||
| }, | ||
| { | ||
| name: "windows CRLF line endings", | ||
| output: "ms-python.python@2024.1.1\r\nms-vscode-remote.remote-ssh@0.123.0\r\n", | ||
| extensionID: "ms-vscode-remote.remote-ssh", | ||
| wantVersion: "0.123.0", | ||
| wantFound: true, | ||
| minVersion: "0.120.0", | ||
| wantAtLeast: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| version, found := parseExtensionVersion(tt.output, tt.extensionID) | ||
| assert.Equal(t, tt.wantFound, found) | ||
| assert.Equal(t, tt.wantVersion, version) | ||
| if found { | ||
| assert.Equal(t, tt.wantAtLeast, isExtensionVersionAtLeast(version, tt.minVersion)) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestIsExtensionVersionAtLeast(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| version string | ||
| minVersion string | ||
| want bool | ||
| }{ | ||
| {name: "above minimum", version: "0.123.0", minVersion: "0.120.0", want: true}, | ||
| {name: "exact minimum", version: "0.120.0", minVersion: "0.120.0", want: true}, | ||
| {name: "below minimum", version: "0.100.0", minVersion: "0.120.0", want: false}, | ||
| {name: "major version ahead", version: "1.0.0", minVersion: "0.120.0", want: true}, | ||
| {name: "prerelease below release", version: "0.120.0-beta.1", minVersion: "0.120.0", want: false}, | ||
| {name: "prerelease above prior release", version: "0.121.0-beta.1", minVersion: "0.120.0", want: true}, | ||
| {name: "two-component version is valid", version: "1.0", minVersion: "0.120.0", want: true}, | ||
| {name: "empty version", version: "", minVersion: "0.120.0", want: false}, | ||
| {name: "garbage version", version: "abc", minVersion: "0.120.0", want: false}, | ||
| {name: "four-component version is invalid", version: "0.120.0.1", minVersion: "0.120.0", want: false}, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. version "0.121.0.0" would also be considered invalid? that's slightly confusing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not semver compatible, and vscode only supports semver - https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prerelease-extensions They technically don't even support pre release tags (e.g. |
||
| {name: "cursor exact minimum", version: "1.0.32", minVersion: "1.0.32", want: true}, | ||
| {name: "cursor above minimum", version: "1.1.0", minVersion: "1.0.32", want: true}, | ||
| {name: "cursor below minimum", version: "1.0.31", minVersion: "1.0.32", want: false}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| assert.Equal(t, tt.want, isExtensionVersionAtLeast(tt.version, tt.minVersion)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // createFakeIDEExecutable creates a fake IDE command that outputs the given text | ||
| // when called with --list-extensions --show-versions. | ||
| func createFakeIDEExecutable(t *testing.T, dir, command, output string) { | ||
| t.Helper() | ||
| if runtime.GOOS == "windows" { | ||
| // Write output to a temp file and use "type" to print it, avoiding escaping issues. | ||
| payloadPath := filepath.Join(dir, command+"-payload.txt") | ||
| err := os.WriteFile(payloadPath, []byte(output), 0o644) | ||
| require.NoError(t, err) | ||
| script := fmt.Sprintf("@echo off\ntype \"%s\"\n", payloadPath) | ||
| err = os.WriteFile(filepath.Join(dir, command+".cmd"), []byte(script), 0o755) | ||
| require.NoError(t, err) | ||
| } else { | ||
| // Use printf (a shell builtin) instead of cat to avoid PATH issues in tests. | ||
| script := fmt.Sprintf("#!/bin/sh\nprintf '%%s' '%s'\n", output) | ||
| err := os.WriteFile(filepath.Join(dir, command), []byte(script), 0o755) | ||
| require.NoError(t, err) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckIDESSHExtension_UpToDate(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| t.Setenv("PATH", tmpDir) | ||
| ctx, _ := cmdio.NewTestContextWithStdout(t.Context()) | ||
|
|
||
| extensionOutput := "ms-python.python@2024.1.1\nms-vscode-remote.remote-ssh@0.123.0\n" | ||
| createFakeIDEExecutable(t, tmpDir, "code", extensionOutput) | ||
|
|
||
| err := CheckIDESSHExtension(ctx, VSCodeOption) | ||
| assert.NoError(t, err) | ||
| } | ||
|
|
||
| func TestCheckIDESSHExtension_ExactMinVersion(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| t.Setenv("PATH", tmpDir) | ||
| ctx, _ := cmdio.NewTestContextWithStdout(t.Context()) | ||
|
|
||
| extensionOutput := "ms-vscode-remote.remote-ssh@0.120.0\n" | ||
| createFakeIDEExecutable(t, tmpDir, "code", extensionOutput) | ||
|
|
||
| err := CheckIDESSHExtension(ctx, VSCodeOption) | ||
| assert.NoError(t, err) | ||
| } | ||
|
|
||
| func TestCheckIDESSHExtension_Missing(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| t.Setenv("PATH", tmpDir) | ||
| ctx, _ := cmdio.NewTestContextWithStdout(t.Context()) | ||
|
|
||
| extensionOutput := "ms-python.python@2024.1.1\n" | ||
| createFakeIDEExecutable(t, tmpDir, "code", extensionOutput) | ||
|
|
||
| err := CheckIDESSHExtension(ctx, VSCodeOption) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), `"Remote - SSH"`) | ||
| assert.Contains(t, err.Error(), "not installed") | ||
| } | ||
|
|
||
| func TestCheckIDESSHExtension_Outdated(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| t.Setenv("PATH", tmpDir) | ||
| ctx, _ := cmdio.NewTestContextWithStdout(t.Context()) | ||
|
|
||
| extensionOutput := "ms-vscode-remote.remote-ssh@0.100.0\n" | ||
| createFakeIDEExecutable(t, tmpDir, "code", extensionOutput) | ||
|
|
||
| err := CheckIDESSHExtension(ctx, VSCodeOption) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "0.100.0") | ||
| assert.Contains(t, err.Error(), ">= 0.120.0") | ||
| } | ||
|
|
||
| func TestCheckIDESSHExtension_Cursor(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| t.Setenv("PATH", tmpDir) | ||
| ctx, _ := cmdio.NewTestContextWithStdout(t.Context()) | ||
|
|
||
| extensionOutput := "anysphere.remote-ssh@1.0.32\n" | ||
| createFakeIDEExecutable(t, tmpDir, "cursor", extensionOutput) | ||
|
|
||
| err := CheckIDESSHExtension(ctx, CursorOption) | ||
| assert.NoError(t, err) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a justified reason to use --force here? --force re-installs the extension even if it is already installed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only run the command if the extension is missing or if it's too old. Force flag ensures that the update actually happens in the second case (and for missing extensions force doesn't change the installation behavior)