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.
312 lines
8.7 KiB
Go
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")
|
|
}
|
|
}
|