banger/internal/system/system_test.go
Thales Maciel 5018bc6170
Add regression coverage for VM failure paths
Dangerous lifecycle, store, system, and RPC paths still had little or no automated confidence, and the live smoke harness failed opaquely when guest boot timing drifted. This adds targeted unit coverage for store allocation and decode failures, system helper failure ordering and cleanup, RPC error handling, and daemon lookup/reconcile/editing/stats/preflight edge cases.

It also makes verify.sh wait for daemon-observable VM readiness before SSH, reuse a bounded boot deadline for the SSH phase, and dump VM metadata, logs, tap state, socket state, and NAT rules on timeout so host-level failures are diagnosable instead of surfacing only connection refused.

Validation: go test ./..., go test ./... -cover, bash -n verify.sh. No live ./verify.sh boot was run in this environment.
2026-03-16 15:46:54 -03:00

312 lines
8.7 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
steps []systemStep
wantErr string
wantCalls int
}{
{
name: "truncate failure",
steps: []systemStep{
{call: systemCall{name: "truncate", args: []string{"-s", "4096", "/tmp/root.ext4"}}, err: errors.New("truncate failed")},
},
wantErr: "truncate failed",
wantCalls: 1,
},
{
name: "e2fsck failure",
steps: []systemStep{
{call: systemCall{name: "truncate", args: []string{"-s", "4096", "/tmp/root.ext4"}}},
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", "/tmp/root.ext4"}}, err: errors.New("e2fsck failed")},
},
wantErr: "e2fsck failed",
wantCalls: 2,
},
{
name: "resize2fs failure",
steps: []systemStep{
{call: systemCall{name: "truncate", args: []string{"-s", "4096", "/tmp/root.ext4"}}},
{call: systemCall{name: "e2fsck", args: []string{"-p", "-f", "/tmp/root.ext4"}}},
{call: systemCall{name: "resize2fs", args: []string{"/tmp/root.ext4"}}, err: errors.New("resize2fs failed")},
},
wantErr: "resize2fs failed",
wantCalls: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
runner := &scriptedRunner{t: t, steps: tt.steps}
err := ResizeExt4Image(context.Background(), runner, "/tmp/root.ext4", 4096)
if err == nil || !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 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 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")
}
}