diff --git a/config/config.go b/config/config.go index ed270c21..c4694ff7 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ package config import ( "context" + "io/fs" "strconv" "strings" @@ -29,6 +30,19 @@ type PluginRepoCheckDuration struct { var pluginRepoCheckDurationDefault = PluginRepoCheckDuration{Every: 60} +// Config is the primary value this package builds and returns +type Config struct { + Home string + ConfigFile string `env:"ASDF_CONFIG_FILE, overwrite"` + DefaultToolVersionsFilename string `env:"ASDF_DEFAULT_TOOL_VERSIONS_FILENAME, overwrite"` + // Unclear if this value will be needed with the golang implementation. + // AsdfDir string + DataDir string `env:"ASDF_DATA_DIR, overwrite"` + ForcePrepend bool `env:"ASDF_FORCE_PREPEND, overwrite"` + // Field that stores the settings struct if it is loaded + Settings Settings +} + // Settings is a struct that stores config values from the asdfrc file type Settings struct { Loaded bool @@ -41,17 +55,13 @@ type Settings struct { DisablePluginShortNameRepository bool } -// Config is the primary value this package builds and returns -type Config struct { - Home string - ConfigFile string `env:"ASDF_CONFIG_FILE, overwrite"` - DefaultToolVersionsFilename string `env:"ASDF_DEFAULT_TOOL_VERSIONS_FILENAME, overwrite"` - // Unclear if this value will be needed with the golang implementation. - // AsdfDir string - DataDir string `env:"ASDF_DATA_DIR, overwrite"` - ForcePrepend bool `env:"ASDF_FORCE_PREPEND, overwrite"` - // Field that stores the settings struct if it is loaded - Settings Settings +func defaultConfig(dataDir, configFile string) *Config { + return &Config{ + ForcePrepend: forcePrependDefault, + DataDir: dataDir, + ConfigFile: configFile, + DefaultToolVersionsFilename: defaultToolVersionsFilenameDefault, + } } func defaultSettings() *Settings { @@ -151,7 +161,11 @@ func (c *Config) GetHook(hook string) (string, error) { return "", err } - return c.Settings.Raw.Key(hook).String(), nil + if c.Settings.Raw != nil { + return c.Settings.Raw.Key(hook).String(), nil + } + + return "", nil } func (c *Config) loadSettings() error { @@ -160,12 +174,18 @@ func (c *Config) loadSettings() error { } settings, err := loadSettings(c.ConfigFile) - if err != nil { - return err - } c.Settings = settings + if err != nil { + _, ok := err.(*fs.PathError) + if ok { + return nil + } + + return err + } + return nil } @@ -180,29 +200,25 @@ func loadConfigEnv() (Config, error) { return Config{}, err } - config := Config{ - ForcePrepend: forcePrependDefault, - DataDir: dataDir, - ConfigFile: configFile, - DefaultToolVersionsFilename: defaultToolVersionsFilenameDefault, - } + config := defaultConfig(dataDir, configFile) context := context.Background() - err = envconfig.Process(context, &config) + err = envconfig.Process(context, config) - return config, err + return *config, err } func loadSettings(asdfrcPath string) (Settings, error) { + settings := defaultSettings() + // asdfrc is effectively formatted as ini config, err := ini.Load(asdfrcPath) if err != nil { - return Settings{}, err + return *settings, err } mainConf := config.Section("") - settings := defaultSettings() settings.Raw = mainConf settings.Loaded = true diff --git a/config/config_test.go b/config/config_test.go index 97e475e8..1e450aed 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -92,6 +92,30 @@ func TestConfigMethods(t *testing.T) { assert.Nil(t, err, "Returned error when loading settings") assert.True(t, DisablePluginShortNameRepository, "Expected DisablePluginShortNameRepository to be set") }) + + t.Run("When file does not exist returns settings struct with defaults", func(t *testing.T) { + config := Config{ConfigFile: "non-existant"} + + legacy, err := config.LegacyVersionFile() + assert.Nil(t, err) + assert.False(t, legacy) + + keepDownload, err := config.AlwaysKeepDownload() + assert.Nil(t, err) + assert.False(t, keepDownload) + + lastCheck, err := config.PluginRepositoryLastCheckDuration() + assert.Nil(t, err) + assert.False(t, lastCheck.Never) + + checkDuration, err := config.PluginRepositoryLastCheckDuration() + assert.Nil(t, err) + assert.Equal(t, checkDuration.Every, 60) + + shortName, err := config.DisablePluginShortNameRepository() + assert.Nil(t, err) + assert.False(t, shortName) + }) } func TestConfigGetHook(t *testing.T) { @@ -124,4 +148,12 @@ func TestConfigGetHook(t *testing.T) { assert.Nil(t, err) assert.Equal(t, hookCmd, "echo 'Executing' \"with args: $@\"") }) + + t.Run("works if no config file", func(t *testing.T) { + config := Config{} + + hookCmd, err := config.GetHook("some_hook") + assert.Nil(t, err) + assert.Empty(t, hookCmd) + }) } diff --git a/internal/resolve/resolve.go b/internal/resolve/resolve.go index d3b9c891..4f090a00 100644 --- a/internal/resolve/resolve.go +++ b/internal/resolve/resolve.go @@ -15,19 +15,55 @@ import ( // ToolVersions represents a tool along with versions specified for it type ToolVersions struct { - Name string - Versions []string - Source string + Versions []string + Directory string + Source string } -func findVersionsInDir(conf config.Config, plugin plugins.Plugin, directory string) (versions []string, found bool, err error) { - filename := conf.DefaultToolVersionsFilename - filepath := path.Join(directory, filename) +// Version takes a plugin and a directory and resolves the tool to one or more +// versions. +func Version(conf config.Config, plugin plugins.Plugin, directory string) (versions ToolVersions, found bool, err error) { + version, envVariableName, found := findVersionsInEnv(plugin.Name) + if found { + return ToolVersions{Versions: version, Source: envVariableName}, true, nil + } + + for !found { + versions, found, err = findVersionsInDir(conf, plugin, directory) + if err != nil { + return versions, false, err + } + + nextDir := path.Dir(directory) + if nextDir == directory { + break + } + directory = nextDir + } + + return versions, found, err +} + +func findVersionsInDir(conf config.Config, plugin plugins.Plugin, directory string) (versions ToolVersions, found bool, err error) { + legacyFiles, err := conf.LegacyVersionFile() + if err != nil { + return versions, found, err + } + + if legacyFiles { + versions, found, err := findVersionsInLegacyFile(plugin, directory) + + if found || err != nil { + return versions, found, err + } + } + + filepath := path.Join(directory, conf.DefaultToolVersionsFilename) if _, err = os.Stat(filepath); err == nil { versions, found, err := toolversions.FindToolVersions(filepath, plugin.Name) if found || err != nil { - return versions, found, err + return ToolVersions{Versions: versions, Source: conf.DefaultToolVersionsFilename, Directory: directory}, found, err } } @@ -35,36 +71,36 @@ func findVersionsInDir(conf config.Config, plugin plugins.Plugin, directory stri } // findVersionsInEnv returns the version from the environment if present -func findVersionsInEnv(pluginName string) ([]string, bool) { +func findVersionsInEnv(pluginName string) ([]string, string, bool) { envVariableName := "ASDF_" + strings.ToUpper(pluginName) + "_VERSION" versionString := os.Getenv(envVariableName) if versionString == "" { - return []string{}, false + return []string{}, envVariableName, false } - return parseVersion(versionString), true + return parseVersion(versionString), envVariableName, true } // findVersionsInLegacyFile looks up a legacy version in the given directory if // the specified plugin has a list-legacy-filenames callback script. If the // callback script exists asdf will look for files with the given name in the // current and extract the version from them. -func findVersionsInLegacyFile(plugin plugins.Plugin, directory string) (versions []string, found bool, err error) { +func findVersionsInLegacyFile(plugin plugins.Plugin, directory string) (versions ToolVersions, found bool, err error) { var legacyFileNames []string legacyFileNames, err = plugin.LegacyFilenames() if err != nil { - return []string{}, false, err + return versions, false, err } for _, filename := range legacyFileNames { filepath := path.Join(directory, filename) if _, err := os.Stat(filepath); err == nil { - versions, err := plugin.ParseLegacyVersionFile(filepath) + versionsSlice, err := plugin.ParseLegacyVersionFile(filepath) - if len(versions) == 0 || (len(versions) == 1 && versions[0] == "") { - return nil, false, nil + if len(versionsSlice) == 0 || (len(versionsSlice) == 1 && versionsSlice[0] == "") { + return versions, false, nil } - return versions, err == nil, err + return ToolVersions{Versions: versionsSlice, Source: filename, Directory: directory}, err == nil, err } } diff --git a/internal/resolve/resolve_test.go b/internal/resolve/resolve_test.go index a03fe01a..25b23b73 100644 --- a/internal/resolve/resolve_test.go +++ b/internal/resolve/resolve_test.go @@ -12,9 +12,66 @@ import ( "github.com/stretchr/testify/assert" ) +func TestVersion(t *testing.T) { + testDataDir := t.TempDir() + currentDir := t.TempDir() + conf := config.Config{DataDir: testDataDir, DefaultToolVersionsFilename: ".tool-versions", ConfigFile: "testdata/asdfrc"} + _, err := repotest.InstallPlugin("dummy_plugin", conf.DataDir, "lua") + assert.Nil(t, err) + plugin := plugins.New(conf, "lua") + + t.Run("returns empty slice when non-existent version passed", func(t *testing.T) { + toolVersion, found, err := Version(conf, plugin, t.TempDir()) + assert.Nil(t, err) + assert.False(t, found) + assert.Empty(t, toolVersion.Versions) + }) + + t.Run("returns single version from .tool-versions file", func(t *testing.T) { + // write a version file + data := []byte("lua 1.2.3") + err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666) + + toolVersion, found, err := Version(conf, plugin, currentDir) + assert.Nil(t, err) + assert.True(t, found) + assert.Equal(t, toolVersion.Versions, []string{"1.2.3"}) + }) + + t.Run("returns version from env when env variable set", func(t *testing.T) { + // Set env + t.Setenv("ASDF_LUA_VERSION", "2.3.4") + + // write a version file + data := []byte("lua 1.2.3") + err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666) + + // assert env variable takes precedence + toolVersion, found, err := Version(conf, plugin, currentDir) + assert.Nil(t, err) + assert.True(t, found) + assert.Equal(t, toolVersion.Versions, []string{"2.3.4"}) + }) + + t.Run("returns single version from .tool-versions file in parent directory", func(t *testing.T) { + // write a version file + data := []byte("lua 1.2.3") + err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666) + + subDir := filepath.Join(currentDir, "subdir") + err = os.MkdirAll(subDir, 0o777) + assert.Nil(t, err) + + toolVersion, found, err := Version(conf, plugin, subDir) + assert.Nil(t, err) + assert.True(t, found) + assert.Equal(t, toolVersion.Versions, []string{"1.2.3"}) + }) +} + func TestFindVersionsInDir(t *testing.T) { testDataDir := t.TempDir() - conf := config.Config{DataDir: testDataDir, DefaultToolVersionsFilename: ".tool-versions"} + conf := config.Config{DataDir: testDataDir, DefaultToolVersionsFilename: ".tool-versions", ConfigFile: "testdata/asdfrc"} _, err := repotest.InstallPlugin("dummy_plugin", conf.DataDir, "lua") assert.Nil(t, err) plugin := plugins.New(conf, "lua") @@ -35,9 +92,9 @@ func TestFindVersionsInDir(t *testing.T) { data := []byte("lua 1.2.3") err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666) - versions, found, err := findVersionsInDir(conf, plugin, currentDir) + toolVersion, found, err := findVersionsInDir(conf, plugin, currentDir) - assert.Equal(t, versions, []string{"1.2.3"}) + assert.Equal(t, toolVersion.Versions, []string{"1.2.3"}) assert.True(t, found) assert.Nil(t, err) }) @@ -48,9 +105,9 @@ func TestFindVersionsInDir(t *testing.T) { data := []byte("lua 1.2.3 2.3.4") err = os.WriteFile(filepath.Join(currentDir, ".tool-versions"), data, 0o666) - versions, found, err := findVersionsInDir(conf, plugin, currentDir) + toolVersion, found, err := findVersionsInDir(conf, plugin, currentDir) - assert.Equal(t, versions, []string{"1.2.3", "2.3.4"}) + assert.Equal(t, toolVersion.Versions, []string{"1.2.3", "2.3.4"}) assert.True(t, found) assert.Nil(t, err) }) @@ -62,9 +119,22 @@ func TestFindVersionsInDir(t *testing.T) { data := []byte("lua 1.2.3 2.3.4") err = os.WriteFile(filepath.Join(currentDir, "custom-file"), data, 0o666) - versions, found, err := findVersionsInDir(conf, plugin, currentDir) + toolVersion, found, err := findVersionsInDir(conf, plugin, currentDir) - assert.Equal(t, versions, []string{"1.2.3", "2.3.4"}) + assert.Equal(t, toolVersion.Versions, []string{"1.2.3", "2.3.4"}) + assert.True(t, found) + assert.Nil(t, err) + }) + + t.Run("when legacy file support is on looks up version in legacy file", func(t *testing.T) { + currentDir := t.TempDir() + + data := []byte("1.2.3 2.3.4") + err = os.WriteFile(filepath.Join(currentDir, ".dummy-version"), data, 0o666) + + toolVersion, found, err := findVersionsInDir(conf, plugin, currentDir) + + assert.Equal(t, toolVersion.Versions, []string{"1.2.3", "2.3.4"}) assert.True(t, found) assert.Nil(t, err) }) @@ -82,15 +152,15 @@ func TestFindVersionsLegacyFiles(t *testing.T) { _, err := repotest.InstallPlugin("dummy_plugin_no_download", conf.DataDir, pluginName) assert.Nil(t, err) plugin := plugins.New(conf, pluginName) - versions, found, err := findVersionsInLegacyFile(plugin, t.TempDir()) - assert.Empty(t, versions) + toolVersion, found, err := findVersionsInLegacyFile(plugin, t.TempDir()) + assert.Empty(t, toolVersion.Versions) assert.False(t, found) assert.Nil(t, err) }) t.Run("when given tool that has a list-legacy-filenames callback but file not found returns empty versions list", func(t *testing.T) { - versions, found, err := findVersionsInLegacyFile(plugin, t.TempDir()) - assert.Empty(t, versions) + toolVersion, found, err := findVersionsInLegacyFile(plugin, t.TempDir()) + assert.Empty(t, toolVersion.Versions) assert.False(t, found) assert.Nil(t, err) }) @@ -102,8 +172,8 @@ func TestFindVersionsLegacyFiles(t *testing.T) { err = os.WriteFile(filepath.Join(currentDir, ".dummy-version"), data, 0o666) assert.Nil(t, err) - versions, found, err := findVersionsInLegacyFile(plugin, currentDir) - assert.Equal(t, versions, []string{"1.2.3"}) + toolVersion, found, err := findVersionsInLegacyFile(plugin, currentDir) + assert.Equal(t, toolVersion.Versions, []string{"1.2.3"}) assert.True(t, found) assert.Nil(t, err) }) @@ -111,24 +181,28 @@ func TestFindVersionsLegacyFiles(t *testing.T) { func TestFindVersionsInEnv(t *testing.T) { t.Run("when env variable isn't set returns empty list of versions", func(t *testing.T) { - versions, found := findVersionsInEnv("non-existent") + versions, envVariableName, found := findVersionsInEnv("non-existent") assert.False(t, found) assert.Empty(t, versions) + assert.Equal(t, envVariableName, "ASDF_NON-EXISTENT_VERSION") }) t.Run("when env variable is set returns version", func(t *testing.T) { os.Setenv("ASDF_LUA_VERSION", "5.4.5") - versions, found := findVersionsInEnv("lua") + versions, envVariableName, found := findVersionsInEnv("lua") assert.True(t, found) assert.Equal(t, versions, []string{"5.4.5"}) + assert.Equal(t, envVariableName, "ASDF_LUA_VERSION") + os.Unsetenv("ASDF_LUA_VERSION") }) t.Run("when env variable is set to multiple versions", func(t *testing.T) { os.Setenv("ASDF_LUA_VERSION", "5.4.5 5.4.6") - versions, found := findVersionsInEnv("lua") + versions, envVariableName, found := findVersionsInEnv("lua") assert.True(t, found) assert.Equal(t, versions, []string{"5.4.5", "5.4.6"}) + assert.Equal(t, envVariableName, "ASDF_LUA_VERSION") os.Unsetenv("ASDF_LUA_VERSION") }) }