From b23e5a320fd231c4fa55baa3b32d90b55a6ff4f1 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Tue, 20 Aug 2024 08:15:05 -0400 Subject: [PATCH] feat(golang-rewrite): BATS test fixes & `latest` command * Get asdf info BATS tests working * Create `versions.Installed` function * Update `versions.Latest` to return single version * Implement `latest` asdf command --- cmd/cmd.go | 85 ++++++++++++++++++++++++++++++ internal/versions/versions.go | 61 ++++++++++++--------- internal/versions/versions_test.go | 32 +++++++---- main_test.go | 12 ++--- test/latest_command.bats | 2 +- test/test_helpers.bash | 14 ++++- 6 files changed, 163 insertions(+), 43 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 58ae8b87..74eaa74b 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -60,6 +60,22 @@ func Execute(version string) { return installCommand(logger, args.Get(0), args.Get(1)) }, }, + { + Name: "latest", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Usage: "Show latest version of all tools", + }, + }, + Action: func(cCtx *cli.Context) error { + tool := cCtx.Args().Get(0) + pattern := cCtx.Args().Get(1) + all := cCtx.Bool("all") + + return latestCommand(logger, all, tool, pattern) + }, + }, { Name: "plugin", Action: func(_ *cli.Context) error { @@ -313,3 +329,72 @@ func parseInstallVersion(version string) (string, string) { return version, "" } + +func latestCommand(logger *log.Logger, all bool, toolName, pattern string) (err error) { + conf, err := config.LoadConfig() + if err != nil { + logger.Printf("error loading config: %s", err) + return err + } + + if !all { + err = latestForPlugin(conf, toolName, pattern, false) + if err != nil { + os.Exit(1) + } + + return err + } + + plugins, err := plugins.List(conf, false, false) + if err != nil { + logger.Printf("error loading plugin list: %s", err, false) + return err + } + + var maybeErr error + // loop over all plugins and show latest for each one. + for _, plugin := range plugins { + maybeErr = latestForPlugin(conf, plugin.Name, "", true) + if maybeErr != nil { + err = maybeErr + } + } + + if err != nil { + os.Exit(1) + return maybeErr + } + return nil +} + +func latestForPlugin(conf config.Config, toolName, pattern string, showStatus bool) error { + // show single plugin + plugin := plugins.New(conf, toolName) + latest, err := versions.Latest(plugin, pattern) + if err != nil && err.Error() != "no latest version found" { + fmt.Printf("unable to load latest version: %s\n", err) + return err + } + + if latest == "" { + err := fmt.Errorf("No compatible versions available (%s %s)", toolName, pattern) + fmt.Println(err.Error()) + return err + } + + if showStatus { + installed := versions.Installed(conf, plugin, latest) + fmt.Printf("%s\t%s\t%s\n", plugin.Name, latest, installedStatus(installed)) + } else { + fmt.Printf("%s\n", latest) + } + return nil +} + +func installedStatus(installed bool) string { + if installed { + return "installed" + } + return "missing" +} diff --git a/internal/versions/versions.go b/internal/versions/versions.go index 8510088b..09f5043a 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -23,7 +23,6 @@ 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" ) @@ -98,16 +97,10 @@ func InstallVersion(conf config.Config, plugin plugins.Plugin, version string, p } if version == latestVersion { - versions, err := Latest(plugin, pattern) + version, err = Latest(plugin, pattern) if err != nil { return err } - - if len(versions) < 1 { - return errors.New(noLatestVersionErrMsg) - } - - version = versions[0] } return InstallOneVersion(conf, plugin, version, stdOut, stdErr) @@ -128,8 +121,7 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string installDir := installPath(conf, plugin, version) versionType, version := ParseString(version) - // Check if version already installed - if _, err = os.Stat(installDir); !os.IsNotExist(err) { + if Installed(conf, plugin, version) { return fmt.Errorf("version %s of %s is already installed", version, plugin.Name) } @@ -177,41 +169,50 @@ func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string return nil } +// Installed checks if a specific version of a tool is installed +func Installed(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 // provided. -func Latest(plugin plugins.Plugin, query string) (versions []string, err error) { - if query == "" { - query = defaultQuery - } - +func Latest(plugin plugins.Plugin, query string) (version string, err error) { 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 + return version, err } allVersions, err := AllVersionsFiltered(plugin, query) if err != nil { - return versions, err + return version, err } - versions = filterByRegex(allVersions, latestFilterRegex, false) + versions := filterOutByRegex(allVersions, latestFilterRegex) if len(versions) < 1 { - return versions, nil + return version, errors.New(noLatestVersionErrMsg) } - return []string{versions[len(versions)-1]}, nil + return versions[len(versions)-1], nil } // parse stdOut and return version - versions = parseVersions(stdOut.String()) - return versions, nil + allVersions := parseVersions(stdOut.String()) + versions := filterOutByRegex(allVersions, latestFilterRegex) + if len(versions) < 1 { + return version, errors.New(noLatestVersionErrMsg) + } + return versions[len(versions)-1], nil } // AllVersions returns a slice of all available versions for the tool managed by @@ -238,13 +239,23 @@ func AllVersionsFiltered(plugin plugins.Plugin, query string) (versions []string return versions, err } - return filterByRegex(all, query, true), err + return filterByExactMatch(all, query), err } -func filterByRegex(allVersions []string, pattern string, include bool) (versions []string) { +func filterByExactMatch(allVersions []string, pattern string) (versions []string) { + for _, version := range allVersions { + if strings.HasPrefix(version, pattern) { + versions = append(versions, version) + } + } + + return versions +} + +func filterOutByRegex(allVersions []string, pattern string) (versions []string) { for _, version := range allVersions { match, _ := regexp.MatchString(pattern, version) - if match && include || !match && !include { + if !match { versions = append(versions, version) } } diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go index 836113d9..564dcd73 100644 --- a/internal/versions/versions_test.go +++ b/internal/versions/versions_test.go @@ -263,6 +263,20 @@ func TestInstallOneVersion(t *testing.T) { }) } +func TestInstalled(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, Installed(conf, plugin, "4.0.0")) + }) + t.Run("returns true when installed", func(t *testing.T) { + assert.True(t, Installed(conf, plugin, "1.0.0")) + }) +} + func TestLatest(t *testing.T) { pluginName := "latest_test" conf, _ := generateConfig(t) @@ -276,27 +290,27 @@ func TestLatest(t *testing.T) { assert.Nil(t, err) plugin := plugins.New(conf, pluginName) - versions, err := Latest(plugin, "") + version, err := Latest(plugin, "") assert.Nil(t, err) - assert.Equal(t, []string{"2.0.0"}, versions) + assert.Equal(t, "2.0.0", version) }) 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) + version, err := Latest(plugin, "impossible-to-satisfy-query") + assert.Error(t, err, "no latest version found") + assert.Equal(t, version, "") }) t.Run("when given no query returns latest version of plugin", func(t *testing.T) { - versions, err := Latest(plugin, "") + version, err := Latest(plugin, "") assert.Nil(t, err) - assert.Equal(t, []string{"5.1.0"}, versions) + assert.Equal(t, "5.1.0", version) }) t.Run("when given no query returns latest version of plugin", func(t *testing.T) { - versions, err := Latest(plugin, "^4") + version, err := Latest(plugin, "4") assert.Nil(t, err) - assert.Equal(t, []string{"4.0.0"}, versions) + assert.Equal(t, "4.0.0", version) }) } diff --git a/main_test.go b/main_test.go index 4ba3974b..0a93c8c3 100644 --- a/main_test.go +++ b/main_test.go @@ -27,17 +27,17 @@ func TestBatsTests(t *testing.T) { // runBatsFile(t, dir, "help_command.bats") //}) - //t.Run("info_command", func(t *testing.T) { - // runBatsFile(t, dir, "info_command.bats") - //}) + t.Run("info_command", func(t *testing.T) { + runBatsFile(t, dir, "info_command.bats") + }) //t.Run("install_command", func(t *testing.T) { // runBatsFile(t, dir, "install_command.bats") //}) - //t.Run("latest_command", func(t *testing.T) { - // runBatsFile(t, dir, "latest_command.bats") - //}) + t.Run("latest_command", func(t *testing.T) { + runBatsFile(t, dir, "latest_command.bats") + }) //t.Run("list_command", func(t *testing.T) { // runBatsFile(t, dir, "list_command.bats") diff --git a/test/latest_command.bats b/test/latest_command.bats index 9c0bd5af..2681dc1c 100644 --- a/test/latest_command.bats +++ b/test/latest_command.bats @@ -50,7 +50,7 @@ teardown() { @test "[latest_command - dummy_legacy_plugin] No stable version should return an error" { run asdf latest legacy-dummy 3 - [ -z "$output" ] + [ "No compatible versions available (legacy-dummy 3)" = "$output" ] [ "$status" -eq 1 ] } diff --git a/test/test_helpers.bash b/test/test_helpers.bash index cf821222..a78ad5cb 100644 --- a/test/test_helpers.bash +++ b/test/test_helpers.bash @@ -36,7 +36,9 @@ setup_asdf_dir() { install_mock_plugin() { local plugin_name=$1 local location="${2:-$ASDF_DIR}" - cp -r "$BATS_TEST_DIRNAME/fixtures/dummy_plugin" "$location/plugins/$plugin_name" + plugin_dir="$location/plugins/$plugin_name" + cp -r "$BATS_TEST_DIRNAME/fixtures/dummy_plugin" "$plugin_dir" + init_git_repo "$plugin_dir" } install_mock_plugin_no_download() { @@ -48,7 +50,9 @@ install_mock_plugin_no_download() { install_mock_legacy_plugin() { local plugin_name=$1 local location="${2:-$ASDF_DIR}" - cp -r "$BATS_TEST_DIRNAME/fixtures/dummy_legacy_plugin" "$location/plugins/$plugin_name" + plugin_dir="$location/plugins/$plugin_name" + cp -r "$BATS_TEST_DIRNAME/fixtures/dummy_legacy_plugin" "$plugin_dir" + init_git_repo "$plugin_dir" } install_mock_broken_plugin() { @@ -61,11 +65,17 @@ install_mock_plugin_repo() { local plugin_name=$1 local location="${BASE_DIR}/repo-${plugin_name}" cp -r "$BATS_TEST_DIRNAME/fixtures/dummy_plugin" "${location}" + init_git_repo "${location}" +} + +init_git_repo() { + location="$1" git -C "${location}" init -q git -C "${location}" config user.name "Test" git -C "${location}" config user.email "test@example.com" git -C "${location}" add -A git -C "${location}" commit -q -m "asdf ${plugin_name} plugin" + git -C "${location}" remote add origin "https://asdf-vm.com/fake-repo" } install_mock_plugin_version() {