package shims import ( "errors" "fmt" "os" "path/filepath" "strings" "testing" "asdf/internal/config" "asdf/internal/installs" "asdf/internal/installtest" "asdf/internal/plugins" "asdf/internal/toolversions" "asdf/repotest" "github.com/stretchr/testify/assert" "golang.org/x/sys/unix" ) const testPluginName = "lua" func TestFindExecutable(t *testing.T) { version := "1.1.0" conf, plugin := generateConfig(t) installVersion(t, conf, plugin, version) stdout, stderr := buildOutputs() assert.Nil(t, GenerateAll(conf, &stdout, &stderr)) currentDir := t.TempDir() t.Run("returns error when shim with name does not exist", func(t *testing.T) { executable, _, version, found, err := FindExecutable(conf, "foo", currentDir) assert.Empty(t, executable) assert.False(t, found) assert.Empty(t, version) assert.Equal(t, err.(UnknownCommandError).Error(), "unknown command: foo") }) t.Run("returns error when shim is present but no version is set", func(t *testing.T) { executable, _, version, found, err := FindExecutable(conf, "dummy", currentDir) assert.Empty(t, executable) assert.False(t, found) assert.Empty(t, version) assert.Equal(t, err.(NoVersionSetError).Error(), "no versions set for dummy") }) t.Run("returns string containing path to executable when found", func(t *testing.T) { // write a version file data := []byte("lua 1.1.0") assert.Nil(t, os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666)) executable, gotPlugin, version, found, err := FindExecutable(conf, "dummy", currentDir) assert.Equal(t, filepath.Base(filepath.Dir(filepath.Dir(executable))), "1.1.0") assert.Equal(t, filepath.Base(executable), "dummy") assert.Equal(t, plugin, gotPlugin) assert.Equal(t, version, "1.1.0") assert.True(t, found) assert.Nil(t, err) }) t.Run("returns string containing path to system executable when system version set", func(t *testing.T) { // Create dummy `ls` executable versionStruct := toolversions.Version{Type: "version", Value: version} path := filepath.Join(installs.InstallPath(conf, plugin, versionStruct), "bin", "ls") assert.Nil(t, os.WriteFile(path, []byte("echo 'I'm ls'"), 0o777)) // write system version to version file toolpath := filepath.Join(currentDir, ".tool-versions") assert.Nil(t, os.WriteFile(toolpath, []byte("lua system\n"), 0o666)) assert.Nil(t, GenerateAll(conf, &stdout, &stderr)) executable, gotPlugin, version, found, err := FindExecutable(conf, "ls", currentDir) assert.Equal(t, plugin, gotPlugin) assert.Equal(t, version, "system") assert.True(t, found) assert.Nil(t, err) // see that it actually returns path to system ls assert.Equal(t, filepath.Base(executable), "ls") assert.NotEqual(t, executable, path) }) t.Run("returns path to executable on path when path version set", func(t *testing.T) { // write system version to version file toolpath := filepath.Join(currentDir, ".tool-versions") dir := installs.InstallPath(conf, plugin, toolversions.Version{Type: "version", Value: "1.1.0"}) pathVersion := fmt.Sprintf("path:%s/./", dir) assert.Nil(t, os.WriteFile(toolpath, []byte(fmt.Sprintf("lua %s\n", pathVersion)), 0o666)) assert.Nil(t, GenerateAll(conf, &stdout, &stderr)) executable, gotPlugin, version, found, err := FindExecutable(conf, "dummy", currentDir) assert.Equal(t, plugin, gotPlugin) assert.Equal(t, version, pathVersion) assert.True(t, found) assert.Nil(t, err) // see that it actually returns path to system ls assert.Equal(t, filepath.Base(executable), "dummy") assert.True(t, strings.HasPrefix(executable, dir)) }) } func TestGetExecutablePath(t *testing.T) { version := toolversions.Version{Type: "version", Value: "1.1.0"} conf, plugin := generateConfig(t) installVersion(t, conf, plugin, version.Value) t.Run("returns path to executable", func(t *testing.T) { path, err := GetExecutablePath(conf, plugin, "dummy", version) assert.Nil(t, err) assert.Equal(t, filepath.Base(path), "dummy") assert.Equal(t, filepath.Base(filepath.Dir(filepath.Dir(path))), version.Value) }) t.Run("returns error when executable with name not found", func(t *testing.T) { path, err := GetExecutablePath(conf, plugin, "foo", version) assert.ErrorContains(t, err, "executable not found") assert.Equal(t, path, "") }) t.Run("returns custom path when plugin has exec-path callback", func(t *testing.T) { // Create exec-path callback installDummyExecPathScript(t, conf, plugin, version, "dummy", "echo 'bin/custom/dummy'") path, err := GetExecutablePath(conf, plugin, "dummy", version) assert.Nil(t, err) assert.Equal(t, filepath.Base(filepath.Dir(path)), "custom") // Doesn't contain any trailing whitespace (newlines as the last char are common) assert.Equal(t, path, strings.TrimSpace(path)) }) t.Run("returns default path when plugin has exec-path callback that prints third argument", func(t *testing.T) { // Create exec-path callback installDummyExecPathScript(t, conf, plugin, version, "dummy", "echo \"$3\"") path, err := GetExecutablePath(conf, plugin, "dummy", version) assert.Nil(t, err) assert.Equal(t, filepath.Base(path), "dummy") assert.Equal(t, filepath.Base(filepath.Dir(path)), "bin") // Doesn't contain any trailing whitespace (newlines as the last char are common) assert.Equal(t, path, strings.TrimSpace(path)) }) } func TestRemoveAll(t *testing.T) { version := "1.1.0" conf, plugin := generateConfig(t) installVersion(t, conf, plugin, version) executables, err := ToolExecutables(conf, plugin, toolversions.Version{Type: "version", Value: version}) assert.Nil(t, err) stdout, stderr := buildOutputs() t.Run("removes all files in shim directory", func(t *testing.T) { assert.Nil(t, GenerateAll(conf, &stdout, &stderr)) assert.Nil(t, RemoveAll(conf)) // check for generated shims for _, executable := range executables { _, err := os.Stat(Path(conf, filepath.Base(executable))) assert.True(t, errors.Is(err, os.ErrNotExist)) } }) } func TestGenerateAll(t *testing.T) { version := "1.1.0" version2 := "2.0.0" conf, plugin := generateConfig(t) installVersion(t, conf, plugin, version) installPlugin(t, conf, "dummy_plugin", "ruby") installVersion(t, conf, plugin, version2) executables, err := ToolExecutables(conf, plugin, toolversions.Version{Type: "version", Value: version}) assert.Nil(t, err) stdout, stderr := buildOutputs() t.Run("generates shim script for every executable in every version of every tool", func(t *testing.T) { assert.Nil(t, GenerateAll(conf, &stdout, &stderr)) // check for generated shims for _, executable := range executables { shimName := filepath.Base(executable) shimPath := Path(conf, shimName) assert.Nil(t, unix.Access(shimPath, unix.X_OK)) // shim exists and has expected contents content, err := os.ReadFile(shimPath) assert.Nil(t, err) want := fmt.Sprintf("#!/usr/bin/env bash\n# asdf-plugin: lua 2.0.0\n# asdf-plugin: lua 1.1.0\nexec asdf exec \"%s\" \"$@\"", shimName) assert.Equal(t, want, string(content)) } }) } func TestGenerateForPluginVersions(t *testing.T) { t.Setenv("ASDF_CONFIG_FILE", "testdata/asdfrc") version := "1.1.0" version2 := "2.0.0" conf, plugin := generateConfig(t) installVersion(t, conf, plugin, version) installVersion(t, conf, plugin, version2) executables, err := ToolExecutables(conf, plugin, toolversions.Version{Type: "version", Value: version}) assert.Nil(t, err) stdout, stderr := buildOutputs() t.Run("generates shim script for every executable in every version the tool", func(t *testing.T) { assert.Nil(t, GenerateForPluginVersions(conf, plugin, &stdout, &stderr)) // check for generated shims for _, executable := range executables { shimName := filepath.Base(executable) shimPath := Path(conf, shimName) assert.Nil(t, unix.Access(shimPath, unix.X_OK)) // shim exists and has expected contents content, err := os.ReadFile(shimPath) assert.Nil(t, err) want := fmt.Sprintf("#!/usr/bin/env bash\n# asdf-plugin: lua 2.0.0\n# asdf-plugin: lua 1.1.0\nexec asdf exec \"%s\" \"$@\"", shimName) assert.Equal(t, want, string(content)) } }) t.Run("runs pre and post reshim hooks", func(t *testing.T) { stdout, stderr := buildOutputs() assert.Nil(t, GenerateForPluginVersions(conf, plugin, &stdout, &stderr)) want := "pre_reshim 1.1.0\npost_reshim 1.1.0\npre_reshim 2.0.0\npost_reshim 2.0.0\n" assert.Equal(t, want, stdout.String()) }) } func TestGenerateForVersion(t *testing.T) { version := toolversions.Version{Type: "version", Value: "1.1.0"} version2 := toolversions.Version{Type: "version", Value: "2.0.0"} conf, plugin := generateConfig(t) installVersion(t, conf, plugin, version.Value) installVersion(t, conf, plugin, version2.Value) executables, err := ToolExecutables(conf, plugin, version) assert.Nil(t, err) t.Run("generates shim script for every executable in version", func(t *testing.T) { stdout, stderr := buildOutputs() assert.Nil(t, GenerateForVersion(conf, plugin, version, &stdout, &stderr)) // check for generated shims for _, executable := range executables { shimName := filepath.Base(executable) shimPath := Path(conf, shimName) assert.Nil(t, unix.Access(shimPath, unix.X_OK)) } }) t.Run("updates existing shims for every executable in version", func(t *testing.T) { stdout, stderr := buildOutputs() assert.Nil(t, GenerateForVersion(conf, plugin, version, &stdout, &stderr)) assert.Nil(t, GenerateForVersion(conf, plugin, version2, &stdout, &stderr)) // check for generated shims for _, executable := range executables { shimName := filepath.Base(executable) shimPath := Path(conf, shimName) assert.Nil(t, unix.Access(shimPath, unix.X_OK)) } }) } func TestWrite(t *testing.T) { version := toolversions.Version{Type: "version", Value: "1.1.0"} version2 := toolversions.Version{Type: "version", Value: "2.0.0"} conf, plugin := generateConfig(t) installVersion(t, conf, plugin, version.Value) installVersion(t, conf, plugin, version2.Value) executables, err := ToolExecutables(conf, plugin, version) executable := executables[0] assert.Nil(t, err) t.Run("writes a new shim file when doesn't exist", func(t *testing.T) { executable := executables[0] err = Write(conf, plugin, version, executable) assert.Nil(t, err) // shim is executable shimName := filepath.Base(executable) shimPath := Path(conf, shimName) assert.Nil(t, unix.Access(shimPath, unix.X_OK)) // shim exists and has expected contents content, err := os.ReadFile(shimPath) assert.Nil(t, err) want := "#!/usr/bin/env bash\n# asdf-plugin: lua 1.1.0\nexec asdf exec \"dummy\" \"$@\"" assert.Equal(t, want, string(content)) os.Remove(shimPath) }) t.Run("updates an existing shim file when already present", func(t *testing.T) { // Write same shim for two versions assert.Nil(t, Write(conf, plugin, version, executable)) assert.Nil(t, Write(conf, plugin, version2, executable)) // shim is still executable shimName := filepath.Base(executable) shimPath := Path(conf, shimName) assert.Nil(t, unix.Access(shimPath, unix.X_OK)) // has expected contents content, err := os.ReadFile(shimPath) assert.Nil(t, err) want := "#!/usr/bin/env bash\n# asdf-plugin: lua 2.0.0\n# asdf-plugin: lua 1.1.0\nexec asdf exec \"dummy\" \"$@\"" assert.Equal(t, want, string(content)) os.Remove(shimPath) }) t.Run("doesn't add the same version to a shim file twice", func(t *testing.T) { assert.Nil(t, Write(conf, plugin, version, executable)) assert.Nil(t, Write(conf, plugin, version, executable)) // Shim doesn't contain any duplicate lines shimPath := Path(conf, filepath.Base(executable)) content, err := os.ReadFile(shimPath) assert.Nil(t, err) want := "#!/usr/bin/env bash\n# asdf-plugin: lua 1.1.0\nexec asdf exec \"dummy\" \"$@\"" assert.Equal(t, want, string(content)) os.Remove(shimPath) }) } func TestToolExecutables(t *testing.T) { version := toolversions.Version{Type: "version", Value: "1.1.0"} conf, plugin := generateConfig(t) installVersion(t, conf, plugin, version.Value) t.Run("returns list of executables for plugin", func(t *testing.T) { executables, err := ToolExecutables(conf, plugin, version) assert.Nil(t, err) var filenames []string for _, executablePath := range executables { assert.True(t, strings.HasPrefix(executablePath, conf.DataDir)) filenames = append(filenames, filepath.Base(executablePath)) } assert.Equal(t, filenames, []string{"dummy"}) }) t.Run("returns list of executables for version installed in arbitrary directory", func(t *testing.T) { // Reference regular install by path to validate this behavior path := installs.InstallPath(conf, plugin, version) executables, err := ToolExecutables(conf, plugin, toolversions.Version{Type: "path", Value: path}) assert.Nil(t, err) var filenames []string for _, executablePath := range executables { assert.True(t, strings.HasPrefix(executablePath, conf.DataDir)) filenames = append(filenames, filepath.Base(executablePath)) } assert.Equal(t, filenames, []string{"dummy"}) }) t.Run("returns list of executables even when one directory printed by list-bin-paths doesn't exist", func(t *testing.T) { // foo is first in list returned by list-bin-paths but doesn't exist, do // we still get the executables in the bin/ dir? repotest.WritePluginCallback(plugin.Dir, "list-bin-paths", "#!/usr/bin/env bash\necho 'foo bin'") executables, err := ToolExecutables(conf, plugin, version) assert.Nil(t, err) var filenames []string for _, executablePath := range executables { assert.True(t, strings.HasPrefix(executablePath, conf.DataDir)) filenames = append(filenames, filepath.Base(executablePath)) } assert.Equal(t, filenames, []string{"dummy"}) }) } func TestExecutablePaths(t *testing.T) { conf, plugin := generateConfig(t) installVersion(t, conf, plugin, "1.2.3") t.Run("returns list only containing 'bin' when list-bin-paths callback missing", func(t *testing.T) { executables, err := ExecutablePaths(conf, plugin, toolversions.Version{Type: "version", Value: "1.2.3"}) path := executables[0] assert.Nil(t, err) assert.Equal(t, filepath.Base(filepath.Dir(path)), "1.2.3") assert.Equal(t, filepath.Base(path), "bin") }) t.Run("returns list of executable paths for tool version", func(t *testing.T) { data := []byte("echo 'foo bar'") err := os.WriteFile(filepath.Join(plugin.Dir, "bin", "list-bin-paths"), data, 0o777) assert.Nil(t, err) executables, err := ExecutablePaths(conf, plugin, toolversions.Version{Type: "version", Value: "1.2.3"}) path1 := executables[0] path2 := executables[1] assert.Nil(t, err) assert.Equal(t, filepath.Base(path1), "foo") assert.Equal(t, filepath.Base(path2), "bar") }) } func TestExecutableDirs(t *testing.T) { conf, plugin := generateConfig(t) installVersion(t, conf, plugin, "1.2.3") t.Run("returns list only containing 'bin' when list-bin-paths callback missing", func(t *testing.T) { executables, err := ExecutableDirs(plugin) assert.Nil(t, err) assert.Equal(t, executables, []string{"bin"}) }) t.Run("returns list of executable paths for tool version", func(t *testing.T) { data := []byte("echo 'foo bar'") err := os.WriteFile(filepath.Join(plugin.Dir, "bin", "list-bin-paths"), data, 0o777) assert.Nil(t, err) executables, err := ExecutableDirs(plugin) assert.Nil(t, err) assert.Equal(t, executables, []string{"foo", "bar"}) }) } // Helper functions func buildOutputs() (strings.Builder, strings.Builder) { var stdout strings.Builder var stderr strings.Builder return stdout, stderr } func generateConfig(t *testing.T) (config.Config, plugins.Plugin) { t.Helper() testDataDir := t.TempDir() conf, err := config.LoadConfig() assert.Nil(t, err) conf.DataDir = testDataDir return conf, installPlugin(t, conf, "dummy_plugin", testPluginName) } func installDummyExecPathScript(t *testing.T, conf config.Config, plugin plugins.Plugin, version toolversions.Version, name, script string) { t.Helper() execPath := filepath.Join(plugin.Dir, "bin", "exec-path") contents := fmt.Sprintf("#!/usr/bin/env bash\n%s\n", script) err := os.WriteFile(execPath, []byte(contents), 0o777) assert.Nil(t, err) installPath := installs.InstallPath(conf, plugin, version) err = os.MkdirAll(filepath.Join(installPath, "bin", "custom"), 0o777) assert.Nil(t, err) err = os.WriteFile(filepath.Join(installPath, "bin", "custom", name), []byte{}, 0o777) assert.Nil(t, err) } func installPlugin(t *testing.T, conf config.Config, fixture, pluginName string) plugins.Plugin { _, err := repotest.InstallPlugin(fixture, conf.DataDir, pluginName) assert.Nil(t, err) return plugins.New(conf, pluginName) } func installVersion(t *testing.T, conf config.Config, plugin plugins.Plugin, version string) { t.Helper() err := installtest.InstallOneVersion(conf, plugin, "version", version) assert.Nil(t, err) }