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

204 lines
6.9 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"},
"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{}}
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.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)
}
}
}
}