Files
kandev/apps/backend/internal/tools/installer/command_runner_strategy_test.go
T
2026-08-05 10:57:24 +01:00

292 lines
8.4 KiB
Go

package installer
import (
"context"
"errors"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/kandev/kandev/internal/common/logger"
)
type recordingCommandRunner struct {
spec CommandSpec
output []byte
err error
environment map[string]string
environmentErr error
}
func (r *recordingCommandRunner) CombinedOutput(_ context.Context, spec CommandSpec) ([]byte, error) {
r.spec = spec
return r.output, r.err
}
func (r *recordingCommandRunner) CommandEnvironment() (map[string]string, error) {
return r.environment, r.environmentErr
}
func installStrategyTestLogger(t *testing.T) *logger.Logger {
t.Helper()
log, err := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json", OutputPath: os.DevNull})
if err != nil {
t.Fatal(err)
}
return log
}
func putFakeExecutableOnPath(t *testing.T, name string) string {
t.Helper()
if runtime.GOOS == windowsOS {
name += ".exe"
}
dir := t.TempDir()
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte("fixture"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir)
return path
}
func putFakeNpmShim(t *testing.T, binDir, binary string) string {
t.Helper()
name := binary
if runtime.GOOS == windowsOS {
name += ".cmd"
}
path := filepath.Join(binDir, "node_modules", ".bin", name)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte("fixture"), 0o755); err != nil {
t.Fatal(err)
}
return path
}
func TestFindCommandInDirectoryUsesPlatformExtensions(t *testing.T) {
directory := t.TempDir()
name := "language-server"
if runtime.GOOS == windowsOS {
name += ".cmd"
}
want := filepath.Join(directory, name)
if err := os.WriteFile(want, []byte("fixture"), 0o755); err != nil {
t.Fatal(err)
}
runner := &recordingCommandRunner{environment: map[string]string{"PATHEXT": ".CMD;.EXE"}}
got, err := FindCommandInDirectory("language-server", directory, runner)
if err != nil {
t.Fatalf("FindCommandInDirectory() error = %v", err)
}
if got != want {
t.Fatalf("FindCommandInDirectory() = %q, want %q", got, want)
}
}
func TestNpmStrategyUsesInjectedCommandRunner(t *testing.T) {
npmPath := putFakeExecutableOnPath(t, "npm")
binDir := t.TempDir()
binaryPath := putFakeNpmShim(t, binDir, "pyright-langserver")
runner := &recordingCommandRunner{}
strategy := NewNpmStrategy(binDir, "pyright-langserver", []string{"pyright"}, installStrategyTestLogger(t), runner)
result, err := strategy.Install(context.Background())
if err != nil {
t.Fatalf("Install() error = %v", err)
}
if result.BinaryPath != binaryPath {
t.Fatalf("BinaryPath = %q, want %q", result.BinaryPath, binaryPath)
}
if runner.spec.Path != npmPath || runner.spec.Dir != binDir {
t.Fatalf("command spec = %+v, want path %q and dir %q", runner.spec, npmPath, binDir)
}
wantArgs := []string{installSubcommand, "--prefix", binDir, "pyright"}
if len(runner.spec.Args) != len(wantArgs) {
t.Fatalf("args = %v, want %v", runner.spec.Args, wantArgs)
}
for i := range wantArgs {
if runner.spec.Args[i] != wantArgs[i] {
t.Fatalf("args[%d] = %q, want %q", i, runner.spec.Args[i], wantArgs[i])
}
}
}
func TestNpmStrategyUsesRunnerEnvironmentForLookup(t *testing.T) {
t.Setenv("PATH", t.TempDir())
taskBin := t.TempDir()
npmName := "npm"
if runtime.GOOS == windowsOS {
npmName += ".exe"
}
npmPath := filepath.Join(taskBin, npmName)
if err := os.WriteFile(npmPath, []byte("fixture"), 0o755); err != nil {
t.Fatal(err)
}
binDir := t.TempDir()
putFakeNpmShim(t, binDir, "pyright-langserver")
runner := &recordingCommandRunner{environment: map[string]string{"PATH": taskBin}}
strategy := NewNpmStrategy(
binDir,
"pyright-langserver",
[]string{"pyright"},
installStrategyTestLogger(t),
runner,
)
if _, err := strategy.Install(context.Background()); err != nil {
t.Fatalf("Install() error = %v", err)
}
if runner.spec.Path != npmPath {
t.Fatalf("command path = %q, want task-environment path %q", runner.spec.Path, npmPath)
}
}
func TestGoInstallStrategyUsesInjectedCommandRunner(t *testing.T) {
goPath := putFakeExecutableOnPath(t, "go")
runnerErr := errors.New("managed runner stopped")
runner := &recordingCommandRunner{output: []byte("compiler output"), err: runnerErr}
strategy := NewGoInstallStrategy("gopls", "golang.org/x/tools/gopls@latest", installStrategyTestLogger(t), runner)
_, err := strategy.Install(context.Background())
if !errors.Is(err, runnerErr) {
t.Fatalf("Install() error = %v, want %v", err, runnerErr)
}
if runner.spec.Path != goPath {
t.Fatalf("command path = %q, want %q", runner.spec.Path, goPath)
}
wantArgs := []string{installSubcommand, "golang.org/x/tools/gopls@latest"}
if len(runner.spec.Args) != len(wantArgs) {
t.Fatalf("args = %v, want %v", runner.spec.Args, wantArgs)
}
for i := range wantArgs {
if runner.spec.Args[i] != wantArgs[i] {
t.Fatalf("args[%d] = %q, want %q", i, runner.spec.Args[i], wantArgs[i])
}
}
}
func TestGoInstallStrategyUsesRunnerEnvironmentForLookupAndResult(t *testing.T) {
parentPath := t.TempDir()
t.Setenv("PATH", parentPath)
t.Setenv("GOBIN", "")
t.Setenv("GOPATH", "")
taskBin := t.TempDir()
goName := "go"
goplsName := "gopls"
if runtime.GOOS == windowsOS {
goName += ".exe"
goplsName += ".exe"
}
goPath := filepath.Join(taskBin, goName)
if err := os.WriteFile(goPath, []byte("fixture"), 0o755); err != nil {
t.Fatal(err)
}
gobin := t.TempDir()
goplsPath := filepath.Join(gobin, goplsName)
if err := os.WriteFile(goplsPath, []byte("fixture"), 0o755); err != nil {
t.Fatal(err)
}
runner := &recordingCommandRunner{environment: map[string]string{
"PATH": taskBin,
"GOBIN": gobin,
"HOME": t.TempDir(),
}}
strategy := NewGoInstallStrategy(
"gopls",
"golang.org/x/tools/gopls@latest",
installStrategyTestLogger(t),
runner,
)
result, err := strategy.Install(context.Background())
if err != nil {
t.Fatalf("Install() error = %v", err)
}
if runner.spec.Path != goPath {
t.Fatalf("command path = %q, want task-environment path %q", runner.spec.Path, goPath)
}
if runner.spec.Env["GOBIN"] != gobin {
t.Fatalf("command GOBIN = %q, want %q", runner.spec.Env["GOBIN"], gobin)
}
if result.BinaryPath != goplsPath {
t.Fatalf("BinaryPath = %q, want task-environment path %q", result.BinaryPath, goplsPath)
}
}
func TestFindGoBinaryWithRunnerUsesUserProfileFallback(t *testing.T) {
userProfile := t.TempDir()
binaryName := "gopls"
if runtime.GOOS == windowsOS {
binaryName += ".exe"
}
binaryPath := filepath.Join(userProfile, "go", "bin", binaryName)
if err := os.MkdirAll(filepath.Dir(binaryPath), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(binaryPath, []byte("fixture"), 0o755); err != nil {
t.Fatal(err)
}
runner := &recordingCommandRunner{environment: map[string]string{
"GOBIN": "",
"GOPATH": "",
"HOME": "",
"USERPROFILE": userProfile,
}}
got, err := FindGoBinaryWithRunner("gopls", runner)
if err != nil {
t.Fatalf("FindGoBinaryWithRunner() error = %v", err)
}
if got != binaryPath {
t.Fatalf("FindGoBinaryWithRunner() = %q, want %q", got, binaryPath)
}
}
func TestFindGoBinaryWithRunnerRejectsRelativeDirectories(t *testing.T) {
t.Chdir(t.TempDir())
binaryName := "gopls"
if runtime.GOOS == windowsOS {
binaryName += ".exe"
}
tests := []struct {
name string
envKey string
envValue string
binaryDir string
}{
{name: "GOBIN", envKey: "GOBIN", envValue: "controlled-gobin", binaryDir: "controlled-gobin"},
{name: "GOPATH", envKey: "GOPATH", envValue: "controlled-gopath", binaryDir: filepath.Join("controlled-gopath", "bin")},
{name: "HOME", envKey: "HOME", envValue: "controlled-home", binaryDir: filepath.Join("controlled-home", "go", "bin")},
{name: "USERPROFILE", envKey: "USERPROFILE", envValue: "controlled-profile", binaryDir: filepath.Join("controlled-profile", "go", "bin")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := os.MkdirAll(tt.binaryDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tt.binaryDir, binaryName), []byte("fixture"), 0o755); err != nil {
t.Fatal(err)
}
env := map[string]string{
"GOBIN": "",
"GOPATH": "",
"HOME": "",
"USERPROFILE": "",
}
env[tt.envKey] = tt.envValue
if got, err := FindGoBinaryWithRunner("gopls", &recordingCommandRunner{environment: env}); err == nil {
t.Fatalf("FindGoBinaryWithRunner() = %q from relative %s, want not found", got, tt.envKey)
}
})
}
}