feat(golang-rewrite): create hooks

* Update tests for config package to use assert package
* Create Config.GetHook method
* Create execute package for running Bash commands
* Create hook package for running asdf hooks
* Run plugin add hooks
This commit is contained in:
Trevor Brown 2024-06-23 16:35:02 -04:00
parent 44e8257dcf
commit b1a47fe2bf
9 changed files with 294 additions and 38 deletions

View File

@ -32,6 +32,7 @@ var pluginRepoCheckDurationDefault = PluginRepoCheckDuration{Every: 60}
// Settings is a struct that stores config values from the asdfrc file
type Settings struct {
Loaded bool
Raw *ini.Section
LegacyVersionFile bool
// I don't think this setting should be supported in the Golang implementation
// UseReleaseCandidates bool
@ -56,6 +57,7 @@ type Config struct {
func defaultSettings() *Settings {
return &Settings{
Loaded: false,
Raw: nil,
LegacyVersionFile: false,
AlwaysKeepDownload: false,
PluginRepositoryLastCheckDuration: pluginRepoCheckDurationDefault,
@ -142,6 +144,16 @@ func (c *Config) DisablePluginShortNameRepository() (bool, error) {
return c.Settings.DisablePluginShortNameRepository, nil
}
// GetHook returns a hook command from config if it is there
func (c *Config) GetHook(hook string) (string, error) {
err := c.loadSettings()
if err != nil {
return "", err
}
return c.Settings.Raw.Key(hook).String(), nil
}
func (c *Config) loadSettings() error {
if c.Settings.Loaded {
return nil
@ -191,6 +203,7 @@ func loadSettings(asdfrcPath string) (Settings, error) {
mainConf := config.Section("")
settings := defaultSettings()
settings.Raw = mainConf
settings.Loaded = true
settings.PluginRepositoryLastCheckDuration = newPluginRepoCheckDuration(mainConf.Key("plugin_repository_last_check_duration").String())

View File

@ -2,22 +2,24 @@ package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadConfig(t *testing.T) {
config, err := LoadConfig()
assert(t, err == nil, "Returned error when building config")
assert.Nil(t, err, "Returned error when building config")
assert(t, config.Home != "", "Expected Home to be set")
assert.NotZero(t, config.Home, "Expected Home to be set")
}
func TestLoadConfigEnv(t *testing.T) {
config, err := loadConfigEnv()
assert(t, err == nil, "Returned error when loading env for config")
assert.Nil(t, err, "Returned error when loading env for config")
assert(t, config.Home == "", "Shouldn't set Home property when loading config")
assert.Zero(t, config.Home, "Shouldn't set Home property when loading config")
}
func TestLoadSettings(t *testing.T) {
@ -36,26 +38,26 @@ func TestLoadSettings(t *testing.T) {
t.Run("When given path to populated asdfrc returns populated settings struct", func(t *testing.T) {
settings, err := loadSettings("testdata/asdfrc")
refuteError(t, err)
assert.Nil(t, err)
assert(t, settings.Loaded, "Expected Loaded field to be set to true")
assert(t, settings.LegacyVersionFile == true, "LegacyVersionFile field has wrong value")
assert(t, settings.AlwaysKeepDownload == true, "AlwaysKeepDownload field has wrong value")
assert(t, settings.PluginRepositoryLastCheckDuration.Never, "PluginRepositoryLastCheckDuration field has wrong value")
assert(t, settings.PluginRepositoryLastCheckDuration.Every == 0, "PluginRepositoryLastCheckDuration field has wrong value")
assert(t, settings.DisablePluginShortNameRepository == true, "DisablePluginShortNameRepository field has wrong value")
assert.True(t, settings.Loaded, "Expected Loaded field to be set to true")
assert.True(t, settings.LegacyVersionFile, "LegacyVersionFile field has wrong value")
assert.True(t, settings.AlwaysKeepDownload, "AlwaysKeepDownload field has wrong value")
assert.True(t, settings.PluginRepositoryLastCheckDuration.Never, "PluginRepositoryLastCheckDuration field has wrong value")
assert.Zero(t, settings.PluginRepositoryLastCheckDuration.Every, "PluginRepositoryLastCheckDuration field has wrong value")
assert.True(t, settings.DisablePluginShortNameRepository, "DisablePluginShortNameRepository field has wrong value")
})
t.Run("When given path to empty file returns settings struct with defaults", func(t *testing.T) {
settings, err := loadSettings("testdata/empty-asdfrc")
refuteError(t, err)
assert.Nil(t, err)
assert(t, settings.LegacyVersionFile == false, "LegacyVersionFile field has wrong value")
assert(t, settings.AlwaysKeepDownload == false, "AlwaysKeepDownload field has wrong value")
assert(t, settings.PluginRepositoryLastCheckDuration.Never == false, "PluginRepositoryLastCheckDuration field has wrong value")
assert(t, settings.PluginRepositoryLastCheckDuration.Every == 60, "PluginRepositoryLastCheckDuration field has wrong value")
assert(t, settings.DisablePluginShortNameRepository == false, "DisablePluginShortNameRepository field has wrong value")
assert.False(t, settings.LegacyVersionFile, "LegacyVersionFile field has wrong value")
assert.False(t, settings.AlwaysKeepDownload, "AlwaysKeepDownload field has wrong value")
assert.False(t, settings.PluginRepositoryLastCheckDuration.Never, "PluginRepositoryLastCheckDuration field has wrong value")
assert.Equal(t, settings.PluginRepositoryLastCheckDuration.Every, 60, "PluginRepositoryLastCheckDuration field has wrong value")
assert.False(t, settings.DisablePluginShortNameRepository, "DisablePluginShortNameRepository field has wrong value")
})
}
@ -64,44 +66,62 @@ func TestConfigMethods(t *testing.T) {
t.Setenv("ASDF_CONFIG_FILE", "testdata/asdfrc")
config, err := LoadConfig()
assert(t, err == nil, "Returned error when building config")
assert.Nil(t, err, "Returned error when building config")
t.Run("Returns LegacyVersionFile from asdfrc file", func(t *testing.T) {
legacyFile, err := config.LegacyVersionFile()
assert(t, err == nil, "Returned error when loading settings")
assert(t, legacyFile == true, "Expected LegacyVersionFile to be set")
assert.Nil(t, err, "Returned error when loading settings")
assert.True(t, legacyFile, "Expected LegacyVersionFile to be set")
})
t.Run("Returns AlwaysKeepDownload from asdfrc file", func(t *testing.T) {
alwaysKeepDownload, err := config.AlwaysKeepDownload()
assert(t, err == nil, "Returned error when loading settings")
assert(t, alwaysKeepDownload == true, "Expected AlwaysKeepDownload to be set")
assert.Nil(t, err, "Returned error when loading settings")
assert.True(t, alwaysKeepDownload, "Expected AlwaysKeepDownload to be set")
})
t.Run("Returns PluginRepositoryLastCheckDuration from asdfrc file", func(t *testing.T) {
checkDuration, err := config.PluginRepositoryLastCheckDuration()
assert(t, err == nil, "Returned error when loading settings")
assert(t, checkDuration.Never == true, "Expected PluginRepositoryLastCheckDuration to be set")
assert(t, checkDuration.Every == 0, "Expected PluginRepositoryLastCheckDuration to be set")
assert.Nil(t, err, "Returned error when loading settings")
assert.True(t, checkDuration.Never, "Expected PluginRepositoryLastCheckDuration to be set")
assert.Zero(t, checkDuration.Every, "Expected PluginRepositoryLastCheckDuration to be set")
})
t.Run("Returns DisablePluginShortNameRepository from asdfrc file", func(t *testing.T) {
DisablePluginShortNameRepository, err := config.DisablePluginShortNameRepository()
assert(t, err == nil, "Returned error when loading settings")
assert(t, DisablePluginShortNameRepository == true, "Expected DisablePluginShortNameRepository to be set")
assert.Nil(t, err, "Returned error when loading settings")
assert.True(t, DisablePluginShortNameRepository, "Expected DisablePluginShortNameRepository to be set")
})
}
func assert(t *testing.T, expr bool, message string) {
t.Helper()
func TestConfigGetHook(t *testing.T) {
// Set the asdf config file location to the test file
t.Setenv("ASDF_CONFIG_FILE", "testdata/asdfrc")
if !expr {
t.Error(message)
}
}
config, err := LoadConfig()
assert.Nil(t, err, "Returned error when building config")
func refuteError(t *testing.T, err error) {
if err != nil {
t.Fatal("Returned unexpected error", err)
}
t.Run("Returns empty string when hook not present in asdfrc file", func(t *testing.T) {
hookCmd, err := config.GetHook("post_asdf_plugin_add")
assert.Nil(t, err)
assert.Zero(t, hookCmd)
})
t.Run("Returns string containing Bash expression when present in asdfrc file", func(t *testing.T) {
hookCmd, err := config.GetHook("pre_asdf_plugin_add")
assert.Nil(t, err)
assert.Equal(t, hookCmd, "echo Executing with args: $@")
})
t.Run("Ignores trailing and leading spaces", func(t *testing.T) {
hookCmd, err := config.GetHook("pre_asdf_plugin_add_test")
assert.Nil(t, err)
assert.Equal(t, hookCmd, "echo Executing with args: $@")
})
t.Run("Preserves quoting", func(t *testing.T) {
hookCmd, err := config.GetHook("pre_asdf_plugin_add_test2")
assert.Nil(t, err)
assert.Equal(t, hookCmd, "echo 'Executing' \"with args: $@\"")
})
}

View File

@ -5,3 +5,8 @@ use_release_candidates = yes
always_keep_download = yes
plugin_repository_last_check_duration = never
disable_plugin_short_name_repository = yes
# Hooks
pre_asdf_plugin_add = echo Executing with args: $@
pre_asdf_plugin_add_test = echo Executing with args: $@
pre_asdf_plugin_add_test2 = echo 'Executing' "with args: $@"

49
execute/execute.go Normal file
View File

@ -0,0 +1,49 @@
// Package execute is a simple package that wraps the os/exec Command features
// for convenient use in asdf. It was inspired by
// https://github.com/chen-keinan/go-command-eval
package execute
import (
"fmt"
"io"
"os/exec"
)
// Command represents a Bash command that can be executed by asdf
type Command struct {
Command string
Args []string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Env map[string]string
}
// New takes a string containing a Bash expression and a slice of string
// arguments and returns a Command struct
func New(command string, args []string) Command {
return Command{Command: command, Args: args}
}
// Run executes a Command with Bash and returns the error if there is one
func (c Command) Run() error {
args := append([]string{"-c", c.Command}, c.Args...)
cmd := exec.Command("bash", args...)
cmd.Env = mapToSlice(c.Env)
cmd.Stdin = c.Stdin
// Capture stdout and stderr
cmd.Stdout = c.Stdout
cmd.Stderr = c.Stderr
return cmd.Run()
}
func mapToSlice(env map[string]string) (slice []string) {
for key, value := range env {
slice = append(slice, fmt.Sprintf("%s=%s", key, value))
}
return slice
}

80
execute/execute_test.go Normal file
View File

@ -0,0 +1,80 @@
package execute
import (
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNew(t *testing.T) {
t.Run("Returns new command", func(t *testing.T) {
cmd := New("echo", []string{"test string"})
assert.Equal(t, "echo", cmd.Command)
})
}
func TestRun(t *testing.T) {
t.Run("command is executed with bash", func(t *testing.T) {
cmd := New("echo $(type -a sh)", []string{})
var stdout strings.Builder
cmd.Stdout = &stdout
err := cmd.Run()
assert.Nil(t, err)
assert.Equal(t, "sh is /bin/sh\n", stdout.String())
})
t.Run("positional args are passed to command", func(t *testing.T) {
cmd := New("echo $0", []string{"test string"})
var stdout strings.Builder
cmd.Stdout = &stdout
err := cmd.Run()
assert.Nil(t, err)
assert.Equal(t, "test string\n", stdout.String())
})
t.Run("environment variables are passed to command", func(t *testing.T) {
cmd := New("echo $MYVAR", []string{})
cmd.Env = map[string]string{"MYVAR": "my var value"}
var stdout strings.Builder
cmd.Stdout = &stdout
err := cmd.Run()
assert.Nil(t, err)
assert.Equal(t, "my var value\n", stdout.String())
})
t.Run("captures stdout and stdin", func(t *testing.T) {
cmd := New("echo 'a test' | tee /dev/stderr", []string{})
cmd.Env = map[string]string{"MYVAR": "my var value"}
var stdout strings.Builder
cmd.Stdout = &stdout
var stderr strings.Builder
cmd.Stderr = &stderr
err := cmd.Run()
assert.Nil(t, err)
assert.Equal(t, "a test\n", stdout.String())
assert.Equal(t, "a test\n", stderr.String())
})
t.Run("returns error when non-zero exit code", func(t *testing.T) {
cmd := New("exit 12", []string{})
var stdout strings.Builder
cmd.Stdout = &stdout
err := cmd.Run()
assert.NotNil(t, err)
assert.Equal(t, "", stdout.String())
assert.Equal(t, 12, err.(*exec.ExitError).ExitCode())
})
}

29
hook/hook.go Normal file
View File

@ -0,0 +1,29 @@
// Package hook provides a simple interface for running hook commands that may
// be defined in the asdfrc file
package hook
import (
"os"
"asdf/config"
"asdf/execute"
)
// Run gets a hook command from config and runs it with the provided arguments
func Run(config config.Config, hookName string, arguments []string) error {
hookCmd, err := config.GetHook(hookName)
if err != nil {
return err
}
if hookCmd == "" {
return nil
}
cmd := execute.New(hookCmd, arguments)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

39
hook/hook_test.go Normal file
View File

@ -0,0 +1,39 @@
package hook
import (
"os/exec"
"testing"
"asdf/config"
"github.com/stretchr/testify/assert"
)
func TestRun(t *testing.T) {
// Set the asdf config file location to the test file
t.Setenv("ASDF_CONFIG_FILE", "testdata/asdfrc")
t.Run("accepts config, hook name, and a slice of string arguments", func(t *testing.T) {
config, err := config.LoadConfig()
assert.Nil(t, err)
err = Run(config, "pre_asdf_plugin_add_test", []string{})
assert.Nil(t, err)
})
t.Run("passes argument to command", func(t *testing.T) {
config, err := config.LoadConfig()
assert.Nil(t, err)
err = Run(config, "pre_asdf_plugin_add_test2", []string{"123"})
assert.Equal(t, 123, err.(*exec.ExitError).ExitCode())
})
t.Run("does not return error when no such hook is defined in asdfrc", func(t *testing.T) {
config, err := config.LoadConfig()
assert.Nil(t, err)
err = Run(config, "nonexistant-hook", []string{})
assert.Nil(t, err)
})
}

7
hook/testdata/asdfrc vendored Normal file
View File

@ -0,0 +1,7 @@
# This is a test asdfrc file containing all possible values. Each field to set
# to a value that is different than the default.
# Hooks
pre_asdf_plugin_add = echo Executing with args: $@
pre_asdf_plugin_add_test = echo Executing with args: $@
pre_asdf_plugin_add_test2 = exit $0

View File

@ -10,6 +10,7 @@ import (
"asdf/config"
"asdf/git"
"asdf/hook"
"asdf/pluginindex"
)
@ -146,7 +147,20 @@ func Add(config config.Config, pluginName, pluginURL string) error {
}
}
return git.NewRepo(pluginDir).Clone(pluginURL)
// Run pre hooks
hook.Run(config, "pre_asdf_plugin_add", []string{})
hook.Run(config, fmt.Sprintf("pre_asdf_plugin_add_%s", pluginName), []string{})
err = git.NewRepo(pluginDir).Clone(pluginURL)
if err != nil {
return err
}
// Run post hooks
hook.Run(config, "post_asdf_plugin_add", []string{})
hook.Run(config, fmt.Sprintf("post_asdf_plugin_add_%s", pluginName), []string{})
return nil
}
// Remove uninstalls a plugin by removing it from the file system if installed