mirror of
https://github.com/asdf-vm/asdf.git
synced 2024-12-23 20:05:09 -07:00
feat(golang-rewrite): create versions.InstallOneVersion
function
* Create Plugin.Exists method * Create internal/versions package * Add missing asdfrc for internal/resolve package tests * Create versions.ParseString function * Create versions.InstallOneVersion function * Update hook package to allow hook output to be received * Write tests
This commit is contained in:
parent
ef91474538
commit
65688915a5
16
hook/hook.go
16
hook/hook.go
@ -3,14 +3,22 @@
|
||||
package hook
|
||||
|
||||
import (
|
||||
"io"
|
||||
"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 {
|
||||
// Run gets a hook command from config and runs it with the provided arguments.
|
||||
// Output is sent to STDOUT and STDERR
|
||||
func Run(conf config.Config, hookName string, arguments []string) error {
|
||||
return RunWithOutput(conf, hookName, arguments, os.Stdout, os.Stderr)
|
||||
}
|
||||
|
||||
// RunWithOutput gets a hook command from config and runs it with the provided
|
||||
// arguments. Output is sent to the provided io.Writers.
|
||||
func RunWithOutput(config config.Config, hookName string, arguments []string, stdOut io.Writer, stdErr io.Writer) error {
|
||||
hookCmd, err := config.GetHook(hookName)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -22,8 +30,8 @@ func Run(config config.Config, hookName string, arguments []string) error {
|
||||
|
||||
cmd := execute.NewExpression(hookCmd, arguments)
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = stdOut
|
||||
cmd.Stderr = stdErr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
1
internal/resolve/testdata/asdfrc
vendored
Normal file
1
internal/resolve/testdata/asdfrc
vendored
Normal file
@ -0,0 +1 @@
|
||||
legacy_version_file = yes
|
3
internal/versions/testdata/asdfrc
vendored
Normal file
3
internal/versions/testdata/asdfrc
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
pre_asdf_download_lua = echo pre_asdf_download_lua $@
|
||||
pre_asdf_install_lua = echo pre_asdf_install_lua $@
|
||||
post_asdf_install_lua = echo post_asdf_install_lua $@
|
126
internal/versions/versions.go
Normal file
126
internal/versions/versions.go
Normal file
@ -0,0 +1,126 @@
|
||||
// Package versions handles all operations pertaining to specific versions.
|
||||
// Install, uninstall, etc...
|
||||
package versions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"asdf/config"
|
||||
"asdf/hook"
|
||||
"asdf/plugins"
|
||||
)
|
||||
|
||||
const (
|
||||
systemVersion = "system"
|
||||
latestVersion = "latest"
|
||||
uninstallableVersionMsg = "uninstallable version: system"
|
||||
dataDirDownloads = "downloads"
|
||||
dataDirInstalls = "installs"
|
||||
)
|
||||
|
||||
// UninstallableVersion is an error returned if someone tries to install the
|
||||
// system version.
|
||||
type UninstallableVersion struct{}
|
||||
|
||||
func (e UninstallableVersion) Error() string {
|
||||
return fmt.Sprint(uninstallableVersionMsg)
|
||||
}
|
||||
|
||||
// TODO: Implement these functions
|
||||
//func InstallAll() {
|
||||
//}
|
||||
|
||||
//func InstallOne() {
|
||||
//}
|
||||
|
||||
// InstallOneVersion installs a specific version of a specific tool
|
||||
func InstallOneVersion(conf config.Config, plugin plugins.Plugin, version string, _ bool, stdOut io.Writer, stdErr io.Writer) error {
|
||||
err := plugin.Exists()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version == systemVersion {
|
||||
return UninstallableVersion{}
|
||||
}
|
||||
|
||||
if version == latestVersion {
|
||||
// TODO: Implement this
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
downloadDir := downloadPath(conf, plugin, version)
|
||||
installDir := installPath(conf, plugin, version)
|
||||
versionType, version := ParseString(version)
|
||||
|
||||
// Check if version already installed
|
||||
if _, err = os.Stat(installDir); !os.IsNotExist(err) {
|
||||
return fmt.Errorf("version %s of %s is already installed", version, plugin.Name)
|
||||
}
|
||||
|
||||
env := map[string]string{
|
||||
"ASDF_INSTALL_TYPE": versionType,
|
||||
"ASDF_INSTALL_VERSION": version,
|
||||
"ASDF_INSTALL_PATH": installDir,
|
||||
"ASDF_DOWNLOAD_PATH": downloadDir,
|
||||
}
|
||||
|
||||
err = os.MkdirAll(downloadDir, 0o777)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create download dir: %w", err)
|
||||
}
|
||||
|
||||
err = hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_download_%s", plugin.Name), []string{version}, stdOut, stdErr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run pre-download hook: %w", err)
|
||||
}
|
||||
|
||||
err = plugin.RunCallback("download", []string{}, env, stdOut, stdErr)
|
||||
if _, ok := err.(plugins.NoCallbackError); err != nil && !ok {
|
||||
return fmt.Errorf("failed to run download callback: %w", err)
|
||||
}
|
||||
|
||||
err = hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_install_%s", plugin.Name), []string{version}, stdOut, stdErr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run pre-install hook: %w", err)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(installDir, 0o777)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create install dir: %w", err)
|
||||
}
|
||||
|
||||
err = plugin.RunCallback("install", []string{}, env, stdOut, stdErr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run install callback: %w", err)
|
||||
}
|
||||
|
||||
err = hook.RunWithOutput(conf, fmt.Sprintf("post_asdf_install_%s", plugin.Name), []string{version}, stdOut, stdErr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run post-install hook: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// ParseString parses a version string into versionType and version components
|
||||
func ParseString(version string) (string, string) {
|
||||
segments := strings.Split(version, ":")
|
||||
if len(segments) >= 1 && segments[0] == "ref" {
|
||||
return "ref", strings.Join(segments[1:], ":")
|
||||
}
|
||||
|
||||
return "version", version
|
||||
}
|
194
internal/versions/versions_test.go
Normal file
194
internal/versions/versions_test.go
Normal file
@ -0,0 +1,194 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"asdf/config"
|
||||
"asdf/plugins"
|
||||
"asdf/repotest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const testPluginName = "lua"
|
||||
|
||||
func TestInstallOneVersion(t *testing.T) {
|
||||
t.Setenv("ASDF_CONFIG_FILE", "testdata/asdfrc")
|
||||
|
||||
t.Run("returns error when plugin doesn't exist", func(t *testing.T) {
|
||||
conf, _ := generateConfig(t)
|
||||
stdout, stderr := buildOutputs()
|
||||
err := InstallOneVersion(conf, plugins.New(conf, "non-existent"), "1.2.3", false, &stdout, &stderr)
|
||||
assert.IsType(t, plugins.PluginMissing{}, err)
|
||||
})
|
||||
|
||||
t.Run("returns error when plugin version is 'system'", func(t *testing.T) {
|
||||
conf, plugin := generateConfig(t)
|
||||
stdout, stderr := buildOutputs()
|
||||
err := InstallOneVersion(conf, plugin, "system", false, &stdout, &stderr)
|
||||
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("returns error when version doesn't exist", func(t *testing.T) {
|
||||
version := "other-dummy"
|
||||
conf, plugin := generateConfig(t)
|
||||
stdout, stderr := buildOutputs()
|
||||
err := InstallOneVersion(conf, plugin, version, false, &stdout, &stderr)
|
||||
assert.Errorf(t, err, "failed to run install callback: exit status 1")
|
||||
|
||||
want := "pre_asdf_download_lua other-dummy\npre_asdf_install_lua other-dummy\nDummy couldn't install version: other-dummy (on purpose)\n"
|
||||
assert.Equal(t, want, stdout.String())
|
||||
|
||||
assertNotInstalled(t, conf.DataDir, plugin.Name, version)
|
||||
})
|
||||
|
||||
t.Run("returns error when version already installed", func(t *testing.T) {
|
||||
conf, plugin := generateConfig(t)
|
||||
stdout, stderr := buildOutputs()
|
||||
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
assertInstalled(t, conf.DataDir, plugin.Name, "1.0.0")
|
||||
|
||||
// Install a second time
|
||||
err = InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr)
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
|
||||
t.Run("creates download directory", func(t *testing.T) {
|
||||
conf, plugin := generateConfig(t)
|
||||
stdout, stderr := buildOutputs()
|
||||
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
downloadPath := filepath.Join(conf.DataDir, "downloads", plugin.Name, "1.0.0")
|
||||
pathInfo, err := os.Stat(downloadPath)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, pathInfo.IsDir())
|
||||
})
|
||||
|
||||
t.Run("creates install directory", func(t *testing.T) {
|
||||
conf, plugin := generateConfig(t)
|
||||
stdout, stderr := buildOutputs()
|
||||
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
installPath := filepath.Join(conf.DataDir, "installs", plugin.Name, "1.0.0")
|
||||
pathInfo, err := os.Stat(installPath)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, pathInfo.IsDir())
|
||||
})
|
||||
|
||||
t.Run("runs pre-download, pre-install and post-install hooks when installation successful", func(t *testing.T) {
|
||||
conf, plugin := generateConfig(t)
|
||||
stdout, stderr := buildOutputs()
|
||||
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "", stderr.String())
|
||||
want := "pre_asdf_download_lua 1.0.0\npre_asdf_install_lua 1.0.0\npost_asdf_install_lua 1.0.0\n"
|
||||
assert.Equal(t, want, stdout.String())
|
||||
})
|
||||
|
||||
t.Run("installs successfully when plugin exists but version does not", func(t *testing.T) {
|
||||
conf, plugin := generateConfig(t)
|
||||
stdout, stderr := buildOutputs()
|
||||
err := InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Check download directory
|
||||
downloadPath := filepath.Join(conf.DataDir, "downloads", plugin.Name, "1.0.0")
|
||||
entries, err := os.ReadDir(downloadPath)
|
||||
assert.Nil(t, err)
|
||||
// mock plugin doesn't write anything
|
||||
assert.Empty(t, entries)
|
||||
|
||||
// Check install directory
|
||||
assertInstalled(t, conf.DataDir, plugin.Name, "1.0.0")
|
||||
})
|
||||
|
||||
t.Run("install successfully when plugin lacks download callback", func(t *testing.T) {
|
||||
conf, _ := generateConfig(t)
|
||||
stdout, stderr := buildOutputs()
|
||||
testPluginName := "no-download"
|
||||
_, err := repotest.InstallPlugin("dummy_plugin_no_download", conf.DataDir, testPluginName)
|
||||
assert.Nil(t, err)
|
||||
plugin := plugins.New(conf, testPluginName)
|
||||
|
||||
err = InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, &stderr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// no-download install script prints 'install'
|
||||
assert.Equal(t, "install", stdout.String())
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
assert.Equal(t, versionType, "version")
|
||||
assert.Equal(t, version, "1.2.3")
|
||||
})
|
||||
|
||||
t.Run("returns 'ref' and reference version when passed a ref version", func(t *testing.T) {
|
||||
versionType, version := ParseString("ref:abc123")
|
||||
assert.Equal(t, versionType, "ref")
|
||||
assert.Equal(t, version, "abc123")
|
||||
})
|
||||
|
||||
t.Run("returns 'ref' and empty string when passed 'ref:'", func(t *testing.T) {
|
||||
versionType, version := ParseString("ref:")
|
||||
assert.Equal(t, versionType, "ref")
|
||||
assert.Equal(t, version, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func buildOutputs() (strings.Builder, strings.Builder) {
|
||||
var stdout strings.Builder
|
||||
var stderr strings.Builder
|
||||
|
||||
return stdout, stderr
|
||||
}
|
||||
|
||||
func assertInstalled(t *testing.T, dataDir, pluginName, version string) {
|
||||
t.Helper()
|
||||
|
||||
installPath := filepath.Join(dataDir, "installs", pluginName, version)
|
||||
entries, err := os.ReadDir(installPath)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var fileNames []string
|
||||
for _, e := range entries {
|
||||
fileNames = append(fileNames, e.Name())
|
||||
}
|
||||
|
||||
assert.Equal(t, fileNames, []string{"bin", "env", "version"})
|
||||
}
|
||||
|
||||
func assertNotInstalled(t *testing.T, dataDir, pluginName, version string) {
|
||||
t.Helper()
|
||||
|
||||
installPath := filepath.Join(dataDir, "installs", pluginName, version)
|
||||
entries, err := os.ReadDir(installPath)
|
||||
assert.Empty(t, entries)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func generateConfig(t *testing.T) (config.Config, plugins.Plugin) {
|
||||
t.Helper()
|
||||
testDataDir := t.TempDir()
|
||||
conf, err := config.LoadConfig()
|
||||
assert.Nil(t, err)
|
||||
conf.DataDir = testDataDir
|
||||
|
||||
_, err = repotest.InstallPlugin("dummy_plugin", testDataDir, testPluginName)
|
||||
assert.Nil(t, err)
|
||||
|
||||
return conf, plugins.New(conf, testPluginName)
|
||||
}
|
@ -33,6 +33,16 @@ func (e PluginAlreadyExists) Error() string {
|
||||
return fmt.Sprintf(pluginAlreadyExistsMsg, e.plugin)
|
||||
}
|
||||
|
||||
// PluginMissing is the error returned when Plugin.Exists is call and the plugin
|
||||
// doesn't exist on disk.
|
||||
type PluginMissing struct {
|
||||
plugin string
|
||||
}
|
||||
|
||||
func (e PluginMissing) Error() string {
|
||||
return fmt.Sprintf(pluginMissingMsg, e.plugin)
|
||||
}
|
||||
|
||||
// NoCallbackError is an error returned by RunCallback when a callback with
|
||||
// particular name does not exist
|
||||
type NoCallbackError struct {
|
||||
@ -48,6 +58,7 @@ const (
|
||||
dataDirPlugins = "plugins"
|
||||
invalidPluginNameMsg = "%s is invalid. Name may only contain lowercase letters, numbers, '_', and '-'"
|
||||
pluginAlreadyExistsMsg = "Plugin named %s already added"
|
||||
pluginMissingMsg = "Plugin named %s not installed"
|
||||
hasNoCallbackMsg = "Plugin named %s does not have a callback named %s"
|
||||
)
|
||||
|
||||
@ -126,6 +137,20 @@ func (p Plugin) ParseLegacyVersionFile(path string) (versions []string, err erro
|
||||
return versions, err
|
||||
}
|
||||
|
||||
// Exists returns a boolean indicating whether or not the plugin exists on disk.
|
||||
func (p Plugin) Exists() error {
|
||||
exists, err := directoryExists(p.Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return PluginMissing{plugin: p.Name}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
@ -285,6 +285,26 @@ func TestUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExists(t *testing.T) {
|
||||
testDataDir := t.TempDir()
|
||||
conf := config.Config{DataDir: testDataDir}
|
||||
_, err := repotest.InstallPlugin("dummy_plugin", testDataDir, testPluginName)
|
||||
assert.Nil(t, err)
|
||||
|
||||
existingPlugin := New(conf, testPluginName)
|
||||
|
||||
t.Run("returns nil if plugin exists", func(t *testing.T) {
|
||||
err := existingPlugin.Exists()
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("returns PluginMissing error when plugin missing", func(t *testing.T) {
|
||||
missingPlugin := New(conf, "non-existent")
|
||||
err := missingPlugin.Exists()
|
||||
assert.Equal(t, err, PluginMissing{plugin: "non-existent"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPluginExists(t *testing.T) {
|
||||
testDataDir := t.TempDir()
|
||||
pluginDir := PluginDirectory(testDataDir, testPluginName)
|
||||
@ -368,10 +388,9 @@ func TestRunCallback(t *testing.T) {
|
||||
|
||||
testDataDir := t.TempDir()
|
||||
conf := config.Config{DataDir: testDataDir}
|
||||
testRepo, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName)
|
||||
_, err := repotest.InstallPlugin("dummy_plugin", 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) {
|
||||
|
Loading…
Reference in New Issue
Block a user