Merge pull request #56 from asdf-vm/tb/latest-version

feat(golang-rewrite): create versions.Latest function
This commit is contained in:
Trevor Brown 2024-08-17 15:58:55 -04:00 committed by Trevor Brown
commit 123162946c
2 changed files with 174 additions and 9 deletions

View File

@ -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)
}

View File

@ -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