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

218 lines
8.3 KiB
Go

package main
// Runs on the bus and the log read once (novox/hq ADR 0508 §1; acceptance 1841/AC1 for the writing of steps,
// 1841/AC2 for the log and pacman_upgrades).
import (
"errors"
"os"
"strings"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
func capturedLog(t *testing.T) string {
t.Helper()
raw, err := os.ReadFile("testdata/pacman-log-three-full-upgrades/pacman.log")
if err != nil {
t.Fatal(err)
}
return string(raw)
}
// The captured log holds three full upgrades among other commands: one that ended before any transaction, one
// whose transaction completed, and a later one that upgraded one package.
func TestEveryFullUpgradeOfTheLogIsARunReadFromIt(t *testing.T) {
runs := PastUpgrades("node-a", capturedLog(t))
if len(runs) != 3 {
t.Fatalf("%d runs, want the three full upgrades and none of the other commands: %+v", len(runs), runs)
}
for _, r := range runs {
if r.Node != "node-a" || r.Kind != RunUpgrade || r.From != FromLog || len(r.Steps) != 2 || r.Steps[0].Step != StepStarted {
t.Errorf("a run read from the log: %+v", r)
}
}
first, second, third := runs[0], runs[1], runs[2]
if first.Run != "20261004T130648Z" || first.Steps[1].Step != StepFinished || first.Steps[1].Why != noTransaction ||
len(first.Steps[1].Changes.Changed) != 0 {
t.Errorf("an upgrade with no transaction says so: %+v", first)
}
end := second.Steps[1]
if second.Run != "20261004T130724Z" || end.Step != StepFinished || !end.At.Equal(time.Date(2026, 10, 4, 13, 22, 50, 0, time.UTC)) {
t.Errorf("the second upgrade finished when its transaction completed: %+v", second)
}
var names []string
for _, c := range end.Changes.Changed {
names = append(names, c.Name)
}
if strings.Join(names, " ") != "bash glibc iana-etc readline tzdata wireshark-qt xfsprogs xpad xterm zathura-pdf-mupdf" {
t.Errorf("what it changed, sorted: %v", names)
}
if c := end.Changes.Changed[0]; c.From != "5.3.15-1" || c.To != "5.3.20-1" {
t.Errorf("bash: %+v", c)
}
if len(end.Changes.Removed) != 0 {
t.Errorf("the removal run between them is no part of an upgrade: %+v", end.Changes.Removed)
}
if c := third.Steps[1].Changes.Changed; third.Run != "20261004T132817Z" || len(c) != 1 || c[0].Name != "dunst" {
t.Errorf("the third upgrade: %+v", third)
}
}
func TestATransactionWithoutAnEndOrThatFailedIsAFailedRun(t *testing.T) {
log := "[2026-10-10T04:00:00+0000] [PACMAN] Running 'pacman -Syu --noconfirm'\n" +
"[2026-10-10T04:00:01+0000] [PACMAN] starting full system upgrade\n" +
"[2026-10-10T04:00:05+0000] [ALPM] transaction started\n" +
"[2026-10-10T04:00:06+0000] [ALPM] upgraded linux (7.2.8.arch1-2 -> 7.2.9.arch1-1)\n" +
"[2026-10-10T04:00:09+0000] [ALPM] transaction failed\n" +
"[2026-10-11T04:00:00+0000] [PACMAN] Running 'pacman -Syu --noconfirm'\n" +
"[2026-10-11T04:00:01+0000] [PACMAN] starting full system upgrade\n" +
"[2026-10-11T04:00:05+0000] [ALPM] transaction started\n"
runs := PastUpgrades("node-b", log)
if len(runs) != 2 {
t.Fatalf("%+v", runs)
}
if s := runs[0].Steps[1]; s.Step != StepFailed || s.Error != "transaction failed" || len(s.Changes.Changed) != 1 {
t.Errorf("a failed transaction: %+v", s)
}
if s := runs[1].Steps[1]; s.Step != StepFailed || !strings.Contains(s.Error, "no end") {
t.Errorf("a transaction the log holds no end of: %+v", s)
}
}
func TestTheLogIsReadIntoRunsOnceAndASecondStartReadsNothing(t *testing.T) {
m := machine(nil, 1000)
reads := 0
log := capturedLog(t)
m.ReadFile = func(path string) ([]byte, error) {
if path != PacmanLog {
t.Fatalf("read %s", path)
}
reads++
return []byte(log), nil
}
runs := newBucket()
other := Run{Node: "node-b", Run: "20261001T040000Z", Kind: RunUpgrade, From: FromRound}
if _, err := runs.Create(NodeKey("node-b", other.Run), other); err != nil {
t.Fatal(err)
}
u := &Upgrades{Node: "node-a", Machine: m, Runs: runs}
n, err := u.ImportLog()
if err != nil || n != 3 || reads != 1 {
t.Fatalf("first start: %d runs, %d reads, %v", n, reads, err)
}
var r Run
if !runs.value("node-a.20261004T132817Z", &r) || r.From != FromLog {
t.Errorf("the run is kept at <node>.<run>, marked as read from the log: %+v", r)
}
before := len(runs.writes)
n, err = u.ImportLog()
if err != nil || n != 0 || reads != 1 || len(runs.writes) != before {
t.Errorf("a second start: %d runs, %d reads, %d writes, %v", n, reads, len(runs.writes)-before, err)
}
}
// A run's key is created at its first step and then only updated from the revision its writer holds: a step
// is written once, and a run another writer changed meanwhile is refused, never overwritten.
func TestAStepIsWrittenOnceAndAnotherWritersChangeIsNeverOverwritten(t *testing.T) {
runs := newBucket()
u := &Upgrades{Node: "node-a", Runs: runs}
r := &openRun{Run: Run{Node: "node-a", Run: "20261024T040000Z", Kind: RunUpgrade, From: FromAsk}}
at := time.Date(2026, 10, 24, 4, 0, 0, 0, time.UTC)
for _, s := range []string{StepRecorded, StepCheckedBefore} {
if err := u.step(r, Step{Step: s, At: at}); err != nil {
t.Fatal(err)
}
}
if w := strings.Join(runs.writes, ", "); w != "create node-a.20261024T040000Z, update node-a.20261024T040000Z" {
t.Errorf("writes: %s", w)
}
var kept Run
runs.value("node-a.20261024T040000Z", &kept)
if len(kept.Steps) != 2 || kept.Steps[1].Step != StepCheckedBefore {
t.Errorf("kept: %+v", kept)
}
if _, err := runs.Put("node-a.20261024T040000Z", kept); err != nil {
t.Fatal(err)
}
if err := u.step(r, Step{Step: StepStarted, At: at}); !errors.Is(err, stdio.ErrChanged) || len(r.Steps) != 2 {
t.Errorf("a step over another writer's change: %v, %d steps held", err, len(r.Steps))
}
}
func TestPacmanUpgradesAnswersAllOneNodeDaysAndOneRun(t *testing.T) {
runs := newBucket()
put := func(node, id string, steps ...Step) {
r := Run{Node: node, Run: id, Kind: RunUpgrade, From: FromRound, Round: "2026-W43", Steps: steps}
if _, err := runs.Create(NodeKey(node, id), r); err != nil {
t.Fatal(err)
}
}
at := time.Date(2026, 10, 24, 4, 30, 0, 0, time.UTC)
passed := &Check{Passed: true}
put("node-a", "20261024T040000Z", Step{Step: StepRecorded, At: at}, Step{Step: StepFinished, At: at}, Step{Step: StepCheckedAfter, At: at, Check: passed})
put("node-b", "20261024T050000Z", Step{Step: StepRecorded, At: at}, Step{Step: StepStarted, At: at})
put("node-b", "20261017T050000Z", Step{Step: StepRefused, At: at, Why: "installed is not current"})
all, err := ListUpgrades(runs, "", "", "", "", 50)
if err != nil {
t.Fatal(err)
}
listed := all["runs"].([]RunSummary)
var order []string
for _, s := range listed {
order = append(order, s.Node+"/"+s.Run+"/"+s.Outcome+"/"+s.Checked)
}
if strings.Join(order, " ") != "node-b/20261024T050000Z/running/ node-a/20261024T040000Z/finished/passed node-b/20261017T050000Z/refused/" {
t.Fatalf("all runs, newest first: %v", order)
}
if listed[2].Error != "installed is not current" {
t.Errorf("a refused run says why: %+v", listed[2])
}
one, _ := ListUpgrades(runs, "node-b", "", "", "", 50)
if one["count"] != 2 {
t.Errorf("one node: %v", one)
}
days, _ := ListUpgrades(runs, "", "2026-10-18", "2026-10-24", "", 50)
if days["count"] != 2 {
t.Errorf("between two days, the last one included: %v", days)
}
limited, _ := ListUpgrades(runs, "", "", "", "", 1)
if limited["count"] != 1 || limited["truncated"] != true {
t.Errorf("limited: %v", limited)
}
whole, err := ListUpgrades(runs, "node-a", "", "", "20261024T040000Z", 50)
if err != nil || len(whole["run"].(Run).Steps) != 3 {
t.Errorf("one run whole: %v %v", whole, err)
}
if _, err := ListUpgrades(runs, "node-a", "", "", "20261024T050000Z", 50); err == nil {
t.Error("a run the node does not have was answered")
}
if _, err := ListUpgrades(runs, "", "", "", "20261024T040000Z", 50); err == nil {
t.Error("a run without its node was answered")
}
}
func TestTheUpgradesToolReadsTheRunsState(t *testing.T) {
runs := newBucket()
saved := upgradeState
upgradeState = func(name string) Bucket {
if name != StateRuns {
t.Fatalf("read %s", name)
}
return runs
}
defer func() { upgradeState = saved }()
for _, tool := range tools(machine(nil, 1000)) {
if tool.Name == "pacman_upgrades" {
out, err := tool.Run(map[string]any{"node": "node-a"})
if err != nil || out.(map[string]any)["count"] != 0 {
t.Errorf("%v %v", out, err)
}
return
}
}
t.Fatal("no pacman_upgrades")
}