banger/internal/system/system_test.go
Thales Maciel a166068fab
Add an experimental Alpine image flow
Stage a complete Alpine x86_64 image stack so \	--image alpineworks like the existing manual Void path instead of relying on Debian-oriented image builds.\n\nAdd make targets plus kernel/rootfs/register helpers that download pinned Alpine artifacts, extract a Firecracker-compatible vmlinux, build a matching mkinitfs initramfs, seed OpenRC services, and register/promote a managed image named alpine.\n\nFold in the bring-up fixes discovered during boot validation: use rootfstype=ext4 in shared boot args, install libgcc/libstdc++ for the opencode binary, and give opencode more time to become ready on cold boots.\n\nValidate with go test ./..., the Alpine helper builds, image promotion, and banger vm create --image alpine --name alp --nat plus guest service and port checks.
2026-03-21 20:25:55 -03:00

460 lines
12 KiB
Go

package system
import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
type systemCall struct {
sudo bool
name string
args []string
}
type systemStep struct {
call systemCall
out []byte
err error
}
type scriptedRunner struct {
t *testing.T
steps []systemStep
calls []systemCall
}
func (r *scriptedRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
return r.next(systemCall{name: name, args: append([]string(nil), args...)})
}
func (r *scriptedRunner) RunSudo(ctx context.Context, args ...string) ([]byte, error) {
return r.next(systemCall{sudo: true, args: append([]string(nil), args...)})
}
func (r *scriptedRunner) next(call systemCall) ([]byte, error) {
r.t.Helper()
r.calls = append(r.calls, call)
if len(r.steps) == 0 {
r.t.Fatalf("unexpected call: %+v", call)
}
step := r.steps[0]
r.steps = r.steps[1:]
if step.call.sudo != call.sudo || step.call.name != call.name || !reflect.DeepEqual(step.call.args, call.args) {
r.t.Fatalf("call mismatch:\n got: %+v\n want: %+v", call, step.call)
}
return step.out, step.err
}
func (r *scriptedRunner) assertExhausted() {
r.t.Helper()
if len(r.steps) != 0 {
r.t.Fatalf("unconsumed steps: %+v", r.steps)
}
}
type funcRunner struct {
run func(ctx context.Context, name string, args ...string) ([]byte, error)
runSudo func(ctx context.Context, args ...string) ([]byte, error)
}
func (r funcRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
if r.run == nil {
return nil, errors.New("unexpected Run call")
}
return r.run(ctx, name, args...)
}
func (r funcRunner) RunSudo(ctx context.Context, args ...string) ([]byte, error) {
if r.runSudo == nil {
return nil, errors.New("unexpected RunSudo call")
}
return r.runSudo(ctx, args...)
}
func TestResizeExt4ImageStopsAtFirstFailure(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setup func(t *testing.T) string
steps func(path string) []systemStep
wantErr string
wantCalls int
}{
{
name: "truncate failure",
setup: func(t *testing.T) string {
return t.TempDir()
},
wantErr: "",
wantCalls: 0,
},
{
name: "e2fsck failure",
steps: func(path string) []systemStep {
return []systemStep{
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", path}}, err: errors.New("e2fsck failed")},
}
},
wantErr: "e2fsck failed",
wantCalls: 1,
},
{
name: "resize2fs failure",
steps: func(path string) []systemStep {
return []systemStep{
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", path}}},
{call: systemCall{name: "resize2fs", args: []string{path}}, err: errors.New("resize2fs failed")},
}
},
wantErr: "resize2fs failed",
wantCalls: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
path := "/tmp/root.ext4"
if tt.setup != nil {
path = tt.setup(t)
} else {
path = filepath.Join(t.TempDir(), "root.ext4")
if err := os.WriteFile(path, []byte("seed"), 0o644); err != nil {
t.Fatalf("WriteFile(%s): %v", path, err)
}
}
var steps []systemStep
if tt.steps != nil {
steps = tt.steps(path)
}
runner := &scriptedRunner{t: t, steps: steps}
err := ResizeExt4Image(context.Background(), runner, path, 4096)
if err == nil {
t.Fatal("ResizeExt4Image() succeeded, want error")
}
if tt.wantErr != "" && !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("ResizeExt4Image() error = %v, want %q", err, tt.wantErr)
}
if len(runner.calls) != tt.wantCalls {
t.Fatalf("call count = %d, want %d", len(runner.calls), tt.wantCalls)
}
runner.assertExhausted()
})
}
}
func TestReadNormalizedLines(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "packages.apt")
if err := os.WriteFile(path, []byte("\n# comment\n git \nless # trailing\n\r\ntmux\r\n"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
got, err := ReadNormalizedLines(path)
if err != nil {
t.Fatalf("ReadNormalizedLines: %v", err)
}
want := []string{"git", "less", "tmux"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("lines = %v, want %v", got, want)
}
}
func TestBuildBootArgsIncludesHostnameInIPField(t *testing.T) {
t.Parallel()
got := BuildBootArgs("devbox", "172.16.0.2", "172.16.0.1", "1.1.1.1")
want := "console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rootfstype=ext4 rw ip=172.16.0.2::172.16.0.1:255.255.255.0:devbox:eth0:off:1.1.1.1 hostname=devbox systemd.mask=home.mount systemd.mask=var.mount"
if got != want {
t.Fatalf("BuildBootArgs() = %q, want %q", got, want)
}
}
func TestWriteExt4FileRemovesTempFileAndReturnsCopyError(t *testing.T) {
t.Parallel()
copyErr := errors.New("e2cp failed")
var tempPath string
runner := funcRunner{
runSudo: func(ctx context.Context, args ...string) ([]byte, error) {
switch args[0] {
case "e2rm":
return nil, errors.New("ignore remove")
case "e2cp":
tempPath = args[1]
if _, err := os.Stat(tempPath); err != nil {
t.Fatalf("temp file missing during e2cp: %v", err)
}
return nil, copyErr
default:
t.Fatalf("unexpected sudo call: %v", args)
return nil, nil
}
},
}
err := WriteExt4File(context.Background(), runner, "/tmp/root.ext4", "/etc/hostname", []byte("devbox\n"))
if !errors.Is(err, copyErr) {
t.Fatalf("WriteExt4File() error = %v, want %v", err, copyErr)
}
if tempPath == "" {
t.Fatal("expected e2cp temp path to be recorded")
}
if _, err := os.Stat(tempPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("temp file still exists after WriteExt4File: %v", err)
}
}
func TestMountTempDirUsesLoopForRegularFilesAndCleanupUsesBackgroundContext(t *testing.T) {
t.Parallel()
source := filepath.Join(t.TempDir(), "root.ext4")
if err := os.WriteFile(source, []byte("rootfs"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
var mountDir string
calls := 0
runner := funcRunner{
runSudo: func(callCtx context.Context, args ...string) ([]byte, error) {
calls++
switch calls {
case 1:
mountDir = args[len(args)-1]
want := []string{"mount", "-o", "ro,loop", source, mountDir}
if !reflect.DeepEqual(args, want) {
t.Fatalf("mount args = %v, want %v", args, want)
}
case 2:
if callCtx.Err() != nil {
t.Fatalf("cleanup context should not be canceled: %v", callCtx.Err())
}
want := []string{"umount", mountDir}
if !reflect.DeepEqual(args, want) {
t.Fatalf("cleanup args = %v, want %v", args, want)
}
default:
t.Fatalf("unexpected RunSudo call %d: %v", calls, args)
}
return nil, nil
},
}
gotMountDir, cleanup, err := MountTempDir(ctx, runner, source, true)
if err != nil {
t.Fatalf("MountTempDir: %v", err)
}
if gotMountDir != mountDir {
t.Fatalf("mount dir = %q, want %q", gotMountDir, mountDir)
}
cancel()
if err := cleanup(); err != nil {
t.Fatalf("cleanup: %v", err)
}
if _, err := os.Stat(mountDir); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("mount dir still exists after cleanup: %v", err)
}
}
func TestParseProcHelpers(t *testing.T) {
t.Parallel()
stat, err := parseProcStat("1234 (firecracker) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22")
if err != nil {
t.Fatalf("parseProcStat: %v", err)
}
if stat.userTicks != 11 || stat.systemTicks != 12 || stat.startTicks != 19 {
t.Fatalf("proc stat = %+v", stat)
}
statm, err := parseProcStatm("200 50 0 0 0 0 0")
if err != nil {
t.Fatalf("parseProcStatm: %v", err)
}
if statm.sizePages != 200 || statm.residentPages != 50 {
t.Fatalf("proc statm = %+v", statm)
}
uptime, err := parseProcUptime("321.50 42.10")
if err != nil {
t.Fatalf("parseProcUptime: %v", err)
}
if uptime != 321.50 {
t.Fatalf("uptime = %v, want 321.50", uptime)
}
}
func TestParseMemTotal(t *testing.T) {
t.Parallel()
total, err := parseMemTotal("MemTotal: 131900020 kB\nMemFree: 1024 kB\n")
if err != nil {
t.Fatalf("parseMemTotal: %v", err)
}
if total != 131900020*1024 {
t.Fatalf("total = %d, want %d", total, int64(131900020*1024))
}
}
func TestParseMemTotalErrorsWhenMissing(t *testing.T) {
t.Parallel()
if _, err := parseMemTotal("MemFree: 123 kB\n"); err == nil {
t.Fatal("parseMemTotal() error = nil, want missing MemTotal")
}
}
func TestReadFilesystemUsage(t *testing.T) {
t.Parallel()
usage, err := ReadFilesystemUsage(t.TempDir())
if err != nil {
t.Fatalf("ReadFilesystemUsage: %v", err)
}
if usage.TotalBytes <= 0 {
t.Fatalf("usage.TotalBytes = %d, want positive", usage.TotalBytes)
}
if usage.FreeBytes < 0 {
t.Fatalf("usage.FreeBytes = %d, want non-negative", usage.FreeBytes)
}
}
func TestMountTempDirRemovesTempDirWhenMountFails(t *testing.T) {
t.Parallel()
source := t.TempDir()
var mountDir string
runner := funcRunner{
runSudo: func(ctx context.Context, args ...string) ([]byte, error) {
mountDir = args[len(args)-1]
return nil, errors.New("mount failed")
},
}
_, _, err := MountTempDir(context.Background(), runner, source, false)
if err == nil || !strings.Contains(err.Error(), "mount failed") {
t.Fatalf("MountTempDir() error = %v, want mount failure", err)
}
if mountDir == "" {
t.Fatal("expected mount path to be recorded")
}
if _, statErr := os.Stat(mountDir); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("mount dir still exists after failed mount: %v", statErr)
}
}
func TestParseMetricsFileHandlesWholeAndLineDelimitedJSON(t *testing.T) {
t.Parallel()
full := filepath.Join(t.TempDir(), "metrics-full.json")
if err := os.WriteFile(full, []byte(`{"uptime":1,"signals":{"sigterm":0}}`), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
got := ParseMetricsFile(full)
if got["uptime"] != float64(1) {
t.Fatalf("ParseMetricsFile(full) = %v", got)
}
lines := filepath.Join(t.TempDir(), "metrics-lines.json")
if err := os.WriteFile(lines, []byte("junk\n{\"uptime\":2}\n"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
got = ParseMetricsFile(lines)
if got["uptime"] != float64(2) {
t.Fatalf("ParseMetricsFile(lines) = %v", got)
}
}
func TestUpdateFSTabStripsLegacyMountsAndAddsDefaultsOnce(t *testing.T) {
t.Parallel()
input := strings.Join([]string{
"/dev/vda / ext4 defaults 0 1",
"/dev/vdb /home ext4 defaults 0 2",
"/dev/vdc /var ext4 defaults 0 2",
"/dev/vdb /root ext4 defaults 0 2",
"tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0",
"",
}, "\n")
got := UpdateFSTab(input)
if strings.Contains(got, "/home") || strings.Contains(got, "/var") {
t.Fatalf("UpdateFSTab() kept legacy mounts: %q", got)
}
if strings.Count(got, "/dev/vdb /root ext4 defaults 0 2") != 1 {
t.Fatalf("UpdateFSTab() duplicated /root mount: %q", got)
}
if strings.Count(got, "tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0") != 1 {
t.Fatalf("UpdateFSTab() duplicated /run mount: %q", got)
}
if !strings.Contains(got, "tmpfs /tmp tmpfs defaults,nodev,nosuid,mode=1777 0 0") {
t.Fatalf("UpdateFSTab() missing /tmp mount: %q", got)
}
}
func TestUseLoopMount(t *testing.T) {
t.Parallel()
file := filepath.Join(t.TempDir(), "root.ext4")
if err := os.WriteFile(file, []byte("rootfs"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
dir := t.TempDir()
if !useLoopMount(file) {
t.Fatalf("useLoopMount(%s) = false, want true", file)
}
if useLoopMount(dir) {
t.Fatalf("useLoopMount(%s) = true, want false", dir)
}
if useLoopMount(filepath.Join(dir, "missing")) {
t.Fatalf("useLoopMount(missing) = true, want false")
}
}
func TestEstimateWorkSeedSizeFallsBackToSudoDuWhenUnreadable(t *testing.T) {
t.Parallel()
rootHome := filepath.Join(t.TempDir(), "root")
if err := os.Mkdir(rootHome, 0o700); err != nil {
t.Fatalf("Mkdir: %v", err)
}
if err := os.WriteFile(filepath.Join(rootHome, "visible.txt"), []byte("seed"), 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := os.Chmod(rootHome, 0o000); err != nil {
t.Fatalf("Chmod: %v", err)
}
defer os.Chmod(rootHome, 0o700)
var sudoCalled bool
runner := funcRunner{
runSudo: func(ctx context.Context, args ...string) ([]byte, error) {
sudoCalled = true
want := []string{"du", "-sb", rootHome}
if !reflect.DeepEqual(args, want) {
t.Fatalf("RunSudo args = %v, want %v", args, want)
}
return []byte("4096\t" + rootHome + "\n"), nil
},
}
sizeBytes, err := estimateWorkSeedSize(context.Background(), runner, rootHome)
if err != nil {
t.Fatalf("estimateWorkSeedSize: %v", err)
}
if !sudoCalled {
t.Fatal("estimateWorkSeedSize did not fall back to sudo du")
}
if sizeBytes != minWorkSeedBytes {
t.Fatalf("sizeBytes = %d, want %d", sizeBytes, minWorkSeedBytes)
}
}