Files
mesh-catalog/modules/pacman/cmd/pacman-tools/aftercheck_test.go
T

122 lines
5.6 KiB
Go

package main
// The after-upgrade check, its first part (novox/hq ADR 0508 §4; acceptance 1841/AC5): .pacnew and .pacsave
// files from the transaction's log lines, the news since the previous upgrade, units newly failed against the
// record, and a restart needed.
import (
"errors"
"strings"
"testing"
"time"
)
const checkLog = "[2026-10-04T12:00:01+0000] [PACMAN] Running 'pacman -Syu --noconfirm'\n" +
"[2026-10-04T12:00:02+0000] [PACMAN] starting full system upgrade\n" +
"[2026-10-04T12:00:09+0000] [ALPM] transaction started\n" +
"[2026-10-04T12:00:10+0000] [ALPM] upgraded pacman-mirrorlist (20260901-1 -> 20261001-1)\n" +
"[2026-10-04T12:00:10+0000] [ALPM] warning: /etc/pacman.d/mirrorlist installed as /etc/pacman.d/mirrorlist.pacnew\n" +
"[2026-10-04T12:00:10+0000] [ALPM] warning: /etc/lemurs/config.toml saved as /etc/lemurs/config.toml.pacsave\n" +
"[2026-10-04T12:00:10+0000] [ALPM] removed lemurs-git (0.4.0.r17.g7151336-2)\n" +
"[2026-10-04T12:00:11+0000] [ALPM] transaction completed\n" +
"[2026-10-04T12:05:00+0000] [PACMAN] Running 'pacman -S --noconfirm made-up'\n" +
"[2026-10-04T12:05:01+0000] [ALPM] transaction started\n" +
"[2026-10-04T12:05:01+0000] [ALPM] warning: /etc/made-up.conf installed as /etc/made-up.conf.pacnew\n" +
"[2026-10-04T12:05:02+0000] [ALPM] transaction completed\n"
const newsFeed = `<?xml version="1.0"?><rss><channel>
<item><title>A manual intervention</title><link>https://example.org/news/2</link><pubDate>Fri, 02 Oct 2026 10:00:00 +0000</pubDate><description>x</description></item>
<item><title>An older post</title><link>https://example.org/news/1</link><pubDate>Mon, 21 Sep 2026 10:00:00 +0000</pubDate><description>y</description></item>
</channel></rss>`
// checkMachine is a machine after an upgrade: these units failed now, this kernel running, and the linux
// package now installing linuxNow.
func checkMachine(failed, linuxNow string) *Machine {
m := machine(byLine(map[string]Ran{
"systemctl list-units --state=failed --plain --no-legend --no-pager": {Stdout: failed},
"pacman -Qq": {Stdout: "bash\nlinux\nlinux-firmware\nlinux-lts\n"},
"dkms status": {Status: 127, Err: "ENOENT"},
"pacman -Qlq -- linux": {Stdout: "/usr/lib/modules/" + linuxNow + "/vmlinuz\n"},
"pacman -Qlq -- linux-firmware": {Stdout: "/usr/lib/firmware/\n"},
"pacman -Qlq -- linux-lts": {Stdout: "/usr/lib/modules/6.18.54-2-lts/vmlinuz\n"},
}, nil), 1000)
m.ReadFile = func(path string) ([]byte, error) {
switch path {
case OSRelease:
return []byte("7.2.8-arch1-2\n"), nil
case PacmanLog:
return []byte(checkLog), nil
}
return nil, errNoFile
}
return m
}
func checkedRun(steps ...string) Run {
r := Run{Node: "node-a", Run: "20261004T120000Z", Kind: RunUpgrade, From: FromAsk}
at := time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC)
for _, s := range steps {
r.Steps = append(r.Steps, Step{Step: s, At: at})
}
return r
}
func checkUpgrades(t *testing.T, m *Machine) *Upgrades {
runs := newBucket()
previous := Run{Node: "node-a", Run: "20260926T040000Z", Kind: RunUpgrade, From: FromRound,
Steps: []Step{{Step: StepStarted, At: time.Date(2026, 9, 26, 4, 0, 0, 0, time.UTC)}}}
if _, err := runs.Create(NodeKey("node-a", previous.Run), previous); err != nil {
t.Fatal(err)
}
saved := fetch
fetch = func(string) ([]byte, error) { return []byte(newsFeed), nil }
t.Cleanup(func() { fetch = saved })
return &Upgrades{Node: "node-a", Machine: m, Runs: runs}
}
func TestTheCheckNamesTheFilesTheTransactionLeftAndTheNewsSinceThePreviousUpgrade(t *testing.T) {
u := checkUpgrades(t, checkMachine("", "7.2.8-arch1-2"))
c := u.AfterCheck(checkedRun(StepStarted, StepFinished), &Record{})
if strings.Join(c.Pacnew, " ") != "/etc/pacman.d/mirrorlist.pacnew" || strings.Join(c.Pacsave, " ") != "/etc/lemurs/config.toml.pacsave" {
t.Errorf("only this transaction's files: %v %v", c.Pacnew, c.Pacsave)
}
if len(c.News) != 1 || c.News[0].Title != "A manual intervention" || c.NewsError != "" {
t.Errorf("the news since the previous upgrade (26 September): %+v %q", c.News, c.NewsError)
}
if !c.Passed || c.RestartNeeded || c.RunningKernel != "7.2.8-arch1-2" || c.InstalledKernel != "7.2.8-arch1-2" {
t.Errorf("%+v", c)
}
}
func TestNewsThatCannotBeReadIsSaidNotShownAsNone(t *testing.T) {
u := checkUpgrades(t, checkMachine("", "7.2.8-arch1-2"))
fetch = func(string) ([]byte, error) { return nil, errors.New("no route to host") }
c := u.AfterCheck(checkedRun(StepStarted, StepFinished), &Record{})
if !strings.Contains(c.NewsError, "no route to host") || !c.Passed {
t.Errorf("%+v", c)
}
}
func TestAUnitNewlyFailedFailsTheCheckAndOneFailedBeforeDoesNot(t *testing.T) {
u := checkUpgrades(t, checkMachine("backup.service loaded failed failed The backup\nweb.service loaded failed failed The web\n", "7.2.8-arch1-2"))
c := u.AfterCheck(checkedRun(StepStarted, StepFinished), &Record{FailedUnits: []string{"backup.service"}})
if strings.Join(c.NewlyFailed, " ") != "web.service" || c.Passed {
t.Errorf("%+v", c)
}
}
func TestARestartIsNeededWhenTheRunningKernelIsNoLongerTheInstalledOne(t *testing.T) {
u := checkUpgrades(t, checkMachine("", "7.2.9-arch1-1"))
c := u.AfterCheck(checkedRun(StepStarted, StepFinished), &Record{})
if !c.RestartNeeded || c.InstalledKernel != "7.2.9-arch1-1" || !c.Passed {
t.Errorf("a restart is said and does not fail the check: %+v", c)
}
}
func TestTheCheckPassesOnlyWhenTheTransactionSucceeded(t *testing.T) {
u := checkUpgrades(t, checkMachine("", "7.2.8-arch1-2"))
if c := u.AfterCheck(checkedRun(StepStarted, StepFailed), &Record{}); c.Passed {
t.Errorf("%+v", c)
}
}