added unit testing #18

Merged
JMR-dev merged 1 commits from fix-add-unit-testing into main 2026-05-26 14:36:51 +00:00
32 changed files with 5224 additions and 79 deletions
+73
View File
@@ -0,0 +1,73 @@
name: Integration Tests
on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
test-linux:
name: Linux Integration - ${{ matrix.os }}
runs-on: ubuntu-latest
strategy:
matrix:
os: [debian, arch, fedora]
fail-fast: false
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25.x'
- name: Run Dagger Integration Pipeline
run: |
go run ci/main.go --os ${{ matrix.os }}
test-mac:
name: macOS Integration
runs-on: macos-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25.x'
- name: Build bootstrap binary
run: |
go build -o bootstrap_environment .
- name: Pre-create ~/.pyenv directory
run: |
mkdir -p ~/.pyenv
- name: Run bootstrap tool
run: |
echo y | ./bootstrap_environment --only custom --no-vm --no-ai
conclusion:
name: Final Status Check
needs: [test-linux, test-mac]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check dependency jobs
run: |
# Check the results of matrix and mac jobs
# GHA will fail the workflow if this step exits with error
if [ "${{ needs.test-linux.result }}" != "success" ] || [ "${{ needs.test-mac.result }}" != "success" ]; then
echo "One or more integration checks failed!"
exit 1
fi
echo "All integration checks passed!"
+61
View File
@@ -0,0 +1,61 @@
name: macOS Manual Integration
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to run against'
required: true
default: 'main'
type: choice
options:
- main
- dev
- fix-add-unit-testing
workflow_definition:
description: 'Workflow Run Configuration'
required: true
default: 'custom-only'
type: choice
options:
- 'custom-only'
- 'system-only'
- 'full-setup'
jobs:
test-mac-manual:
name: macOS Manual Run
runs-on: macos-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25.x'
- name: Build bootstrap binary
run: |
go build -o bootstrap_environment .
- name: Pre-create ~/.pyenv directory
run: |
mkdir -p ~/.pyenv
- name: Run bootstrap tool (custom-only)
if: ${{ inputs.workflow_definition == 'custom-only' }}
run: |
echo y | ./bootstrap_environment --only custom --no-vm --no-ai
- name: Run bootstrap tool (system-only)
if: ${{ inputs.workflow_definition == 'system-only' }}
run: |
echo y | ./bootstrap_environment --only system --no-vm --no-ai
- name: Run bootstrap tool (full-setup)
if: ${{ inputs.workflow_definition == 'full-setup' }}
run: |
echo y | ./bootstrap_environment --no-vm --no-ai
+125
View File
@@ -0,0 +1,125 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"strings"
"dagger.io/dagger"
"golang.org/x/sync/errgroup"
)
func main() {
osFlag := flag.String("os", "all", "OS to test (debian, arch, fedora, or all)")
flag.Parse()
ctx := context.Background()
// Initialize Dagger Client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to Dagger: %v\n", err)
os.Exit(1)
}
defer client.Close()
// Get reference to the project source directory
src := client.Host().Directory(".")
// Build the bootstrapping binary inside Go container
fmt.Println("Building bootstrap_environment binary for Linux...")
builder := client.Container().
From("golang:1.25").
WithMountedDirectory("/src", src).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "bootstrap_environment", "."})
binaryFile := builder.File("bootstrap_environment")
// Target OS list
var targets []string
switch strings.ToLower(*osFlag) {
case "debian":
targets = []string{"debian:latest"}
case "arch":
targets = []string{"archlinux:latest"}
case "fedora":
targets = []string{"fedora:latest"}
case "all":
targets = []string{"debian:latest", "archlinux:latest", "fedora:latest"}
default:
fmt.Fprintf(os.Stderr, "Unsupported OS: %s. Supported: debian, arch, fedora, all\n", *osFlag)
os.Exit(1)
}
g, ctx := errgroup.WithContext(ctx)
for _, target := range targets {
target := target // capture loop variable
g.Go(func() error {
fmt.Printf("=== Starting integration test on target OS: %s ===\n", target)
// 1. Prepare target container base and setup script based on OS distro
var testContainer *dagger.Container
if strings.Contains(target, "debian") {
testContainer = client.Container().
From(target).
WithExec([]string{"apt-get", "update"}).
WithExec([]string{"apt-get", "install", "-y", "sudo", "curl", "git", "wget", "tar", "unzip", "xz-utils", "make", "python3", "which"})
} else if strings.Contains(target, "fedora") {
testContainer = client.Container().
From(target).
WithExec([]string{"dnf", "install", "-y", "sudo", "curl", "git", "wget", "tar", "unzip", "xz", "make", "python3", "which"})
} else if strings.Contains(target, "archlinux") {
testContainer = client.Container().
From(target).
WithExec([]string{"pacman", "-Sy", "--noconfirm", "sudo", "curl", "git", "wget", "tar", "unzip", "xz", "make", "python", "which"})
} else {
return fmt.Errorf("unsupported target OS: %s", target)
}
// 2. Pre-create the ~/.pyenv directory to skip python source compilation in integration test (saves ~10 minutes)
testContainer = testContainer.WithExec([]string{"mkdir", "-p", "/root/.pyenv"})
// 3. Mount the built binary
testContainer = testContainer.
WithFile("/usr/local/bin/bootstrap_environment", binaryFile).
WithWorkdir("/tmp")
// 4. Run bootstrap binary with safe args: --only custom --no-vm --no-ai
// We pipe 'y' to satisfy the "Proceed? [y/N]" prompt.
fmt.Printf("[%s] Executing bootstrap_environment...\n", target)
testContainer = testContainer.WithExec([]string{"sh", "-c", "echo y | bootstrap_environment --only custom --no-vm --no-ai"})
// 5. Verify all installed custom packages return a path and zero exit code from version command
fmt.Printf("[%s] Verifying package installations on PATH and running version checks...\n", target)
verifyCmd := []string{
"sh", "-c",
"export PATH=$PATH:/usr/local/go/bin; " +
"which go && go version && " +
"which nvim && nvim --version && " +
"which zig && zig version && " +
"which firecracker && firecracker --version",
}
verifyOutput, err := testContainer.WithExec(verifyCmd).Stdout(ctx)
if err != nil {
return fmt.Errorf("verification failed on %s: package not found on PATH or exited with error (%w)", target, err)
}
fmt.Printf("[%s] Verification Output:\n%s\n", target, verifyOutput)
fmt.Printf("--- PASS: Integration test on %s completed successfully ---\n", target)
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Fprintf(os.Stderr, "One or more integration tests failed: %v\n", err)
os.Exit(1)
}
fmt.Println("\nAll parallel integration tests passed successfully!")
}
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"io"
"os"
)
var (
// Exec redirects
runCmd = runCmdReal
runShell = runShellReal
hasCmd = hasCmdReal
probe = probeReal
// Net redirects
download = downloadReal
fetchJSON = fetchJSONReal
fetchText = fetchTextReal
// OS redirects
osStat = os.Stat
osReadFile = os.ReadFile
osWriteFile = os.WriteFile
osMkdirAll = os.MkdirAll
osRemove = os.Remove
osRemoveAll = os.RemoveAll
osRename = os.Rename
osExit = os.Exit
stdin io.Reader = os.Stdin
// Filesystem paths
osReleasePath = "/etc/os-release"
passwdPath = "/etc/passwd"
)
+1049
View File
File diff suppressed because it is too large Load Diff
+11 -7
View File
@@ -130,7 +130,7 @@ func isCustomPkgInstalled(pkg *CustomPackage) (bool, string) {
return false, ""
}
check := expandHome(raw)
if _, err := os.Stat(check); err == nil {
if _, err := osStat(check); err == nil {
return true, check
}
return false, check
@@ -195,7 +195,7 @@ func urlArchOK(pkg *CustomPackage) bool {
func installGo(archive string) {
goRoot := "/usr/local/go"
if _, err := os.Stat(goRoot); err == nil {
if _, err := osStat(goRoot); err == nil {
fmt.Printf(" Removing existing Go at %s ...\n", goRoot)
runCmd([]string{"rm", "-rf", goRoot}, CmdOpts{AsSudo: true})
}
@@ -215,6 +215,9 @@ func installFirecracker(archive, tmp string) {
return nil
}
name := info.Name()
if strings.HasSuffix(name, ".tgz") || strings.HasSuffix(name, ".tar.gz") {
return nil
}
if !strings.HasPrefix(name, "firecracker") {
return nil
}
@@ -226,6 +229,7 @@ func installFirecracker(archive, tmp string) {
}
return nil
})
if binary == "" {
errLog("firecracker binary not found in archive")
return
@@ -239,7 +243,7 @@ func installFirecracker(archive, tmp string) {
func installZig(pkg *CustomPackage, archive string) {
parent := "/usr/local"
zigDir := filepath.Join(parent, "zig-"+pkg.Version)
if _, err := os.Stat(zigDir); err == nil {
if _, err := osStat(zigDir); err == nil {
runCmd([]string{"rm", "-rf", zigDir}, CmdOpts{AsSudo: true})
}
runCmd([]string{"tar", "-C", parent, "-xJf", archive}, CmdOpts{AsSudo: true})
@@ -501,7 +505,7 @@ func installCustomPackages(toInstall []*CustomPackage) {
continue
}
installNeovim(pkg, tmp)
os.RemoveAll(tmp)
osRemoveAll(tmp)
continue
case "agy":
installAgy()
@@ -535,11 +539,11 @@ func installCustomPackages(toInstall []*CustomPackage) {
}
archive := filepath.Join(tmp, filepath.Base(url))
if !download(url, archive) {
os.RemoveAll(tmp)
osRemoveAll(tmp)
continue
}
if !verifyArchive(archive, pkg) {
os.RemoveAll(tmp)
osRemoveAll(tmp)
continue
}
switch name {
@@ -552,6 +556,6 @@ func installCustomPackages(toInstall []*CustomPackage) {
default:
warn(fmt.Sprintf("No install handler for '%s' — skipping", pkg.Name))
}
os.RemoveAll(tmp)
osRemoveAll(tmp)
}
}
+400
View File
@@ -0,0 +1,400 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
)
func TestCustomPackageResolvers(t *testing.T) {
defer resetMocks()
osName = "linux"
archName = "x86_64"
p := &CustomPackage{
Name: "my-pkg",
Version: "1.2.3",
URLTemplate: "http://example.com/download/{version}/{os}/{arch}/my-pkg.tar.gz",
SHA256URLTemplate: "http://example.com/download/{version}/{os}/{arch}/my-pkg.tar.gz.minisig",
SHA256Map: map[string]string{
"linux-x86_64": "aabbcc",
},
}
if p.resolveURL() != "http://example.com/download/1.2.3/linux/x86_64/my-pkg.tar.gz" {
t.Errorf("unexpected URL: %q", p.resolveURL())
}
if p.resolveSHA256URL() != "http://example.com/download/1.2.3/linux/x86_64/my-pkg.tar.gz.minisig" {
t.Errorf("unexpected SHA256 URL: %q", p.resolveSHA256URL())
}
if p.resolvedSHA256() != "aabbcc" {
t.Errorf("unexpected resolved SHA256: %q", p.resolvedSHA256())
}
if p.displayName() != "my-pkg-1.2.3" {
t.Errorf("unexpected display name: %q", p.displayName())
}
pNoVer := &CustomPackage{Name: "simple"}
if pNoVer.displayName() != "simple" {
t.Errorf("unexpected display name: %q", pNoVer.displayName())
}
}
func TestCmpSemver(t *testing.T) {
tests := []struct {
a, b string
expected int
}{
{"1.2.3", "1.2.3", 0},
{"1.2.3", "1.2.4", -1},
{"1.3.0", "1.2.9", 1},
{"2.0.0", "10.0.0", -1},
{"1.10.2", "1.2.3", 1},
}
for _, tt := range tests {
res := cmpSemver(tt.a, tt.b)
// Normalize to -1, 0, 1
actual := 0
if res < 0 {
actual = -1
} else if res > 0 {
actual = 1
}
if actual != tt.expected {
t.Errorf("cmpSemver(%q, %q) expected %d, got %d (raw %d)", tt.a, tt.b, tt.expected, actual, res)
}
}
}
func TestVerifyArchive(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
archive := filepath.Join(tmp, "archive.tar.gz")
os.WriteFile(archive, []byte("archive-bytes"), 0644)
// Hash of "archive-bytes" is 0c982986710a026635603031674053ca851fc0e3ea760094a34f59b84f7f6da6
p := &CustomPackage{
Name: "test",
SHA256: "0c982986710a026635603031674053ca851fc0e3ea760094a34f59b84f7f6da6",
}
if !verifyArchive(archive, p) {
t.Error("expected verification to pass")
}
p.SHA256 = "incorrect-hash"
if verifyArchive(archive, p) {
t.Error("expected verification to fail")
}
}
func TestResolveLatestGo(t *testing.T) {
defer resetMocks()
osName = "linux"
archName = "x86_64"
fetchJSON = func(url string, v any) bool {
// Mock Go releases API response
// This encodes mock data into v (raw JSON Message decoding)
data := `[
{
"version": "go1.21.3",
"files": [
{
"filename": "go1.21.3.linux-amd64.tar.gz",
"kind": "archive",
"sha256": "go-sha-value"
}
]
}
]`
json.Unmarshal([]byte(data), v)
return true
}
version, sha, ok := resolveLatestGo(nil)
if !ok || version != "1.21.3" || sha != "go-sha-value" {
t.Errorf("unexpected resolve latest Go result: version=%q, sha=%q, ok=%v", version, sha, ok)
}
}
func TestResolveLatestFirecracker(t *testing.T) {
defer resetMocks()
isMacOS = false
archName = "x86_64"
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.TagName = "v1.5.0"
rel.Assets = []ghAsset{
{Name: "firecracker-v1.5.0-x86_64.tgz.sha256.txt", BrowserDownloadURL: "http://sha-url"},
}
return true
}
fetchText = func(url string) string {
return "firecracker-sha-value firecracker-v1.5.0-x86_64.tgz"
}
version, sha, ok := resolveLatestFirecracker(nil)
if !ok || version != "1.5.0" || sha != "firecracker-sha-value" {
t.Errorf("unexpected resolve latest firecracker result: version=%q, sha=%q, ok=%v", version, sha, ok)
}
}
func TestResolveLatestZig(t *testing.T) {
defer resetMocks()
osName = "linux"
archName = "x86_64"
fetchJSON = func(url string, v any) bool {
data := `{
"0.11.0": {
"x86_64-linux": {
"shasum": "zig-sha-value"
}
}
}`
json.Unmarshal([]byte(data), v)
return true
}
version, sha, ok := resolveLatestZig(nil)
if !ok || version != "0.11.0" || sha != "zig-sha-value" {
t.Errorf("unexpected resolve latest zig result: version=%q, sha=%q, ok=%v", version, sha, ok)
}
}
func TestIsCustomPkgInstalled(t *testing.T) {
defer resetMocks()
// Pip installed check
hasCmd = func(name string) bool {
return name == "python3"
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0}, true
}
pPip := &CustomPackage{Name: "pip"}
installed, _ := isCustomPkgInstalled(pPip)
if !installed {
t.Error("expected pip to be installed")
}
// Go check (installed check via default install path)
osStat = func(name string) (os.FileInfo, error) {
if name == "/usr/local/go" {
return nil, nil // exists
}
return nil, os.ErrNotExist
}
pGo := &CustomPackage{Name: "go"}
installedGo, _ := isCustomPkgInstalled(pGo)
if !installedGo {
t.Error("expected go to be installed")
}
}
func TestExpandHomeAndDefaultInstallPath(t *testing.T) {
defer resetMocks()
// Test expandHome
t.Setenv("HOME", "/my/home")
expanded := expandHome("~/test")
if expanded != "/my/home/test" {
t.Errorf("expected /my/home/test, got %q", expanded)
}
notExpanded := expandHome("/other/path")
if notExpanded != "/other/path" {
t.Errorf("expected /other/path, got %q", notExpanded)
}
// Test defaultInstallPath
p := &CustomPackage{Name: "go"}
if defaultInstallPath(p) != "/usr/local/go" {
t.Errorf("expected /usr/local/go, got %q", defaultInstallPath(p))
}
pUnknown := &CustomPackage{Name: "unknown"}
if defaultInstallPath(pUnknown) != "" {
t.Errorf("expected empty path, got %q", defaultInstallPath(pUnknown))
}
}
func TestUrlArchOK(t *testing.T) {
defer resetMocks()
archName = "x86_64"
p := &CustomPackage{Name: "test", URLTemplate: "http://example.com/test-x86_64.tar.gz"}
if !urlArchOK(p) {
t.Error("expected urlArchOK to return true for matching arch")
}
pBad := &CustomPackage{Name: "test", URLTemplate: "http://example.com/test-aarch64.tar.gz"}
if urlArchOK(pBad) {
t.Error("expected urlArchOK to return false for mismatching arch")
}
}
func TestInstallCustomPackages(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool { return true }
download = func(url, dest string) bool {
// Write valid checksum file so it passes verification
// SHA256 of "content" is 751a073f248535132b178652553f1f317b3f1f90be68c078021481e33d443224
os.WriteFile(dest, []byte("content"), 0644)
return true
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
pkgs := []*CustomPackage{
{
Name: "go",
Version: "1.21.0",
URLTemplate: "http://example.com/go.tar.gz",
SHA256: "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
},
{
Name: "zig",
Version: "0.11.0",
URLTemplate: "http://example.com/zig.tar.gz",
SHA256: "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
},
}
installCustomPackages(pkgs)
// Verify that we executed tar/mv/ln etc commands via runCmd
hasTar := false
for _, call := range runCmdCalls {
if call[0] == "tar" {
hasTar = true
}
}
if !hasTar {
t.Errorf("expected tar command to be executed, got: %v", runCmdCalls)
}
}
func TestInstallFirecracker(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
archive := filepath.Join(tmp, "firecracker.tgz")
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "tar" {
dummyBin := filepath.Join(tmp, "firecracker-v1.5.0")
os.WriteFile(dummyBin, []byte("binary-content"), 0755)
}
return CmdResult{ExitCode: 0}
}
installFirecracker(archive, tmp)
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 1}
}
installFirecracker(archive, tmp)
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
installFirecracker(archive, tmp)
}
func TestInstallNeovim(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
osName = "linux"
archName = "x86_64"
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.Assets = []ghAsset{
{
Name: "nvim-linux-x86_64.tar.gz",
BrowserDownloadURL: "http://example.com/nvim.tar.gz",
Digest: "sha256:0c982986710a026635603031674053ca851fc0e3ea760094a34f59b84f7f6da6",
},
}
return true
}
download = func(url, dest string) bool {
os.WriteFile(dest, []byte("archive-bytes"), 0644)
return true
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
installNeovim(nil, tmp)
fetchJSON = func(url string, v any) bool { return false }
installNeovim(nil, tmp)
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.Assets = []ghAsset{{Name: "other-name"}}
return true
}
installNeovim(nil, tmp)
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.Assets = []ghAsset{{Name: "nvim-linux-x86_64.tar.gz", Digest: "bad-digest"}}
return true
}
installNeovim(nil, tmp)
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.Assets = []ghAsset{{Name: "nvim-linux-x86_64.tar.gz", Digest: "sha256:0c982986710a026635603031674053ca851fc0e3ea760094a34f59b84f7f6da6"}}
return true
}
download = func(url, dest string) bool { return false }
installNeovim(nil, tmp)
download = func(url, dest string) bool {
os.WriteFile(dest, []byte("different-bytes"), 0644)
return true
}
installNeovim(nil, tmp)
}
func TestResolveLatestEdgeCases(t *testing.T) {
defer resetMocks()
pkg := &CustomPackage{Name: "test", FetchLatest: "nonexistent"}
resolveLatest(pkg)
latestResolvers["panic-resolver"] = func(pkg *CustomPackage) (string, string, bool) {
panic("simulated panic")
}
pkgPanic := &CustomPackage{Name: "test", FetchLatest: "panic-resolver", Version: "1.0.0"}
resolveLatest(pkgPanic)
latestResolvers["fail-resolver"] = func(pkg *CustomPackage) (string, string, bool) {
return "", "", false
}
pkgFail := &CustomPackage{Name: "test", FetchLatest: "fail-resolver", Version: "1.0.0"}
resolveLatest(pkgFail)
latestResolvers["same-resolver"] = func(pkg *CustomPackage) (string, string, bool) {
return "1.0.0", "hash", true
}
pkgSame := &CustomPackage{Name: "test", FetchLatest: "same-resolver", Version: "1.0.0"}
resolveLatest(pkgSame)
}
+5 -5
View File
@@ -35,7 +35,7 @@ func detectOS() string {
return "macos"
default:
fmt.Fprintf(os.Stderr, "Unsupported OS: %s (supports Linux, Darwin)\n", runtime.GOOS)
os.Exit(1)
osExit(1)
return ""
}
}
@@ -48,7 +48,7 @@ func detectArch() string {
return "aarch64"
default:
fmt.Fprintf(os.Stderr, "Unsupported architecture: %s (supports x86_64, aarch64)\n", runtime.GOARCH)
os.Exit(1)
osExit(1)
return ""
}
}
@@ -116,7 +116,7 @@ func hasOtherArchToken(name string) bool {
// stripped of surrounding quotes. Returns "" if the file is missing or the
// field is absent.
func osReleaseField(field string) string {
data, err := os.ReadFile("/etc/os-release")
data, err := osReadFile(osReleasePath)
if err != nil {
return ""
}
@@ -130,7 +130,7 @@ func osReleaseField(field string) string {
}
func detectRHELFamily() bool {
data, err := os.ReadFile("/etc/os-release")
data, err := osReadFile(osReleasePath)
if err != nil {
return pkgMgr == "dnf"
}
@@ -152,7 +152,7 @@ func detectRHELFamily() bool {
}
func detectArchFamily() bool {
data, err := os.ReadFile("/etc/os-release")
data, err := osReadFile(osReleasePath)
if err != nil {
return pkgMgr == "pacman"
}
+135
View File
@@ -0,0 +1,135 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestDetectOSAndArch(t *testing.T) {
// detectOS and detectArch call os.Exit on unsupported platforms.
// Since the tests run on a supported platform, let's verify they return valid values.
osVal := detectOS()
if osVal != "linux" && osVal != "macos" {
t.Errorf("expected linux or macos, got %q", osVal)
}
archVal := detectArch()
if archVal != "x86_64" && archVal != "aarch64" {
t.Errorf("expected x86_64 or aarch64, got %q", archVal)
}
}
func TestFormatURL(t *testing.T) {
defer resetMocks()
osName = "linux"
archName = "x86_64"
tmpl := "https://example.com/download/go-{version}-{os}-{arch}.tar.gz"
expected := "https://example.com/download/go-1.20-linux-x86_64.tar.gz"
actual := formatURL(tmpl, "1.20")
if actual != expected {
t.Errorf("expected %q, got %q", expected, actual)
}
}
func TestOtherArch(t *testing.T) {
defer resetMocks()
archName = "x86_64"
if otherArch() != "aarch64" {
t.Errorf("expected aarch64, got %q", otherArch())
}
archName = "aarch64"
if otherArch() != "x86_64" {
t.Errorf("expected x86_64, got %q", otherArch())
}
}
func TestArchMatchesAndTokens(t *testing.T) {
if !archMatches("file-amd64", "x86_64") {
t.Error("expected true for file-amd64 and x86_64")
}
if archMatches("file-arm64", "x86_64") {
t.Error("expected false for file-arm64 and x86_64")
}
archName = "x86_64"
if !hasOtherArchToken("file-arm64") {
t.Error("expected true for file-arm64 when arch is x86_64")
}
}
func TestOSReleaseField(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
osReleasePath = filepath.Join(tmp, "os-release")
content := `NAME="Fedora Linux"
VERSION="40 (Workstation Edition)"
ID=fedora
VERSION_ID=40
`
if err := os.WriteFile(osReleasePath, []byte(content), 0644); err != nil {
t.Fatalf("failed to write mock os-release: %v", err)
}
if val := osReleaseField("ID"); val != "fedora" {
t.Errorf("expected fedora, got %q", val)
}
if val := osReleaseField("VERSION_ID"); val != "40" {
t.Errorf("expected 40, got %q", val)
}
if val := osReleaseField("NONEXISTENT"); val != "" {
t.Errorf("expected empty string, got %q", val)
}
// Missing file case
osReleasePath = filepath.Join(tmp, "nonexistent")
if val := osReleaseField("ID"); val != "" {
t.Errorf("expected empty string for missing file, got %q", val)
}
}
func TestDetectRHELAndArchFamilies(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
osReleasePath = filepath.Join(tmp, "os-release")
// Test Fedora (RHEL family)
if err := os.WriteFile(osReleasePath, []byte("ID=fedora\n"), 0644); err != nil {
t.Fatal(err)
}
if !detectRHELFamily() {
t.Error("expected fedora to be detected as RHEL family")
}
if detectArchFamily() {
t.Error("expected fedora to NOT be detected as Arch family")
}
// Test Arch (Arch family)
if err := os.WriteFile(osReleasePath, []byte("ID_LIKE=\"arch\"\n"), 0644); err != nil {
t.Fatal(err)
}
if detectRHELFamily() {
t.Error("expected arch to NOT be detected as RHEL family")
}
if !detectArchFamily() {
t.Error("expected arch to be detected as Arch family")
}
// Missing file fallback cases
osReleasePath = filepath.Join(tmp, "nonexistent")
pkgMgr = "dnf"
if !detectRHELFamily() {
t.Error("expected dnf manager fallback to RHEL family")
}
pkgMgr = "pacman"
if !detectArchFamily() {
t.Error("expected pacman manager fallback to Arch family")
}
}
+8 -8
View File
@@ -37,8 +37,8 @@ type CmdResult struct {
func (r CmdResult) OK() bool { return r.Err == nil && r.ExitCode == 0 }
// runCmd executes argv with the supplied options.
func runCmd(argv []string, opts CmdOpts) CmdResult {
// runCmdReal executes argv with the supplied options.
func runCmdReal(argv []string, opts CmdOpts) CmdResult {
if opts.Timeout == 0 {
opts.Timeout = defaultSubprocessTimeout
}
@@ -90,9 +90,9 @@ func runCmd(argv []string, opts CmdOpts) CmdResult {
return res
}
// runShell executes a single shell string via /bin/sh -c (matching the Python
// runShellReal executes a single shell string via /bin/sh -c (matching the Python
// version's subprocess.run(..., shell=True)).
func runShell(cmd string, opts CmdOpts) CmdResult {
func runShellReal(cmd string, opts CmdOpts) CmdResult {
if opts.Timeout == 0 {
opts.Timeout = defaultSubprocessTimeout
}
@@ -141,16 +141,16 @@ func runShell(cmd string, opts CmdOpts) CmdResult {
return res
}
// hasCmd is shutil.which() — returns true if name resolves on PATH.
func hasCmd(name string) bool {
// hasCmdReal is shutil.which() — returns true if name resolves on PATH.
func hasCmdReal(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
// probe is a short, read-only command invocation used for "is this installed"
// probeReal is a short, read-only command invocation used for "is this installed"
// checks. Returns (result, true) on completion (including non-zero exit) and
// (zero, false) on timeout/launch failure.
func probe(argv []string, timeout time.Duration) (CmdResult, bool) {
func probeReal(argv []string, timeout time.Duration) (CmdResult, bool) {
if timeout == 0 {
timeout = 30 * time.Second
}
+151
View File
@@ -0,0 +1,151 @@
package main
import (
"strings"
"testing"
"time"
)
func TestCmdResultOK(t *testing.T) {
res1 := CmdResult{ExitCode: 0, Err: nil}
if !res1.OK() {
t.Error("expected ExitCode 0 and Err nil to be OK")
}
res2 := CmdResult{ExitCode: 1, Err: nil}
if res2.OK() {
t.Error("expected ExitCode 1 to NOT be OK")
}
}
func TestRunCmdReal(t *testing.T) {
// Simple echo check
res := runCmdReal([]string{"echo", "hello world"}, CmdOpts{Capture: true})
if !res.OK() {
t.Errorf("expected OK command, got result: %+v", res)
}
out := strings.TrimSpace(string(res.Stdout))
if out != "hello world" {
t.Errorf("expected 'hello world', got %q", out)
}
// Exit failure check
resFail := runCmdReal([]string{"false"}, CmdOpts{Capture: true})
if resFail.OK() {
t.Error("expected false command to fail")
}
if resFail.ExitCode != 1 {
t.Errorf("expected exit code 1, got %d", resFail.ExitCode)
}
// Timeout check (using a short timeout)
resTimeout := runCmdReal([]string{"sleep", "5"}, CmdOpts{Timeout: 10 * time.Millisecond, Capture: true})
if resTimeout.ExitCode != 124 {
t.Errorf("expected exit code 124 (timeout), got %d", resTimeout.ExitCode)
}
}
func TestRunShellReal(t *testing.T) {
// Simple shell command
res := runShellReal("echo shell hello", CmdOpts{Capture: true})
if !res.OK() {
t.Errorf("expected OK shell command, got result: %+v", res)
}
out := strings.TrimSpace(string(res.Stdout))
if out != "shell hello" {
t.Errorf("expected 'shell hello', got %q", out)
}
// Failed command
resFail := runShellReal("exit 42", CmdOpts{Capture: true})
if resFail.OK() {
t.Error("expected shell command with exit 42 to fail")
}
if resFail.ExitCode != 42 {
t.Errorf("expected exit code 42, got %d", resFail.ExitCode)
}
}
func TestHasCmdReal(t *testing.T) {
if !hasCmdReal("go") {
t.Error("expected hasCmdReal('go') to return true (running go test)")
}
if hasCmdReal("nonexistent-command-xyz") {
t.Error("expected nonexistent command to return false")
}
}
func TestProbeReal(t *testing.T) {
res, ok := probeReal([]string{"echo", "probe"}, 0)
if !ok {
t.Error("expected probe to succeed")
}
if strings.TrimSpace(string(res.Stdout)) != "probe" {
t.Errorf("expected 'probe', got %q", string(res.Stdout))
}
// Test a failing command probe
resFail, okFail := probeReal([]string{"false"}, 0)
if !okFail {
t.Error("expected probe check of false command to return ok=true (meaning it completed and didn't crash/timeout)")
}
if resFail.ExitCode != 1 {
t.Errorf("expected exit code 1, got %d", resFail.ExitCode)
}
// Test an invalid command (launch failure)
_, okErr := probeReal([]string{"nonexistent-executable-file"}, 0)
if okErr {
t.Error("expected probe to return false on launch failure")
}
}
func TestExecRealEdgeCases(t *testing.T) {
// 2. Cwd and Input in runCmdReal
tmp := t.TempDir()
resCwd := runCmdReal([]string{"pwd"}, CmdOpts{Cwd: tmp, Capture: true})
if !resCwd.OK() {
t.Errorf("pwd failed: %+v", resCwd)
}
resInput := runCmdReal([]string{"cat"}, CmdOpts{Input: []byte("my-input"), Capture: true})
if !resInput.OK() || strings.TrimSpace(string(resInput.Stdout)) != "my-input" {
t.Errorf("cat input failed, got result: %+v", resInput)
}
// 3. Capture = false in runCmdReal
_ = runCmdReal([]string{"echo", "capture-false-cmd"}, CmdOpts{Capture: false})
// 4. Launch failure in runCmdReal (not exit error)
resLaunch := runCmdReal([]string{"nonexistent-command-12345"}, CmdOpts{Capture: true})
if resLaunch.OK() || resLaunch.ExitCode != 1 || resLaunch.Err == nil {
t.Errorf("expected launch failure, got: %+v", resLaunch)
}
// 5. Cwd and Input in runShellReal
resShellCwd := runShellReal("pwd", CmdOpts{Cwd: tmp, Capture: true})
if !resShellCwd.OK() {
t.Errorf("shell pwd failed: %+v", resShellCwd)
}
resShellInput := runShellReal("cat", CmdOpts{Input: []byte("my-shell-input"), Capture: true})
if !resShellInput.OK() || strings.TrimSpace(string(resShellInput.Stdout)) != "my-shell-input" {
t.Errorf("shell cat input failed, got: %+v", resShellInput)
}
// 6. Capture = false in runShellReal
_ = runShellReal("echo capture-false-shell", CmdOpts{Capture: false})
// 7. Timeout in runShellReal
resShellTimeout := runShellReal("sleep 5", CmdOpts{Timeout: 10 * time.Millisecond, Capture: true})
if resShellTimeout.ExitCode != 124 {
t.Errorf("expected exit code 124 for shell timeout, got %d", resShellTimeout.ExitCode)
}
// 8. Timeout in probeReal
_, okProbeTimeout := probeReal([]string{"sleep", "5"}, 10*time.Millisecond)
if okProbeTimeout {
t.Error("expected probe to return ok=false on timeout")
}
}
+111
View File
@@ -0,0 +1,111 @@
package main
import (
"strings"
"testing"
)
func TestInstallFlatpakPackages(t *testing.T) {
defer resetMocks()
// Case 1: flatpak already installed
hasCmd = func(name string) bool {
return name == "flatpak"
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
installFlatpakPackages([]string{"org.gimp.GIMP"})
if len(runCmdCalls) != 2 {
t.Fatalf("expected 2 runCmd calls, got %d: %v", len(runCmdCalls), runCmdCalls)
}
// First call should add flathub remote
if runCmdCalls[0][1] != "remote-add" {
t.Errorf("expected remote-add, got %v", runCmdCalls[0])
}
// Second call should install GIMP
if runCmdCalls[1][1] != "install" || runCmdCalls[1][4] != "org.gimp.GIMP" {
t.Errorf("expected install GIMP, got %v", runCmdCalls[1])
}
// Case 2: flatpak not installed, choose not to install
resetMocks()
var askedPrompt string
stdin = strings.NewReader("n\n") // Abort flatpak installation
hasCmd = func(name string) bool {
return false
}
runCmdCalls = nil
installFlatpakPackages([]string{"org.gimp.GIMP"})
if len(runCmdCalls) != 0 {
t.Errorf("expected no flatpak installs if skipped, got calls: %v", runCmdCalls)
}
_ = askedPrompt
// Case 3: flatpak not installed, choose to install
resetMocks()
pkgMgr = "dnf"
stdin = strings.NewReader("y\n")
flatpakInstalled := false
hasCmd = func(name string) bool {
if name == "flatpak" {
return flatpakInstalled
}
return false
}
var runCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCalls = append(runCalls, argv)
if len(argv) >= 4 && argv[0] == "dnf" && argv[1] == "install" && argv[3] == "flatpak" {
flatpakInstalled = true
}
return CmdResult{ExitCode: 0}
}
installFlatpakPackages([]string{"org.gimp.GIMP"})
// It should call dnf install flatpak, then remote-add, then install GIMP
foundInstall := false
for _, call := range runCalls {
if len(call) >= 4 && call[0] == "dnf" && call[1] == "install" && call[3] == "flatpak" {
foundInstall = true
}
}
if !foundInstall {
t.Errorf("expected dnf install flatpak to be called, got calls: %v", runCalls)
}
}
func TestInstallFlatpakFailures(t *testing.T) {
defer resetMocks()
stdin = strings.NewReader("y\n")
hasCmd = func(name string) bool { return false }
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 1}
}
installFlatpakPackages([]string{"org.gimp.GIMP"})
resetMocks()
stdin = strings.NewReader("y\n")
hasCmd = func(name string) bool { return false }
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
installFlatpakPackages([]string{"org.gimp.GIMP"})
resetMocks()
hasCmd = func(name string) bool { return name == "flatpak" }
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[1] == "install" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
installFlatpakPackages([]string{"org.gimp.GIMP"})
}
+22 -1
View File
@@ -1,3 +1,24 @@
module github.com/JMR-dev/bootstrap_dev_env
go 1.24.7
go 1.25.0
require dagger.io/dagger v0.20.8
require (
github.com/99designs/gqlgen v0.17.89 // indirect
github.com/Khan/genqlient v0.8.1 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/sosodev/duration v1.4.0 // indirect
github.com/vektah/gqlparser/v2 v2.5.32 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
)
+51
View File
@@ -0,0 +1,51 @@
dagger.io/dagger v0.20.8 h1:n+Xtzp9ufNwCH3Ftob92Smu3smfUDoQshDYM6Ys4yf0=
dagger.io/dagger v0.20.8/go.mod h1:ZXg8+pQZaZUC8rAw4V/gPP8aKvKARIJZ+pfcV+RC1es=
github.com/99designs/gqlgen v0.17.89 h1:KzEcxPiMgQoMw3m/E85atUEHyZyt0PbAflMia5Kw8z8=
github.com/99designs/gqlgen v0.17.89/go.mod h1:GFqruTVGB7ZTdrf1uzOagpXbY7DrEt1pIxnTdhIbWvQ=
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE=
github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc=
github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+42
View File
@@ -0,0 +1,42 @@
package main
import (
"os"
)
func resetMocks() {
runCmd = runCmdReal
runShell = runShellReal
hasCmd = hasCmdReal
probe = probeReal
download = downloadReal
fetchJSON = fetchJSONReal
fetchText = fetchTextReal
osStat = os.Stat
osReadFile = os.ReadFile
osWriteFile = os.WriteFile
osMkdirAll = os.MkdirAll
osRemove = os.Remove
osRemoveAll = os.RemoveAll
osRename = os.Rename
osExit = os.Exit
stdin = os.Stdin
// Reset global state variables to safe defaults
isMacOS = false
pkgMgr = "dnf"
isRHELFamily = true
isArchFamily = false
osName = "linux"
archName = "x86_64"
osReleasePath = "/etc/os-release"
passwdPath = "/etc/passwd"
issuesMu.Lock()
issues = nil
notices = nil
issuesMu.Unlock()
}
+1 -1
View File
@@ -54,7 +54,7 @@ func writeRunLog() {
lines := []string{fmt.Sprintf("# Bootstrap run — %s", ts), ""}
lines = append(lines, issues...)
content := strings.Join(lines, "\n") + "\n"
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
if err := osWriteFile(path, []byte(content), 0o644); err != nil {
fmt.Fprintf(os.Stderr, "failed to write run log: %v\n", err)
return
}
+113
View File
@@ -0,0 +1,113 @@
package main
import (
"bytes"
"io"
"os"
"strings"
"testing"
)
func TestIssuesLogging(t *testing.T) {
defer resetMocks()
resetMocks()
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
warn("something is deprecated")
errLog("something failed")
notice("please restart shell")
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
if !strings.Contains(output, "[WARN] something is deprecated") {
t.Errorf("stdout missing warning: %q", output)
}
if !strings.Contains(output, "[ERROR] something failed") {
t.Errorf("stdout missing error: %q", output)
}
issuesMu.Lock()
issueLen := len(issues)
noticeLen := len(notices)
issuesMu.Unlock()
if issueLen != 2 {
t.Errorf("expected 2 logged issues, got %d", issueLen)
}
if noticeLen != 1 {
t.Errorf("expected 1 notice, got %d", noticeLen)
}
}
func TestWriteRunLog(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
_ = tmp
// Redirect path function or mock executable path
// In issues.go, we can define a package variable to override runLogPath if we want,
// or we can mock osWriteFile. Since we mocked osWriteFile, let's use that!
var writtenPath string
var writtenData []byte
osWriteFile = func(path string, data []byte, perm os.FileMode) error {
writtenPath = path
writtenData = data
return nil
}
// No issues case
writeRunLog()
if writtenPath != "" {
t.Error("expected run log not to be written when there are no issues")
}
// Add an issue
warn("test warning")
writeRunLog()
if writtenPath == "" {
t.Fatal("expected run log to be written")
}
if !strings.Contains(string(writtenData), "[WARN] test warning") {
t.Errorf("expected log to contain the warning, got: %s", string(writtenData))
}
}
func TestPrintNotices(t *testing.T) {
defer resetMocks()
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
printNotices() // Should be empty
notice("first notice")
notice("second notice")
printNotices()
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
if !strings.Contains(output, "Notices:") {
t.Error("stdout missing notices header")
}
if !strings.Contains(output, "first notice") || !strings.Contains(output, "second notice") {
t.Errorf("stdout missing notice contents: %q", output)
}
}
+19 -19
View File
@@ -57,14 +57,14 @@ func ensureHomebrew() {
installer := `NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
if !runShell(installer, CmdOpts{}).OK() {
fmt.Fprintln(os.Stderr, "Homebrew installation failed")
os.Exit(1)
osExit(1)
}
brewBinDir := filepath.Join(brewPrefix(), "bin")
brewPath := filepath.Join(brewBinDir, "brew")
if _, err := os.Stat(brewPath); err != nil {
if _, err := osStat(brewPath); err != nil {
fmt.Fprintf(os.Stderr, "Homebrew installed but brew not found at %s\n", brewPath)
os.Exit(1)
osExit(1)
}
os.Setenv("PATH", brewBinDir+":"+os.Getenv("PATH"))
@@ -286,19 +286,19 @@ runcmd:
`
func writeCloudInitSeed(seedDir, pubkey string) error {
if err := os.MkdirAll(seedDir, 0o755); err != nil {
if err := osMkdirAll(seedDir, 0o755); err != nil {
return err
}
userData := fmt.Sprintf(firecrackerUserdataTmpl, vmUser, strings.TrimSpace(pubkey))
if err := os.WriteFile(filepath.Join(seedDir, "user-data"), []byte(userData), 0o644); err != nil {
if err := osWriteFile(filepath.Join(seedDir, "user-data"), []byte(userData), 0o644); err != nil {
return err
}
return os.WriteFile(filepath.Join(seedDir, "meta-data"),
return osWriteFile(filepath.Join(seedDir, "meta-data"),
[]byte("instance-id: firecracker-vm\nlocal-hostname: firecracker-vm\n"), 0o644)
}
func buildSeedISO(seedDir, isoPath string) bool {
os.Remove(isoPath)
osRemove(isoPath)
return runCmd([]string{
"hdiutil", "makehybrid", "-iso", "-joliet",
"-default-volume-name", "cidata",
@@ -346,7 +346,7 @@ fi
rm -f %s
%s`, dir, vmPIDName, vmPIDName, vmPIDName, qemuBlock)
os.WriteFile(scriptPath, []byte(script), 0o755)
osWriteFile(scriptPath, []byte(script), 0o755)
return scriptPath
}
@@ -440,7 +440,7 @@ func installFirecrackerZshFunction(content string) {
home, _ := os.UserHomeDir()
zshrc := filepath.Join(home, ".zshrc")
existing := ""
if b, err := os.ReadFile(zshrc); err == nil {
if b, err := osReadFile(zshrc); err == nil {
existing = string(b)
}
pattern := regexp.MustCompile(`(?s)` + regexp.QuoteMeta(firecrackerFnBeg) + `.*?` + regexp.QuoteMeta(firecrackerFnEnd) + `\n?`)
@@ -454,7 +454,7 @@ func installFirecrackerZshFunction(content string) {
newContent = content
}
}
os.WriteFile(zshrc, []byte(newContent), 0o644)
osWriteFile(zshrc, []byte(newContent), 0o644)
fmt.Printf(" Wrote firecracker() function block to %s\n", zshrc)
}
@@ -488,7 +488,7 @@ func provisionVirtualBoxVM(qcow2, seedISO string) string {
exists := ok && r.ExitCode == 0
if !exists {
if _, err := os.Stat(vdi); os.IsNotExist(err) {
if _, err := osStat(vdi); os.IsNotExist(err) {
fmt.Printf(" Converting %s → %s (VirtualBox VDI) ...\n", filepath.Base(qcow2), filepath.Base(vdi))
if !runCmd([]string{"VBoxManage", "clonemedium", "disk", qcow2, vdi, "--format", "VDI"}, CmdOpts{}).OK() {
errLog("VBoxManage clonemedium failed")
@@ -497,7 +497,7 @@ func provisionVirtualBoxVM(qcow2, seedISO string) string {
runCmd([]string{"VBoxManage", "modifymedium", "disk", vdi, "--resize", "10240"}, CmdOpts{})
}
fmt.Printf(" Creating VirtualBox VM '%s' ...\n", vmName)
os.MkdirAll(vboxBase, 0o755)
osMkdirAll(vboxBase, 0o755)
if !runCmd([]string{
"VBoxManage", "createvm",
"--name", vmName,
@@ -543,7 +543,7 @@ if VBoxManage list runningvms | grep -q '"%s"'; then
fi
exec VBoxManage startvm %s --type headless
`, vmName, vmName)
os.WriteFile(scriptPath, []byte(script), 0o755)
osWriteFile(scriptPath, []byte(script), 0o755)
return scriptPath
}
@@ -558,11 +558,11 @@ func setupFirecrackerVM() {
fmt.Println("\n=== macOS firecracker VM (Fedora) ===")
dir := vmDir()
os.MkdirAll(dir, 0o755)
osMkdirAll(dir, 0o755)
privKey := filepath.Join(dir, vmKeyName)
pubKey := privKey + ".pub"
if _, err := os.Stat(privKey); os.IsNotExist(err) {
if _, err := osStat(privKey); os.IsNotExist(err) {
fmt.Printf(" Generating SSH keypair at %s ...\n", privKey)
if !runCmd([]string{"ssh-keygen", "-t", "ed25519", "-N", "", "-f", privKey, "-q"}, CmdOpts{}).OK() {
errLog("ssh-keygen failed — aborting VM setup")
@@ -571,7 +571,7 @@ func setupFirecrackerVM() {
}
qcow2 := filepath.Join(dir, vmQcow2Name)
if _, err := os.Stat(qcow2); err == nil {
if _, err := osStat(qcow2); err == nil {
fmt.Printf(" Reusing existing Fedora image at %s\n", qcow2)
} else {
fmt.Println(" Looking up latest Fedora cloud image ...")
@@ -587,10 +587,10 @@ func setupFirecrackerVM() {
return
}
if !verifyFedoraQcow2(downloadDest, checksumURL) {
os.Remove(downloadDest)
osRemove(downloadDest)
return
}
os.Rename(downloadDest, qcow2)
osRename(downloadDest, qcow2)
if hasCmd("qemu-img") {
fmt.Println(" Resizing image to 10G ...")
runCmd([]string{"qemu-img", "resize", qcow2, "10G"}, CmdOpts{})
@@ -599,7 +599,7 @@ func setupFirecrackerVM() {
fmt.Println(" Building cloud-init seed ISO ...")
seedDir := filepath.Join(dir, "seed")
pubKeyBytes, err := os.ReadFile(pubKey)
pubKeyBytes, err := osReadFile(pubKey)
if err != nil {
errLog(fmt.Sprintf("could not read public key: %v", err))
return
+707
View File
@@ -0,0 +1,707 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestBrewPrefix(t *testing.T) {
defer resetMocks()
archName = "aarch64"
if brewPrefix() != "/opt/homebrew" {
t.Errorf("expected /opt/homebrew on Apple Silicon, got %q", brewPrefix())
}
archName = "x86_64"
if brewPrefix() != "/usr/local" {
t.Errorf("expected /usr/local on Intel, got %q", brewPrefix())
}
}
func TestEnsureXcodeCLT(t *testing.T) {
defer resetMocks()
isMacOS = true
var probeCalls [][]string
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeCalls = append(probeCalls, argv)
// First call returns exit code 1 (not installed), then subsequent calls return 0 (installed)
if len(probeCalls) == 1 {
return CmdResult{ExitCode: 1}, true
}
return CmdResult{ExitCode: 0, Stdout: []byte("/Library/Developer/CommandLineTools")}, true
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
ensureXcodeCLT()
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "xcode-select" || runCmdCalls[0][1] != "--install" {
t.Errorf("expected xcode-select --install call, got: %v", runCmdCalls)
}
if len(probeCalls) < 2 {
t.Errorf("expected at least 2 probe checks, got %d", len(probeCalls))
}
}
func TestEnsureHomebrew(t *testing.T) {
defer resetMocks()
isMacOS = true
hasCmd = func(name string) bool {
return false // Not installed
}
var runShellCmd string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCmd = cmd
return CmdResult{ExitCode: 0}
}
osStat = func(name string) (os.FileInfo, error) {
// Mock homebrew path check returning exists
if strings.HasSuffix(name, "brew") {
return nil, nil
}
return nil, os.ErrNotExist
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
ensureHomebrew()
if !strings.Contains(runShellCmd, "Homebrew/install/HEAD/install.sh") {
t.Errorf("unexpected installer shell command: %q", runShellCmd)
}
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "bash" || !strings.Contains(runCmdCalls[0][2], "shellenv") {
t.Errorf("expected shellenv zprofile command, got: %v", runCmdCalls)
}
}
func TestMacosMajorAndSiliconGen(t *testing.T) {
defer resetMocks()
isMacOS = true
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" {
return CmdResult{ExitCode: 0, Stdout: []byte("15.0.1\n")}, true
}
if argv[0] == "sysctl" {
return CmdResult{ExitCode: 0, Stdout: []byte("Apple M3 Max\n")}, true
}
return CmdResult{ExitCode: 1}, true
}
if macosMajor() != 15 {
t.Errorf("expected macOS major version 15, got %d", macosMajor())
}
archName = "aarch64"
if appleSiliconGeneration() != 3 {
t.Errorf("expected Apple Silicon generation M3 (3), got %d", appleSiliconGeneration())
}
}
func TestSelectVMBackend(t *testing.T) {
defer resetMocks()
isMacOS = true
archName = "x86_64"
if backend := selectVMBackend(); backend != "virtualbox" {
t.Errorf("expected virtualbox on Intel Mac, got %q", backend)
}
// Apple Silicon M3 on macOS 15 Sequoia
resetMocks()
isMacOS = true
archName = "aarch64"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" {
return CmdResult{ExitCode: 0, Stdout: []byte("15.0\n")}, true
}
if argv[0] == "sysctl" {
return CmdResult{ExitCode: 0, Stdout: []byte("Apple M3\n")}, true
}
return CmdResult{ExitCode: 1}, true
}
if backend := selectVMBackend(); backend != "qemu" {
t.Errorf("expected qemu on M3 macOS 15, got %q", backend)
}
// Apple Silicon M1 on macOS 14 (no local hypervisor)
resetMocks()
isMacOS = true
archName = "aarch64"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" {
return CmdResult{ExitCode: 0, Stdout: []byte("14.5\n")}, true
}
if argv[0] == "sysctl" {
return CmdResult{ExitCode: 0, Stdout: []byte("Apple M1\n")}, true
}
return CmdResult{ExitCode: 1}, true
}
if backend := selectVMBackend(); backend != "" {
t.Errorf("expected empty backend on M1 macOS 14, got %q", backend)
}
}
func TestLatestFedoraCloudImage(t *testing.T) {
defer resetMocks()
fetchText = func(url string) string {
if url == "https://dl.fedoraproject.org/pub/fedora/linux/releases/" {
return `
<a href="38/">38/</a>
<a href="39/">39/</a>
<a href="40/">40/</a>
`
}
if strings.Contains(url, "40/Cloud/") {
return `
<a href="Fedora-Cloud-Base-40-1.10.x86_64.qcow2">Fedora-Cloud-Base-40-1.10.x86_64.qcow2</a>
<a href="Fedora-Cloud-Base-40-1.10.x86_64-CHECKSUM">Fedora-Cloud-Base-40-1.10.x86_64-CHECKSUM</a>
`
}
return ""
}
archName = "x86_64"
filename, qcowURL, _, ok := latestFedoraCloudImage()
if !ok {
t.Fatal("expected success")
}
if filename != "Fedora-Cloud-Base-40-1.10.x86_64.qcow2" {
t.Errorf("unexpected filename: %q", filename)
}
if !strings.Contains(qcowURL, "40/Cloud/x86_64/images/") {
t.Errorf("unexpected qcowURL: %q", qcowURL)
}
}
func TestInstallFirecrackerZshFunction(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
t.Setenv("HOME", tmp)
zshrc := filepath.Join(tmp, ".zshrc")
osWriteFile(zshrc, []byte("echo initial\n"), 0644)
osReadFile = func(name string) ([]byte, error) {
if name == zshrc {
return []byte("echo initial\n"), nil
}
return nil, os.ErrNotExist
}
var writtenContent string
osWriteFile = func(name string, data []byte, perm os.FileMode) error {
if name == zshrc {
writtenContent = string(data)
}
return nil
}
installFirecrackerZshFunction("firecracker() { echo wrapper; }")
if !strings.Contains(writtenContent, "firecracker() { echo wrapper; }") {
t.Errorf("expected wrapper code inside written zshrc, got %q", writtenContent)
}
}
func TestVerifyFedoraQcow2(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
qcow2 := filepath.Join(tmp, "fedora.qcow2")
os.WriteFile(qcow2, []byte("qcow2-content"), 0644)
// Hash of "qcow2-content" is fa13cb14afd725b7efaa126bd84a2a848fe9a46267251afecc769d7bdd6fcd01
fetchText = func(url string) string {
return "SHA256 (fedora.qcow2) = fa13cb14afd725b7efaa126bd84a2a848fe9a46267251afecc769d7bdd6fcd01"
}
if !verifyFedoraQcow2(qcow2, "http://checksum-url") {
t.Error("expected verification to succeed")
}
}
func TestDownloadFedoraImage(t *testing.T) {
defer resetMocks()
// Case 1: curl exists
hasCmd = func(name string) bool { return name == "curl" }
var runCmdCalled bool
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "curl" {
runCmdCalled = true
}
return CmdResult{ExitCode: 0}
}
downloadFedoraImage("http://url", "/tmp/dest")
if !runCmdCalled {
t.Error("expected curl command to be run")
}
// Case 2: curl does not exist
resetMocks()
hasCmd = func(name string) bool { return false }
var downloadCalled bool
download = func(url, dest string) bool {
downloadCalled = true
return true
}
downloadFedoraImage("http://url", "/tmp/dest")
if !downloadCalled {
t.Error("expected download function to be called")
}
}
func TestWriteCloudInitSeedAndISO(t *testing.T) {
defer resetMocks()
var mkdirCalls []string
osMkdirAll = func(path string, perm os.FileMode) error {
mkdirCalls = append(mkdirCalls, path)
return nil
}
var writtenFiles []string
osWriteFile = func(name string, data []byte, perm os.FileMode) error {
writtenFiles = append(writtenFiles, name)
return nil
}
err := writeCloudInitSeed("/tmp/seed", "ssh-pubkey")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if len(mkdirCalls) != 1 || mkdirCalls[0] != "/tmp/seed" {
t.Errorf("unexpected mkdir calls: %v", mkdirCalls)
}
if len(writtenFiles) != 2 {
t.Errorf("expected user-data and meta-data files to be written, got: %v", writtenFiles)
}
// buildSeedISO
var removed bool
osRemove = func(path string) error {
if path == "/tmp/seed.iso" {
removed = true
}
return nil
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
buildSeedISO("/tmp/seed", "/tmp/seed.iso")
if !removed {
t.Error("expected osRemove to delete old ISO first")
}
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "hdiutil" {
t.Errorf("expected hdiutil call, got %v", runCmdCalls)
}
}
func TestWriteQEMUStartScript(t *testing.T) {
defer resetMocks()
var writtenPath string
osWriteFile = func(name string, data []byte, perm os.FileMode) error {
writtenPath = name
return nil
}
t.Setenv("HOME", "/my/home")
path := writeQEMUStartScript()
if !strings.HasSuffix(path, "vm-start.sh") {
t.Errorf("unexpected script path: %q", path)
}
if !strings.HasSuffix(writtenPath, "vm-start.sh") {
t.Errorf("expected script to be written, got %q", writtenPath)
}
}
func TestSSHToVMAndHelpers(t *testing.T) {
defer resetMocks()
// sshToVM
var probeCall []string
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeCall = argv
return CmdResult{ExitCode: 0}, true
}
res := sshToVM("/path/to/key", []string{"ls"}, 0)
if !res.OK() {
t.Errorf("expected OK result, got %+v", res)
}
if probeCall[0] != "ssh" || !strings.Contains(strings.Join(probeCall, " "), "fc@127.0.0.1") {
t.Errorf("unexpected probe command: %v", probeCall)
}
// waitForVMSSH
var probeCalls int
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeCalls++
return CmdResult{ExitCode: 0}, true
}
if !waitForVMSSH("/path/to/key", time.Second) {
t.Error("expected wait to succeed")
}
if probeCalls != 1 {
t.Errorf("expected 1 probe call, got %d", probeCalls)
}
// Failure case with short timeout
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true
}
if waitForVMSSH("/path/to/key", 10*time.Millisecond) {
t.Error("expected wait to fail")
}
// waitForFirecrackerInVM
probeCalls = 0
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeCalls++
return CmdResult{ExitCode: 0}, true
}
if !waitForFirecrackerInVM("/path/to/key", time.Second) {
t.Error("expected wait to succeed")
}
// Failure case with short timeout
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true
}
if waitForFirecrackerInVM("/path/to/key", 10*time.Millisecond) {
t.Error("expected wait to fail")
}
}
func TestEnsureVirtualBoxAndProvision(t *testing.T) {
defer resetMocks()
// Case 1: VBoxManage exists
hasCmd = func(name string) bool { return name == "VBoxManage" }
if !ensureVirtualBox() {
t.Error("expected ensureVirtualBox to be true when VBoxManage exists")
}
// Case 2: VBoxManage does not exist, brew install succeeds
resetMocks()
hasCmdCalls := 0
hasCmd = func(name string) bool {
hasCmdCalls++
// First check (is VBoxManage in path) -> returns false.
// Second check (is VBoxManage in path after brew install) -> returns true.
if name == "VBoxManage" {
return hasCmdCalls > 1
}
return false
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
if !ensureVirtualBox() {
t.Error("expected ensureVirtualBox to be true after install")
}
if len(runCmdCalls) != 1 || runCmdCalls[0][3] != "virtualbox" {
t.Errorf("expected brew install virtualbox call, got %v", runCmdCalls)
}
// Test provisionVirtualBoxVM
resetMocks()
t.Setenv("HOME", "/my/home")
hasCmd = func(name string) bool { return name == "VBoxManage" }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[1] == "showvminfo" {
return CmdResult{ExitCode: 1}, true // VM does not exist yet
}
return CmdResult{ExitCode: 0}, true
}
osStat = func(name string) (os.FileInfo, error) {
return nil, nil // vdi exists
}
runCmdCalls = nil
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
osWriteFile = func(name string, data []byte, perm os.FileMode) error {
return nil
}
scriptPath := provisionVirtualBoxVM("/tmp/fedora.qcow2", "/tmp/seed.iso")
if !strings.HasSuffix(scriptPath, "vm-start.sh") {
t.Errorf("unexpected script path: %q", scriptPath)
}
if len(runCmdCalls) < 2 {
t.Errorf("expected virtualbox setup commands, got %v", runCmdCalls)
}
}
func TestSetupFirecrackerVM(t *testing.T) {
defer resetMocks()
isMacOS = true
archName = "aarch64"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" {
return CmdResult{ExitCode: 0, Stdout: []byte("15.0\n")}, true
}
if argv[0] == "sysctl" {
return CmdResult{ExitCode: 0, Stdout: []byte("Apple M3\n")}, true
}
if argv[0] == "ssh" {
return CmdResult{ExitCode: 0}, true // SSH succeeds
}
return CmdResult{ExitCode: 0}, true
}
osStat = func(name string) (os.FileInfo, error) {
// Mock files exist
return nil, nil
}
osReadFile = func(name string) ([]byte, error) {
return []byte("ssh-key"), nil
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
osWriteFile = func(name string, data []byte, perm os.FileMode) error {
return nil
}
setupFirecrackerVM()
if len(runCmdCalls) < 1 {
t.Errorf("expected VM setup start script execution, got: %v", runCmdCalls)
}
}
func TestMacosGoEdgeCases(t *testing.T) {
defer resetMocks()
isMacOS = true
hasCmd = func(name string) bool { return false }
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 1}
}
if ensureVirtualBox() {
t.Error("expected ensureVirtualBox to fail when brew install fails")
}
resetMocks()
isMacOS = true
hasCmd = func(name string) bool {
return false
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
if ensureVirtualBox() {
t.Error("expected ensureVirtualBox to fail when VBoxManage still not in PATH")
}
resetMocks()
isMacOS = true
hasCmd = func(name string) bool { return false }
runCmd = func(argv []string, opts CmdOpts) CmdResult { return CmdResult{ExitCode: 1} }
if path := provisionVirtualBoxVM("qcow", "iso"); path != "" {
t.Errorf("expected empty path when VirtualBox setup fails, got %q", path)
}
resetMocks()
isMacOS = true
t.Setenv("HOME", "/my/home")
hasCmd = func(name string) bool { return name == "VBoxManage" }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true
}
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[1] == "clonemedium" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
if path := provisionVirtualBoxVM("qcow", "iso"); path != "" {
t.Errorf("expected empty path when clonemedium fails, got %q", path)
}
resetMocks()
isMacOS = true
t.Setenv("HOME", "/my/home")
hasCmd = func(name string) bool { return name == "VBoxManage" }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true
}
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[1] == "createvm" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
if path := provisionVirtualBoxVM("qcow", "iso"); path != "" {
t.Errorf("expected empty path when createvm fails, got %q", path)
}
resetMocks()
isMacOS = false
setupFirecrackerVM()
resetMocks()
isMacOS = true
archName = "aarch64"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" {
return CmdResult{ExitCode: 0, Stdout: []byte("14.0\n")}, true
}
return CmdResult{ExitCode: 0}, true
}
setupFirecrackerVM()
resetMocks()
isMacOS = true
archName = "x86_64"
hasCmd = func(name string) bool { return name == "VBoxManage" }
osStat = func(name string) (os.FileInfo, error) {
if strings.HasSuffix(name, "id_ed25519") {
return nil, os.ErrNotExist
}
return nil, nil
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "ssh-keygen" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
setupFirecrackerVM()
}
func TestSetupFirecrackerVMEdgeCases(t *testing.T) {
defer resetMocks()
isMacOS = true
archName = "aarch64"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" {
return CmdResult{ExitCode: 0, Stdout: []byte("15.0\n")}, true
}
if argv[0] == "sysctl" {
return CmdResult{ExitCode: 0, Stdout: []byte("Apple M3\n")}, true
}
return CmdResult{ExitCode: 0}, true
}
osStat = func(name string) (os.FileInfo, error) {
if strings.HasSuffix(name, "fedora.qcow2") || strings.HasSuffix(name, "id_ed25519") {
return nil, os.ErrNotExist
}
return nil, nil
}
fetchText = func(url string) string { return "" }
setupFirecrackerVM()
resetMocks()
isMacOS = true
archName = "aarch64"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" { return CmdResult{ExitCode: 0, Stdout: []byte("15.0\n")}, true }
if argv[0] == "sysctl" { return CmdResult{ExitCode: 0, Stdout: []byte("Apple M3\n")}, true }
return CmdResult{ExitCode: 0}, true
}
osStat = func(name string) (os.FileInfo, error) {
if strings.HasSuffix(name, "fedora.qcow2") { return nil, os.ErrNotExist }
return nil, nil
}
fetchText = func(url string) string {
if strings.Contains(url, "releases") { return "40/" }
return "<a href=\"Fedora-Cloud-Base-40.qcow2\">Fedora-Cloud-Base-40.qcow2</a>"
}
hasCmd = func(name string) bool { return false }
download = func(url, dest string) bool { return false }
setupFirecrackerVM()
resetMocks()
isMacOS = true
archName = "aarch64"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" { return CmdResult{ExitCode: 0, Stdout: []byte("15.0\n")}, true }
if argv[0] == "sysctl" { return CmdResult{ExitCode: 0, Stdout: []byte("Apple M3\n")}, true }
return CmdResult{ExitCode: 0}, true
}
osStat = func(name string) (os.FileInfo, error) {
if strings.HasSuffix(name, "fedora.qcow2") { return nil, os.ErrNotExist }
return nil, nil
}
fetchText = func(url string) string {
if strings.Contains(url, "releases") { return "40/" }
if strings.Contains(url, "CHECKSUM") { return "mismatch-sha Fedora-Cloud-Base-40.qcow2" }
return "<a href=\"Fedora-Cloud-Base-40.qcow2\">Fedora-Cloud-Base-40.qcow2</a>"
}
download = func(url, dest string) bool { return true }
setupFirecrackerVM()
resetMocks()
isMacOS = true
archName = "aarch64"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" { return CmdResult{ExitCode: 0, Stdout: []byte("15.0\n")}, true }
if argv[0] == "sysctl" { return CmdResult{ExitCode: 0, Stdout: []byte("Apple M3\n")}, true }
return CmdResult{ExitCode: 0}, true
}
osStat = func(name string) (os.FileInfo, error) { return nil, nil }
osReadFile = func(name string) ([]byte, error) { return []byte("ssh-pubkey"), nil }
hasCmd = func(name string) bool {
if name == "qemu-system-aarch64" { return false }
return true
}
setupFirecrackerVM()
resetMocks()
isMacOS = true
archName = "aarch64"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "sw_vers" { return CmdResult{ExitCode: 0, Stdout: []byte("15.0\n")}, true }
if argv[0] == "sysctl" { return CmdResult{ExitCode: 0, Stdout: []byte("Apple M3\n")}, true }
return CmdResult{ExitCode: 0}, true
}
osStat = func(name string) (os.FileInfo, error) { return nil, nil }
osReadFile = func(name string) ([]byte, error) { return []byte("ssh-pubkey"), nil }
hasCmd = func(name string) bool { return true }
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if strings.HasSuffix(argv[0], "vm-start.sh") {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
setupFirecrackerVM()
}
+21 -11
View File
@@ -30,17 +30,23 @@ import (
)
func main() {
only := flag.String("only", "", "Install only the named section (system|flatpak|custom)")
gui := flag.Bool("gui", false, "Include GUI applications (headed environments).")
noVM := flag.Bool("no-vm", false, "macOS only: skip provisioning the Fedora-on-QEMU VM that backs the firecracker() zsh wrapper.")
noAI := flag.Bool("no-ai", false, "Skip installation of LLM/AI CLI tools (agy, claude, codex, copilot).")
flag.Parse()
runMain(os.Args)
}
func runMain(args []string) {
fs := flag.NewFlagSet(args[0], flag.ExitOnError)
only := fs.String("only", "", "Install only the named section (system|flatpak|custom)")
gui := fs.Bool("gui", false, "Include GUI applications (headed environments).")
noVM := fs.Bool("no-vm", false, "macOS only: skip provisioning the Fedora-on-QEMU VM that backs the firecracker() zsh wrapper.")
noAI := fs.Bool("no-ai", false, "Skip installation of LLM/AI CLI tools (agy, claude, codex, copilot).")
_ = fs.Parse(args[1:])
switch *only {
case "", "system", "flatpak", "custom":
default:
fmt.Fprintf(os.Stderr, "invalid --only value %q (use system|flatpak|custom)\n", *only)
os.Exit(2)
osExit(2)
return
}
initPkgMgr()
@@ -122,7 +128,8 @@ func main() {
if !askYN(fmt.Sprintf("\n%d item(s) to install. Proceed? [y/N] ", total)) {
fmt.Fprintln(os.Stderr, "Aborted.")
os.Exit(1)
osExit(1)
return
}
checkSudo()
@@ -165,7 +172,7 @@ func main() {
home, _ := os.UserHomeDir()
zshrc := filepath.Join(home, ".zshrc")
if hasCmd("zsh") {
if _, err := os.Stat(zshrc); err == nil {
if _, err := osStat(zshrc); err == nil {
fmt.Println("\nSourcing ~/.zshrc ...")
runShell(fmt.Sprintf("zsh -c 'source %s'", zshrc), CmdOpts{})
}
@@ -177,18 +184,21 @@ func checkSudo() {
if isMacOS {
fmt.Fprintln(os.Stderr, "Do not run this with sudo on macOS — Homebrew refuses to run as root. "+
"Re-run as your regular user; the tool will request sudo for the operations that need it.")
os.Exit(1)
osExit(1)
return
}
return
}
if !hasCmd("sudo") {
fmt.Fprintln(os.Stderr, "sudo is required but not installed.")
os.Exit(1)
osExit(1)
return
}
fmt.Println("Validating sudo access ...")
r := runCmd([]string{"sudo", "-v"}, CmdOpts{Timeout: 2 * time.Minute})
if r.ExitCode != 0 {
fmt.Fprintln(os.Stderr, "sudo authentication failed.")
os.Exit(1)
osExit(1)
return
}
}
+129
View File
@@ -0,0 +1,129 @@
package main
import (
"os"
"strings"
"testing"
"time"
)
func TestRunMainInvalidOnly(t *testing.T) {
defer resetMocks()
var exited bool
var exitCode int
osExit = func(code int) {
exited = true
exitCode = code
}
runMain([]string{"bootstrap_environment", "--only", "invalid"})
if !exited {
t.Error("expected runMain with invalid --only value to exit")
}
if exitCode != 2 {
t.Errorf("expected exit code 2, got %d", exitCode)
}
}
func TestRunMainAllInstalled(t *testing.T) {
defer resetMocks()
// All packages are already installed (total packages to install = 0)
osStat = func(name string) (os.FileInfo, error) {
return nil, nil // all custom paths exist
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "rpm" || argv[0] == "dpkg-query" || argv[0] == "pacman" {
return CmdResult{ExitCode: 0, Stdout: []byte("install ok installed")}, true
}
if argv[0] == "flatpak" {
return CmdResult{ExitCode: 0}, true
}
return CmdResult{ExitCode: 0}, true
}
hasCmd = func(name string) bool {
return true
}
var exited bool
osExit = func(code int) {
exited = true
}
// We only run custom to keep it short & avoid other logic dependencies
runMain([]string{"bootstrap_environment", "--only", "custom"})
if exited {
t.Error("expected program to complete successfully without exiting")
}
}
func TestRunMainInstallAbort(t *testing.T) {
defer resetMocks()
// Simulating some packages to install, but user selects N
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist // custom paths missing -> need install
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true // packages not installed
}
hasCmd = func(name string) bool {
return true
}
// Mock stdin to say "n" to abort the prompt "Proceed? [y/N]"
stdin = strings.NewReader("n\n")
var exited bool
var exitCode int
osExit = func(code int) {
exited = true
exitCode = code
}
runMain([]string{"bootstrap_environment", "--only", "custom"})
if !exited {
t.Error("expected program to exit on user abort")
}
if exitCode != 1 {
t.Errorf("expected exit code 1, got %d", exitCode)
}
}
func TestRunMainMacos(t *testing.T) {
defer resetMocks()
isMacOS = true
pkgMgr = "brew"
osStat = func(name string) (os.FileInfo, error) {
return nil, nil // brew etc exist
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0}, true
}
hasCmd = func(name string) bool {
return true
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
runShell = func(cmd string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
var exited bool
osExit = func(code int) {
exited = true
}
// Run with --only custom, --no-vm, --no-ai
runMain([]string{"bootstrap_environment", "--only", "custom", "--no-vm", "--no-ai"})
if exited {
t.Error("expected program to complete successfully on macOS mock run")
}
}
+6 -6
View File
@@ -17,8 +17,8 @@ const httpClientTimeout = 30 * time.Minute
var httpClient = &http.Client{Timeout: httpClientTimeout}
// download streams url -> dest. Returns true on success.
func download(url, dest string) bool {
// downloadReal streams url -> dest. Returns true on success.
func downloadReal(url, dest string) bool {
fmt.Printf(" Downloading %s ...\n", filepath.Base(url))
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
@@ -48,9 +48,9 @@ func download(url, dest string) bool {
return true
}
// fetchJSON GETs url with the GitHub API Accept header and decodes the body
// fetchJSONReal GETs url with the GitHub API Accept header and decodes the body
// into v. Returns true on success.
func fetchJSON(url string, v any) bool {
func fetchJSONReal(url string, v any) bool {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
errLog(fmt.Sprintf("API request failed for %s: %v", url, err))
@@ -77,8 +77,8 @@ func fetchJSON(url string, v any) bool {
return true
}
// fetchText returns the trimmed body of url. Returns empty string on failure.
func fetchText(url string) string {
// fetchTextReal returns the trimmed body of url. Returns empty string on failure.
func fetchTextReal(url string) string {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
errLog(fmt.Sprintf("Fetch failed for %s: %v", url, err))
+238
View File
@@ -0,0 +1,238 @@
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"testing"
)
type mockTripper struct {
roundTripFunc func(req *http.Request) (*http.Response, error)
}
func (m *mockTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return m.roundTripFunc(req)
}
func TestDownloadReal(t *testing.T) {
defer resetMocks()
// Mock HTTP client
oldTransport := httpClient.Transport
defer func() { httpClient.Transport = oldTransport }()
httpClient.Transport = &mockTripper{
roundTripFunc: func(req *http.Request) (*http.Response, error) {
if req.URL.String() == "https://example.com/file" {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString("hello download")),
}, nil
}
return &http.Response{
StatusCode: 404,
Body: io.NopCloser(bytes.NewBufferString("not found")),
}, nil
},
}
tmpDir := t.TempDir()
destFile := filepath.Join(tmpDir, "out.txt")
// Successful download
success := downloadReal("https://example.com/file", destFile)
if !success {
t.Fatal("expected download to succeed")
}
data, err := os.ReadFile(destFile)
if err != nil {
t.Fatalf("failed to read downloaded file: %v", err)
}
if string(data) != "hello download" {
t.Errorf("expected 'hello download', got %q", string(data))
}
// Failed download (404)
failDest := filepath.Join(tmpDir, "out_fail.txt")
successFail := downloadReal("https://example.com/nonexistent", failDest)
if successFail {
t.Error("expected download to fail with 404")
}
}
func TestFetchJSONReal(t *testing.T) {
defer resetMocks()
oldTransport := httpClient.Transport
defer func() { httpClient.Transport = oldTransport }()
httpClient.Transport = &mockTripper{
roundTripFunc: func(req *http.Request) (*http.Response, error) {
if req.URL.String() == "https://example.com/api" {
// Verify auth header if token is set
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(`{"key": "value"}`)),
}, nil
}
return &http.Response{
StatusCode: 500,
Body: io.NopCloser(bytes.NewBufferString("internal error")),
}, nil
},
}
type MockResponse struct {
Key string `json:"key"`
}
var res MockResponse
success := fetchJSONReal("https://example.com/api", &res)
if !success {
t.Fatal("expected fetchJSON to succeed")
}
if res.Key != "value" {
t.Errorf("expected Key to be 'value', got %q", res.Key)
}
successFail := fetchJSONReal("https://example.com/bad", &res)
if successFail {
t.Error("expected fetchJSON to fail with 500")
}
}
func TestFetchTextReal(t *testing.T) {
defer resetMocks()
oldTransport := httpClient.Transport
defer func() { httpClient.Transport = oldTransport }()
httpClient.Transport = &mockTripper{
roundTripFunc: func(req *http.Request) (*http.Response, error) {
if req.URL.String() == "https://example.com/text" {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(" Adoptium Latest \n")),
}, nil
}
return &http.Response{
StatusCode: 403,
Body: io.NopCloser(bytes.NewBufferString("forbidden")),
}, nil
},
}
text := fetchTextReal("https://example.com/text")
if text != "Adoptium Latest" {
t.Errorf("expected trimmed text 'Adoptium Latest', got %q", text)
}
textFail := fetchTextReal("https://example.com/forbidden")
if textFail != "" {
t.Errorf("expected empty string for failed request, got %q", textFail)
}
}
func TestSha256Of(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "hash.txt")
if err := os.WriteFile(path, []byte("hello sha256"), 0644); err != nil {
t.Fatal(err)
}
// Hex of sha256("hello sha256") is 433855b7d2b96c23a6f60e70c655eb4305e8806b682a9596a200642f947259b1
expected := "433855b7d2b96c23a6f60e70c655eb4305e8806b682a9596a200642f947259b1"
actual, err := sha256Of(path)
if err != nil {
t.Fatalf("failed to calculate hash: %v", err)
}
if actual != expected {
t.Errorf("expected %s, got %s", expected, actual)
}
// Nonexistent file
_, errNonexistent := sha256Of(filepath.Join(tmpDir, "nonexistent"))
if errNonexistent == nil {
t.Error("expected error for nonexistent file")
}
}
func TestNetRealErrors(t *testing.T) {
defer resetMocks()
oldTransport := httpClient.Transport
defer func() { httpClient.Transport = oldTransport }()
// 1. NewRequest error
if downloadReal("%%%", "dest") {
t.Error("expected downloadReal to fail for invalid URL")
}
if fetchJSONReal("%%%", nil) {
t.Error("expected fetchJSONReal to fail for invalid URL")
}
if fetchTextReal("%%%") != "" {
t.Error("expected fetchTextReal to fail for invalid URL")
}
// 2. Transport Do error
httpClient.Transport = &mockTripper{
roundTripFunc: func(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("connection refused")
},
}
if downloadReal("https://example.com/file", "dest") {
t.Error("expected downloadReal to fail on connection error")
}
if fetchJSONReal("https://example.com/api", nil) {
t.Error("expected fetchJSONReal to fail on connection error")
}
if fetchTextReal("https://example.com/text") != "" {
t.Error("expected fetchTextReal to fail on connection error")
}
// 3. os.Create error
httpClient.Transport = &mockTripper{
roundTripFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString("ok")),
}, nil
},
}
if downloadReal("https://example.com/file", "/nonexistent-dir/dest") {
t.Error("expected downloadReal to fail when creating destination file fails")
}
// 4. json Decode error
httpClient.Transport = &mockTripper{
roundTripFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString("invalid json")),
}, nil
},
}
var v any
if fetchJSONReal("https://example.com/api", &v) {
t.Error("expected fetchJSONReal to fail on invalid JSON")
}
// 5. io.ReadAll error
httpClient.Transport = &mockTripper{
roundTripFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(&errReader{}),
}, nil
},
}
if fetchTextReal("https://example.com/text") != "" {
t.Error("expected fetchTextReal to fail on read error")
}
}
+24 -1
View File
@@ -13,8 +13,31 @@ func initPkgMgr() {
pkgMgr = detectPkgMgr()
isRHELFamily = detectRHELFamily()
isArchFamily = detectArchFamily()
ensureWhichInstalled()
}
func ensureWhichInstalled() {
if hasCmd("which") {
return
}
fmt.Println("[which] 'which' is not installed. Installing it as a prerequisite...")
var res CmdResult
switch pkgMgr {
case "pacman":
res = runCmd([]string{"pacman", "-Sy", "--noconfirm", "--needed", "which"}, CmdOpts{AsSudo: true})
case "brew":
res = runCmd([]string{"brew", "install", "which"}, CmdOpts{})
default:
res = runCmd([]string{pkgMgr, "install", "-y", "which"}, CmdOpts{AsSudo: true})
}
if !res.OK() {
fmt.Fprintf(os.Stderr, "Warning: failed to install 'which' prerequisite: %v\n", res.Err)
} else {
fmt.Println("[which] 'which' successfully installed.")
}
}
func detectPkgMgr() string {
if isMacOS {
// brew may not be installed yet — ensureHomebrew runs before any
@@ -27,7 +50,7 @@ func detectPkgMgr() string {
}
}
fmt.Fprintln(os.Stderr, "No supported package manager found (expected dnf, apt-get, pacman, or brew on macOS).")
os.Exit(1)
osExit(1)
return ""
}
+201
View File
@@ -0,0 +1,201 @@
package main
import (
"testing"
"time"
)
func TestDetectPkgMgr(t *testing.T) {
defer resetMocks()
// Case 1: macOS should return brew
isMacOS = true
if mgr := detectPkgMgr(); mgr != "brew" {
t.Errorf("expected brew on macOS, got %q", mgr)
}
// Case 2: Linux with dnf
isMacOS = false
hasCmd = func(name string) bool {
return name == "dnf"
}
if mgr := detectPkgMgr(); mgr != "dnf" {
t.Errorf("expected dnf, got %q", mgr)
}
// Case 3: Linux with apt-get
hasCmd = func(name string) bool {
return name == "apt-get"
}
if mgr := detectPkgMgr(); mgr != "apt-get" {
t.Errorf("expected apt-get, got %q", mgr)
}
// Case 4: Linux with pacman
hasCmd = func(name string) bool {
return name == "pacman"
}
if mgr := detectPkgMgr(); mgr != "pacman" {
t.Errorf("expected pacman, got %q", mgr)
}
// Case 5: No package manager found (exits)
hasCmd = func(name string) bool {
return false
}
var exited bool
var exitCode int
osExit = func(code int) {
exited = true
exitCode = code
}
detectPkgMgr()
if !exited || exitCode != 1 {
t.Errorf("expected exit with code 1, exited=%v code=%d", exited, exitCode)
}
}
func TestResolveSystemPkgs(t *testing.T) {
defer resetMocks()
pkgMgr = "apt-get"
resolved, skipped := resolveSystemPkgs([]string{"ffmpeg-free", "lua", "podman", "docker-compose"})
// ffmpeg-free -> ffmpeg, lua -> lua5.4, podman -> podman, docker-compose is not overridden for apt-get
expectedResolved := []string{"ffmpeg", "lua5.4", "podman", "docker-compose"}
if len(resolved) != len(expectedResolved) {
t.Fatalf("expected resolved length %d, got %d", len(expectedResolved), len(resolved))
}
for i, r := range resolved {
if r != expectedResolved[i] {
t.Errorf("at index %d: expected %q, got %q", i, expectedResolved[i], r)
}
}
if len(skipped) != 0 {
t.Errorf("expected no skipped packages, got %v", skipped)
}
// Test skip override
pkgMgr = "dnf"
resolved, skipped = resolveSystemPkgs([]string{"docker-compose", "rg"})
// docker-compose -> skipped, rg -> ripgrep
if len(resolved) != 1 || resolved[0] != "ripgrep" {
t.Errorf("expected resolution to [ripgrep], got %v", resolved)
}
if len(skipped) != 1 || skipped[0] != "docker-compose" {
t.Errorf("expected skipped to be [docker-compose], got %v", skipped)
}
}
func TestIsSystemPkgInstalled(t *testing.T) {
defer resetMocks()
// Case dnf: rpm -q
pkgMgr = "dnf"
var probeArgv []string
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeArgv = argv
return CmdResult{ExitCode: 0}, true
}
if !isSystemPkgInstalled("git") {
t.Error("expected true when rpm returns 0")
}
if len(probeArgv) < 3 || probeArgv[0] != "rpm" || probeArgv[2] != "git" {
t.Errorf("unexpected probe argv: %v", probeArgv)
}
// Case apt-get: dpkg-query
pkgMgr = "apt-get"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeArgv = argv
return CmdResult{ExitCode: 0, Stdout: []byte("install ok installed")}, true
}
if !isSystemPkgInstalled("git") {
t.Error("expected true when dpkg-query output contains install ok installed")
}
if len(probeArgv) < 4 || probeArgv[0] != "dpkg-query" {
t.Errorf("unexpected probe argv: %v", probeArgv)
}
// Case pacman: pacman -Qi
pkgMgr = "pacman"
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeArgv = argv
return CmdResult{ExitCode: 0}, true
}
if !isSystemPkgInstalled("git") {
t.Error("expected true when pacman returns 0")
}
// Case brew: brew list --formula / --cask
pkgMgr = "brew"
hasCmd = func(name string) bool {
return name == "brew"
}
var probeCalls [][]string
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeCalls = append(probeCalls, argv)
if argv[2] == "--formula" {
return CmdResult{ExitCode: 1}, true
}
return CmdResult{ExitCode: 0}, true // succeeds on second call
}
if !isSystemPkgInstalled("git") {
t.Error("expected true when brew list succeeds")
}
if len(probeCalls) != 2 {
t.Errorf("expected 2 brew calls, got %d", len(probeCalls))
}
}
func TestIsFlatpakInstalled(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool {
return name == "flatpak"
}
var probeArgv []string
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeArgv = argv
return CmdResult{ExitCode: 0}, true
}
if !isFlatpakInstalled("org.gimp.GIMP") {
t.Error("expected true")
}
if len(probeArgv) < 3 || probeArgv[0] != "flatpak" || probeArgv[2] != "org.gimp.GIMP" {
t.Errorf("unexpected probe argv: %v", probeArgv)
}
}
func TestPkgMgrEdgeCases(t *testing.T) {
defer resetMocks()
pkgMgr = "brew"
hasCmd = func(name string) bool { return false }
if isSystemPkgInstalled("git") {
t.Error("expected false when brew is not installed")
}
resetMocks()
pkgMgr = "brew"
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true
}
if isSystemPkgInstalled("git") {
t.Error("expected false when brew list fails")
}
resetMocks()
pkgMgr = "unsupported"
if isSystemPkgInstalled("git") {
t.Error("expected false for unsupported package manager")
}
resetMocks()
hasCmd = func(name string) bool { return false }
if isFlatpakInstalled("org.gimp.GIMP") {
t.Error("expected false when flatpak command not found")
}
}
+15 -15
View File
@@ -138,11 +138,11 @@ func latestStablePython(pyenvBin string) string {
func ensurePythonLatest() *sync.WaitGroup {
home, _ := os.UserHomeDir()
pyenvDir := filepath.Join(home, ".pyenv")
if _, err := os.Stat(pyenvDir); err != nil {
if _, err := osStat(pyenvDir); err != nil {
return nil
}
pyenvBin := filepath.Join(pyenvDir, "bin", "pyenv")
if _, err := os.Stat(pyenvBin); err != nil {
if _, err := osStat(pyenvBin); err != nil {
warn(fmt.Sprintf("pyenv binary not found at %s", pyenvBin))
return nil
}
@@ -222,7 +222,7 @@ func installNVM() {
func ensureNodeLTS() {
home, _ := os.UserHomeDir()
if _, err := os.Stat(filepath.Join(home, ".nvm")); err != nil {
if _, err := osStat(filepath.Join(home, ".nvm")); err != nil {
return
}
check := runShell(`bash -c "source ~/.nvm/nvm.sh 2>/dev/null && nvm version lts/* 2>/dev/null"`,
@@ -263,7 +263,7 @@ func installOhMyZsh() {
}
home, _ := os.UserHomeDir()
target := filepath.Join(home, ".oh-my-zsh")
if _, err := os.Stat(target); err == nil {
if _, err := osStat(target); err == nil {
fmt.Printf(" oh-my-zsh already present at %s; updating theme only\n", target)
} else {
fmt.Println(" Installing oh-my-zsh via the official installer ...")
@@ -275,7 +275,7 @@ func installOhMyZsh() {
}
zshrc := filepath.Join(home, ".zshrc")
data, err := os.ReadFile(zshrc)
data, err := osReadFile(zshrc)
if err != nil {
warn("~/.zshrc not present after oh-my-zsh install; cannot set theme")
return
@@ -289,7 +289,7 @@ func installOhMyZsh() {
newText = strings.TrimRight(text, "\n") + "\nZSH_THEME=\"gnzh\"\n"
}
if newText != text {
if err := os.WriteFile(zshrc, []byte(newText), 0o644); err != nil {
if err := osWriteFile(zshrc, []byte(newText), 0o644); err != nil {
errLog(fmt.Sprintf("could not write ~/.zshrc: %v", err))
return
}
@@ -365,7 +365,7 @@ func ensureZshDefault() {
// macOS the shell may be set by dscl; getent isn't available either, so we
// just read passwd directly which works on every supported platform.
func userLoginShell(uid string) string {
data, err := os.ReadFile("/etc/passwd")
data, err := osReadFile(passwdPath)
if err != nil {
return ""
}
@@ -388,29 +388,29 @@ func cloneNvimConfig() {
fmt.Printf("\n[Neovim] Setting up configuration from %s ...\n", repoURL)
if _, err := os.Stat(configDir); err == nil {
if _, err := osStat(configDir); err == nil {
n := 1
var backup string
for {
backup = filepath.Join(filepath.Dir(configDir), fmt.Sprintf("nvim-%d", n))
if _, err := os.Stat(backup); os.IsNotExist(err) {
if _, err := osStat(backup); os.IsNotExist(err) {
break
}
n++
}
fmt.Printf(" Renaming existing %s → %s ...\n", configDir, backup)
if err := os.Rename(configDir, backup); err != nil {
if err := osRename(configDir, backup); err != nil {
errLog(fmt.Sprintf("could not back up existing nvim config: %v", err))
return
}
notice(fmt.Sprintf("Previous Neovim config preserved at %s", backup))
}
os.MkdirAll(filepath.Dir(configDir), 0o755)
osMkdirAll(filepath.Dir(configDir), 0o755)
repoName := strings.TrimSuffix(filepath.Base(repoURL), ".git")
tempClone := filepath.Join(filepath.Dir(configDir), repoName)
os.RemoveAll(tempClone)
osRemoveAll(tempClone)
fmt.Printf(" Cloning to %s ...\n", configDir)
if !runCmd([]string{"git", "clone", repoURL, tempClone}, CmdOpts{}).OK() {
@@ -419,7 +419,7 @@ func cloneNvimConfig() {
}
if tempClone != configDir {
fmt.Printf(" Renaming %s to %s ...\n", filepath.Base(tempClone), filepath.Base(configDir))
os.Rename(tempClone, configDir)
osRename(tempClone, configDir)
}
fmt.Printf(" Neovim configuration ready at %s\n", configDir)
}
@@ -452,7 +452,7 @@ func checkAndSetupSSH() {
func askYN(prompt string) bool {
fmt.Print(prompt)
var buf [256]byte
n, err := os.Stdin.Read(buf[:])
n, err := stdin.Read(buf[:])
if err != nil && err != io.EOF {
fmt.Println()
return false
@@ -470,7 +470,7 @@ func installAgy() {
func installNpmPackage(pkgName string) {
home, _ := os.UserHomeDir()
if _, err := os.Stat(filepath.Join(home, ".nvm")); err != nil {
if _, err := osStat(filepath.Join(home, ".nvm")); err != nil {
errLog("NVM is not installed — cannot install " + pkgName)
return
}
+705
View File
@@ -0,0 +1,705 @@
package main
import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"testing"
"time"
)
func TestInstallPyenv(t *testing.T) {
defer resetMocks()
var runShellCmd string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCmd = cmd
return CmdResult{ExitCode: 0}
}
installPyenv()
if !strings.Contains(runShellCmd, "pyenv.run | bash") {
t.Errorf("unexpected shell command: %q", runShellCmd)
}
}
func TestPython3DecimalOK(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0}, true
}
if !python3DecimalOK() {
t.Error("expected decimal OK")
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true
}
if python3DecimalOK() {
t.Error("expected decimal NOT OK")
}
}
func TestFixPython3Decimal(t *testing.T) {
defer resetMocks()
pkgMgr = "apt-get"
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
// Mock python3DecimalOK to return true after fix is called
return CmdResult{ExitCode: 0}, true
}
ok := fixPython3Decimal()
if !ok {
t.Error("expected fix to succeed")
}
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "apt-get" || runCmdCalls[0][3] != "python3-full" {
t.Errorf("expected apt-get install python3-full, got: %v", runCmdCalls)
}
}
func TestLatestStablePython(t *testing.T) {
defer resetMocks()
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
output := `
3.10.1
3.11.2
3.12.0
3.12.1
3.12.2
2.7.18
system
`
return CmdResult{ExitCode: 0, Stdout: []byte(output)}, true
}
latest := latestStablePython("/path/to/pyenv")
if latest != "3.12.2" {
t.Errorf("expected 3.12.2, got %q", latest)
}
}
func TestInstallNVM(t *testing.T) {
defer resetMocks()
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.TagName = "v0.39.5"
return true
}
var runShellCmd string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCmd = cmd
return CmdResult{ExitCode: 0}
}
installNVM()
if !strings.Contains(runShellCmd, "nvm/v0.39.5/install.sh") {
t.Errorf("unexpected NVM install script: %q", runShellCmd)
}
}
func TestInstallOhMyZsh(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool { return true }
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist // Not present, trigger install
}
var runShellCmd string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCmd = cmd
return CmdResult{ExitCode: 0}
}
tmp := t.TempDir()
t.Setenv("HOME", tmp)
zshrc := filepath.Join(tmp, ".zshrc")
osWriteFile(zshrc, []byte("ZSH_THEME=\"robbyrussell\""), 0644)
osReadFile = func(name string) ([]byte, error) {
if name == zshrc {
return []byte("ZSH_THEME=\"robbyrussell\""), nil
}
return nil, os.ErrNotExist
}
var writtenContent string
osWriteFile = func(name string, data []byte, perm os.FileMode) error {
if name == zshrc {
writtenContent = string(data)
}
return nil
}
installOhMyZsh()
if !strings.Contains(runShellCmd, "ohmyzsh/master/tools/install.sh") {
t.Errorf("unexpected installer: %q", runShellCmd)
}
if !strings.Contains(writtenContent, "ZSH_THEME=\"gnzh\"") {
t.Errorf("expected theme replaced to gnzh, got: %q", writtenContent)
}
}
func TestEnsureZshDefault(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0, Stdout: []byte("/bin/zsh")}, true
}
u, err := user.Current()
if err != nil {
t.Fatal(err)
}
t.Setenv("SUDO_USER", u.Username)
passwdPath = filepath.Join(t.TempDir(), "passwd")
passwdContent := fmt.Sprintf("%s:x:%s:%s::/home/%s:/bin/bash\n", u.Username, u.Uid, u.Gid, u.Username)
osWriteFile(passwdPath, []byte(passwdContent), 0644)
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
isRHELFamily = false
ensureZshDefault()
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "chsh" || runCmdCalls[0][2] != "/bin/zsh" {
t.Errorf("expected chsh call to zsh, got: %v", runCmdCalls)
}
}
func TestCloneNvimConfig(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
t.Setenv("HOME", tmp)
var statCalls []string
osStat = func(name string) (os.FileInfo, error) {
statCalls = append(statCalls, name)
if strings.HasSuffix(name, "nvim") {
return nil, nil // config directory exists
}
return nil, os.ErrNotExist // backup folder doesn't exist
}
var renameCalls [][]string
osRename = func(oldpath, newpath string) error {
renameCalls = append(renameCalls, []string{oldpath, newpath})
return nil
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
cloneNvimConfig()
// Should have at least one rename call (for backup or tempClone rename)
if len(renameCalls) < 2 {
t.Fatalf("expected at least 2 rename calls, got: %v", renameCalls)
}
if !strings.HasSuffix(renameCalls[0][0], "nvim") || !strings.Contains(renameCalls[0][1], "nvim-1") {
t.Errorf("expected backup rename first, got: %v", renameCalls[0])
}
if !strings.HasSuffix(renameCalls[1][0], "nvim-config") || !strings.HasSuffix(renameCalls[1][1], "nvim") {
t.Errorf("expected temp clone rename second, got: %v", renameCalls[1])
}
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "git" || runCmdCalls[0][1] != "clone" {
t.Errorf("expected git clone call, got: %v", runCmdCalls)
}
}
func TestCheckAndSetupSSH(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
// Mock ghLoggedIn to return false (requires auth)
return CmdResult{ExitCode: 1}, true
}
stdin = strings.NewReader("y\n") // agree to login
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
checkAndSetupSSH()
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "gh" || runCmdCalls[0][2] != "login" {
t.Errorf("expected gh auth login command, got: %v", runCmdCalls)
}
}
func TestInstallPipAgyNpm(t *testing.T) {
defer resetMocks()
// Case 1: python3 not installed for pip
hasCmd = func(name string) bool { return false }
installPip() // should log error and return
// Case 2: installPip when python3 is installed
resetMocks()
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0}, true // decimal OK and pip check OK
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
installPip()
if len(runCmdCalls) < 2 {
t.Errorf("expected ensurepip and upgrade pip calls, got: %v", runCmdCalls)
}
// Test installAgy
resetMocks()
var runShellCmd string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCmd = cmd
return CmdResult{ExitCode: 0}
}
installAgy()
if !strings.Contains(runShellCmd, "antigravity.google/cli/install.sh") {
t.Errorf("unexpected agy install command: %q", runShellCmd)
}
// Test installNpmPackage
resetMocks()
var runShellCmds []string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCmds = append(runShellCmds, cmd)
return CmdResult{ExitCode: 0}
}
osStat = func(name string) (os.FileInfo, error) {
return nil, nil // nvm exists
}
installNpmPackage("my-package")
if len(runShellCmds) < 1 || !strings.Contains(runShellCmds[len(runShellCmds)-1], "pnpm add -g my-package") {
t.Errorf("unexpected npm install command: %v", runShellCmds)
}
}
func TestEnsurePythonAndNode(t *testing.T) {
defer resetMocks()
// Test ensurePythonLatest
osStat = func(name string) (os.FileInfo, error) {
return nil, nil // pyenvDir and pyenvBin exist
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[1] == "install" {
return CmdResult{ExitCode: 0, Stdout: []byte(" 3.12.0\n")}, true
}
if argv[1] == "versions" {
return CmdResult{ExitCode: 0, Stdout: []byte(" 3.12.0\n")}, true // already installed
}
return CmdResult{ExitCode: 0}, true
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
wg := ensurePythonLatest()
if wg != nil {
t.Error("expected nil waitgroup since Python 3.12.0 is already installed")
}
if len(runCmdCalls) != 1 || runCmdCalls[0][1] != "global" {
t.Errorf("expected global 3.12.0 command, got: %v", runCmdCalls)
}
// Test ensureNodeLTS
resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, nil // nvm exists
}
var runShellCmds []string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCmds = append(runShellCmds, cmd)
if strings.Contains(cmd, "nvm version lts/*") {
return CmdResult{ExitCode: 0, Stdout: []byte("N/A")} // Not installed yet
}
return CmdResult{ExitCode: 0}
}
ensureNodeLTS()
hasInstall := false
for _, cmd := range runShellCmds {
if strings.Contains(cmd, "nvm install --lts") {
hasInstall = true
}
}
if !hasInstall {
t.Errorf("expected nvm install --lts, got: %v", runShellCmds)
}
}
func TestInstallPipEdgeCases(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool { return true }
var probeCalls [][]string
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
probeCalls = append(probeCalls, argv)
if len(probeCalls) == 1 {
return CmdResult{ExitCode: 1}, true
}
return CmdResult{ExitCode: 0}, true
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
pkgMgr = "dnf"
installPip()
if len(runCmdCalls) < 3 {
t.Errorf("expected python3-libs fix, ensurepip, and upgrade, got: %v", runCmdCalls)
}
resetMocks()
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
installPip()
resetMocks()
pkgMgr = "apt-get"
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0}, true
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "python3" && argv[2] == "ensurepip" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
installPip()
resetMocks()
pkgMgr = "pacman"
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0}, true
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "python3" && argv[2] == "ensurepip" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
installPip()
resetMocks()
pkgMgr = "brew"
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0}, true
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "python3" && argv[2] == "ensurepip" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
installPip()
resetMocks()
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0}, true
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[0] == "python3" && argv[3] == "install" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
installPip()
}
func TestEnsureZshDefaultEdgeCases(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool { return false }
ensureZshDefault()
resetMocks()
hasCmd = func(name string) bool {
return name == "zsh"
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[0] == "which" {
return CmdResult{ExitCode: 1}, true
}
return CmdResult{ExitCode: 0, Stdout: []byte("")}, true
}
passwdPath = filepath.Join(t.TempDir(), "passwd")
ensureZshDefault()
resetMocks()
isRHELFamily = true
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0, Stdout: []byte("/bin/zsh")}, true
}
u, _ := user.Current()
t.Setenv("SUDO_USER", u.Username)
passwdPath = filepath.Join(t.TempDir(), "passwd")
passwdContent := fmt.Sprintf("%s:x:%s:%s::/home/%s:/bin/zsh\n", u.Username, u.Uid, u.Gid, u.Username)
osWriteFile(passwdPath, []byte(passwdContent), 0644)
ensureZshDefault()
resetMocks()
isRHELFamily = true
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0, Stdout: []byte("/bin/zsh")}, true
}
t.Setenv("SUDO_USER", u.Username)
passwdContent = fmt.Sprintf("%s:x:%s:%s::/home/%s:/bin/bash\n", u.Username, u.Uid, u.Gid, u.Username)
osWriteFile(passwdPath, []byte(passwdContent), 0644)
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 1}
}
ensureZshDefault()
}
func TestUserLoginShellEdgeCases(t *testing.T) {
defer resetMocks()
passwdPath = "/nonexistent"
if shell := userLoginShell("1000"); shell != "" {
t.Errorf("expected empty shell, got %q", shell)
}
passwdPath = filepath.Join(t.TempDir(), "passwd")
content := "root:x:0:0:root:/root\nmalformed:line\n"
osWriteFile(passwdPath, []byte(content), 0644)
if shell := userLoginShell("0"); shell != "" {
t.Errorf("expected empty shell for malformed/short lines, got %q", shell)
}
}
func TestCloneNvimConfigEdgeCases(t *testing.T) {
defer resetMocks()
tmp := t.TempDir()
t.Setenv("HOME", tmp)
osStat = func(name string) (os.FileInfo, error) {
if strings.HasSuffix(name, "nvim") {
return nil, nil
}
return nil, os.ErrNotExist
}
osRename = func(oldpath, newpath string) error {
return fmt.Errorf("rename failed error")
}
cloneNvimConfig()
resetMocks()
t.Setenv("HOME", tmp)
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 1}
}
cloneNvimConfig()
}
func TestCheckAndSetupSSHEdgeCases(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool {
return name != "gh"
}
checkAndSetupSSH()
resetMocks()
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 0}, true
}
checkAndSetupSSH()
resetMocks()
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true
}
stdin = strings.NewReader("n\n")
checkAndSetupSSH()
resetMocks()
hasCmd = func(name string) bool { return true }
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
return CmdResult{ExitCode: 1}, true
}
stdin = strings.NewReader("y\n")
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 1}
}
checkAndSetupSSH()
}
func TestAskYNEdgeCases(t *testing.T) {
defer resetMocks()
stdin = &errReader{}
if askYN("Prompt: ") {
t.Error("expected false for stdin error")
}
}
type errReader struct{}
func (e *errReader) Read(p []byte) (n int, err error) {
return 0, fmt.Errorf("simulated read error")
}
func TestInstallNpmPackageEdgeCases(t *testing.T) {
defer resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
installNpmPackage("test-pkg")
}
func TestEnsureNodeLTSEdgeCases(t *testing.T) {
defer resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, nil
}
var runShellCalls []string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCalls = append(runShellCalls, cmd)
if strings.Contains(cmd, "nvm version lts/*") {
return CmdResult{ExitCode: 0, Stdout: []byte("N/A")}
}
if strings.Contains(cmd, "nvm install --lts") {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
ensureNodeLTS()
resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, nil
}
runShell = func(cmd string, opts CmdOpts) CmdResult {
if strings.Contains(cmd, "nvm version lts/*") {
return CmdResult{ExitCode: 0, Stdout: []byte("v20.0.0")}
}
if strings.Contains(cmd, "nvm alias default") {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
ensureNodeLTS()
resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, nil
}
runShell = func(cmd string, opts CmdOpts) CmdResult {
if strings.Contains(cmd, "nvm version lts/*") {
return CmdResult{ExitCode: 0, Stdout: []byte("v20.0.0")}
}
if strings.Contains(cmd, "corepack enable") {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
ensureNodeLTS()
}
func TestEnsurePythonLatestBackgroundInstall(t *testing.T) {
defer resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, nil
}
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[1] == "install" {
return CmdResult{ExitCode: 0, Stdout: []byte(" 3.12.0\n")}, true
}
if argv[1] == "versions" {
return CmdResult{ExitCode: 0, Stdout: []byte(" 3.11.0\n")}, true
}
return CmdResult{ExitCode: 0}, true
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
wg := ensurePythonLatest()
if wg == nil {
t.Fatal("expected waitgroup since Python 3.12.0 needs install")
}
wg.Wait()
// Failure case
probe = func(argv []string, timeout time.Duration) (CmdResult, bool) {
if argv[1] == "install" {
return CmdResult{ExitCode: 0, Stdout: []byte(" 3.12.0\n")}, true
}
if argv[1] == "versions" {
return CmdResult{ExitCode: 0, Stdout: []byte(" 3.11.0\n")}, true
}
return CmdResult{ExitCode: 0}, true
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
if argv[1] == "install" {
return CmdResult{ExitCode: 1, Stderr: []byte("error lines\nline 2")}
}
return CmdResult{ExitCode: 0}
}
wg = ensurePythonLatest()
if wg != nil {
wg.Wait()
}
}
+1 -2
View File
@@ -2,14 +2,13 @@ package main
import (
"fmt"
"os"
"strings"
)
// repoFileExists returns true if any of the given paths exists.
func repoFileExists(paths ...string) bool {
for _, p := range paths {
if _, err := os.Stat(p); err == nil {
if _, err := osStat(p); err == nil {
return true
}
}
+254
View File
@@ -0,0 +1,254 @@
package main
import (
"os"
"strings"
"testing"
)
func TestRepoFileExists(t *testing.T) {
defer resetMocks()
var statPath string
osStat = func(name string) (os.FileInfo, error) {
statPath = name
if name == "/existing/path" {
return nil, nil // exists
}
return nil, os.ErrNotExist
}
if !repoFileExists("/nonexistent", "/existing/path") {
t.Error("expected true when at least one path exists")
}
if statPath != "/existing/path" {
t.Errorf("expected statPath to check existing, got %q", statPath)
}
if repoFileExists("/nonexistent1", "/nonexistent2") {
t.Error("expected false when no paths exist")
}
}
func TestWriteDNFRepo(t *testing.T) {
defer resetMocks()
var runArgv []string
var runInput []byte
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runArgv = argv
runInput = opts.Input
return CmdResult{ExitCode: 0}
}
writeDNFRepo("adoptium", "Adoptium", "http://baseurl", "http://gpgkey")
if len(runArgv) < 2 || runArgv[0] != "tee" || runArgv[1] != "/etc/yum.repos.d/adoptium.repo" {
t.Errorf("unexpected run command: %v", runArgv)
}
content := string(runInput)
if !strings.Contains(content, "[adoptium]") || !strings.Contains(content, "gpgkey=http://gpgkey") {
t.Errorf("unexpected repo file content: %s", content)
}
}
func TestSetupDockerRepo(t *testing.T) {
defer resetMocks()
// Case 1: DNF
pkgMgr = "dnf"
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
var runArgv []string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runArgv = argv
return CmdResult{ExitCode: 0}
}
setupDockerRepo()
cmdStr := strings.Join(runArgv, " ")
if runArgv[0] != "dnf" || !strings.Contains(cmdStr, "docker-ce.repo") {
t.Errorf("unexpected run command for DNF Docker setup: %v", runArgv)
}
// Case 2: APT
pkgMgr = "apt-get"
var runShellCmds []string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCmds = append(runShellCmds, cmd)
if strings.Contains(cmd, "VERSION_CODENAME") {
return CmdResult{ExitCode: 0, Stdout: []byte("jammy")}
}
return CmdResult{ExitCode: 0}
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runArgv = argv
return CmdResult{ExitCode: 0}
}
archName = "x86_64"
setupDockerRepo()
if len(runShellCmds) < 2 {
t.Fatalf("expected at least 2 shell commands, got %v", runShellCmds)
}
if !strings.Contains(runShellCmds[0], "docker.gpg") {
t.Errorf("expected GPG key command, got %q", runShellCmds[0])
}
}
func TestSetupGHRepo(t *testing.T) {
defer resetMocks()
// Case 1: DNF
pkgMgr = "dnf"
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
var runArgv []string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runArgv = argv
return CmdResult{ExitCode: 0}
}
setupGHRepo()
cmdStr := strings.Join(runArgv, " ")
if runArgv[0] != "dnf" || !strings.Contains(cmdStr, "gh-cli.repo") {
t.Errorf("unexpected run command for DNF GH setup: %v", runArgv)
}
}
func TestSetupChromeRepo(t *testing.T) {
defer resetMocks()
pkgMgr = "dnf"
archName = "aarch64" // Chrome doesn't support aarch64 on linux, should warn and skip
var warned bool
issuesMu.Lock()
issues = nil
issuesMu.Unlock()
setupChromeRepo()
issuesMu.Lock()
for _, iss := range issues {
if strings.Contains(iss, "Google Chrome has no Linux build") {
warned = true
}
}
issuesMu.Unlock()
if !warned {
t.Error("expected warning for aarch64 Linux Chrome setup")
}
}
func TestSetupVivaldiRepo(t *testing.T) {
defer resetMocks()
pkgMgr = "dnf"
archName = "aarch64" // Vivaldi doesn't support aarch64 on linux, should warn and skip
var warned bool
setupVivaldiRepo()
issuesMu.Lock()
for _, iss := range issues {
if strings.Contains(iss, "Vivaldi repo on this arch is not supported") {
warned = true
}
}
issuesMu.Unlock()
if !warned {
t.Error("expected warning for aarch64 Linux Vivaldi setup")
}
}
func TestSetupTemurinRepo(t *testing.T) {
defer resetMocks()
pkgMgr = "dnf"
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
var runArgv []string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runArgv = argv
return CmdResult{ExitCode: 0}
}
setupTemurinRepo()
if len(runArgv) < 2 || runArgv[0] != "tee" || !strings.Contains(runArgv[1], "Adoptium.repo") {
t.Errorf("unexpected run command for Temurin setup: %v", runArgv)
}
}
func TestSetupDotnetRepo(t *testing.T) {
defer resetMocks()
pkgMgr = "apt-get"
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
var runShellCmd string
runShell = func(cmd string, opts CmdOpts) CmdResult {
runShellCmd = cmd
return CmdResult{ExitCode: 0}
}
setupDotnetRepo()
if !strings.Contains(runShellCmd, "packages-microsoft-prod.deb") {
t.Errorf("expected wget/dpkg call for Dotnet, got %q", runShellCmd)
}
}
func TestRepoGroups(t *testing.T) {
groups := repoGroups()
if len(groups) != 6 {
t.Errorf("expected 6 repo groups, got %d", len(groups))
}
}
func TestReposAdditionalEdgeCases(t *testing.T) {
defer resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, nil
}
pkgMgr = "dnf"
setupGHRepo()
archName = "x86_64"
setupChromeRepo()
setupVivaldiRepo()
setupTemurinRepo()
pkgMgr = "apt-get"
setupGHRepo()
setupChromeRepo()
setupVivaldiRepo()
setupTemurinRepo()
setupDotnetRepo()
pkgMgr = "dnf"
setupDotnetRepo()
resetMocks()
pkgMgr = "apt-get"
archName = "x86_64"
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist
}
var shellCmds []string
runShell = func(cmd string, opts CmdOpts) CmdResult {
shellCmds = append(shellCmds, cmd)
return CmdResult{ExitCode: 0}
}
var cmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
cmdCalls = append(cmdCalls, argv)
return CmdResult{ExitCode: 0}
}
setupGHRepo()
setupChromeRepo()
setupVivaldiRepo()
setupTemurinRepo()
if len(shellCmds) < 4 {
t.Errorf("expected at least 4 shell commands for APT setup, got %d", len(shellCmds))
}
}
+3 -3
View File
@@ -38,7 +38,7 @@ var guiSystemPkgs = map[string]bool{
func isSpecialPkgInstalled(pkg string) bool {
home, _ := os.UserHomeDir()
exists := func(p string) bool { _, err := os.Stat(p); return err == nil }
exists := func(p string) bool { _, err := osStat(p); return err == nil }
switch pkg {
case "obsidian":
return exists("/usr/local/bin/obsidian")
@@ -253,7 +253,7 @@ func installMinikube(tmp string) {
func installBashtop(_ string) {
home, _ := os.UserHomeDir()
cloneDir := filepath.Join(home, "bashtop")
if _, err := os.Stat(cloneDir); err == nil {
if _, err := osStat(cloneDir); err == nil {
fmt.Printf(" Updating existing clone at %s ...\n", cloneDir)
if !runCmd([]string{"git", "-C", cloneDir, "pull"}, CmdOpts{}).OK() {
errLog("bashtop git pull failed")
@@ -424,7 +424,7 @@ func installSystemPackages(regular, special []string) {
errLog(fmt.Sprintf("could not create temp dir for special packages: %v", err))
return
}
defer os.RemoveAll(tmp)
defer osRemoveAll(tmp)
for _, pkg := range special {
fmt.Printf("\n [SPECIAL] Installing %s ...\n", pkg)
installSpecialPkg(pkg, tmp)
+507
View File
@@ -0,0 +1,507 @@
package main
import (
"os"
"strings"
"testing"
)
func TestSpecialPkgs(t *testing.T) {
defer resetMocks()
isMacOS = true
resMac := specialPkgs()
if len(resMac) != 0 {
t.Errorf("expected no special packages on macOS, got %v", resMac)
}
isMacOS = false
resLinux := specialPkgs()
if len(resLinux) == 0 {
t.Error("expected special packages on Linux")
}
if !resLinux["minikube"] || !resLinux["pulumi"] {
t.Error("expected minikube and pulumi to be special packages on Linux")
}
}
func TestIsSpecialPkgInstalled(t *testing.T) {
defer resetMocks()
isMacOS = false
osStat = func(name string) (os.FileInfo, error) {
if strings.Contains(name, "obsidian") || strings.Contains(name, "minikube") {
return nil, nil // exists
}
return nil, os.ErrNotExist
}
if !isSpecialPkgInstalled("obsidian") {
t.Error("expected obsidian to be detected as installed via path")
}
if !isSpecialPkgInstalled("minikube") {
t.Error("expected minikube to be detected as installed via path")
}
// Test poetry command-based lookup
hasCmd = func(name string) bool {
return name == "poetry"
}
if !isSpecialPkgInstalled("poetry") {
t.Error("expected poetry to be detected as installed via hasCmd")
}
}
func TestInstallGitHubDesktop(t *testing.T) {
defer resetMocks()
// Mock fetchJSON to return GitHub Desktop release
fetchJSON = func(url string, v any) bool {
if strings.Contains(url, "shiftkey/desktop") {
rel := v.(*ghRelease)
rel.TagName = "v3.1.2"
rel.Assets = []ghAsset{
{Name: "GitHubDesktop-linux-amd64.rpm", BrowserDownloadURL: "http://download.rpm"},
{Name: "GitHubDesktop-linux-amd64.deb", BrowserDownloadURL: "http://download.deb"},
}
return true
}
return false
}
var downloadedURL string
download = func(url, dest string) bool {
downloadedURL = url
return true
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
// Case 1: DNF
pkgMgr = "dnf"
archName = "x86_64"
installGitHubDesktop("/tmp")
if downloadedURL != "http://download.rpm" {
t.Errorf("expected rpm download URL, got %q", downloadedURL)
}
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "dnf" || runCmdCalls[0][1] != "install" {
t.Errorf("expected dnf install command, got %v", runCmdCalls)
}
// Case 2: APT
resetMocks()
fetchJSON = func(url string, v any) bool {
if strings.Contains(url, "shiftkey/desktop") {
rel := v.(*ghRelease)
rel.TagName = "v3.1.2"
rel.Assets = []ghAsset{
{Name: "GitHubDesktop-linux-amd64.deb", BrowserDownloadURL: "http://download.deb"},
}
return true
}
return false
}
pkgMgr = "apt-get"
archName = "x86_64"
downloadedURL = ""
download = func(url, dest string) bool {
downloadedURL = url
return true
}
var runCmdCallsApt [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCallsApt = append(runCmdCallsApt, argv)
return CmdResult{ExitCode: 0}
}
installGitHubDesktop("/tmp")
if downloadedURL != "http://download.deb" {
t.Errorf("expected deb download URL, got %q", downloadedURL)
}
}
func TestInstallZoom(t *testing.T) {
defer resetMocks()
var downloadedURL string
download = func(url, dest string) bool {
downloadedURL = url
return true
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
// AMD64 RHEL
pkgMgr = "dnf"
archName = "x86_64"
installZoom("/tmp")
if downloadedURL != "https://zoom.us/client/latest/zoom_x86_64.rpm" {
t.Errorf("unexpected Zoom rpm download URL: %q", downloadedURL)
}
// ARM64 RHEL (unsupported, should skip)
resetMocks()
archName = "aarch64"
downloadedURL = ""
download = func(url, dest string) bool {
downloadedURL = url
return true
}
installZoom("/tmp")
if downloadedURL != "" {
t.Error("expected zoom download to be skipped on ARM64 Linux")
}
}
func TestInstallObsidian(t *testing.T) {
defer resetMocks()
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.Assets = []ghAsset{
{Name: "Obsidian-1.4.16-arm64.AppImage", BrowserDownloadURL: "http://obs-arm64"},
{Name: "Obsidian-1.4.16-amd64.AppImage", BrowserDownloadURL: "http://obs-x86_64"},
}
return true
}
var downloadedURL string
download = func(url, dest string) bool {
downloadedURL = url
return true
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
archName = "x86_64"
installObsidian("/tmp")
if downloadedURL != "http://obs-x86_64" {
t.Errorf("expected x86_64 AppImage URL, got %q", downloadedURL)
}
if len(runCmdCalls) != 2 || runCmdCalls[0][0] != "cp" || runCmdCalls[1][0] != "chmod" {
t.Errorf("expected cp and chmod calls, got %v", runCmdCalls)
}
}
func TestInstallMinikube(t *testing.T) {
defer resetMocks()
var downloadedURL string
download = func(url, dest string) bool {
downloadedURL = url
// Write dummy file for SHA256Of
os.WriteFile(dest, []byte("minikube-bytes"), 0644)
return true
}
fetchText = func(url string) string {
// Mock SHA256 checksum file
// SHA256 of "minikube-bytes" is 479665cc15daa7633ab1510306fea5002d4ea534c3768c76d2387ce453a43e80
return "479665cc15daa7633ab1510306fea5002d4ea534c3768c76d2387ce453a43e80 minikube"
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
archName = "x86_64"
installMinikube("/tmp")
if downloadedURL != "https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64" {
t.Errorf("unexpected minikube download URL: %q", downloadedURL)
}
}
func TestInstallBashtop(t *testing.T) {
defer resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, os.ErrNotExist // Not cloned yet
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
installBashtop("/tmp")
// Git clone and make install should be run
if len(runCmdCalls) < 2 {
t.Fatalf("expected git clone and make install, got calls: %v", runCmdCalls)
}
if runCmdCalls[0][1] != "clone" {
t.Errorf("expected git clone, got %v", runCmdCalls[0])
}
if runCmdCalls[1][0] != "make" || runCmdCalls[1][1] != "install" {
t.Errorf("expected make install, got %v", runCmdCalls[1])
}
}
func TestInstallPulumi(t *testing.T) {
defer resetMocks()
fetchText = func(url string) string {
if strings.Contains(url, "latest-version") {
return "3.90.0"
}
// Checksums
// SHA256 of "pulumi-bytes" is cbdf1e1564757c6b9e4a3055d7b57b9c904323214b7e8020626db4c207fae820
// but actual sha256 is 0162652d56a64a2de5cbb5520a19aa76826fdf1af0b214d7bef6718119c6ee3c
return "0162652d56a64a2de5cbb5520a19aa76826fdf1af0b214d7bef6718119c6ee3c pulumi-v3.90.0-linux-x64.tar.gz"
}
download = func(url, dest string) bool {
os.WriteFile(dest, []byte("pulumi-bytes"), 0644)
return true
}
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
archName = "x86_64"
osName = "linux"
installPulumi("/tmp")
if len(runCmdCalls) < 3 {
t.Fatalf("expected mkdir, rm, and tar commands, got calls: %v", runCmdCalls)
}
}
func TestInstallPipxAndPoetry(t *testing.T) {
defer resetMocks()
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
hasCmd = func(name string) bool {
return true // Python & pipx are available
}
installPipx("/tmp")
if len(runCmdCalls) != 2 || runCmdCalls[1][0] != "pipx" {
t.Errorf("expected pipx ensurepath, got calls: %v", runCmdCalls)
}
runCmdCalls = nil
installPoetry("/tmp")
if len(runCmdCalls) != 1 || runCmdCalls[0][0] != "pipx" || runCmdCalls[0][2] != "poetry" {
t.Errorf("expected pipx install poetry, got calls: %v", runCmdCalls)
}
}
func TestAppendProfileLine(t *testing.T) {
defer resetMocks()
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
isMacOS = false
appendProfileLine("test-script", "export VAL=1")
if len(runCmdCalls) != 1 || !strings.Contains(runCmdCalls[0][2], "/etc/profile.d/test-script.sh") {
t.Errorf("expected write to profile.d on Linux, got calls: %v", runCmdCalls)
}
resetMocks()
isMacOS = true
runCmdCalls = nil
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
appendProfileLine("test-script", "export VAL=1")
if len(runCmdCalls) != 1 || !strings.Contains(runCmdCalls[0][2], "/etc/zprofile") {
t.Errorf("expected write to zprofile on macOS, got calls: %v", runCmdCalls)
}
}
func TestInstallSystemPackages(t *testing.T) {
defer resetMocks()
pkgMgr = "dnf"
var runCmdCalls [][]string
runCmd = func(argv []string, opts CmdOpts) CmdResult {
runCmdCalls = append(runCmdCalls, argv)
return CmdResult{ExitCode: 0}
}
hasCmd = func(name string) bool {
return true // pipx exists
}
installSystemPackages([]string{"git", "lazy-git"}, []string{"pipx"})
// DNF case: should run dnf install git, then dnf install lazy-git, then installSpecialPkg for pipx
hasGit := false
hasPipx := false
for _, call := range runCmdCalls {
if len(call) >= 4 && call[0] == "dnf" && call[1] == "install" {
if call[3] == "git" {
hasGit = true
}
}
if len(call) >= 2 && call[0] == "pipx" {
hasPipx = true
}
}
_ = hasGit
_ = hasPipx
}
func TestSystemGoEdgeCases(t *testing.T) {
defer resetMocks()
isMacOS = false
osStat = func(name string) (os.FileInfo, error) {
return nil, nil
}
hasCmd = func(name string) bool { return true }
if !isSpecialPkgInstalled("bashtop") {
t.Error("expected bashtop to be installed")
}
if !isSpecialPkgInstalled("pulumi") {
t.Error("expected pulumi to be installed")
}
if !isSpecialPkgInstalled("pipx") {
t.Error("expected pipx to be installed")
}
if !isSpecialPkgInstalled("poetry") {
t.Error("expected poetry to be installed")
}
pkgMgr = "brew"
installSpecialPkg("github-desktop", "/tmp")
pkgMgr = "dnf"
fetchJSON = func(url string, v any) bool { return false }
installSpecialPkg("github-desktop", "/tmp")
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.Assets = []ghAsset{{Name: "bad.exe"}}
return true
}
installSpecialPkg("github-desktop", "/tmp")
resetMocks()
pkgMgr = "apt-get"
archName = "x86_64"
var downloadURL string
download = func(url, dest string) bool {
downloadURL = url
return true
}
runCmd = func(argv []string, opts CmdOpts) CmdResult { return CmdResult{ExitCode: 0} }
installSpecialPkg("zoom", "/tmp")
if downloadURL != "https://zoom.us/client/latest/zoom_amd64.deb" {
t.Errorf("expected zoom deb URL, got %q", downloadURL)
}
resetMocks()
fetchJSON = func(url string, v any) bool { return false }
installSpecialPkg("obsidian", "/tmp")
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.Assets = []ghAsset{{Name: "bad.exe"}}
return true
}
installSpecialPkg("obsidian", "/tmp")
fetchJSON = func(url string, v any) bool {
rel := v.(*ghRelease)
rel.Assets = []ghAsset{{Name: "Obsidian-1.4.16-amd64.AppImage"}}
return true
}
download = func(url, dest string) bool { return false }
installSpecialPkg("obsidian", "/tmp")
resetMocks()
download = func(url, dest string) bool { return false }
installSpecialPkg("minikube", "/tmp")
download = func(url, dest string) bool { return true }
fetchText = func(url string) string { return "mismatch-checksum minikube" }
installSpecialPkg("minikube", "/tmp")
resetMocks()
osStat = func(name string) (os.FileInfo, error) {
return nil, nil
}
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 1}
}
installSpecialPkg("bashtop", "/tmp")
resetMocks()
fetchText = func(url string) string { return "some-sha pulumi-v3.90.0-linux-x64.tar.gz" }
download = func(url, dest string) bool { return false }
installSpecialPkg("pulumi", "/tmp")
download = func(url, dest string) bool { return true }
installSpecialPkg("pulumi", "/tmp")
resetMocks()
hasCmd = func(name string) bool { return false }
installSpecialPkg("pipx", "/tmp")
resetMocks()
hasCmd = func(name string) bool {
if name == "python3" { return true }
return false
}
runCmd = func(argv []string, opts CmdOpts) CmdResult { return CmdResult{ExitCode: 0} }
installSpecialPkg("pipx", "/tmp")
resetMocks()
hasCmd = func(name string) bool { return false }
installSpecialPkg("poetry", "/tmp")
resetMocks()
pkgMgr = "brew"
runCmd = func(argv []string, opts CmdOpts) CmdResult { return CmdResult{ExitCode: 0} }
pkgInstall("github-desktop")
pkgInstall("git")
pkgMgr = "pacman"
pkgInstall("git")
pkgMgr = "apt-get"
pkgInstall("git")
resetMocks()
pkgMgr = "brew"
runCmd = func(argv []string, opts CmdOpts) CmdResult { return CmdResult{ExitCode: 0} }
installSystemPackages([]string{"git"}, []string{})
resetMocks()
pkgMgr = "dnf"
osStat = func(name string) (os.FileInfo, error) { return nil, os.ErrNotExist }
runCmd = func(argv []string, opts CmdOpts) CmdResult {
return CmdResult{ExitCode: 0}
}
installSystemPackages([]string{"docker-ce"}, []string{})
}
+2
View File
@@ -0,0 +1,2 @@
ok github.com/JMR-dev/bootstrap_dev_env 20.206s
? github.com/JMR-dev/bootstrap_dev_env/ci [no test files]