From be52d8f39c3aa253496c5469963cd0ecc995d19c Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Thu, 5 Sep 2024 09:55:13 -0400 Subject: [PATCH] feat(golang-rewrite): create installs and installtest packages to avoid circular dependency * Correct `go test` command in GitHub test workflow * Update execute tests to work on Github Actions * Check in `shims/testdata` directory * Create `installtest` helper package * Create `installs` package --- .github/workflows/tests.yml | 2 +- cmd/cmd.go | 3 +- internal/execute/execute_test.go | 4 +- internal/installs/installs.go | 64 +++++++++++++++++++++++ internal/installs/installs_test.go | 72 ++++++++++++++++++++++++++ internal/installtest/installtest.go | 78 +++++++++++++++++++++++++++++ internal/shims/shims.go | 6 +-- internal/shims/shims_test.go | 5 +- internal/shims/testdata/asdfrc | 2 + internal/versions/versions.go | 56 ++------------------- internal/versions/versions_test.go | 38 -------------- 11 files changed, 230 insertions(+), 100 deletions(-) create mode 100644 internal/installs/installs.go create mode 100644 internal/installs/installs_test.go create mode 100644 internal/installtest/installtest.go create mode 100644 internal/shims/testdata/asdfrc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7edab3c9..1e599044 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,7 +52,7 @@ jobs: - name: Install dependencies run: go get . - name: Run Go tests - run: go test + run: go test -coverprofile=/tmp/coverage.out -bench= -race ./... # Because I changed the test helper code Bash tests now fail. I removed them # from here to get passing checks. They can be added back at a later time if diff --git a/cmd/cmd.go b/cmd/cmd.go index 1fc89c8e..b0e53fbb 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -10,6 +10,7 @@ import ( "asdf/internal/config" "asdf/internal/info" + "asdf/internal/installs" "asdf/internal/plugins" "asdf/internal/shims" "asdf/internal/versions" @@ -425,7 +426,7 @@ func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bo } if showStatus { - installed := versions.IsInstalled(conf, plugin, latest) + installed := installs.IsInstalled(conf, plugin, latest) fmt.Printf("%s\t%s\t%s\n", plugin.Name, latest, installedStatus(installed)) } else { fmt.Printf("%s\n", latest) diff --git a/internal/execute/execute_test.go b/internal/execute/execute_test.go index 7486ab5b..da11b1f0 100644 --- a/internal/execute/execute_test.go +++ b/internal/execute/execute_test.go @@ -33,7 +33,7 @@ func TestRun_Command(t *testing.T) { err := cmd.Run() assert.Nil(t, err) - assert.Equal(t, "sh is /bin/sh\n", stdout.String()) + assert.Contains(t, stdout.String(), "sh is /") }) t.Run("positional arg is passed to command", func(t *testing.T) { @@ -108,7 +108,7 @@ func TestRun_Expression(t *testing.T) { err := cmd.Run() assert.Nil(t, err) - assert.Equal(t, "sh is /bin/sh\n", stdout.String()) + assert.Contains(t, stdout.String(), "sh is /") }) t.Run("positional arg is passed to expression", func(t *testing.T) { diff --git a/internal/installs/installs.go b/internal/installs/installs.go new file mode 100644 index 00000000..fd18553e --- /dev/null +++ b/internal/installs/installs.go @@ -0,0 +1,64 @@ +// Package installs contains tool installation logic. It is "dumb" when it comes +// to versions and treats versions as opaque strings. It cannot depend on the +// versions package because the versions package relies on this page. +package installs + +import ( + "io/fs" + "os" + "path/filepath" + + "asdf/internal/config" + "asdf/internal/plugins" +) + +const ( + dataDirInstalls = "installs" + dataDirDownloads = "downloads" +) + +// Installed returns a slice of all installed versions for a given plugin +func Installed(conf config.Config, plugin plugins.Plugin) (versions []string, err error) { + installDirectory := pluginInstallPath(conf, plugin) + files, err := os.ReadDir(installDirectory) + if err != nil { + if _, ok := err.(*fs.PathError); ok { + return versions, nil + } + + return versions, err + } + + for _, file := range files { + if !file.IsDir() { + continue + } + + versions = append(versions, file.Name()) + } + + return versions, err +} + +// InstallPath returns the path to a tool installation +func InstallPath(conf config.Config, plugin plugins.Plugin, version string) string { + return filepath.Join(pluginInstallPath(conf, plugin), version) +} + +// DownloadPath returns the download path for a particular plugin and version +func DownloadPath(conf config.Config, plugin plugins.Plugin, version string) string { + return filepath.Join(conf.DataDir, dataDirDownloads, plugin.Name, version) +} + +// IsInstalled checks if a specific version of a tool is installed +func IsInstalled(conf config.Config, plugin plugins.Plugin, version string) bool { + installDir := InstallPath(conf, plugin, version) + + // Check if version already installed + _, err := os.Stat(installDir) + return !os.IsNotExist(err) +} + +func pluginInstallPath(conf config.Config, plugin plugins.Plugin) string { + return filepath.Join(conf.DataDir, dataDirInstalls, plugin.Name) +} diff --git a/internal/installs/installs_test.go b/internal/installs/installs_test.go new file mode 100644 index 00000000..6a9e4cd6 --- /dev/null +++ b/internal/installs/installs_test.go @@ -0,0 +1,72 @@ +package installs + +import ( + "os" + "testing" + + "asdf/internal/config" + "asdf/internal/installtest" + "asdf/internal/plugins" + "asdf/repotest" + + "github.com/stretchr/testify/assert" +) + +const testPluginName = "lua" + +func TestInstalled(t *testing.T) { + conf, plugin := generateConfig(t) + + t.Run("returns empty slice for newly installed plugin", func(t *testing.T) { + installedVersions, err := Installed(conf, plugin) + assert.Nil(t, err) + assert.Empty(t, installedVersions) + }) + + t.Run("returns slice of all installed versions for a tool", func(t *testing.T) { + mockInstall(t, conf, plugin, "1.0.0") + + installedVersions, err := Installed(conf, plugin) + assert.Nil(t, err) + assert.Equal(t, installedVersions, []string{"1.0.0"}) + }) +} + +func TestIsInstalled(t *testing.T) { + conf, plugin := generateConfig(t) + installVersion(t, conf, plugin, "1.0.0") + + t.Run("returns false when not installed", func(t *testing.T) { + assert.False(t, IsInstalled(conf, plugin, "4.0.0")) + }) + t.Run("returns true when installed", func(t *testing.T) { + assert.True(t, IsInstalled(conf, plugin, "1.0.0")) + }) +} + +// helper functions +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 + + _, err = repotest.InstallPlugin("dummy_plugin", testDataDir, testPluginName) + assert.Nil(t, err) + + return conf, plugins.New(conf, testPluginName) +} + +func mockInstall(t *testing.T, conf config.Config, plugin plugins.Plugin, version string) { + t.Helper() + path := InstallPath(conf, plugin, version) + err := os.MkdirAll(path, os.ModePerm) + assert.Nil(t, err) +} + +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) +} diff --git a/internal/installtest/installtest.go b/internal/installtest/installtest.go new file mode 100644 index 00000000..e1e64c19 --- /dev/null +++ b/internal/installtest/installtest.go @@ -0,0 +1,78 @@ +// Package installtest provides functions used by various asdf tests for +// installing versions of tools. It provides a simplified version of the +// versions.InstallOneVersion function. +package installtest + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "asdf/internal/config" + "asdf/internal/plugins" +) + +const ( + dataDirInstalls = "installs" + dataDirDownloads = "downloads" +) + +// InstallOneVersion is a simplified version of versions.InstallOneVersion +// function for use in Go tests. +func InstallOneVersion(conf config.Config, plugin plugins.Plugin, versionType, version string) error { + var stdOut strings.Builder + var stdErr strings.Builder + + err := plugin.Exists() + if err != nil { + return err + } + + downloadDir := DownloadPath(conf, plugin, version) + installDir := InstallPath(conf, plugin, version) + + env := map[string]string{ + "ASDF_INSTALL_TYPE": versionType, + "ASDF_INSTALL_VERSION": version, + "ASDF_INSTALL_PATH": installDir, + "ASDF_DOWNLOAD_PATH": downloadDir, + "ASDF_CONCURRENCY": "1", + } + + err = os.MkdirAll(downloadDir, 0o777) + if err != nil { + return fmt.Errorf("unable to create download dir: %w", err) + } + + err = plugin.RunCallback("download", []string{}, env, &stdOut, &stdErr) + if _, ok := err.(plugins.NoCallbackError); err != nil && !ok { + return fmt.Errorf("failed to run download callback: %w", err) + } + + err = os.MkdirAll(installDir, 0o777) + if err != nil { + return fmt.Errorf("unable to create install dir: %w", err) + } + + err = plugin.RunCallback("install", []string{}, env, &stdOut, &stdErr) + if err != nil { + return fmt.Errorf("failed to run install callback: %w", err) + } + + return nil +} + +// InstallPath returns the path to a tool installation +func InstallPath(conf config.Config, plugin plugins.Plugin, version string) string { + return filepath.Join(pluginInstallPath(conf, plugin), version) +} + +// DownloadPath returns the download path for a particular plugin and version +func DownloadPath(conf config.Config, plugin plugins.Plugin, version string) string { + return filepath.Join(conf.DataDir, dataDirDownloads, plugin.Name, version) +} + +func pluginInstallPath(conf config.Config, plugin plugins.Plugin) string { + return filepath.Join(conf.DataDir, dataDirInstalls, plugin.Name) +} diff --git a/internal/shims/shims.go b/internal/shims/shims.go index 2650a6dc..cfc9edd0 100644 --- a/internal/shims/shims.go +++ b/internal/shims/shims.go @@ -11,9 +11,9 @@ import ( "asdf/internal/config" "asdf/internal/hook" + "asdf/internal/installs" "asdf/internal/plugins" "asdf/internal/toolversions" - "asdf/internal/versions" "golang.org/x/sys/unix" ) @@ -56,7 +56,7 @@ func GenerateAll(conf config.Config, stdOut io.Writer, stdErr io.Writer) error { // GenerateForPluginVersions generates all shims for all installed versions of // a tool. func GenerateForPluginVersions(conf config.Config, plugin plugins.Plugin, stdOut io.Writer, stdErr io.Writer) error { - installedVersions, err := versions.Installed(conf, plugin) + installedVersions, err := installs.Installed(conf, plugin) if err != nil { return err } @@ -135,7 +135,7 @@ func ToolExecutables(conf config.Config, plugin plugins.Plugin, version string) return executables, err } - installPath := versions.InstallPath(conf, plugin, version) + installPath := installs.InstallPath(conf, plugin, version) paths := dirsToPaths(dirs, installPath) for _, path := range paths { diff --git a/internal/shims/shims_test.go b/internal/shims/shims_test.go index 913fa315..bb9ab093 100644 --- a/internal/shims/shims_test.go +++ b/internal/shims/shims_test.go @@ -9,8 +9,8 @@ import ( "testing" "asdf/internal/config" + "asdf/internal/installtest" "asdf/internal/plugins" - "asdf/internal/versions" "asdf/repotest" "github.com/stretchr/testify/assert" @@ -266,7 +266,6 @@ func installPlugin(t *testing.T, conf config.Config, fixture, pluginName string) func installVersion(t *testing.T, conf config.Config, plugin plugins.Plugin, version string) { t.Helper() - stdout, stderr := buildOutputs() - err := versions.InstallOneVersion(conf, plugin, version, &stdout, &stderr) + err := installtest.InstallOneVersion(conf, plugin, "version", version) assert.Nil(t, err) } diff --git a/internal/shims/testdata/asdfrc b/internal/shims/testdata/asdfrc new file mode 100644 index 00000000..53671b97 --- /dev/null +++ b/internal/shims/testdata/asdfrc @@ -0,0 +1,2 @@ +pre_asdf_reshim_lua = echo pre_reshim $@ +post_asdf_reshim_lua = echo post_reshim $@ diff --git a/internal/versions/versions.go b/internal/versions/versions.go index 9eadb086..3d1c7a3e 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -6,14 +6,13 @@ import ( "errors" "fmt" "io" - "io/fs" "os" - "path/filepath" "regexp" "strings" "asdf/internal/config" "asdf/internal/hook" + "asdf/internal/installs" "asdf/internal/plugins" "asdf/internal/resolve" ) @@ -22,8 +21,6 @@ const ( systemVersion = "system" latestVersion = "latest" uninstallableVersionMsg = "uninstallable version: system" - dataDirDownloads = "downloads" - dataDirInstalls = "installs" latestFilterRegex = "(?i)(^Available versions:|-src|-dev|-latest|-stm|[-\\.]rc|-milestone|-alpha|-beta|[-\\.]pre|-next|(a|b|c)[0-9]+|snapshot|master)" noLatestVersionErrMsg = "no latest version found" ) @@ -49,29 +46,6 @@ func (e NoVersionSetError) Error() string { return "no version set" } -// Installed returns a slice of all installed versions for a given plugin -func Installed(conf config.Config, plugin plugins.Plugin) (versions []string, err error) { - installDirectory := pluginInstallPath(conf, plugin) - files, err := os.ReadDir(installDirectory) - if err != nil { - if _, ok := err.(*fs.PathError); ok { - return versions, nil - } - - return versions, err - } - - for _, file := range files { - if !file.IsDir() { - continue - } - - versions = append(versions, file.Name()) - } - - return versions, err -} - // InstallAll installs all specified versions of every tool for the current // directory. Typically this will just be a single version, if not already // installed, but it may be multiple versions if multiple versions for the tool @@ -154,11 +128,11 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string return UninstallableVersionError{} } - downloadDir := downloadPath(conf, plugin, version) - installDir := InstallPath(conf, plugin, version) + downloadDir := installs.DownloadPath(conf, plugin, version) + installDir := installs.InstallPath(conf, plugin, version) versionType, version := ParseString(version) - if IsInstalled(conf, plugin, version) { + if installs.IsInstalled(conf, plugin, version) { return fmt.Errorf("version %s of %s is already installed", version, plugin.Name) } @@ -222,15 +196,6 @@ func asdfConcurrency(conf config.Config) string { return val } -// IsInstalled checks if a specific version of a tool is installed -func IsInstalled(conf config.Config, plugin plugins.Plugin, version string) bool { - installDir := InstallPath(conf, plugin, version) - - // Check if version already installed - _, err := os.Stat(installDir) - return !os.IsNotExist(err) -} - // Latest invokes the plugin's latest-stable callback if it exists and returns // the version it returns. If the callback is missing it invokes the list-all // callback and returns the last version matching the query, if a query is @@ -338,16 +303,3 @@ func ParseString(version string) (string, string) { return "version", version } - -func downloadPath(conf config.Config, plugin plugins.Plugin, version string) string { - return filepath.Join(conf.DataDir, dataDirDownloads, plugin.Name, version) -} - -// InstallPath returns the path to a tool installation -func InstallPath(conf config.Config, plugin plugins.Plugin, version string) string { - return filepath.Join(pluginInstallPath(conf, plugin), version) -} - -func pluginInstallPath(conf config.Config, plugin plugins.Plugin) string { - return filepath.Join(conf.DataDir, dataDirInstalls, plugin.Name) -} diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go index e8111577..e9909090 100644 --- a/internal/versions/versions_test.go +++ b/internal/versions/versions_test.go @@ -16,30 +16,6 @@ import ( const testPluginName = "lua" -func TestInstalled(t *testing.T) { - conf, plugin := generateConfig(t) - //stdout, stderr := buildOutputs() - //currentDir := t.TempDir() - //secondPlugin := installPlugin(t, conf, "dummy_plugin", "another") - //version := "1.0.0" - - t.Run("returns empty slice for newly installed plugin", func(t *testing.T) { - installedVersions, err := Installed(conf, plugin) - assert.Nil(t, err) - assert.Empty(t, installedVersions) - }) - - t.Run("returns slice of all installed versions for a tool", func(t *testing.T) { - stdout, stderr := buildOutputs() - err := InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr) - assert.Nil(t, err) - - installedVersions, err := Installed(conf, plugin) - assert.Nil(t, err) - assert.Equal(t, installedVersions, []string{"1.0.0"}) - }) -} - func TestInstallAll(t *testing.T) { t.Run("installs multiple tools when multiple tool versions are specified", func(t *testing.T) { conf, plugin := generateConfig(t) @@ -287,20 +263,6 @@ func TestInstallOneVersion(t *testing.T) { }) } -func TestIsInstalled(t *testing.T) { - conf, plugin := generateConfig(t) - stdout, stderr := buildOutputs() - err := InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr) - assert.Nil(t, err) - - t.Run("returns false when not installed", func(t *testing.T) { - assert.False(t, IsInstalled(conf, plugin, "4.0.0")) - }) - t.Run("returns true when installed", func(t *testing.T) { - assert.True(t, IsInstalled(conf, plugin, "1.0.0")) - }) -} - func TestLatest(t *testing.T) { pluginName := "latest_test" conf, _ := generateConfig(t)