refactor(state-mgmt): move state management code to own module

This commit is contained in:
Marc 2024-01-23 12:45:08 -05:00
parent 1bec59c814
commit c8faae8637
Signed by: marc
GPG key ID: 048E042F22B5DC79
9 changed files with 60 additions and 53 deletions

7
cli.go
View file

@ -2,6 +2,7 @@ package main
import (
"strings"
state "v/state"
)
type Flags struct {
@ -14,7 +15,7 @@ type Flags struct {
// Command definition for CLI subcommands.
type Command struct {
Label string
Handler func([]string, Flags, State) error
Handler func([]string, Flags, state.State) error
Usage string
Description string
}
@ -34,7 +35,7 @@ type CLI struct {
// This specifies a label that is used to route the user input to
// the right command, a handler that is called when the label is used,
// and usage/description details that get included in autogenerated help messaging.
func (c CLI) AddCommand(label string, handler func([]string, Flags, State) error, usage string, description string) CLI {
func (c CLI) AddCommand(label string, handler func([]string, Flags, state.State) error, usage string, description string) CLI {
if c.Commands == nil {
c.Commands = map[string]Command{}
c.OrderedCommands = []string{}
@ -48,7 +49,7 @@ func (c CLI) AddCommand(label string, handler func([]string, Flags, State) error
// Executes one of the registered commands if any match the provided
// user arguments.
func (c CLI) Run(args []string, currentState State) error {
func (c CLI) Run(args []string, currentState state.State) error {
if len(args) == 0 {
c.Help()
return nil

View file

@ -3,6 +3,7 @@ package main
import (
"os"
"slices"
state "v/state"
)
var DIRECTORIES = []string{
@ -29,42 +30,42 @@ func writeShim(shimPath string) error {
// Sets up directories and files used to store downloaded archives,
// installed runtimes and metadata.
func Initialize(args []string, flags Flags, currentState State) error {
func Initialize(args []string, flags Flags, currentState state.State) error {
if flags.AddPath {
InfoLogger.Printf("export PATH=%s:$PATH\n", GetStatePath("shims"))
InfoLogger.Printf("export PATH=%s:$PATH\n", state.GetStatePath("shims"))
return nil
}
os.Mkdir(GetStatePath(), DEFAULT_PERMISSION)
os.Mkdir(state.GetStatePath(), DEFAULT_PERMISSION)
for _, dir := range DIRECTORIES {
os.Mkdir(GetStatePath(dir), DEFAULT_PERMISSION)
os.Mkdir(state.GetStatePath(dir), DEFAULT_PERMISSION)
}
for _, shim := range SHIMS {
writeShim(GetStatePath("shims", shim))
writeShim(state.GetStatePath("shims", shim))
}
return nil
}
func UninstallPython(args []string, flags Flags, currentState State) error {
runtimePath := GetStatePath("runtimes", "py-"+args[1])
func UninstallPython(args []string, flags Flags, currentState state.State) error {
runtimePath := state.GetStatePath("runtimes", "py-"+args[1])
err := os.RemoveAll(runtimePath)
return err
}
func InstallPython(args []string, flags Flags, currentState State) error {
func InstallPython(args []string, flags Flags, currentState state.State) error {
version := args[1]
return InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
}
func Use(args []string, flags Flags, currentState State) error {
func Use(args []string, flags Flags, currentState state.State) error {
version := args[1]
if err := ValidateVersion(version); err != nil {
return err
}
availableVersions := GetAvailableVersions()
availableVersions := state.GetAvailableVersions()
found := false
for _, v := range availableVersions {
@ -79,12 +80,12 @@ func Use(args []string, flags Flags, currentState State) error {
InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
}
WriteState(version)
state.WriteState(version)
InfoLogger.Printf("Now using Python %s\n", version)
return nil
}
func ListVersions(args []string, flags Flags, currentState State) error {
func ListVersions(args []string, flags Flags, currentState state.State) error {
installedVersions, err := ListInstalledVersions()
if err != nil {
@ -104,7 +105,7 @@ func ListVersions(args []string, flags Flags, currentState State) error {
}
// Which prints out the system path to the executable being used by `python`.
func Which(args []string, flags Flags, currentState State) error {
func Which(args []string, flags Flags, currentState state.State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
@ -116,7 +117,7 @@ func Which(args []string, flags Flags, currentState State) error {
printedPath = sysPath + " (system)"
} else if isInstalled {
tag := VersionStringToStruct(selectedVersion.Version)
printedPath = GetStatePath("runtimes", "py-"+selectedVersion.Version, "bin", "python"+tag.MajorMinor())
printedPath = state.GetStatePath("runtimes", "py-"+selectedVersion.Version, "bin", "python"+tag.MajorMinor())
} else {
InfoLogger.Printf("The desired version (%s) is not installed.\n", selectedVersion.Version)
return nil
@ -137,7 +138,7 @@ func Which(args []string, flags Flags, currentState State) error {
// CurrentVersion (called via `v version`) outputs the currently selected version
// and what configures it. If the version is configured by a file, the file is returned
// under "source", if the system Python is used, "system" is returned as a source.
func CurrentVersion(args []string, flags Flags, currentState State) error {
func CurrentVersion(args []string, flags Flags, currentState state.State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)

View file

@ -5,18 +5,19 @@ import (
"os"
"strings"
"testing"
state "v/state"
)
func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
os.Mkdir(GetStatePath("runtimes"), 0750)
os.Mkdir(state.GetStatePath("runtimes"), 0750)
var out bytes.Buffer
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
ListVersions([]string{}, Flags{}, State{})
ListVersions([]string{}, Flags{}, state.State{})
captured := out.String()
if captured != "No versions installed!\n" {
@ -27,13 +28,13 @@ func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
func TestListVersionOutputsVersionsInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
os.MkdirAll(GetStatePath("runtimes", "py-1.2.3"), 0750)
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
var out bytes.Buffer
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
ListVersions([]string{}, Flags{}, State{})
ListVersions([]string{}, Flags{}, state.State{})
captured := out.String()
if captured != "1.2.3\n" {
@ -49,7 +50,7 @@ func TestListVersionReturnsErrorOnFailure(t *testing.T) {
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
err := ListVersions([]string{}, Flags{}, State{})
err := ListVersions([]string{}, Flags{}, state.State{})
captured := out.String()
if captured != "" {
@ -69,7 +70,7 @@ func TestListVersionOutputsVersionSelectedAndWarnsNotInstalled(t *testing.T) {
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
Which([]string{}, Flags{}, State{GlobalVersion: "1.2.3"})
Which([]string{}, Flags{}, state.State{GlobalVersion: "1.2.3"})
captured := out.String()
if captured != "The desired version (1.2.3) is not installed.\n" {
@ -85,11 +86,11 @@ func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) {
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
os.MkdirAll(GetStatePath("runtimes", "py-1.2.3"), 0750)
Which([]string{}, Flags{}, State{GlobalVersion: "1.2.3"})
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
Which([]string{}, Flags{}, state.State{GlobalVersion: "1.2.3"})
captured := strings.TrimSpace(out.String())
expected := GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
if !strings.Contains(captured, expected) {
t.Errorf("Unexpected message: %s, not %s", captured, expected)
}
@ -103,7 +104,7 @@ func TestWhichOutputsSystemVersionIfNoneSelected(t *testing.T) {
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
Which([]string{}, Flags{RawOutput: true}, State{})
Which([]string{}, Flags{RawOutput: true}, state.State{})
captured := strings.TrimSpace(out.String())
@ -120,11 +121,11 @@ func TestWhichOutputsVersionWithoutPrefixesIfRawOutput(t *testing.T) {
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
os.MkdirAll(GetStatePath("runtimes", "py-1.2.3"), 0750)
Which([]string{}, Flags{RawOutput: true}, State{GlobalVersion: "1.2.3"})
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
Which([]string{}, Flags{RawOutput: true}, state.State{GlobalVersion: "1.2.3"})
captured := strings.TrimSpace(out.String())
expected := GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
expected := state.GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
if captured != expected {
t.Errorf("Unexpected message: %s, not %s", captured, expected)
}

View file

@ -9,6 +9,7 @@ import (
"path"
"strings"
"time"
state "v/state"
)
var pythonReleasesBaseURL = "https://www.python.org/ftp/python"
@ -51,7 +52,7 @@ func InstallPythonDistribution(version string, noCache bool, verbose bool) error
// Fetches the Python tarball for version <version> from python.org.
func downloadSource(version string, skipCache bool) (PackageMetadata, error) {
archiveName := "Python-" + version + ".tgz"
archivePath := GetStatePath("cache", archiveName)
archivePath := state.GetStatePath("cache", archiveName)
sourceUrl, _ := url.JoinPath(pythonReleasesBaseURL, version, archiveName)
client := http.Client{}
@ -94,7 +95,7 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
InfoLogger.Println("Unpacking source for " + pkgMeta.ArchivePath)
_, untarErr := RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, GetStatePath("cache"), !verbose)
_, untarErr := RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, state.GetStatePath("cache"), !verbose)
if untarErr != nil {
return pkgMeta, untarErr
@ -104,7 +105,7 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
InfoLogger.Println("Configuring installer")
targetDirectory := GetStatePath("runtimes", "py-"+pkgMeta.Version)
targetDirectory := state.GetStatePath("runtimes", "py-"+pkgMeta.Version)
_, configureErr := RunCommand([]string{"./configure", "--prefix=" + targetDirectory, "--enable-optimizations"}, unzippedRoot, !verbose)

View file

@ -5,6 +5,7 @@ import (
"os"
"path"
"strings"
state "v/state"
)
type SelectedVersion struct {
@ -13,7 +14,7 @@ type SelectedVersion struct {
}
func ListInstalledVersions() ([]string, error) {
runtimesDir := GetStatePath("runtimes")
runtimesDir := state.GetStatePath("runtimes")
entries, err := os.ReadDir(runtimesDir)
if err != nil {
@ -65,7 +66,7 @@ func SearchForPythonVersionFile() (SelectedVersion, bool) {
// file that would indicate which version is preferred. If none are found, the global
// user-defined version (via `v use <version>`) is used. If there is none, the system
// Python version is used.
func DetermineSelectedPythonVersion(currentState State) (SelectedVersion, error) {
func DetermineSelectedPythonVersion(currentState state.State) (SelectedVersion, error) {
pythonFileVersion, pythonFileVersionFound := SearchForPythonVersionFile()
if pythonFileVersionFound {
@ -73,7 +74,7 @@ func DetermineSelectedPythonVersion(currentState State) (SelectedVersion, error)
}
if len(currentState.GlobalVersion) != 0 {
return SelectedVersion{Version: currentState.GlobalVersion, Source: GetStatePath("state.json")}, nil
return SelectedVersion{Version: currentState.GlobalVersion, Source: state.GetStatePath("state.json")}, nil
}
systemVersion, _ := DetermineSystemPython()
@ -83,7 +84,7 @@ func DetermineSelectedPythonVersion(currentState State) (SelectedVersion, error)
// DetermineSystemPython returns the unshimmed Python version and path.
// It assumes that /bin/python is where system Python lives.
func DetermineSystemPython() (string, string) {
versionOut, _ := RunCommand([]string{"/bin/python", "--version"}, GetStatePath(), true)
versionOut, _ := RunCommand([]string{"/bin/python", "--version"}, state.GetStatePath(), true)
detectedVersion, _ := strings.CutPrefix(versionOut, "Python")
return strings.TrimSpace(detectedVersion), "/bin/python"
}

View file

@ -8,6 +8,7 @@ import (
"path"
"slices"
"testing"
state "v/state"
)
// SetupAndCleanupEnvironment sets up a test directory and
@ -29,12 +30,12 @@ func SetupAndCleanupEnvironment(t *testing.T) func() {
func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
ioutil.WriteFile(GetStatePath("shims", "python"), []byte("#!/bin/bash\necho \"Python 4.5.6\""), 0777)
ioutil.WriteFile(state.GetStatePath("shims", "python"), []byte("#!/bin/bash\necho \"Python 4.5.6\""), 0777)
mockSystemPythonPath := t.TempDir()
mockSystemPythonExecPath := path.Join(mockSystemPythonPath, "python")
oldPath := os.Getenv("PATH")
os.Setenv("PATH", fmt.Sprintf("%s:/usr/bin", GetStatePath("shims")))
os.Setenv("PATH", fmt.Sprintf("%s:/usr/bin", state.GetStatePath("shims")))
defer os.Setenv("PATH", oldPath)
sysVersion, sysPath := DetermineSystemPython()
@ -51,8 +52,8 @@ func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T
defer SetupAndCleanupEnvironment(t)()
// Writing a mock user-defined state.
mockState := State{GlobalVersion: "1.0.0"}
statePath := GetStatePath("state.json")
mockState := state.State{GlobalVersion: "1.0.0"}
statePath := state.GetStatePath("state.json")
stateData, _ := json.Marshal(mockState)
ioutil.WriteFile(statePath, stateData, 0750)
@ -60,7 +61,7 @@ func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T
os.Chdir(temporaryWd)
ioutil.WriteFile(path.Join(temporaryWd, ".python-version"), []byte("1.2.3"), 0750)
version, err := DetermineSelectedPythonVersion(ReadState())
version, err := DetermineSelectedPythonVersion(state.ReadState())
if err != nil || version.Version != "1.2.3" {
t.Errorf("Expected version to be %s, got %s instead.", "1.2.3", version.Version)
@ -71,12 +72,12 @@ func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
// Writing a mock user-defined state.
mockState := State{GlobalVersion: "1.0.0"}
statePath := GetStatePath("state.json")
mockState := state.State{GlobalVersion: "1.0.0"}
statePath := state.GetStatePath("state.json")
stateData, _ := json.Marshal(mockState)
ioutil.WriteFile(statePath, stateData, 0750)
version, err := DetermineSelectedPythonVersion(ReadState())
version, err := DetermineSelectedPythonVersion(state.ReadState())
if err != nil || version.Version != mockState.GlobalVersion {
t.Errorf("Expected version to be %s, got %s instead.", mockState.GlobalVersion, version)
@ -86,7 +87,7 @@ func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
version, err := DetermineSelectedPythonVersion(ReadState())
version, err := DetermineSelectedPythonVersion(state.ReadState())
if err != nil || version.Source != "system" {
t.Errorf("Expected version to be 'SYSTEM', got %s instead.", version)
@ -139,9 +140,9 @@ func TestListInstalledVersion(t *testing.T) {
versions := []string{"1.2.3", "4.5.6", "7.8.9"}
os.Mkdir(GetStatePath("runtimes"), 0750)
os.Mkdir(state.GetStatePath("runtimes"), 0750)
for _, version := range versions {
os.Mkdir(GetStatePath("runtimes", "py-"+version), 0750)
os.Mkdir(state.GetStatePath("runtimes", "py-"+version), 0750)
}
installedVersions, _ := ListInstalledVersions()
@ -154,7 +155,7 @@ func TestListInstalledVersion(t *testing.T) {
func TestListInstalledVersionNoVersionsInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
os.Mkdir(GetStatePath("runtimes"), 0750)
os.Mkdir(state.GetStatePath("runtimes"), 0750)
installedVersions, _ := ListInstalledVersions()

View file

@ -1,4 +1,4 @@
package main
package state
import (
"encoding/json"

View file

@ -1,4 +1,4 @@
package main
package state
import (
"encoding/json"

3
v.go
View file

@ -2,6 +2,7 @@ package main
import (
"os"
state "v/state"
)
const (
@ -13,7 +14,7 @@ const (
// Main entrypoint.
func main() {
args := os.Args[1:]
currentState := ReadState()
currentState := state.ReadState()
cli := CLI{
Metadata: map[string]string{