Files
mesh-catalog/modules/pacman/cmd/pacman-tools/busshapes_test.go
T
jschoubben 2b495c4818 pacman keeps one mirror list for the mesh, ranked in the write that opens each round and written by every node (ADR 0585, issue 1876)
Each node's reflector ranked its own list on its own timer, and on 2026-10-31 one node's
lagging first mirror left it a Node.js version behind the rest. The list is now the
module's state mirrors, key list: the node that opens a week's round ranks it with the
module's reflector options, keeps only mirrors whose last sync is no older than the
newest found, and the round's key names the list's revision. Every keeper writes
/etc/pacman.d/mirrorlist whole from the key through the root helper's new verb
mirrorlist, when the key changes and when the file differs, and never writes an empty
or unreadable list. reflector.timer is declared stopped and disabled, and reflector.conf
saves nothing. pacman_mirrors answers the shared list, its ranker and age and whether
this node's file matches; with refresh it asks this node's keeper to rank again,
refused while a round runs, naming it. The round's integration test proves the opening
write and both nodes' files.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 16:18:22 +01:00

207 lines
7.0 KiB
Go

package main
// The keeper's bus shapes carry only their named fields (novox/hq ADR 0507 §1, AC7; ADR 0508): every key's value
// and every event's body, filled whole, marshals to exactly the fields named here, and no shape holds a map or a
// free-form value through which anything else could reach the bus.
import (
"encoding/json"
"reflect"
"regexp"
"sort"
"strings"
"testing"
"time"
)
// busFields is every shape's JSON fields, by Go type: the interface the keeper and the verbs share.
var busFields = map[string][]string{
"InstalledValue": {"at", "packages"},
"InstalledPackage": {"name", "version", "explicit", "repo"},
"NotCurrent": {"bytes", "since"},
"UpdatesValue": {"reachable", "packages"},
"VersionChange": {"name", "from", "to"},
"PackageVersion": {"name", "version"},
"Run": {"node", "run", "kind", "from", "round", "ask", "of", "steps"},
"Step": {"step", "at", "why", "held", "unit", "ignored", "plan", "exit", "error", "changes", "check"},
"HeldKernel": {"package", "version", "module", "bound"},
"Changes": {"added", "removed", "changed"},
"Check": {"passed", "pacnew", "pacsave", "news", "news_error", "mapped", "mapped_error", "newly_failed", "restart_needed", "running_kernel",
"installed_kernel", "unbuilt", "dkms_error"},
"NewsTitle": {"title", "link", "published"},
"Mapped": {"unit", "container", "executable"},
"DKMSBuild": {"module", "version", "kernel"},
"RollbackItem": {"name", "from", "to", "source", "location"},
"Record": {"node", "run", "at", "installed", "kernel", "failed_units", "dkms"},
"DKMSModule": {"module", "version", "kernels"},
"Round": {"round", "opened", "opened_by", "order", "mirrors", "mirrors_note"},
"MirrorList": {"servers", "ranked_at", "ranked_by", "options", "newest_sync", "round"},
"RoundNode": {"round", "node", "run", "pass"},
"Pass": {"reason", "ask", "asked", "answer", "answered"},
"Ask": {"id", "node", "act", "at", "of", "remove_added", "round", "pass", "reason"},
"PackageChanged": {"node", "at", "added", "removed", "changed"},
"UpgradeRecorded": {"node", "run", "packages", "kernel"},
"KernelHeld": {"node", "run", "held"},
"UpgradeStarted": {"node", "run", "from", "round", "unit", "ignored"},
"UpgradeFinished": {"node", "run", "changes"},
"UpgradeFailed": {"node", "run", "step", "exit", "error"},
"UpgradeChecked": {"node", "run", "kind", "passed"},
"RollbackStarted": {"node", "run", "of", "unit"},
"RollbackFinished": {"node", "run", "of", "exit", "error"},
}
// The value of each state's keys.
var stateValues = []any{InstalledValue{}, NotCurrent{}, UpdatesValue{}, Run{}, Record{}, Round{}, RoundNode{}, Ask{}, MirrorList{}}
var stamp = time.Date(2026, 10, 24, 4, 0, 12, 0, time.UTC)
// fill sets every field of v, recursively, to a value that is not empty: one element in each list, every pointer
// set, so that every field marshals, omitempty or not.
func fill(v reflect.Value) {
switch v.Kind() {
case reflect.String:
v.SetString("x")
case reflect.Bool:
v.SetBool(true)
case reflect.Int, reflect.Int64:
v.SetInt(1)
case reflect.Uint64:
v.SetUint(1)
case reflect.Pointer:
v.Set(reflect.New(v.Type().Elem()))
fill(v.Elem())
case reflect.Slice:
s := reflect.MakeSlice(v.Type(), 1, 1)
fill(s.Index(0))
v.Set(s)
case reflect.Struct:
if v.Type() == reflect.TypeOf(time.Time{}) {
v.Set(reflect.ValueOf(stamp))
return
}
for i := 0; i < v.NumField(); i++ {
fill(v.Field(i))
}
}
}
// fieldsOf walks a filled value against its marshalled JSON: each struct's object holds exactly its named
// fields. It answers the struct types it met.
func fieldsOf(t *testing.T, v reflect.Value, raw json.RawMessage, met map[string]bool) {
t.Helper()
switch v.Kind() {
case reflect.Map, reflect.Interface:
t.Errorf("%s holds a free-form value: a bus shape names its fields", v.Type())
case reflect.Pointer:
fieldsOf(t, v.Elem(), raw, met)
case reflect.Slice:
var items []json.RawMessage
if err := json.Unmarshal(raw, &items); err != nil || len(items) != v.Len() {
t.Errorf("%s: %s is not its list", v.Type(), raw)
return
}
for i := range items {
fieldsOf(t, v.Index(i), items[i], met)
}
case reflect.Struct:
if v.Type() == reflect.TypeOf(time.Time{}) {
return
}
name := v.Type().Name()
met[name] = true
var obj map[string]json.RawMessage
if err := json.Unmarshal(raw, &obj); err != nil {
t.Errorf("%s: %s is not an object", name, raw)
return
}
var got []string
for k := range obj {
got = append(got, k)
}
want, named := busFields[name]
if !named {
t.Errorf("%s is on the bus and its fields are not named in busFields", name)
return
}
want = append([]string(nil), want...)
sort.Strings(got)
sort.Strings(want)
if strings.Join(got, " ") != strings.Join(want, " ") {
t.Errorf("%s marshals to %v, its named fields are %v", name, got, want)
}
for i := 0; i < v.NumField(); i++ {
tag, _, _ := strings.Cut(v.Type().Field(i).Tag.Get("json"), ",")
if r, ok := obj[tag]; ok {
fieldsOf(t, v.Field(i), r, met)
}
}
}
}
func TestEveryStateValueAndEventBodyCarriesOnlyItsNamedFields(t *testing.T) {
roots := append([]any(nil), stateValues...)
for _, e := range Events {
roots = append(roots, e.Body)
}
met := map[string]bool{}
for _, root := range roots {
v := reflect.New(reflect.TypeOf(root)).Elem()
fill(v)
raw, err := json.Marshal(v.Interface())
if err != nil {
t.Fatal(err)
}
fieldsOf(t, v, raw, met)
}
for name := range busFields {
if !met[name] {
t.Errorf("busFields names %s, which no state value or event body holds", name)
}
}
}
func TestEveryEventIsNamedOnceWithItsBody(t *testing.T) {
seen := map[string]bool{}
for _, e := range Events {
if seen[e.Name] {
t.Errorf("%s twice", e.Name)
}
seen[e.Name] = true
if e.Body == nil || reflect.TypeOf(e.Body).Kind() != reflect.Struct {
t.Errorf("%s has no body type", e.Name)
}
}
if len(Events) != 9 {
t.Errorf("package.changed and the eight upgrade and rollback events, not %d", len(Events))
}
}
// A key is node and id tokens joined by dots; each token is one NATS key token.
func TestKeysAreTokensJoinedByDots(t *testing.T) {
token := regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
run := RunID(time.Date(2026, 10, 24, 6, 0, 12, 0, time.FixedZone("CEST", 2*3600)))
if run != "20261024T040012Z" {
t.Errorf("RunID: %s", run)
}
round := RoundID(time.Date(2026, 10, 24, 4, 0, 0, 0, time.Local))
if round != "2026-W43" {
t.Errorf("RoundID: %s", round)
}
if RoundID(time.Date(2027, 1, 2, 4, 0, 0, 0, time.UTC)) != "2026-W53" {
t.Errorf("the ISO week of 2 January 2027 is 2026's 53rd: %s", RoundID(time.Date(2027, 1, 2, 4, 0, 0, 0, time.UTC)))
}
for key, parts := range map[string]int{
NodeKey("node-a", run): 2, NotCurrentKey("node-a"): 2, RoundNodeKey(round, "node-b"): 2,
} {
ts := strings.Split(key, ".")
if len(ts) != parts {
t.Errorf("%s: %d tokens", key, len(ts))
}
for _, x := range ts {
if !token.MatchString(x) {
t.Errorf("%s: token %q", key, x)
}
}
}
}