test(metis): complete per-file coverage gate
This commit is contained in:
parent
096735fe89
commit
ef9124e5c0
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
@ -57,11 +58,22 @@ func TestSentinelHelpers(t *testing.T) {
|
|||||||
if got := getenvInt("METIS_SENTINEL_INTERVAL_SEC", 300); got != 300 {
|
if got := getenvInt("METIS_SENTINEL_INTERVAL_SEC", 300); got != 300 {
|
||||||
t.Fatalf("getenvInt fallback = %d", got)
|
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")
|
t.Setenv("METIS_SENTINEL_INTERVAL_SEC", "5")
|
||||||
if got := getenvInt("METIS_SENTINEL_INTERVAL_SEC", 300); got != 5 {
|
if got := getenvInt("METIS_SENTINEL_INTERVAL_SEC", 300); got != 5 {
|
||||||
t.Fatalf("getenvInt = %d", got)
|
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()
|
dir := t.TempDir()
|
||||||
snap := &sentinel.Snapshot{Hostname: "titan-13", Kernel: "6.6.63"}
|
snap := &sentinel.Snapshot{Hostname: "titan-13", Kernel: "6.6.63"}
|
||||||
writeHistory(dir, snap)
|
writeHistory(dir, snap)
|
||||||
@ -87,6 +99,49 @@ func TestSentinelHelpers(t *testing.T) {
|
|||||||
if err := pushSnapshot(srv.URL, snap); err != nil {
|
if err := pushSnapshot(srv.URL, snap); err != nil {
|
||||||
t.Fatalf("pushSnapshot: %v", err)
|
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 {
|
func fakeSentinelCommands(t *testing.T) string {
|
||||||
|
|||||||
400
cmd/metis/remote_edge_test.go
Normal file
400
cmd/metis/remote_edge_test.go
Normal file
@ -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"`,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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 {
|
func fakeSentinelCommands(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user