481 lines
18 KiB
Go
481 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"metis/pkg/facts"
|
|
"metis/pkg/inventory"
|
|
"metis/pkg/sentinel"
|
|
)
|
|
|
|
func TestSettingsHelpersAndSmallUtilities(t *testing.T) {
|
|
dataDir := filepath.Join(t.TempDir(), "data")
|
|
t.Setenv("METIS_DATA_DIR", dataDir)
|
|
t.Setenv("METIS_FLASH_HOSTS", "a, b,, c")
|
|
t.Setenv("METIS_MAX_DEVICE_BYTES", "12345")
|
|
t.Setenv("METIS_DEFAULT_FLASH_HOST", "flash-1")
|
|
t.Setenv("METIS_LOCAL_HOST", "local-1")
|
|
t.Setenv("METIS_REMOTE_POD_TIMEOUT_SEC", "1800")
|
|
|
|
settings := FromEnv()
|
|
if got, want := settings.CacheDir, filepath.Join(dataDir, "cache"); got != want {
|
|
t.Fatalf("CacheDir = %q, want %q", got, want)
|
|
}
|
|
if settings.DefaultFlashHost != "flash-1" || settings.LocalHost != "local-1" {
|
|
t.Fatalf("unexpected env settings: %+v", settings)
|
|
}
|
|
if settings.MaxDeviceBytes != 12345 {
|
|
t.Fatalf("expected MaxDeviceBytes=12345, got %d", settings.MaxDeviceBytes)
|
|
}
|
|
if settings.RemotePodTimeout != 1800 {
|
|
t.Fatalf("expected RemotePodTimeout=1800, got %d", settings.RemotePodTimeout)
|
|
}
|
|
if !reflect.DeepEqual(splitList("a, b,, c"), []string{"a", "b", "c"}) {
|
|
t.Fatalf("splitList mismatch")
|
|
}
|
|
if got := getenvInt64("METIS_MAX_DEVICE_BYTES", 1); got != 12345 {
|
|
t.Fatalf("getenvInt64 = %d", got)
|
|
}
|
|
if got := hostnameOr("fallback"); got == "" {
|
|
t.Fatal("hostnameOr returned empty string")
|
|
}
|
|
if got := humanBytes(1536); got != "1.5 KiB" {
|
|
t.Fatalf("humanBytes = %q", got)
|
|
}
|
|
if got := firstLine("alpha\nbeta"); got != "alpha" {
|
|
t.Fatalf("firstLine = %q", got)
|
|
}
|
|
if got := moveToFront([]string{"b", "a", "c"}, "a"); !reflect.DeepEqual(got, []string{"a", "b", "c"}) {
|
|
t.Fatalf("moveToFront = %#v", got)
|
|
}
|
|
if got := errorString(nil); got != "" {
|
|
t.Fatalf("errorString(nil) = %q", got)
|
|
}
|
|
if got := preferredDevice([]Device{{Path: "/dev/sda"}}); got != "/dev/sda" {
|
|
t.Fatalf("preferredDevice = %q", got)
|
|
}
|
|
if got := deviceScore(Device{Name: "mmcblk0", Model: "Micro SD card", Removable: true, Hotplug: true, Transport: "usb"}); got <= 0 {
|
|
t.Fatalf("expected positive device score, got %d", got)
|
|
}
|
|
if got := inventoryNodeArch(nil, nil); got != "arm64" {
|
|
t.Fatalf("inventoryNodeArch fallback = %q", got)
|
|
}
|
|
if got := (&App{settings: Settings{HarborRegistry: "reg/", HarborProject: "/proj/"}}).artifactRepo("node"); got != "reg/proj/node" {
|
|
t.Fatalf("artifactRepo = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAppJobDeviceAndStateHelpers(t *testing.T) {
|
|
app := newTestApp(t)
|
|
app.settings.HistoryPath = filepath.Join(t.TempDir(), "history.jsonl")
|
|
app.settings.ArtifactStatePath = filepath.Join(t.TempDir(), "artifacts.json")
|
|
|
|
job := app.newJob("build", "titan-15", "titan-22", "/dev/sdz")
|
|
if job.Status != JobQueued {
|
|
t.Fatalf("new job status = %s", job.Status)
|
|
}
|
|
app.setJob(job.ID, func(j *Job) {
|
|
j.Status = JobRunning
|
|
j.Stage = "build"
|
|
})
|
|
if got := app.job(job.ID); got == nil || got.Status != JobRunning {
|
|
t.Fatalf("setJob did not update job: %#v", got)
|
|
}
|
|
app.completeJob(job.ID, func(j *Job) {
|
|
j.Message = "done"
|
|
})
|
|
if got := app.job(job.ID); got == nil || got.Status != JobDone || got.FinishedAt.IsZero() {
|
|
t.Fatalf("completeJob did not finish job: %#v", got)
|
|
}
|
|
failed := app.newJob("replace", "titan-15", "titan-22", "/dev/sdz")
|
|
app.failJob(failed.ID, os.ErrNotExist)
|
|
if got := app.job(failed.ID); got == nil || got.Status != JobError || got.Error == "" {
|
|
t.Fatalf("failJob did not mark error: %#v", got)
|
|
}
|
|
|
|
app.appendEvent(Event{Kind: "one", Summary: "first"})
|
|
app.appendEvent(Event{Kind: "two", Summary: "second"})
|
|
events := app.recentEvents(1)
|
|
if len(events) != 1 || events[0].Kind != "two" {
|
|
t.Fatalf("recentEvents returned %#v", events)
|
|
}
|
|
|
|
app.recordDevices("titan-22", []Device{{Name: "sda", Path: "/dev/sda"}}, nil)
|
|
devices, err := app.cachedDevices("titan-22")
|
|
if err != nil || len(devices) != 1 || devices[0].Path != "/dev/sda" {
|
|
t.Fatalf("cachedDevices = %#v err=%v", devices, err)
|
|
}
|
|
devices[0].Path = "/dev/mutated"
|
|
again, _ := app.cachedDevices("titan-22")
|
|
if again[0].Path != "/dev/sda" {
|
|
t.Fatalf("cachedDevices should return a copy, got %#v", again)
|
|
}
|
|
|
|
app.recordDevices("titan-22", nil, os.ErrPermission)
|
|
if _, err := app.cachedDevices("titan-22"); err == nil {
|
|
t.Fatal("expected cached device error")
|
|
}
|
|
|
|
app.recordDevices("titan-22", []Device{{Path: "/dev/sda"}}, nil)
|
|
state := app.State("titan-22")
|
|
if state.SelectedHost != "titan-22" || state.PreferredDevice == "" {
|
|
t.Fatalf("unexpected state: %+v", state)
|
|
}
|
|
}
|
|
|
|
func TestBuildAndReplaceRejectDuplicateActiveNodeJobs(t *testing.T) {
|
|
app := newTestApp(t)
|
|
active := app.newJob("replace", "titan-15", "titan-22", "/dev/sdz")
|
|
if _, err := app.Build("titan-15"); err == nil || !strings.Contains(err.Error(), active.ID) {
|
|
t.Fatalf("expected build conflict mentioning %s, got %v", active.ID, err)
|
|
}
|
|
if _, err := app.Replace("titan-15", "titan-22", "/dev/sdz"); err == nil || !strings.Contains(err.Error(), "active replace job") {
|
|
t.Fatalf("expected replace conflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAppPersistenceAndTargets(t *testing.T) {
|
|
dir := t.TempDir()
|
|
invPath := filepath.Join(dir, "inventory.yaml")
|
|
if err := os.WriteFile(invPath, []byte(`
|
|
classes:
|
|
- name: rpi4
|
|
arch: arm64
|
|
os: armbian
|
|
image: file:///tmp/base.img
|
|
nodes:
|
|
- name: titan-15
|
|
class: rpi4
|
|
hostname: titan-15
|
|
ip: 192.168.22.43
|
|
k3s_role: agent
|
|
`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snapshotsPath := filepath.Join(dir, "snapshots.json")
|
|
targetsPath := filepath.Join(dir, "targets.json")
|
|
artifactStatePath := filepath.Join(dir, "artifacts.json")
|
|
|
|
seedSnapshots := map[string]SnapshotRecord{
|
|
"titan-15": {
|
|
Node: "titan-15",
|
|
CollectedAt: testTime(t),
|
|
Snapshot: sentinel.Snapshot{Hostname: "titan-15", Kernel: "6.6.63", K3sVersion: "v1.31.5+k3s1"},
|
|
},
|
|
}
|
|
data, _ := json.MarshalIndent(seedSnapshots, "", " ")
|
|
if err := os.WriteFile(snapshotsPath, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
seedTargets := map[string]facts.Targets{
|
|
"rpi4": {Kernel: "6.6.63"},
|
|
}
|
|
data, _ = json.MarshalIndent(seedTargets, "", " ")
|
|
if err := os.WriteFile(targetsPath, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
seedArtifacts := map[string]ArtifactSummary{
|
|
"titan-15": {Node: "titan-15", Ref: "reg/proj/titan-15:latest"},
|
|
}
|
|
data, _ = json.MarshalIndent(seedArtifacts, "", " ")
|
|
if err := os.WriteFile(artifactStatePath, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
app, err := NewApp(Settings{
|
|
InventoryPath: invPath,
|
|
CacheDir: filepath.Join(dir, "cache"),
|
|
ArtifactDir: filepath.Join(dir, "artifacts"),
|
|
ArtifactStatePath: artifactStatePath,
|
|
HistoryPath: filepath.Join(dir, "history.jsonl"),
|
|
SnapshotsPath: snapshotsPath,
|
|
TargetsPath: targetsPath,
|
|
DefaultFlashHost: "titan-22",
|
|
FlashHosts: []string{"titan-22"},
|
|
LocalHost: "titan-22",
|
|
AllowedGroups: []string{"admin"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewApp: %v", err)
|
|
}
|
|
|
|
if got := app.artifacts()["titan-15"].Ref; got != "reg/proj/titan-15:latest" {
|
|
t.Fatalf("artifacts() = %q", got)
|
|
}
|
|
if err := app.recordArtifact(ArtifactSummary{Node: "titan-15", Ref: "reg/proj/titan-15:v2"}); err != nil {
|
|
t.Fatalf("recordArtifact: %v", err)
|
|
}
|
|
if err := app.loadArtifacts(); err != nil {
|
|
t.Fatalf("loadArtifacts: %v", err)
|
|
}
|
|
if got := app.artifacts()["titan-15"].Ref; got != "reg/proj/titan-15:v2" {
|
|
t.Fatalf("recordArtifact/persist mismatch: %q", got)
|
|
}
|
|
|
|
if err := app.StoreSnapshot(SnapshotRecord{Node: "titan-15", Snapshot: sentinel.Snapshot{Hostname: "titan-15"}}); err != nil {
|
|
t.Fatalf("StoreSnapshot: %v", err)
|
|
}
|
|
if event, err := app.WatchSentinel(); err != nil || event == nil || event.Kind != "sentinel.watch" {
|
|
t.Fatalf("WatchSentinel: event=%#v err=%v", event, err)
|
|
}
|
|
}
|
|
|
|
func TestHelperBranchesAndPersistenceFailures(t *testing.T) {
|
|
app := newTestApp(t)
|
|
|
|
if got := cachedImageName("/tmp/archive/base.img.xz"); got != "base.img" {
|
|
t.Fatalf("cachedImageName = %q", got)
|
|
}
|
|
if got := humanBytes(1); got != "1 B" {
|
|
t.Fatalf("humanBytes(1) = %q", got)
|
|
}
|
|
if got := humanBytes(1024 * 1024); got != "1.0 MiB" {
|
|
t.Fatalf("humanBytes(1MiB) = %q", got)
|
|
}
|
|
if got := errorString(fmt.Errorf("boom")); got != "boom" {
|
|
t.Fatalf("errorString = %q", got)
|
|
}
|
|
if got := moveToFront([]string{"a", "b", "c"}, "missing"); !reflect.DeepEqual(got, []string{"a", "b", "c"}) {
|
|
t.Fatalf("moveToFront missing = %#v", got)
|
|
}
|
|
if targetsEqual(facts.Targets{Kernel: "a"}, facts.Targets{Kernel: "b"}) {
|
|
t.Fatal("targetsEqual should reject differing kernels")
|
|
}
|
|
if got := deviceScore(Device{Name: "reader", Model: "Card reader", Transport: "usb", Removable: true, Hotplug: true}); got < 75 {
|
|
t.Fatalf("unexpected deviceScore: %d", got)
|
|
}
|
|
|
|
if got := cachedImageName("foo.xz"); got != "foo" {
|
|
t.Fatalf("cachedImageName alias = %q", got)
|
|
}
|
|
if got := app.flashHosts(); len(got) == 0 {
|
|
t.Fatal("flashHosts returned empty list")
|
|
}
|
|
|
|
app.settings.SnapshotsPath = filepath.Join(t.TempDir(), "missing", "snapshots.json")
|
|
if err := app.loadSnapshots(); err == nil {
|
|
t.Fatal("expected loadSnapshots error for missing file")
|
|
}
|
|
app.settings.TargetsPath = filepath.Join(t.TempDir(), "missing", "targets.json")
|
|
if err := app.loadTargets(); err == nil {
|
|
t.Fatal("expected loadTargets error for missing file")
|
|
}
|
|
app.settings.ArtifactStatePath = filepath.Join(t.TempDir(), "missing", "artifacts.json")
|
|
if err := app.loadArtifacts(); err == nil {
|
|
t.Fatal("expected loadArtifacts error for missing file")
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
app.settings.SnapshotsPath = tmpDir
|
|
if err := app.persistSnapshots(); err == nil {
|
|
t.Fatal("expected persistSnapshots error when path is a directory")
|
|
}
|
|
app.settings.TargetsPath = tmpDir
|
|
if err := app.persistTargets(); err == nil {
|
|
t.Fatal("expected persistTargets error when path is a directory")
|
|
}
|
|
app.settings.ArtifactStatePath = tmpDir
|
|
if err := app.persistArtifacts(); err == nil {
|
|
t.Fatal("expected persistArtifacts error when path is a directory")
|
|
}
|
|
|
|
if err := app.StoreSnapshot(SnapshotRecord{}); err == nil {
|
|
t.Fatal("expected snapshot validation error")
|
|
}
|
|
blockedSnapshotParent := filepath.Join(t.TempDir(), "blocked-snapshot-parent")
|
|
if err := os.WriteFile(blockedSnapshotParent, []byte("block"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app.settings.SnapshotsPath = filepath.Join(blockedSnapshotParent, "valid-snapshot.json")
|
|
if err := app.StoreSnapshot(SnapshotRecord{Node: "titan-15"}); err == nil {
|
|
t.Fatal("expected snapshot persistence error")
|
|
}
|
|
if _, err := app.Build("missing"); err == nil {
|
|
t.Fatal("expected Build to reject unknown node")
|
|
}
|
|
if _, err := app.Replace("missing", "", ""); err == nil {
|
|
t.Fatal("expected Replace to reject unknown node")
|
|
}
|
|
}
|
|
|
|
func TestNewAppReportsInventoryErrors(t *testing.T) {
|
|
settings := Settings{
|
|
InventoryPath: filepath.Join(t.TempDir(), "missing.yaml"),
|
|
CacheDir: t.TempDir(),
|
|
ArtifactDir: t.TempDir(),
|
|
ArtifactStatePath: filepath.Join(t.TempDir(), "artifacts.json"),
|
|
HistoryPath: filepath.Join(t.TempDir(), "history.jsonl"),
|
|
SnapshotsPath: filepath.Join(t.TempDir(), "snapshots.json"),
|
|
TargetsPath: filepath.Join(t.TempDir(), "targets.json"),
|
|
}
|
|
if _, err := NewApp(settings); err == nil {
|
|
t.Fatal("expected NewApp inventory error")
|
|
}
|
|
}
|
|
|
|
func TestNewAppReportsDirectoryCreationErrors(t *testing.T) {
|
|
dir := t.TempDir()
|
|
inventoryPath := filepath.Join(dir, "inventory.yaml")
|
|
if err := os.WriteFile(inventoryPath, []byte("classes: []\nnodes: []\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
blocked := filepath.Join(dir, "blocked")
|
|
if err := os.WriteFile(blocked, []byte("not-a-dir"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
base := Settings{
|
|
InventoryPath: inventoryPath,
|
|
CacheDir: filepath.Join(dir, "cache"),
|
|
ArtifactDir: filepath.Join(dir, "artifacts"),
|
|
ArtifactStatePath: filepath.Join(dir, "artifacts.json"),
|
|
HistoryPath: filepath.Join(dir, "history.jsonl"),
|
|
SnapshotsPath: filepath.Join(dir, "snapshots.json"),
|
|
TargetsPath: filepath.Join(dir, "targets.json"),
|
|
}
|
|
for name, mutate := range map[string]func(*Settings){
|
|
"cache": func(s *Settings) { s.CacheDir = filepath.Join(blocked, "cache") },
|
|
"artifact": func(s *Settings) {
|
|
s.ArtifactDir = filepath.Join(blocked, "artifacts")
|
|
},
|
|
"history": func(s *Settings) {
|
|
s.HistoryPath = filepath.Join(blocked, "history.jsonl")
|
|
},
|
|
} {
|
|
settings := base
|
|
mutate(&settings)
|
|
if _, err := NewApp(settings); err == nil {
|
|
t.Fatalf("expected NewApp %s directory error", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppHelperNoopAndInvalidStateBranches(t *testing.T) {
|
|
app := newTestApp(t)
|
|
app.setJob("missing", func(*Job) { t.Fatal("setJob should not run for missing job") })
|
|
app.completeJob("missing", func(*Job) { t.Fatal("completeJob should not run for missing job") })
|
|
app.failJob("missing", os.ErrNotExist)
|
|
if got := (*activeNodeJobError)(nil).Error(); !strings.Contains(got, "active metis job") {
|
|
t.Fatalf("nil activeNodeJobError = %q", got)
|
|
}
|
|
if got := app.activeJobForNode(""); got != nil {
|
|
t.Fatalf("activeJobForNode empty = %#v", got)
|
|
}
|
|
now := time.Now()
|
|
app.jobs["ignored"] = nil
|
|
app.jobs["done"] = &Job{ID: "done", Kind: "build", Node: "titan-15", Status: JobDone, StartedAt: now}
|
|
app.jobs["other-kind"] = &Job{ID: "other-kind", Kind: "watch", Node: "titan-15", Status: JobRunning, StartedAt: now}
|
|
app.jobs["newer"] = &Job{ID: "newer", Kind: "replace", Node: "titan-15", Status: JobRunning, StartedAt: now}
|
|
app.jobs["older"] = &Job{ID: "older", Kind: "build", Node: "titan-15", Status: JobQueued, StartedAt: now.Add(-time.Minute)}
|
|
if got := app.activeJobForNode("titan-15"); got == nil || got.ID != "older" {
|
|
t.Fatalf("activeJobForNode earliest active = %#v", got)
|
|
}
|
|
if _, err := app.reserveJob("build", "titan-15", "", ""); err == nil {
|
|
t.Fatal("expected reserveJob to reject active node")
|
|
}
|
|
app.appendEvent(Event{Kind: "bad", Details: map[string]any{"bad": make(chan int)}})
|
|
|
|
if replacementReady(nil, nil) {
|
|
t.Fatal("replacementReady nil should be false")
|
|
}
|
|
if replacementReady(&inventory.NodeSpec{}, &inventory.NodeClass{}) {
|
|
t.Fatal("replacementReady empty should be false")
|
|
}
|
|
app.inventory = &inventory.Inventory{}
|
|
if got := app.replacementNodes(); len(got) != 0 {
|
|
t.Fatalf("replacementNodes empty inventory = %#v", got)
|
|
}
|
|
app.inventory = &inventory.Inventory{
|
|
Classes: []inventory.NodeClass{{Name: "ready", Image: "img", Checksum: "sum"}},
|
|
Nodes: []inventory.NodeSpec{
|
|
{Name: "z-node", Class: "ready", Hostname: "z-node", IP: "192.168.22.11", K3sRole: "server", K3sToken: "token", SSHUser: "atlas", SSHAuthorized: []string{"ssh-ed25519 AAA"}},
|
|
{Name: "a-node", Class: "ready", Hostname: "a-node", IP: "192.168.22.10", K3sRole: "server", K3sToken: "token", SSHUser: "atlas", SSHAuthorized: []string{"ssh-ed25519 AAA"}},
|
|
{Name: "missing-class", Class: "missing", Hostname: "missing-class", IP: "192.168.22.12", K3sRole: "server", K3sToken: "token", SSHUser: "atlas", SSHAuthorized: []string{"ssh-ed25519 AAA"}},
|
|
},
|
|
}
|
|
if got := app.replacementNodes(); len(got) != 2 || got[0].Name != "a-node" || got[1].Name != "z-node" {
|
|
t.Fatalf("replacementNodes sorted/filtered = %#v", got)
|
|
}
|
|
app.settings.FlashHosts = []string{"titan-22"}
|
|
app.settings.DefaultFlashHost = ""
|
|
if got := app.flashHosts(); len(got) == 0 {
|
|
t.Fatal("flashHosts should still include cluster nodes")
|
|
}
|
|
if !replacementReady(&inventory.NodeSpec{
|
|
Name: "ready",
|
|
Hostname: "ready",
|
|
IP: "192.168.22.10",
|
|
K3sRole: "agent",
|
|
K3sURL: "https://192.168.22.1:6443",
|
|
K3sToken: "token",
|
|
SSHUser: "atlas",
|
|
SSHAuthorized: []string{"ssh-ed25519 AAA"},
|
|
}, &inventory.NodeClass{Image: "img", Checksum: "sum"}) {
|
|
t.Fatal("replacementReady valid node should be true")
|
|
}
|
|
for name, node := range map[string]inventory.NodeSpec{
|
|
"missing-role": {Name: "n", Hostname: "n", IP: "1.1.1.1", K3sToken: "token", SSHUser: "atlas", SSHAuthorized: []string{"ssh"}},
|
|
"missing-url": {Name: "n", Hostname: "n", IP: "1.1.1.1", K3sRole: "agent", K3sToken: "token", SSHUser: "atlas", SSHAuthorized: []string{"ssh"}},
|
|
"missing-token": {Name: "n", Hostname: "n", IP: "1.1.1.1", K3sRole: "server", SSHUser: "atlas", SSHAuthorized: []string{"ssh"}},
|
|
"missing-ssh": {Name: "n", Hostname: "n", IP: "1.1.1.1", K3sRole: "server", K3sToken: "token"},
|
|
} {
|
|
if replacementReady(&node, &inventory.NodeClass{Image: "img", Checksum: "sum"}) {
|
|
t.Fatalf("replacementReady should reject %s", name)
|
|
}
|
|
}
|
|
|
|
fileParent := filepath.Join(t.TempDir(), "blocked")
|
|
if err := os.WriteFile(fileParent, []byte("block"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app.settings.HistoryPath = filepath.Join(fileParent, "history.jsonl")
|
|
app.appendEvent(Event{Kind: "noop"})
|
|
if got := app.recentEvents(1); got != nil {
|
|
t.Fatalf("recentEvents missing file = %#v", got)
|
|
}
|
|
historyPath := filepath.Join(t.TempDir(), "history.jsonl")
|
|
if err := os.WriteFile(historyPath, []byte("{bad-json}\n{\"kind\":\"ok\"}\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app.settings.HistoryPath = historyPath
|
|
if got := app.recentEvents(10); len(got) != 1 || got[0].Kind != "ok" {
|
|
t.Fatalf("recentEvents should skip invalid rows: %#v", got)
|
|
}
|
|
app.settings.SnapshotsPath = historyPath
|
|
if err := app.loadSnapshots(); err == nil {
|
|
t.Fatal("expected loadSnapshots invalid JSON error")
|
|
}
|
|
app.settings.TargetsPath = historyPath
|
|
if err := app.loadTargets(); err == nil {
|
|
t.Fatal("expected loadTargets invalid JSON error")
|
|
}
|
|
if targetsEqual(facts.Targets{Packages: map[string]string{"a": "1"}}, facts.Targets{Packages: map[string]string{"a": "1", "b": "2"}}) {
|
|
t.Fatal("targetsEqual should reject package length mismatch")
|
|
}
|
|
if targetsEqual(facts.Targets{Packages: map[string]string{"a": "1"}}, facts.Targets{Packages: map[string]string{"a": "2"}}) {
|
|
t.Fatal("targetsEqual should reject package value mismatch")
|
|
}
|
|
if got := firstLine(" one\ntwo "); got != "one" {
|
|
t.Fatalf("firstLine = %q", got)
|
|
}
|
|
|
|
kube := fakeKubeServer(t)
|
|
installKubeFactory(t, kube)
|
|
if err := deleteNodeObjectInCluster("titan-15"); err != nil {
|
|
t.Fatalf("deleteNodeObjectInCluster success: %v", err)
|
|
}
|
|
}
|
|
|
|
func testTime(t *testing.T) time.Time {
|
|
t.Helper()
|
|
return time.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC)
|
|
}
|