mirror of
https://github.com/asdf-vm/asdf.git
synced 2024-12-24 12:25:28 -07:00
Merge pull request #48 from asdf-vm/tb/plugin-callback-invocation
feat(golang-rewrite): create `RunCallback` method for `Plugin` struct
This commit is contained in:
commit
259959c6e7
@ -7,28 +7,45 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
Command string
|
||||
Expression 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
|
||||
// New takes a string containing the path to a Bash script, and a slice of
|
||||
// string arguments and returns a Command struct
|
||||
func New(command string, args []string) Command {
|
||||
return Command{Command: command, Args: args}
|
||||
}
|
||||
|
||||
// NewExpression takes a string containing a Bash expression and a slice of
|
||||
// string arguments and returns a Command struct
|
||||
func NewExpression(expression string, args []string) Command {
|
||||
return Command{Expression: expression, 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...)
|
||||
var command string
|
||||
if c.Expression != "" {
|
||||
// Expressions need to be invoked inside a Bash function, so variables like
|
||||
// $0 and $@ are available
|
||||
command = fmt.Sprintf("fn() { %s; }; fn %s", c.Expression, formatArgString(c.Args))
|
||||
} else {
|
||||
// Scripts can be invoked directly, with args provided
|
||||
command = fmt.Sprintf("%s %s", c.Command, formatArgString(c.Args))
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", command)
|
||||
|
||||
cmd.Env = mapToSlice(c.Env)
|
||||
cmd.Stdin = c.Stdin
|
||||
@ -40,6 +57,14 @@ func (c Command) Run() error {
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func formatArgString(args []string) string {
|
||||
var newArgs []string
|
||||
for _, str := range args {
|
||||
newArgs = append(newArgs, fmt.Sprintf("\"%s\"", str))
|
||||
}
|
||||
return strings.Join(newArgs, " ")
|
||||
}
|
||||
|
||||
func mapToSlice(env map[string]string) (slice []string) {
|
||||
for key, value := range env {
|
||||
slice = append(slice, fmt.Sprintf("%s=%s", key, value))
|
||||
|
@ -12,12 +12,21 @@ 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)
|
||||
assert.Equal(t, "", cmd.Expression)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
func TestNewExpression(t *testing.T) {
|
||||
t.Run("Returns new command expression", func(t *testing.T) {
|
||||
cmd := NewExpression("echo", []string{"test string"})
|
||||
assert.Equal(t, "echo", cmd.Expression)
|
||||
assert.Equal(t, "", cmd.Command)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRun_Command(t *testing.T) {
|
||||
t.Run("command is executed with bash", func(t *testing.T) {
|
||||
cmd := New("echo $(type -a sh)", []string{})
|
||||
cmd := New("echo $(type -a sh);", []string{})
|
||||
|
||||
var stdout strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
@ -27,8 +36,8 @@ func TestRun(t *testing.T) {
|
||||
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"})
|
||||
t.Run("positional arg is passed to command", func(t *testing.T) {
|
||||
cmd := New("testdata/script", []string{"test string"})
|
||||
|
||||
var stdout strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
@ -38,8 +47,19 @@ func TestRun(t *testing.T) {
|
||||
assert.Equal(t, "test string\n", stdout.String())
|
||||
})
|
||||
|
||||
t.Run("positional args are passed to command", func(t *testing.T) {
|
||||
cmd := New("testdata/script", []string{"test string", "another string"})
|
||||
|
||||
var stdout strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
err := cmd.Run()
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test string another string\n", stdout.String())
|
||||
})
|
||||
|
||||
t.Run("environment variables are passed to command", func(t *testing.T) {
|
||||
cmd := New("echo $MYVAR", []string{})
|
||||
cmd := New("echo $MYVAR;", []string{})
|
||||
cmd.Env = map[string]string{"MYVAR": "my var value"}
|
||||
|
||||
var stdout strings.Builder
|
||||
@ -78,3 +98,78 @@ func TestRun(t *testing.T) {
|
||||
assert.Equal(t, 12, err.(*exec.ExitError).ExitCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRun_Expression(t *testing.T) {
|
||||
t.Run("expression is executed with bash", func(t *testing.T) {
|
||||
cmd := NewExpression("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 arg is passed to expression", func(t *testing.T) {
|
||||
cmd := NewExpression("echo $1; true", []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("positional args are passed to expression", func(t *testing.T) {
|
||||
cmd := NewExpression("echo $@; true", []string{"test string", "another string"})
|
||||
|
||||
var stdout strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
err := cmd.Run()
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test string another string\n", stdout.String())
|
||||
})
|
||||
|
||||
t.Run("environment variables are passed to expression", func(t *testing.T) {
|
||||
cmd := NewExpression("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 := NewExpression("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 := NewExpression("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())
|
||||
})
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ func Run(config config.Config, hookName string, arguments []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := execute.New(hookCmd, arguments)
|
||||
cmd := execute.NewExpression(hookCmd, arguments)
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
@ -29,6 +29,14 @@ func TestRun(t *testing.T) {
|
||||
assert.Equal(t, 123, err.(*exec.ExitError).ExitCode())
|
||||
})
|
||||
|
||||
t.Run("passes arguments to command", func(t *testing.T) {
|
||||
config, err := config.LoadConfig()
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = Run(config, "pre_asdf_plugin_add_test3", []string{"exit 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)
|
||||
|
3
hook/testdata/asdfrc
vendored
3
hook/testdata/asdfrc
vendored
@ -4,4 +4,5 @@
|
||||
# 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
|
||||
pre_asdf_plugin_add_test2 = exit $1
|
||||
pre_asdf_plugin_add_test3 = eval $@
|
||||
|
50
main_test.go
50
main_test.go
@ -1,7 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -45,9 +47,9 @@ func TestBatsTests(t *testing.T) {
|
||||
// runBatsFile(t, dir, "list_command.bats")
|
||||
//})
|
||||
|
||||
//t.Run("plugin_add_command", func(t *testing.T) {
|
||||
// runBatsFile(t, dir, "plugin_add_command.bats")
|
||||
//})
|
||||
t.Run("plugin_add_command", func(t *testing.T) {
|
||||
runBatsFile(t, dir, "plugin_add_command.bats")
|
||||
})
|
||||
|
||||
//t.Run("plugin_extension_command", func(t *testing.T) {
|
||||
// runBatsFile(t, dir, "plugin_extension_command.bats")
|
||||
@ -118,32 +120,32 @@ func TestBatsTests(t *testing.T) {
|
||||
//})
|
||||
}
|
||||
|
||||
// func runBatsFile(t *testing.T, dir, filename string) {
|
||||
// t.Helper()
|
||||
func runBatsFile(t *testing.T, dir, filename string) {
|
||||
t.Helper()
|
||||
|
||||
// cmd := exec.Command("bats", "--verbose-run", fmt.Sprintf("test/%s", filename))
|
||||
cmd := exec.Command("bats", "--verbose-run", fmt.Sprintf("test/%s", filename))
|
||||
|
||||
// // Capture stdout and stderr
|
||||
// var stdout strings.Builder
|
||||
// var stderr strings.Builder
|
||||
// cmd.Stdout = &stdout
|
||||
// cmd.Stderr = &stderr
|
||||
// Capture stdout and stderr
|
||||
var stdout strings.Builder
|
||||
var stderr strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// // Add dir to asdf test variables
|
||||
// asdfTestHome := fmt.Sprintf("BASE_DIR=%s", dir)
|
||||
// asdfBinPath := fmt.Sprintf("ASDF_BIN=%s", dir)
|
||||
// cmd.Env = []string{asdfBinPath, asdfTestHome}
|
||||
// Add dir to asdf test variables
|
||||
asdfTestHome := fmt.Sprintf("BASE_DIR=%s", dir)
|
||||
asdfBinPath := fmt.Sprintf("ASDF_BIN=%s", dir)
|
||||
cmd.Env = []string{asdfBinPath, asdfTestHome}
|
||||
|
||||
// err := cmd.Run()
|
||||
// if err != nil {
|
||||
// // If command fails print both stderr and stdout
|
||||
// fmt.Println("stdout:", stdout.String())
|
||||
// fmt.Println("stderr:", stderr.String())
|
||||
// t.Fatal("bats command failed to run test file successfully")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
// If command fails print both stderr and stdout
|
||||
fmt.Println("stdout:", stdout.String())
|
||||
fmt.Println("stderr:", stderr.String())
|
||||
t.Fatal("bats command failed to run test file successfully")
|
||||
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func buildAsdf(t *testing.T, dir string) {
|
||||
cmd := exec.Command("go", "build", "-o", dir)
|
||||
|
@ -4,11 +4,13 @@ package plugins
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"asdf/config"
|
||||
"asdf/execute"
|
||||
"asdf/git"
|
||||
"asdf/hook"
|
||||
"asdf/pluginindex"
|
||||
@ -30,10 +32,22 @@ func (e PluginAlreadyExists) Error() string {
|
||||
return fmt.Sprintf(pluginAlreadyExistsMsg, e.plugin)
|
||||
}
|
||||
|
||||
// NoCallbackError is an error returned by RunCallback when a callback with
|
||||
// particular name does not exist
|
||||
type NoCallbackError struct {
|
||||
callback string
|
||||
plugin string
|
||||
}
|
||||
|
||||
func (e NoCallbackError) Error() string {
|
||||
return fmt.Sprintf(hasNoCallbackMsg, e.plugin, e.callback)
|
||||
}
|
||||
|
||||
const (
|
||||
dataDirPlugins = "plugins"
|
||||
invalidPluginNameMsg = "%s is invalid. Name may only contain lowercase letters, numbers, '_', and '-'"
|
||||
pluginAlreadyExistsMsg = "Plugin named %s already added"
|
||||
hasNoCallbackMsg = "Plugin named %s does not have a callback named %s"
|
||||
)
|
||||
|
||||
// Plugin struct represents an asdf plugin to all asdf code. The name and dir
|
||||
@ -46,6 +60,32 @@ type Plugin struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
// New takes config and a plugin name and returns a Plugin struct. It is
|
||||
// intended for functions that need to quickly initialize a plugin.
|
||||
func New(config config.Config, name string) Plugin {
|
||||
pluginsDir := DataDirectory(config.DataDir)
|
||||
dir := filepath.Join(pluginsDir, name)
|
||||
return Plugin{Dir: dir, Name: name}
|
||||
}
|
||||
|
||||
// RunCallback invokes a callback with the given name if it exists for the plugin
|
||||
func (p Plugin) RunCallback(name string, arguments []string, environment map[string]string, stdOut io.Writer, errOut io.Writer) error {
|
||||
callback := filepath.Join(p.Dir, "bin", name)
|
||||
|
||||
_, err := os.Stat(callback)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return NoCallbackError{callback: name, plugin: p.Name}
|
||||
}
|
||||
|
||||
cmd := execute.New(fmt.Sprintf("'%s'", callback), arguments)
|
||||
cmd.Env = environment
|
||||
|
||||
cmd.Stdout = stdOut
|
||||
cmd.Stderr = errOut
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// List takes config and flags for what to return and builds a list of plugins
|
||||
// representing the currently installed plugins on the system.
|
||||
func List(config config.Config, urls, refs bool) (plugins []Plugin, err error) {
|
||||
@ -117,11 +157,7 @@ func Add(config config.Config, pluginName, pluginURL string) error {
|
||||
return NewPluginAlreadyExists(pluginName)
|
||||
}
|
||||
|
||||
pluginDir := PluginDirectory(config.DataDir, pluginName)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create plugin directory: %w", err)
|
||||
}
|
||||
plugin := New(config, pluginName)
|
||||
|
||||
if pluginURL == "" {
|
||||
// Ignore error here as the default value is fine
|
||||
@ -147,18 +183,23 @@ func Add(config config.Config, pluginName, pluginURL string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Run pre hooks
|
||||
hook.Run(config, "pre_asdf_plugin_add", []string{})
|
||||
hook.Run(config, fmt.Sprintf("pre_asdf_plugin_add_%s", pluginName), []string{})
|
||||
plugin.URL = pluginURL
|
||||
|
||||
err = git.NewRepo(pluginDir).Clone(pluginURL)
|
||||
// Run pre hooks
|
||||
hook.Run(config, "pre_asdf_plugin_add", []string{plugin.Name})
|
||||
hook.Run(config, fmt.Sprintf("pre_asdf_plugin_add_%s", plugin.Name), []string{})
|
||||
|
||||
err = git.NewRepo(plugin.Dir).Clone(plugin.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
env := map[string]string{"ASDF_PLUGIN_SOURCE_URL": plugin.URL, "ASDF_PLUGIN_PATH": plugin.Dir}
|
||||
plugin.RunCallback("post-plugin-add", []string{}, env, os.Stdout, os.Stderr)
|
||||
|
||||
// Run post hooks
|
||||
hook.Run(config, "post_asdf_plugin_add", []string{})
|
||||
hook.Run(config, fmt.Sprintf("post_asdf_plugin_add_%s", pluginName), []string{})
|
||||
hook.Run(config, "post_asdf_plugin_add", []string{plugin.Name})
|
||||
hook.Run(config, fmt.Sprintf("post_asdf_plugin_add_%s", plugin.Name), []string{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -73,6 +73,18 @@ func TestList(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
testDataDir := t.TempDir()
|
||||
conf := config.Config{DataDir: testDataDir}
|
||||
|
||||
t.Run("returns Plugin struct with Dir and Name fields set correctly", func(t *testing.T) {
|
||||
plugin := New(conf, "test-plugin")
|
||||
|
||||
assert.Equal(t, "test-plugin", plugin.Name)
|
||||
assert.Equal(t, filepath.Join(testDataDir, "plugins", "test-plugin"), plugin.Dir)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
testDataDir := t.TempDir()
|
||||
|
||||
@ -300,7 +312,7 @@ func TestPluginDirectory(t *testing.T) {
|
||||
func TestValidatePluginName(t *testing.T) {
|
||||
t.Run("returns no error when plugin name is valid", func(t *testing.T) {
|
||||
err := validatePluginName(testPluginName)
|
||||
refuteError(t, err)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
invalids := []string{"plugin^name", "plugin%name", "plugin name", "PLUGIN_NAME"}
|
||||
@ -316,10 +328,55 @@ func TestValidatePluginName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func refuteError(t *testing.T, err error) {
|
||||
if err != nil {
|
||||
t.Fatal("Returned unexpected error", err)
|
||||
}
|
||||
func TestRunCallback(t *testing.T) {
|
||||
emptyEnv := map[string]string{}
|
||||
|
||||
testDataDir := t.TempDir()
|
||||
conf := config.Config{DataDir: testDataDir}
|
||||
testRepo, err := installMockPluginRepo(testDataDir, testPluginName)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = Add(conf, testPluginName, testRepo)
|
||||
plugin := New(conf, testPluginName)
|
||||
|
||||
t.Run("returns NoCallback error when callback with name not found", func(t *testing.T) {
|
||||
var stdout strings.Builder
|
||||
var stderr strings.Builder
|
||||
|
||||
err = plugin.RunCallback("non-existant", []string{}, emptyEnv, &stdout, &stderr)
|
||||
|
||||
assert.Equal(t, err.(NoCallbackError).Error(), "Plugin named lua does not have a callback named non-existant")
|
||||
})
|
||||
|
||||
t.Run("passes argument to command", func(t *testing.T) {
|
||||
var stdout strings.Builder
|
||||
var stderr strings.Builder
|
||||
|
||||
err = plugin.RunCallback("debug", []string{"123"}, emptyEnv, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "123\n", stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
})
|
||||
|
||||
t.Run("passes arguments to command", func(t *testing.T) {
|
||||
var stdout strings.Builder
|
||||
var stderr strings.Builder
|
||||
|
||||
err = plugin.RunCallback("debug", []string{"123", "test string"}, emptyEnv, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "123 test string\n", stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
})
|
||||
|
||||
t.Run("passes env to command", func(t *testing.T) {
|
||||
var stdout strings.Builder
|
||||
var stderr strings.Builder
|
||||
|
||||
err = plugin.RunCallback("post-plugin-update", []string{}, map[string]string{"ASDF_PLUGIN_PREV_REF": "TEST"}, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "plugin updated path= old git-ref=TEST new git-ref=\n", stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
})
|
||||
}
|
||||
|
||||
func touchFile(name string) error {
|
||||
|
3
test/fixtures/dummy_plugin/bin/debug
vendored
Executable file
3
test/fixtures/dummy_plugin/bin/debug
vendored
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo $@
|
@ -16,7 +16,7 @@ setup_asdf_dir() {
|
||||
mkdir "$BASE_DIR"
|
||||
|
||||
# HOME is now defined by the Golang test code in main_test.go
|
||||
HOME="$BASE_DIR/home"
|
||||
HOME="$BASE_DIR"
|
||||
export HOME
|
||||
ASDF_DIR="$HOME/.asdf"
|
||||
mkdir -p "$ASDF_DIR/plugins"
|
||||
|
Loading…
Reference in New Issue
Block a user