Merge pull request #17 from mcataford/refactor/modularization

refactor: modularization
This commit is contained in:
Marc 2024-01-25 23:25:10 -05:00 committed by GitHub
commit b6d8cc534d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 379 additions and 344 deletions

View file

@ -1,7 +1,9 @@
package main
package cli
import (
"strings"
logger "v/logger"
state "v/state"
)
type Flags struct {
@ -14,7 +16,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 +36,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 +50,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
@ -68,10 +70,10 @@ func (c CLI) Run(args []string, currentState State) error {
// Prints autogenerated help documentation specifying command usage
// and descriptions based on registered commands (see: AddCommand).
func (c CLI) Help() {
InfoLogger.Printf("v: A simple version manager. (v%s)\n---", Version)
logger.InfoLogger.Printf("v: A simple version manager. (v%s)\n---", c.Metadata["Version"])
for _, commandLabel := range c.OrderedCommands {
command := c.Commands[commandLabel]
InfoLogger.Printf("\033[1m%-30s\033[0m%s\n", command.Usage, command.Description)
logger.InfoLogger.Printf("\033[1m%-30s\033[0m%s\n", command.Usage, command.Description)
}
}

View file

@ -2,7 +2,9 @@ package main
import (
"os"
"slices"
cli "v/cli"
logger "v/logger"
state "v/state"
)
var DIRECTORIES = []string{
@ -29,128 +31,20 @@ 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 cli.Flags, currentState state.State) error {
if flags.AddPath {
InfoLogger.Printf("export PATH=%s:$PATH\n", GetStatePath("shims"))
logger.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])
err := os.RemoveAll(runtimePath)
return err
}
func InstallPython(args []string, flags Flags, currentState State) error {
version := args[1]
return InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
}
func Use(args []string, flags Flags, currentState State) error {
version := args[1]
if err := ValidateVersion(version); err != nil {
return err
}
availableVersions := GetAvailableVersions()
found := false
for _, v := range availableVersions {
if v == version {
found = true
break
}
}
if !found {
InfoLogger.Println("Version not installed. Installing it first.")
InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
}
WriteState(version)
InfoLogger.Printf("Now using Python %s\n", version)
return nil
}
func ListVersions(args []string, flags Flags, currentState State) error {
installedVersions, err := ListInstalledVersions()
if err != nil {
return err
}
if len(installedVersions) == 0 {
InfoLogger.Println("No versions installed!")
return nil
}
for _, d := range installedVersions {
InfoLogger.Println(d)
}
return nil
}
// Which prints out the system path to the executable being used by `python`.
func Which(args []string, flags Flags, currentState State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
var printedPath string
if selectedVersion.Source == "system" {
_, sysPath := DetermineSystemPython()
printedPath = sysPath + " (system)"
} else if isInstalled {
tag := VersionStringToStruct(selectedVersion.Version)
printedPath = GetStatePath("runtimes", "py-"+selectedVersion.Version, "bin", "python"+tag.MajorMinor())
} else {
InfoLogger.Printf("The desired version (%s) is not installed.\n", selectedVersion.Version)
return nil
}
prefix := "Python path: "
if flags.RawOutput {
prefix = ""
} else {
printedPath = Bold(printedPath)
}
InfoLogger.Printf("%s%s\n", prefix, printedPath)
return nil
}
// 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 {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
if !isInstalled {
InfoLogger.Println(Bold(Yellow("WARNING: This version is not installed.")))
}
if flags.RawOutput {
InfoLogger.Println(selectedVersion.Version)
return nil
}
InfoLogger.Printf("Python version: %s\nSource: %s\n", Bold(selectedVersion.Version), Bold(selectedVersion.Source))
return nil
}

View file

@ -1,131 +0,0 @@
package main
import (
"bytes"
"os"
"strings"
"testing"
)
func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
os.Mkdir(GetStatePath("runtimes"), 0750)
var out bytes.Buffer
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
ListVersions([]string{}, Flags{}, State{})
captured := out.String()
if captured != "No versions installed!\n" {
t.Errorf("Unexpected message: %s", captured)
}
}
func TestListVersionOutputsVersionsInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
os.MkdirAll(GetStatePath("runtimes", "py-1.2.3"), 0750)
var out bytes.Buffer
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
ListVersions([]string{}, Flags{}, State{})
captured := out.String()
if captured != "1.2.3\n" {
t.Errorf("Unexpected message: %s", captured)
}
}
func TestListVersionReturnsErrorOnFailure(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
err := ListVersions([]string{}, Flags{}, State{})
captured := out.String()
if captured != "" {
t.Errorf("Captured unexpected message: %s", captured)
}
if err == nil {
t.Errorf("Expected error returned, did not get one.")
}
}
func TestListVersionOutputsVersionSelectedAndWarnsNotInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
Which([]string{}, Flags{}, State{GlobalVersion: "1.2.3"})
captured := out.String()
if captured != "The desired version (1.2.3) is not installed.\n" {
t.Errorf("Unexpected message: %s", captured)
}
}
func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
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"})
captured := strings.TrimSpace(out.String())
expected := GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
if !strings.Contains(captured, expected) {
t.Errorf("Unexpected message: %s, not %s", captured, expected)
}
}
func TestWhichOutputsSystemVersionIfNoneSelected(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
InfoLogger.SetOutput(&out)
defer InfoLogger.SetOutput(os.Stdout)
Which([]string{}, Flags{RawOutput: true}, State{})
captured := strings.TrimSpace(out.String())
if captured != "/bin/python (system)" {
t.Errorf("%s != %s", captured, "/bin/python (system)")
}
}
func TestWhichOutputsVersionWithoutPrefixesIfRawOutput(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
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"})
captured := strings.TrimSpace(out.String())
expected := GetStatePath("runtimes", "py-1.2.3", "bin", "python1.2")
if captured != expected {
t.Errorf("Unexpected message: %s, not %s", captured, expected)
}
}

View file

@ -1,26 +1,11 @@
package main
package exec
import (
"errors"
"os"
"os/exec"
"strings"
)
func VersionStringToStruct(version string) VersionTag {
splitVersion := strings.Split(version, ".")
return VersionTag{Major: splitVersion[0], Minor: splitVersion[1], Patch: splitVersion[2]}
}
func ValidateVersion(version string) error {
if splitVersion := strings.Split(version, "."); len(splitVersion) != 3 {
return errors.New("Invalid version string. Expected format 'a.b.c'.")
}
return nil
}
// RunCommand is a thin wrapper around running command-line calls
// programmatically. It abstracts common configuration like routing
// output and handling the directory the calls are made from.

View file

@ -1,4 +1,4 @@
package main
package logger
import (
"log"

View file

@ -1,4 +1,4 @@
package main
package logger
const (
RESET = "\033[0m"

119
python/commands.go Normal file
View file

@ -0,0 +1,119 @@
package python
import (
"os"
"slices"
cli "v/cli"
logger "v/logger"
state "v/state"
)
func UninstallPython(args []string, flags cli.Flags, currentState state.State) error {
runtimePath := state.GetStatePath("runtimes", "py-"+args[1])
err := os.RemoveAll(runtimePath)
return err
}
func InstallPython(args []string, flags cli.Flags, currentState state.State) error {
version := args[1]
return InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
}
func Use(args []string, flags cli.Flags, currentState state.State) error {
version := args[1]
if err := ValidateVersion(version); err != nil {
return err
}
availableVersions := state.GetAvailableVersions()
found := false
for _, v := range availableVersions {
if v == version {
found = true
break
}
}
if !found {
logger.InfoLogger.Println("Version not installed. Installing it first.")
InstallPythonDistribution(version, flags.NoCache, flags.Verbose)
}
state.WriteState(version)
logger.InfoLogger.Printf("Now using Python %s\n", version)
return nil
}
func ListVersions(args []string, flags cli.Flags, currentState state.State) error {
installedVersions, err := ListInstalledVersions()
if err != nil {
return err
}
if len(installedVersions) == 0 {
logger.InfoLogger.Println("No versions installed!")
return nil
}
for _, d := range installedVersions {
logger.InfoLogger.Println(d)
}
return nil
}
// Which prints out the system path to the executable being used by `python`.
func Which(args []string, flags cli.Flags, currentState state.State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
var printedPath string
if selectedVersion.Source == "system" {
_, sysPath := DetermineSystemPython()
printedPath = sysPath + " (system)"
} else if isInstalled {
tag := VersionStringToStruct(selectedVersion.Version)
printedPath = state.GetStatePath("runtimes", "py-"+selectedVersion.Version, "bin", "python"+tag.MajorMinor())
} else {
logger.InfoLogger.Printf("The desired version (%s) is not installed.\n", selectedVersion.Version)
return nil
}
prefix := "Python path: "
if flags.RawOutput {
prefix = ""
} else {
printedPath = logger.Bold(printedPath)
}
logger.InfoLogger.Printf("%s%s\n", prefix, printedPath)
return nil
}
// 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 cli.Flags, currentState state.State) error {
selectedVersion, _ := DetermineSelectedPythonVersion(currentState)
installedVersions, _ := ListInstalledVersions()
isInstalled := slices.Contains(installedVersions, selectedVersion.Version)
if !isInstalled {
logger.InfoLogger.Println(logger.Bold(logger.Yellow("WARNING: This version is not installed.")))
}
if flags.RawOutput {
logger.InfoLogger.Println(selectedVersion.Version)
return nil
}
logger.InfoLogger.Printf("Python version: %s\nSource: %s\n", logger.Bold(selectedVersion.Version), logger.Bold(selectedVersion.Source))
return nil
}

135
python/commands_test.go Normal file
View file

@ -0,0 +1,135 @@
package python
import (
"bytes"
"os"
"strings"
"testing"
cli "v/cli"
logger "v/logger"
state "v/state"
testutils "v/testutils"
)
func TestListVersionOutputsNoticeIfNoVersionsInstalled(t *testing.T) {
defer testutils.SetupAndCleanupEnvironment(t)()
os.Mkdir(state.GetStatePath("runtimes"), 0750)
var out bytes.Buffer
logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout)
ListVersions([]string{}, cli.Flags{}, state.State{})
captured := out.String()
if captured != "No versions installed!\n" {
t.Errorf("Unexpected message: %s", captured)
}
}
func TestListVersionOutputsVersionsInstalled(t *testing.T) {
defer testutils.SetupAndCleanupEnvironment(t)()
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
var out bytes.Buffer
logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout)
ListVersions([]string{}, cli.Flags{}, state.State{})
captured := out.String()
if captured != "1.2.3\n" {
t.Errorf("Unexpected message: %s", captured)
}
}
func TestListVersionReturnsErrorOnFailure(t *testing.T) {
defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout)
err := ListVersions([]string{}, cli.Flags{}, state.State{})
captured := out.String()
if captured != "" {
t.Errorf("Captured unexpected message: %s", captured)
}
if err == nil {
t.Errorf("Expected error returned, did not get one.")
}
}
func TestListVersionOutputsVersionSelectedAndWarnsNotInstalled(t *testing.T) {
defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout)
Which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"})
captured := out.String()
if captured != "The desired version (1.2.3) is not installed.\n" {
t.Errorf("Unexpected message: %s", captured)
}
}
func TestWhichOutputsVersionSelectedIfInstalled(t *testing.T) {
defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout)
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
Which([]string{}, cli.Flags{}, state.State{GlobalVersion: "1.2.3"})
captured := strings.TrimSpace(out.String())
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)
}
}
func TestWhichOutputsSystemVersionIfNoneSelected(t *testing.T) {
defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout)
Which([]string{}, cli.Flags{RawOutput: true}, state.State{})
captured := strings.TrimSpace(out.String())
if captured != "/bin/python (system)" {
t.Errorf("%s != %s", captured, "/bin/python (system)")
}
}
func TestWhichOutputsVersionWithoutPrefixesIfRawOutput(t *testing.T) {
defer testutils.SetupAndCleanupEnvironment(t)()
var out bytes.Buffer
logger.InfoLogger.SetOutput(&out)
defer logger.InfoLogger.SetOutput(os.Stdout)
os.MkdirAll(state.GetStatePath("runtimes", "py-1.2.3"), 0750)
Which([]string{}, cli.Flags{RawOutput: true}, state.State{GlobalVersion: "1.2.3"})
captured := strings.TrimSpace(out.String())
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

@ -1,4 +1,4 @@
package main
package python
import (
"errors"
@ -9,6 +9,9 @@ import (
"path"
"strings"
"time"
exec "v/exec"
logger "v/logger"
state "v/state"
)
var pythonReleasesBaseURL = "https://www.python.org/ftp/python"
@ -51,20 +54,20 @@ 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{}
InfoLogger.Println(Bold("Downloading source for Python " + version))
InfoLogger.SetPrefix(" ")
defer InfoLogger.SetPrefix("")
logger.InfoLogger.Println(logger.Bold("Downloading source for Python " + version))
logger.InfoLogger.SetPrefix(" ")
defer logger.InfoLogger.SetPrefix("")
start := time.Now()
_, err := os.Stat(archivePath)
if errors.Is(err, os.ErrNotExist) || skipCache {
InfoLogger.Println("Fetching from " + sourceUrl)
logger.InfoLogger.Println("Fetching from " + sourceUrl)
resp, err := client.Get(sourceUrl)
@ -78,23 +81,23 @@ func downloadSource(version string, skipCache bool) (PackageMetadata, error) {
defer file.Close()
} else {
InfoLogger.Println("Found in cache: " + archivePath)
logger.InfoLogger.Println("Found in cache: " + archivePath)
}
InfoLogger.Printf("✅ Done (%s)\n", time.Since(start))
logger.InfoLogger.Printf("✅ Done (%s)\n", time.Since(start))
return PackageMetadata{ArchivePath: archivePath, Version: version}, nil
}
func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, error) {
InfoLogger.Println(Bold("Building from source"))
InfoLogger.SetPrefix(" ")
defer InfoLogger.SetPrefix("")
logger.InfoLogger.Println(logger.Bold("Building from source"))
logger.InfoLogger.SetPrefix(" ")
defer logger.InfoLogger.SetPrefix("")
start := time.Now()
InfoLogger.Println("Unpacking source for " + pkgMeta.ArchivePath)
logger.InfoLogger.Println("Unpacking source for " + pkgMeta.ArchivePath)
_, untarErr := RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, GetStatePath("cache"), !verbose)
_, untarErr := exec.RunCommand([]string{"tar", "zxvf", pkgMeta.ArchivePath}, state.GetStatePath("cache"), !verbose)
if untarErr != nil {
return pkgMeta, untarErr
@ -102,18 +105,18 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
unzippedRoot := strings.TrimSuffix(pkgMeta.ArchivePath, path.Ext(pkgMeta.ArchivePath))
InfoLogger.Println("Configuring installer")
logger.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)
_, configureErr := exec.RunCommand([]string{"./configure", "--prefix=" + targetDirectory, "--enable-optimizations"}, unzippedRoot, !verbose)
if configureErr != nil {
return pkgMeta, configureErr
}
InfoLogger.Println("Building")
_, buildErr := RunCommand([]string{"make", "altinstall", "-j4"}, unzippedRoot, !verbose)
logger.InfoLogger.Println("Building")
_, buildErr := exec.RunCommand([]string{"make", "altinstall", "-j4"}, unzippedRoot, !verbose)
if buildErr != nil {
return pkgMeta, buildErr
@ -125,7 +128,7 @@ func buildFromSource(pkgMeta PackageMetadata, verbose bool) (PackageMetadata, er
pkgMeta.InstallPath = targetDirectory
InfoLogger.Printf("Installed Python %s at %s\n", pkgMeta.Version, pkgMeta.InstallPath)
InfoLogger.Printf("✅ Done (%s)\n", time.Since(start))
logger.InfoLogger.Printf("Installed Python %s at %s\n", pkgMeta.Version, pkgMeta.InstallPath)
logger.InfoLogger.Printf("✅ Done (%s)\n", time.Since(start))
return pkgMeta, nil
}

View file

@ -1,4 +1,4 @@
package main
package python
import (
"encoding/json"
@ -8,33 +8,19 @@ import (
"path"
"slices"
"testing"
state "v/state"
testutils "v/testutils"
)
// SetupAndCleanupEnvironment sets up a test directory and
// environment variables before the test and returns a cleanup
// function that can be deferred to cleanup any changes to the
// system.
func SetupAndCleanupEnvironment(t *testing.T) func() {
os.Setenv("V_ROOT", t.TempDir())
temporaryWd := t.TempDir()
os.Chdir(temporaryWd)
return func() {
os.Unsetenv("V_ROOT")
}
}
func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.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()
@ -48,11 +34,11 @@ func TestDetermineSystemPythonGetsUnshimmedPythonRuntime(t *testing.T) {
}
func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.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 +46,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)
@ -68,15 +54,15 @@ func TestDetermineSelectedPythonVersionUsesPythonVersionFileIfFound(t *testing.T
}
func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.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)
@ -84,9 +70,9 @@ func TestDetermineSelectedPythonVersionGetsUserDefinedVersion(t *testing.T) {
}
func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.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)
@ -94,7 +80,7 @@ func TestDetermineSelectedPythonVersionDefaultsToSystem(t *testing.T) {
}
func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.SetupAndCleanupEnvironment(t)()
temporaryWd := t.TempDir()
os.Chdir(temporaryWd)
@ -108,7 +94,7 @@ func TestSearchForPythonVersionFileFindsFileInCwd(t *testing.T) {
}
func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.SetupAndCleanupEnvironment(t)()
temporaryWd := t.TempDir()
@ -125,7 +111,7 @@ func TestSearchForPythonVersionFileFindsFileInParents(t *testing.T) {
}
func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.SetupAndCleanupEnvironment(t)()
versionFound, found := SearchForPythonVersionFile()
@ -135,13 +121,13 @@ func TestSearchForPythonVersionFileReturnsOnRootIfNoneFound(t *testing.T) {
}
func TestListInstalledVersion(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.SetupAndCleanupEnvironment(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()
@ -152,9 +138,9 @@ func TestListInstalledVersion(t *testing.T) {
}
func TestListInstalledVersionNoVersionsInstalled(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.SetupAndCleanupEnvironment(t)()
os.Mkdir(GetStatePath("runtimes"), 0750)
os.Mkdir(state.GetStatePath("runtimes"), 0750)
installedVersions, _ := ListInstalledVersions()
@ -164,7 +150,7 @@ func TestListInstalledVersionNoVersionsInstalled(t *testing.T) {
}
func TestListInstalledVersionNoRuntimesDir(t *testing.T) {
defer SetupAndCleanupEnvironment(t)()
defer testutils.SetupAndCleanupEnvironment(t)()
installedVersions, err := ListInstalledVersions()

View file

@ -1,19 +1,36 @@
package main
package python
import (
"errors"
"io/ioutil"
"os"
"path"
"strings"
exec "v/exec"
state "v/state"
)
func VersionStringToStruct(version string) VersionTag {
splitVersion := strings.Split(version, ".")
return VersionTag{Major: splitVersion[0], Minor: splitVersion[1], Patch: splitVersion[2]}
}
func ValidateVersion(version string) error {
if splitVersion := strings.Split(version, "."); len(splitVersion) != 3 {
return errors.New("Invalid version string. Expected format 'a.b.c'.")
}
return nil
}
type SelectedVersion struct {
Version string
Source string
}
func ListInstalledVersions() ([]string, error) {
runtimesDir := GetStatePath("runtimes")
runtimesDir := state.GetStatePath("runtimes")
entries, err := os.ReadDir(runtimesDir)
if err != nil {
@ -65,7 +82,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 +90,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 +100,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, _ := exec.RunCommand([]string{"/bin/python", "--version"}, state.GetStatePath(), true)
detectedVersion, _ := strings.CutPrefix(versionOut, "Python")
return strings.TrimSpace(detectedVersion), "/bin/python"
}

View file

@ -1,4 +1,4 @@
#!/bin/bash
go get golang.org/x/tools/cmd/cover
go test -cover -v -coverprofile=cov.out
go test ./... -cover -v -coverprofile=cov.out

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"

22
testutils/setup.go Normal file
View file

@ -0,0 +1,22 @@
package testutils
import (
"os"
"testing"
)
// SetupAndCleanupEnvironment sets up a test directory and
// environment variables before the test and returns a cleanup
// function that can be deferred to cleanup any changes to the
// system.
func SetupAndCleanupEnvironment(t *testing.T) func() {
os.Setenv("V_ROOT", t.TempDir())
temporaryWd := t.TempDir()
os.Chdir(temporaryWd)
return func() {
os.Unsetenv("V_ROOT")
}
}

19
v.go
View file

@ -2,6 +2,9 @@ package main
import (
"os"
cli "v/cli"
python "v/python"
state "v/state"
)
const (
@ -13,26 +16,26 @@ const (
// Main entrypoint.
func main() {
args := os.Args[1:]
currentState := ReadState()
currentState := state.ReadState()
cli := CLI{
cli := cli.CLI{
Metadata: map[string]string{
"Version": Version,
},
}
err := cli.AddCommand(
"install", InstallPython, "v install <version>", "Downloads, builds and installs a new version of Python.",
"install", python.InstallPython, "v install <version>", "Downloads, builds and installs a new version of Python.",
).AddCommand(
"uninstall", UninstallPython, "v uninstall <version>", "Uninstalls the given Python version.",
"uninstall", python.UninstallPython, "v uninstall <version>", "Uninstalls the given Python version.",
).AddCommand(
"use", Use, "v use <version>", "Selects which Python version to use.",
"use", python.Use, "v use <version>", "Selects which Python version to use.",
).AddCommand(
"ls", ListVersions, "v ls", "Lists the installed Python versions.",
"ls", python.ListVersions, "v ls", "Lists the installed Python versions.",
).AddCommand(
"version", CurrentVersion, "v version", "Prints the current version and its source.",
"version", python.CurrentVersion, "v version", "Prints the current version and its source.",
).AddCommand(
"which", Which, "v which", "Prints the path to the current Python version.",
"which", python.Which, "v which", "Prints the path to the current Python version.",
).AddCommand(
"init", Initialize, "v init", "Initializes the v state.",
).Run(args, currentState)