Test the weekly upgrade's timer and its health (issue 1574, ADR 0477)

This commit is contained in:
2026-10-21 08:00:21 +00:00
parent 1f5046f47e
commit e3ac017c28
2 changed files with 85 additions and 0 deletions
@@ -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)
}
}
@@ -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)
}
}