From e3ac017c284439fed978d56f9184f8f63a09428c Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Wed, 21 Oct 2026 08:00:21 +0000 Subject: [PATCH] Test the weekly upgrade's timer and its health (issue 1574, ADR 0477) --- .../pacman/cmd/pacman-tools/manifest_test.go | 50 +++++++++++++++++++ .../pacman/cmd/pacman-tools/pacman_test.go | 35 +++++++++++++ 2 files changed, 85 insertions(+) diff --git a/modules/pacman/cmd/pacman-tools/manifest_test.go b/modules/pacman/cmd/pacman-tools/manifest_test.go index b55f6908..da7fb4ad 100644 --- a/modules/pacman/cmd/pacman-tools/manifest_test.go +++ b/modules/pacman/cmd/pacman-tools/manifest_test.go @@ -101,3 +101,53 @@ func TestTheReflectorPackageIsDeclaredBeforeTheFileItShips(t *testing.T) { t.Fatalf("a package's file and timer come after the package: %v", order) } } + +// The weekly upgrade (issue 1574, ADR 0477 clause 5): a timer on Saturday 04:00 local time, persistent so a +// node that was off upgrades at its next start, the same moment on every node (no random delay), starting a +// oneshot service that runs the very upgrade pacman_upgrade runs. +func TestTheWeeklyUpgradeIsATimerOnSaturdayFourPersistentRunningTheToolsUpgrade(t *testing.T) { + m := manifest(t) + timer := m.resource(t, "upgrade-timer") + service := m.resource(t, "upgrade-service") + if timer["path"] != "/etc/systemd/system/mesh-pacman-weekly-upgrade.timer" || service["path"] != "/etc/systemd/system/"+WeeklyUnit { + t.Fatalf("paths: %v %v", timer["path"], service["path"]) + } + tt, st := templateText(t, timer), templateText(t, service) + for _, want := range []string{"\nOnCalendar=Sat *-*-* 04:00:00\n", "\nPersistent=true\n", "\nWantedBy=timers.target\n"} { + if !strings.Contains(tt, want) { + t.Errorf("the timer lacks %q:\n%s", strings.TrimSpace(want), tt) + } + } + for _, not := range []string{"RandomizedDelaySec", "OnBootSec", "OnUnitActiveSec", "UTC"} { + if strings.Contains(tt, not) { + t.Errorf("the timer says %s: the same local moment on every node, and no other moment", not) + } + } + if strings.Count(tt, "OnCalendar=") != 1 { + t.Errorf("one moment only:\n%s", tt) + } + if !strings.Contains(st, "\nType=oneshot\n") { + t.Errorf("the service is a oneshot:\n%s", st) + } + if want := "\nExecStart=/usr/bin/pacman " + strings.Join(UpgradeArgs, " ") + "\n"; !strings.Contains(st, want) { + t.Errorf("the service does not run the upgrade pacman_upgrade runs (%q):\n%s", want, st) + } + if strings.Contains(st, "reboot") || strings.Contains(tt, "reboot") || strings.Contains(st, "systemctl") { + t.Error("the mesh restarts no machine for an upgrade") + } + r := m.resource(t, "weekly-upgrade") + if r["type"] != "service" || r["unit"] != "mesh-pacman-weekly-upgrade.timer" || r["state"] != "running" || r["boot"] != "enabled" { + t.Fatalf("the timer runs and is enabled: %v", r) + } + h, _ := r["health"].(map[string]any) + if h["kind"] != "tool" || h["tool"] != "pacman_upgrade_health" { + t.Errorf("the upgrade's outcome is the resource's health: %v", r["health"]) + } + order := map[string]int{} + for i, x := range m.Resources { + order[x["id"].(string)] = i + } + if order["upgrade-service"] > order["weekly-upgrade"] || order["upgrade-timer"] > order["weekly-upgrade"] || order["package"] > order["upgrade-service"] { + t.Errorf("the units are placed before the timer is started: %v", order) + } +} diff --git a/modules/pacman/cmd/pacman-tools/pacman_test.go b/modules/pacman/cmd/pacman-tools/pacman_test.go index 83e5196b..c17b9d48 100644 --- a/modules/pacman/cmd/pacman-tools/pacman_test.go +++ b/modules/pacman/cmd/pacman-tools/pacman_test.go @@ -374,3 +374,38 @@ func TestConfigIsReadAsPacmanConfResolvesIt(t *testing.T) { t.Fatalf("%+v", c) } } + +// The weekly upgrade's outcome is the module's health (issue 1574, ADR 0477 clause 5). +func upgradeHealthMachine(props, journal string) *Machine { + return machine(byLine(map[string]Ran{ + "systemctl show " + WeeklyUnit + " --no-pager --property=LoadState --property=ActiveState --property=Result --property=ExecMainStatus --property=ExecMainExitTimestamp": {Stdout: props}, + "sudo -n journalctl --no-pager -o cat -n 40 -u " + WeeklyUnit: {Stdout: journal}, + }, nil), 1000) +} + +func TestAFailedWeeklyUpgradeIsUnhealthyNamingTheError(t *testing.T) { + h := upgradeHealthMachine("LoadState=loaded\nActiveState=failed\nResult=exit-code\nExecMainStatus=1\nExecMainExitTimestamp=Sat 2026-10-24 04:03:11 CEST\n", + ":: Synchronizing package databases...\nerror: failed to commit transaction (conflicting files)\nErrors occurred, no packages were upgraded.\n").UpgradeHealth() + if h.Healthy || !strings.Contains(h.Why, "failed to commit transaction (conflicting files)") || !strings.Contains(h.Why, "exit-code") || !strings.Contains(h.Why, "Sat 2026-10-24 04:03:11 CEST") { + t.Fatalf("%+v", h) + } + h = upgradeHealthMachine("LoadState=loaded\nActiveState=failed\nResult=exit-code\nExecMainStatus=1\nExecMainExitTimestamp=\n", "").UpgradeHealth() + if h.Healthy || !strings.Contains(h.Why, "status 1") { + t.Fatalf("a failure without a journal is still unhealthy: %+v", h) + } +} + +func TestAPassedRunningOrNeverRunWeeklyUpgradeIsHealthy(t *testing.T) { + for name, props := range map[string]string{ + "passed": "LoadState=loaded\nActiveState=inactive\nResult=success\nExecMainStatus=0\nExecMainExitTimestamp=Sat 2026-10-24 04:09:00 CEST\n", + "running": "LoadState=loaded\nActiveState=activating\nResult=success\nExecMainStatus=0\nExecMainExitTimestamp=\n", + "never run": "LoadState=loaded\nActiveState=inactive\nResult=success\nExecMainStatus=0\nExecMainExitTimestamp=\n", + } { + if h := upgradeHealthMachine(props, "").UpgradeHealth(); !h.Healthy { + t.Errorf("%s: %+v", name, h) + } + } + if h := machine(fake(func(call) Ran { return Ran{Status: 1, Stderr: "boom"} }, nil), 1000).UpgradeHealth(); h.Healthy { + t.Errorf("a unit that cannot be read is not healthy: %+v", h) + } +}