From 9f6a65f5dda41d25a9e894ff6a8482810fd6dec8 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Sat, 17 Aug 2024 14:10:16 -0400 Subject: [PATCH] feat(golang-rewrite): create versions.Latest function * Create `versions.ListAll` function * Create `versions.Latest` function * Update `versions.InstallOneVersion` to install latest version --- internal/versions/versions.go | 112 +++++++++++++++++++++++++++-- internal/versions/versions_test.go | 71 +++++++++++++++++- 2 files changed, 174 insertions(+), 9 deletions(-) diff --git a/internal/versions/versions.go b/internal/versions/versions.go index ed1dbb9f..a57ccd8f 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -8,6 +8,7 @@ import ( "io" "os" "path/filepath" + "regexp" "strings" "asdf/config" @@ -21,6 +22,9 @@ const ( uninstallableVersionMsg = "uninstallable version: system" dataDirDownloads = "downloads" dataDirInstalls = "installs" + defaultQuery = "[0-9]" + 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" ) // UninstallableVersion is an error returned if someone tries to install the @@ -50,8 +54,16 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string } if version == latestVersion { - // TODO: Implement this - return errors.New("not implemented") + versions, err := Latest(plugin, "") + if err != nil { + return err + } + + if len(versions) < 1 { + return errors.New(noLatestVersionErrMsg) + } + + version = versions[0] } downloadDir := downloadPath(conf, plugin, version) @@ -107,12 +119,92 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string return nil } -func downloadPath(conf config.Config, plugin plugins.Plugin, version string) string { - return filepath.Join(conf.DataDir, dataDirDownloads, plugin.Name, version) +// 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 +// provided. +func Latest(plugin plugins.Plugin, query string) (versions []string, err error) { + if query == "" { + query = defaultQuery + } + + var stdOut strings.Builder + var stdErr strings.Builder + + err = plugin.RunCallback("latest-stable", []string{query}, map[string]string{}, &stdOut, &stdErr) + if err != nil { + if _, ok := err.(plugins.NoCallbackError); !ok { + return versions, err + } + + allVersions, err := AllVersionsFiltered(plugin, query) + if err != nil { + return versions, err + } + + versions = filterByRegex(allVersions, latestFilterRegex, false) + + if len(versions) < 1 { + return versions, nil + } + + return []string{versions[len(versions)-1]}, nil + } + + // parse stdOut and return version + versions = parseVersions(stdOut.String()) + return versions, nil } -func installPath(conf config.Config, plugin plugins.Plugin, version string) string { - return filepath.Join(conf.DataDir, dataDirInstalls, plugin.Name, version) +// AllVersions returns a slice of all available versions for the tool managed by +// the given plugin by invoking the plugin's list-all callback +func AllVersions(plugin plugins.Plugin) (versions []string, err error) { + var stdout strings.Builder + var stderr strings.Builder + + err = plugin.RunCallback("list-all", []string{}, map[string]string{}, &stdout, &stderr) + if err != nil { + return versions, err + } + + versions = parseVersions(stdout.String()) + + return versions, err +} + +// AllVersionsFiltered returns a list of existing versions that match a regex +// query provided by the user. +func AllVersionsFiltered(plugin plugins.Plugin, query string) (versions []string, err error) { + all, err := AllVersions(plugin) + if err != nil { + return versions, err + } + + return filterByRegex(all, query, true), err +} + +func filterByRegex(allVersions []string, pattern string, include bool) (versions []string) { + for _, version := range allVersions { + match, _ := regexp.MatchString(pattern, version) + if match && include || !match && !include { + versions = append(versions, version) + } + } + + return versions +} + +// future refactoring opportunity: this function is an exact copy of +// resolve.parseVersion +func parseVersions(rawVersions string) []string { + var versions []string + for _, version := range strings.Split(rawVersions, " ") { + version = strings.TrimSpace(version) + if len(version) > 0 { + versions = append(versions, version) + } + } + return versions } // ParseString parses a version string into versionType and version components @@ -124,3 +216,11 @@ 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) +} + +func installPath(conf config.Config, plugin plugins.Plugin, version string) string { + return filepath.Join(conf.DataDir, dataDirInstalls, plugin.Name, version) +} diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go index 5a9d4e99..92582a01 100644 --- a/internal/versions/versions_test.go +++ b/internal/versions/versions_test.go @@ -32,9 +32,12 @@ func TestInstallOneVersion(t *testing.T) { assert.IsType(t, UninstallableVersion{}, err) }) - //t.Run("installs latest version of tool when version is 'latest'", func(t *testing.T) { - // t.Fatal("not implemented") - //}) + t.Run("installs latest version of tool when version is 'latest'", func(t *testing.T) { + conf, plugin := generateConfig(t) + stdout, stderr := buildOutputs() + err := InstallOneVersion(conf, plugin, "latest", false, &stdout, &stderr) + assert.Nil(t, err) + }) t.Run("returns error when version doesn't exist", func(t *testing.T) { version := "other-dummy" @@ -128,6 +131,43 @@ func TestInstallOneVersion(t *testing.T) { }) } +func TestLatest(t *testing.T) { + pluginName := "latest_test" + conf, _ := generateConfig(t) + _, err := repotest.InstallPlugin("dummy_legacy_plugin", conf.DataDir, pluginName) + assert.Nil(t, err) + plugin := plugins.New(conf, pluginName) + + t.Run("when plugin has a latest-stable callback invokes it and returns version it printed", func(t *testing.T) { + pluginName := "latest-with-callback" + _, err := repotest.InstallPlugin("dummy_plugin", conf.DataDir, pluginName) + assert.Nil(t, err) + plugin := plugins.New(conf, pluginName) + + versions, err := Latest(plugin, "") + assert.Nil(t, err) + assert.Equal(t, []string{"2.0.0"}, versions) + }) + + t.Run("when given query matching no versions return empty slice of versions", func(t *testing.T) { + versions, err := Latest(plugin, "impossible-to-satisfy-query") + assert.Nil(t, err) + assert.Empty(t, versions) + }) + + t.Run("when given no query returns latest version of plugin", func(t *testing.T) { + versions, err := Latest(plugin, "") + assert.Nil(t, err) + assert.Equal(t, []string{"5.1.0"}, versions) + }) + + t.Run("when given no query returns latest version of plugin", func(t *testing.T) { + versions, err := Latest(plugin, "^4") + assert.Nil(t, err) + assert.Equal(t, []string{"4.0.0"}, versions) + }) +} + func TestParseString(t *testing.T) { t.Run("returns 'version', and unmodified version when passed semantic version", func(t *testing.T) { versionType, version := ParseString("1.2.3") @@ -148,6 +188,31 @@ func TestParseString(t *testing.T) { }) } +func TestAllVersions(t *testing.T) { + pluginName := "list-all-test" + conf, _ := generateConfig(t) + _, err := repotest.InstallPlugin("dummy_plugin", conf.DataDir, pluginName) + assert.Nil(t, err) + plugin := plugins.New(conf, pluginName) + + t.Run("returns slice of available versions from plugin", func(t *testing.T) { + versions, err := AllVersions(plugin) + assert.Nil(t, err) + assert.Equal(t, versions, []string{"1.0.0", "1.1.0", "2.0.0"}) + }) + + t.Run("returns error when callback missing", func(t *testing.T) { + pluginName = "list-all-fail" + _, err := repotest.InstallPlugin("dummy_plugin_no_download", conf.DataDir, pluginName) + assert.Nil(t, err) + plugin := plugins.New(conf, pluginName) + + versions, err := AllVersions(plugin) + assert.Equal(t, err.(plugins.NoCallbackError).Error(), "Plugin named list-all-fail does not have a callback named list-all") + assert.Empty(t, versions) + }) +} + // Helper functions func buildOutputs() (strings.Builder, strings.Builder) { var stdout strings.Builder