From ef9124e5c0d93ea4ef358cfd5380c2b16f004325 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Apr 2026 06:21:16 -0300 Subject: [PATCH] test(metis): complete per-file coverage gate --- cmd/metis-sentinel/main_test.go | 55 +++++ cmd/metis/remote_edge_test.go | 400 ++++++++++++++++++++++++++++++++ pkg/sentinel/collector_test.go | 102 ++++++++ 3 files changed, 557 insertions(+) create mode 100644 cmd/metis/remote_edge_test.go diff --git a/cmd/metis-sentinel/main_test.go b/cmd/metis-sentinel/main_test.go index 71ab7d3..425ce5f 100644 --- a/cmd/metis-sentinel/main_test.go +++ b/cmd/metis-sentinel/main_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" "os" @@ -57,11 +58,22 @@ func TestSentinelHelpers(t *testing.T) { if got := getenvInt("METIS_SENTINEL_INTERVAL_SEC", 300); got != 300 { t.Fatalf("getenvInt fallback = %d", got) } + t.Setenv("METIS_SENTINEL_INTERVAL_SEC", "not-int") + if got := getenvInt("METIS_SENTINEL_INTERVAL_SEC", 300); got != 300 { + t.Fatalf("getenvInt invalid fallback = %d", got) + } t.Setenv("METIS_SENTINEL_INTERVAL_SEC", "5") if got := getenvInt("METIS_SENTINEL_INTERVAL_SEC", 300); got != 5 { t.Fatalf("getenvInt = %d", got) } + writeHistory("", &sentinel.Snapshot{Hostname: "skip"}) + blocker := filepath.Join(t.TempDir(), "blocker") + if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + writeHistory(filepath.Join(blocker, "child"), &sentinel.Snapshot{Hostname: "skip"}) + dir := t.TempDir() snap := &sentinel.Snapshot{Hostname: "titan-13", Kernel: "6.6.63"} writeHistory(dir, snap) @@ -87,6 +99,49 @@ func TestSentinelHelpers(t *testing.T) { if err := pushSnapshot(srv.URL, snap); err != nil { t.Fatalf("pushSnapshot: %v", err) } + if err := pushSnapshot("://bad-url", snap); err == nil { + t.Fatal("expected pushSnapshot bad URL error") + } + failSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "nope", http.StatusBadGateway) + })) + defer failSrv.Close() + if err := pushSnapshot(failSrv.URL, snap); err == nil || !strings.Contains(err.Error(), "502") { + t.Fatalf("expected pushSnapshot status error, got %v", err) + } +} + +func TestSentinelMainPushAndEncodeFailures(t *testing.T) { + fakeDir := fakeSentinelCommands(t) + t.Setenv("PATH", fakeDir+string(os.PathListSeparator)+os.Getenv("PATH")) + failSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "nope", http.StatusBadGateway) + })) + defer failSrv.Close() + t.Setenv("METIS_SENTINEL_RUN_ONCE", "1") + t.Setenv("METIS_SENTINEL_PUSH_URL", failSrv.URL) + main() + + oldFatalf := fatalf + fatalf = func(format string, args ...any) { + panic(fmt.Sprintf(format, args...)) + } + defer func() { fatalf = oldFatalf }() + oldStdout := os.Stdout + reader, writer, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout = writer + _ = reader.Close() + _ = writer.Close() + defer func() { os.Stdout = oldStdout }() + defer func() { + if got := recover(); got == nil || !strings.Contains(fmt.Sprint(got), "encode") { + t.Fatalf("expected encode fatal, got %v", got) + } + }() + main() } func fakeSentinelCommands(t *testing.T) string { diff --git a/cmd/metis/remote_edge_test.go b/cmd/metis/remote_edge_test.go new file mode 100644 index 0000000..6deb0e2 --- /dev/null +++ b/cmd/metis/remote_edge_test.go @@ -0,0 +1,400 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "math" + "os" + "path/filepath" + "strings" + "testing" + + "metis/pkg/service" +) + +func TestRemoteLocalDeviceAndProgressEdges(t *testing.T) { + dir := fakeCommandDir(t, map[string]string{ + "lsblk": `cat <<'JSON' +{"blockdevices":[ + {"name":"loop0","path":"/dev/loop0","rm":false,"hotplug":false,"size":"100","model":"loop","tran":"","type":"loop"}, + {"name":"huge","path":"/dev/sdx","rm":true,"hotplug":true,"size":"999999999999","model":"huge","tran":"usb","type":"disk"}, + {"name":"nvme0n1","path":"/dev/nvme0n1","rm":false,"hotplug":false,"size":32000000000,"model":"nvme","tran":"nvme","type":"disk"}, + {"name":"mounted","path":"/dev/sdy","rm":true,"hotplug":true,"size":"32000000000","model":"SD","tran":"usb","type":"disk","mountpoint":"/mnt"}, + {"name":"child-mounted","path":"/dev/sdz","rm":true,"hotplug":true,"size":"32000000000","model":"SD","tran":"usb","type":"disk","children":[{"mountpoint":"/boot"}]}, + {"name":"bad-size","path":"/dev/sdw","rm":true,"hotplug":true,"size":"bad","model":"SD","tran":"usb","type":"disk"}, + {"name":"good","path":"/dev/sdv","rm":true,"hotplug":true,"size":16000000000,"model":"SD Card","tran":"usb","type":"disk"} +]} +JSON`, + }) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + devices, err := localFlashDevices(40000000000, "/host-tmp/") + if err != nil { + t.Fatalf("localFlashDevices: %v", err) + } + if len(devices) != 2 || devices[0].Path != "/dev/sdv" || !strings.Contains(devices[1].Note, "/tmp") { + t.Fatalf("unexpected filtered devices: %#v", devices) + } + if got := localDeviceScore(service.Device{Path: "hosttmp:///tmp"}); got != -100 { + t.Fatalf("hosttmp localDeviceScore = %d", got) + } + if got := humanHostPath("/host-tmp/"); got != "/tmp" { + t.Fatalf("humanHostPath root = %q", got) + } + if got := humanHostPath("/host-tmp"); got != "/tmp" { + t.Fatalf("humanHostPath mount root = %q", got) + } + if got := getenvOr("METIS_REMOTE_UNSET", "fallback"); got != "fallback" { + t.Fatalf("getenvOr fallback = %q", got) + } + + stdout, _ := captureStreams(t, func() { + emitProgress(service.RemoteProgressUpdate{ProgressPct: math.NaN()}) + emitter := newProgressEmitter("flash", 92, 98, "Writing", true) + emitter(1, 0) + emitter(20, 10) + emitter(20, 10) + }) + if strings.Count(stdout, "METIS_PROGRESS ") != 1 || !strings.Contains(stdout, `"progress_pct":98`) { + t.Fatalf("unexpected progress output: %q", stdout) + } +} + +func TestRemoteDevicesSortAndDecodeEdges(t *testing.T) { + t.Run("invalid lsblk json", func(t *testing.T) { + dir := fakeCommandDir(t, map[string]string{"lsblk": `printf '{'`}) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + if _, err := localFlashDevices(40000000000, "/tmp"); err == nil { + t.Fatal("expected invalid lsblk JSON error") + } + }) + + t.Run("remote devices tie-break by size and path", func(t *testing.T) { + dir := fakeCommandDir(t, map[string]string{ + "lsblk": `cat <<'JSON' +{"blockdevices":[ + {"name":"sdc","path":"/dev/sdc","rm":true,"hotplug":true,"size":"64000000000","model":"Micro SD","tran":"usb","type":"disk"}, + {"name":"sdb","path":"/dev/sdb","rm":true,"hotplug":true,"size":"32000000000","model":"Micro SD","tran":"usb","type":"disk"}, + {"name":"sda","path":"/dev/sda","rm":true,"hotplug":true,"size":"32000000000","model":"Micro SD","tran":"usb","type":"disk"} +]} +JSON`, + }) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + stdout, _ := captureStreams(t, func() { + remoteDevicesCmd([]string{"--max-device-bytes", "100000000000"}) + }) + if strings.Index(stdout, "/dev/sda") > strings.Index(stdout, "/dev/sdb") || strings.Index(stdout, "/dev/sdb") > strings.Index(stdout, "/dev/sdc") { + t.Fatalf("devices not sorted by size/path: %s", stdout) + } + }) +} + +func TestRemoteArtifactFailureEdges(t *testing.T) { + dir := fakeCommandDir(t, map[string]string{ + "oras": `printf 'oras failed' >&2; exit 7`, + }) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + artifactDir := t.TempDir() + imagePath := filepath.Join(artifactDir, "image.img") + metadataPath := filepath.Join(artifactDir, "metadata.json") + if err := os.WriteFile(imagePath, []byte("image"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(metadataPath, []byte("{}"), 0o644); err != nil { + t.Fatal(err) + } + + for name, fn := range map[string]func() error{ + "login": func() error { return orasLogin("registry.example", "u", "p") }, + "push": func() error { return orasPush("registry.example/metis/node:tag", imagePath, metadataPath) }, + "tag": func() error { return orasTag("registry.example/metis/node:tag", "latest") }, + "pull": func() error { return orasPull("registry.example/metis/node:latest", artifactDir) }, + } { + t.Run(name, func(t *testing.T) { + if err := fn(); err == nil || !strings.Contains(err.Error(), "oras failed") { + t.Fatalf("expected oras failure, got %v", err) + } + }) + } + + emptyDir := t.TempDir() + if _, _, err := resolvePulledArtifact(emptyDir); err == nil || !strings.Contains(err.Error(), "no .img") { + t.Fatalf("expected empty artifact dir error, got %v", err) + } + if err := orasPush("ref", filepath.Join(artifactDir, "one", "a.img"), filepath.Join(artifactDir, "two", "metadata.json")); err == nil || !strings.Contains(err.Error(), "one directory") { + t.Fatalf("expected push invocation error, got %v", err) + } +} + +func TestRemoteCommandFatalEdges(t *testing.T) { + expectFatal := func(t *testing.T, want string, fn func()) { + t.Helper() + oldFatalf := fatalf + fatalf = func(format string, args ...any) { + panic(fmt.Sprintf(format, args...)) + } + defer func() { fatalf = oldFatalf }() + defer func() { + got := recover() + if got == nil { + t.Fatalf("expected fatal containing %q", want) + } + if !strings.Contains(fmt.Sprint(got), want) { + t.Fatalf("fatal = %v, want %q", got, want) + } + }() + fn() + } + + t.Run("remote devices lsblk failure", func(t *testing.T) { + dir := fakeCommandDir(t, map[string]string{"lsblk": `exit 2`}) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + expectFatal(t, "remote devices", func() { remoteDevicesCmd(nil) }) + }) + + t.Run("remote build mkdir failure", func(t *testing.T) { + root := t.TempDir() + blocker := filepath.Join(root, "blocker") + if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + expectFatal(t, "mkdir workdir", func() { + remoteBuildCmd([]string{"--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(blocker, "child")}) + }) + }) + + t.Run("remote build plan failure", func(t *testing.T) { + root := t.TempDir() + invPath, _ := writeTestInventory(t, root) + expectFatal(t, "build plan", func() { + remoteBuildCmd([]string{"--inventory", invPath, "--node", "missing", "--artifact-ref", "registry.example/metis/missing", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "build")}) + }) + }) + + t.Run("remote flash mkdir failure", func(t *testing.T) { + root := t.TempDir() + blocker := filepath.Join(root, "blocker") + if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + expectFatal(t, "mkdir workdir", func() { + remoteFlashCmd([]string{"--node", "titan-15", "--device", "/dev/sdz", "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(blocker, "child")}) + }) + }) + + t.Run("remote flash missing credentials", func(t *testing.T) { + expectFatal(t, "oras login", func() { + remoteFlashCmd([]string{"--node", "titan-15", "--device", "/dev/sdz", "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", t.TempDir()}) + }) + }) +} + +func TestPlanAndBurnCommandFlagBranches(t *testing.T) { + root := t.TempDir() + invPath, _ := writeTestInventory(t, root) + boot := filepath.Join(root, "boot") + rootfs := filepath.Join(root, "root") + stdout, _ := captureStreams(t, func() { + planCmd([]string{"--inventory", invPath, "--node", "titan-15", "--boot", boot, "--root", rootfs}) + }) + if !strings.Contains(stdout, `"node": "titan-15"`) || os.Getenv("METIS_BOOT_PATH") != boot || os.Getenv("METIS_ROOT_PATH") != rootfs { + t.Fatalf("plan command did not apply boot/root flags: stdout=%s", stdout) + } + + fakeTools := fakeCommandDir(t, map[string]string{ + "sfdisk": `cat <<'JSON' +{"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}} +JSON`, + "debugfs": `exit 0`, + }) + t.Setenv("PATH", fakeTools+string(os.PathListSeparator)+os.Getenv("PATH")) + stdout, _ = captureStreams(t, func() { + burnCmd([]string{"--inventory", invPath, "--node", "titan-15", "--device", filepath.Join(root, "out.img"), "--boot", boot, "--root", rootfs, "--auto-mount", "--yes"}) + }) + if !strings.Contains(stdout, "Burn complete") || os.Getenv("METIS_AUTO_MOUNT") != "1" { + t.Fatalf("burn command did not complete confirmed path: stdout=%s", stdout) + } +} + +func TestPlanBurnAndRemoteBuildFatalStageEdges(t *testing.T) { + root := t.TempDir() + invPath, baseImage := writeTestInventory(t, root) + expectCommandFatal(t, "--node is required", func() { planCmd(nil) }) + expectCommandFatal(t, "build plan", func() { + planCmd([]string{"--inventory", invPath, "--node", "missing"}) + }) + expectCommandFatal(t, "--node and --device are required", func() { burnCmd(nil) }) + expectCommandFatal(t, "burn", func() { + burnCmd([]string{"--inventory", invPath, "--node", "missing", "--device", filepath.Join(root, "out.img")}) + }) + + t.Run("remote build download checksum failure", func(t *testing.T) { + badInv := filepath.Join(root, "bad-inventory.yaml") + data, err := os.ReadFile(invPath) + if err != nil { + t.Fatal(err) + } + data = []byte(strings.Replace(string(data), "checksum: sha256:"+sha256SumHex(t, make([]byte, 4096)), "checksum: sha256:0000", 1)) + if err := os.WriteFile(badInv, data, 0o644); err != nil { + t.Fatal(err) + } + expectCommandFatal(t, "download image", func() { + remoteBuildCmd([]string{"--inventory", badInv, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "download-fail"), "--cache", filepath.Join(root, "cache-download")}) + }) + }) + + t.Run("remote build xz failure", func(t *testing.T) { + tools := remoteBuildToolDir(t, baseImage, `exit 9`, `exit 0`) + t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH")) + expectCommandFatal(t, "xz compress", func() { + remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "xz-fail"), "--cache", filepath.Join(root, "cache-xz"), "--harbor-username", "admin", "--harbor-password", "pw"}) + }) + }) + + t.Run("remote build missing compressed artifact", func(t *testing.T) { + tools := remoteBuildToolDir(t, baseImage, `exit 0`, `exit 0`) + t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH")) + expectCommandFatal(t, "stat compressed image", func() { + remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "stat-fail"), "--cache", filepath.Join(root, "cache-stat"), "--harbor-username", "admin", "--harbor-password", "pw"}) + }) + }) + + t.Run("remote build missing harbor credentials", func(t *testing.T) { + tools := remoteBuildToolDir(t, baseImage, `cp "${@: -1}" "${@: -1}.xz"`, `exit 0`) + t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("METIS_HARBOR_USERNAME", "") + t.Setenv("METIS_HARBOR_PASSWORD", "") + expectCommandFatal(t, "oras login", func() { + remoteBuildCmd([]string{"--inventory", invPath, "--node", "titan-15", "--artifact-ref", "registry.example/metis/titan-15", "--build-tag", "build-1", "--work-dir", filepath.Join(root, "login-fail"), "--cache", filepath.Join(root, "cache-login")}) + }) + }) +} + +func TestRemoteFlashFatalAndDeviceFlushEdges(t *testing.T) { + root := t.TempDir() + rawTools := fakeCommandDir(t, map[string]string{ + "oras": `case "${1:-}" in + login) exit 0 ;; + pull) outdir="${@: -1}"; printf 'image' > "${outdir}/artifact.img"; exit 0 ;; +esac +exit 0`, + "sync": `exit 0`, + "blockdev": `exit 0`, + }) + t.Setenv("PATH", rawTools+string(os.PathListSeparator)+os.Getenv("PATH")) + + blocker := filepath.Join(root, "blocker") + if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + expectCommandFatal(t, "mkdir host tmp dir", func() { + remoteFlashCmd([]string{"--node", "titan-15", "--device", "hosttmp:///tmp", "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-hosttmp"), "--host-tmp-dir", filepath.Join(blocker, "child"), "--harbor-username", "admin", "--harbor-password", "pw"}) + }) + expectCommandFatal(t, "write image", func() { + remoteFlashCmd([]string{"--node", "titan-15", "--device", filepath.Join(blocker, "child"), "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-write"), "--harbor-username", "admin", "--harbor-password", "pw"}) + }) + + pullFailTools := fakeCommandDir(t, map[string]string{ + "oras": `case "${1:-}" in + login) exit 0 ;; + pull) printf 'pull failed' >&2; exit 4 ;; +esac +exit 0`, + }) + t.Setenv("PATH", pullFailTools+string(os.PathListSeparator)+os.Getenv("PATH")) + expectCommandFatal(t, "oras pull", func() { + remoteFlashCmd([]string{"--node", "titan-15", "--device", filepath.Join(root, "out.img"), "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-pull"), "--harbor-username", "admin", "--harbor-password", "pw"}) + }) + + emptyPullTools := fakeCommandDir(t, map[string]string{ + "oras": `case "${1:-}" in + login) exit 0 ;; + pull) exit 0 ;; +esac +exit 0`, + }) + t.Setenv("PATH", emptyPullTools+string(os.PathListSeparator)+os.Getenv("PATH")) + expectCommandFatal(t, "resolve artifact", func() { + remoteFlashCmd([]string{"--node", "titan-15", "--device", filepath.Join(root, "out.img"), "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-empty"), "--harbor-username", "admin", "--harbor-password", "pw"}) + }) + + compressedTools := fakeCommandDir(t, map[string]string{ + "oras": `case "${1:-}" in + login) exit 0 ;; + pull) outdir="${@: -1}"; printf 'compressed' > "${outdir}/artifact.img.xz"; exit 0 ;; +esac +exit 0`, + "xz": `exit 8`, + }) + t.Setenv("PATH", compressedTools+string(os.PathListSeparator)+os.Getenv("PATH")) + expectCommandFatal(t, "xz stream decompress", func() { + remoteFlashCmd([]string{"--node", "titan-15", "--device", filepath.Join(root, "out.img"), "--artifact-ref", "registry.example/metis/titan-15", "--work-dir", filepath.Join(root, "flash-xz"), "--harbor-username", "admin", "--harbor-password", "pw"}) + }) +} + +func TestCaptureStreamsAllowsBinaryOutput(t *testing.T) { + out, _ := captureStreams(t, func() { + _, _ = io.Copy(os.Stdout, bytes.NewBuffer([]byte("ok"))) + }) + if out != "ok" { + t.Fatalf("captureStreams = %q", out) + } + expectCommandFatal(t, "encode result", func() { + writeStructuredResult(map[string]any{"bad": func() {}}) + }) +} + +func expectCommandFatal(t *testing.T, want string, fn func()) { + t.Helper() + oldFatalf := fatalf + fatalf = func(format string, args ...any) { + panic(fmt.Sprintf(format, args...)) + } + defer func() { fatalf = oldFatalf }() + defer func() { + got := recover() + if got == nil { + t.Fatalf("expected fatal containing %q", want) + } + if !strings.Contains(fmt.Sprint(got), want) { + t.Fatalf("fatal = %v, want %q", got, want) + } + }() + fn() +} + +func remoteBuildToolDir(t *testing.T, baseImage, xzBody, orasBody string) string { + t.Helper() + return fakeCommandDir(t, map[string]string{ + "sfdisk": `cat <<'JSON' +{"partitiontable":{"sectorsize":512,"partitions":[{"start":3,"size":1,"type":"ef"},{"start":1,"size":2,"type":"83"}]}} +JSON`, + "debugfs": `if [[ "${1:-}" == "-w" ]]; then + cp "${3:-}" "${4:-}.commands" + exit 0 +fi +if [[ "${1:-}" == "-R" ]]; then + state="${3:-}.commands" + set -- $2 + case "${1:-}" in + stat) + mode="$(awk -v path="${2:-}" '$1=="sif" && $2==path {print $4}' "${state}" | tail -n1)" + mode="${mode: -4}" + printf 'Mode: %s\n' "${mode}" + exit 0 + ;; + dump) + local_path="$(awk -v path="${2:-}" '$1=="write" && $3==path {print $2}' "${state}" | tail -n1)" + cat "${local_path}" > "${3:-}" + exit 0 + ;; + esac +fi +exit 0`, + "xz": xzBody, + "oras": orasBody, + "sync": `exit 0`, + "blockdev": `exit 0`, + "cp-base": `cp "` + baseImage + `" "$1"`, + }) +} diff --git a/pkg/sentinel/collector_test.go b/pkg/sentinel/collector_test.go index c3129d6..7f594cd 100644 --- a/pkg/sentinel/collector_test.go +++ b/pkg/sentinel/collector_test.go @@ -63,6 +63,108 @@ func TestRunAndTrimAndPkgVersionFallbacks(t *testing.T) { } } +func TestCollectUSBScratchMissingAndMalformedConfig(t *testing.T) { + cases := []struct { + name string + cat string + }{ + {name: "cat error", cat: `exit 1`}, + {name: "empty config", cat: `printf ' '`}, + {name: "malformed config", cat: `printf '{'`}, + {name: "missing scratch config", cat: `printf '{}'`}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dir := fakeCollectorCommands(t, map[string]string{"cat": tc.cat}) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + if got := collectUSBScratch(); got != nil { + t.Fatalf("expected nil USB scratch config, got %#v", got) + } + }) + } +} + +func TestCollectUSBScratchDeviceResolutionAndBindHealth(t *testing.T) { + dir := fakeCollectorCommands(t, map[string]string{ + "cat": `printf '%s\n' '{"usb_scratch":{"mountpoint":"/mnt/scratch","uuid":"u1","label":"scratch","fs":"xfs","bind_targets":["","/bad","/bind"]}}'`, + "findmnt": `target="" +for ((i=1; i<=$#; i++)); do + if [[ "${!i}" == "-T" ]]; then + j=$((i + 1)) + target="${!j}" + break + fi +done +case "${target}" in + /mnt/scratch) printf 'SOURCE="" TARGET="/mnt/scratch" FSTYPE="ext4"\n' ;; + /bind) printf 'SOURCE="/mnt/scratch" TARGET="/bind" FSTYPE="none"\n' ;; + *) exit 1 ;; +esac`, + "blkid": `case "${1:-}" in + -U) printf '/dev/by-uuid/u1\n' ;; + -L) printf '/dev/by-label/scratch\n' ;; + -o) printf 'UUID=other\nLABEL=scratch\nTYPE=xfs\n' ;; +esac`, + }) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + + scratch := collectUSBScratch() + if scratch == nil { + t.Fatal("expected USB scratch data") + } + if scratch.MountHealthy || scratch.UUIDHealthy || !scratch.LabelHealthy || scratch.BindHealthy { + t.Fatalf("unexpected scratch health: %#v", scratch) + } + if len(scratch.BindTargets) != 3 || scratch.BindTargets[2].Path != "/bind" || !scratch.BindTargets[2].Healthy { + t.Fatalf("unexpected bind target health: %#v", scratch.BindTargets) + } +} + +func TestCollectUSBScratchDefaultsAndParserEdges(t *testing.T) { + dir := fakeCollectorCommands(t, map[string]string{ + "cat": `printf '%s\n' '{"usb_scratch":{"mountpoint":"/mnt/scratch"}}'`, + "findmnt": `printf 'SOURCE="/dev/sdz1" TARGET="/mnt/scratch" FSTYPE="ext4"\n'`, + "blkid": `exit 1`, + }) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + + scratch := collectUSBScratch() + if scratch == nil || !scratch.MountHealthy || !scratch.BindHealthy || scratch.FS != "ext4" { + t.Fatalf("unexpected default scratch health: %#v", scratch) + } + if source, fsType, mounted := mountInfo(""); source != "" || fsType != "" || mounted { + t.Fatalf("empty mountInfo = %q %q %v", source, fsType, mounted) + } + if bindHealthy("", "/mnt/scratch") || bindHealthy("/bind", "") { + t.Fatal("empty bind inputs should be unhealthy") + } + if got := resolveDeviceByUUID(""); got != "" { + t.Fatalf("empty UUID resolved to %q", got) + } + if got := resolveDeviceByLabel(""); got != "" { + t.Fatalf("empty label resolved to %q", got) + } + if got := blkidExport(""); len(got) != 0 { + t.Fatalf("empty blkid export = %#v", got) + } + values := parseKeyValues(`BROKEN A="b" C=d`) + if values["A"] != "b" || values["C"] != "d" { + t.Fatalf("parseKeyValues = %#v", values) + } +} + +func fakeCollectorCommands(t *testing.T, scripts map[string]string) string { + t.Helper() + dir := t.TempDir() + for name, body := range scripts { + path := filepath.Join(dir, name) + if err := os.WriteFile(path, []byte("#!/usr/bin/env bash\nset -eu\n"+body+"\n"), 0o755); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + return dir +} + func fakeSentinelCommands(t *testing.T) string { t.Helper() dir := t.TempDir()