Files
mesh-catalog/modules/zfs/cmd/zfs-tools/manifest_test.go
T
jschoubben 53351dd30d
mesh/delivery merged: source.main moved to novox/mesh-catalog@6ba73d4e2a4f
mesh/delivery-group group feat/47-G-join12 merged: every member is merged
mesh/check failed (2): mesh-delivery-checker: no published of the tagged version of go-meshconditions of me…; and 1 more | 53351dd30df2 on b1125c02cfe6
Merge main into feat/47-G-join11: the zfs module and pacman's config first (issue 1979); zfs's findings in G's form
mesh-conditions keeps G's form: the keeper reads no observer's state (no reads, no Observers list), so main's zfs entry there goes. The zfs follower says its findings through G's findings writer over its keeper: the keeper gains the publish of the writer's three events on the finding stream finding-events and the hear of mesh-conditions.findings-asked on its own durable, nothing else; each pool or device not ONLINE is scope module, id zfs.<pool>.<device> (the device's own name), kind not-online, on the node. The manifest asks its state findings by template in requires (G §529, §577), and gains the finding-events stream and the consumes, and the keeper's bus the three events and the consumes.

Builds: design 47 G §383, §384, §387, §529, §577, A-339, A-340, A-341, A-342; design 42 zfs-owns-zfs §27, A-32, A-37, A-39, A-41

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-13 09:32:48 +01:00

569 lines
28 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
)
type manifest struct {
Tools []string `json:"tools"`
Claims []any `json:"claims"`
Emits []string `json:"emits"`
Replaces map[string][]string `json:"replaces"`
Resources []map[string]any `json:"resources"`
}
func readManifest(t *testing.T) (manifest, string) {
t.Helper()
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
return m, string(raw)
}
// The manifest lists exactly the tools the bundle serves; every one but zfs_snapshot says it only reads,
// and zfs_snapshot says it acts. Each says what it replaces (novox/hq ADR 0245), but the health check,
// which replaces nothing. No seat: a new mesh or node seat is the controller's to define (ADR 0121).
// handTools are the tools that start or steer the scheduled run, which replace no command.
var handTools = map[string]bool{"zfs_snapshots_run_now": true, "zfs_snapshots_prune_now": true, "zfs_config_apply": true,
"zfs_snapshots_pause": true, "zfs_snapshots_resume": true, "zfs_snapshots_record": true,
// The module's own read tools (§28) replace no command.
"zfs_config": true, "zfs_snapshots_schedule": true, "zfs_module_events": true, "zfs_snapshots_check": true, "zfs_findings": true}
var ownReads = map[string]bool{"zfs_config": true, "zfs_snapshots_schedule": true, "zfs_module_events": true, "zfs_snapshots_check": true, "zfs_findings": true}
func TestTheManifestNamesTheToolsTheBundleServesAndWhatEachReplaces(t *testing.T) {
m, raw := readManifest(t)
var served []string
for _, tl := range Tools(newFake(t).machine()) {
served = append(served, tl.Name)
want := "(r)"
if tl.Name == "zfs_snapshot" || strings.HasPrefix(tl.Name, "zfs_scrub_") || (handTools[tl.Name] && !ownReads[tl.Name]) {
want = "(a)"
}
if !strings.HasSuffix(tl.Description, want) {
t.Errorf("%s does not end with %s", tl.Name, want)
}
if tl.Name != "zfs_health" && !handTools[tl.Name] && len(m.Replaces[tl.Name]) == 0 {
t.Errorf("%s says nothing of what it replaces", tl.Name)
}
for _, c := range m.Replaces[tl.Name] {
if !strings.Contains(tl.Description, c) {
t.Errorf("%s replaces %q in the manifest and not in its description", tl.Name, c)
}
}
}
sort.Strings(served)
listed := append([]string(nil), m.Tools...)
sort.Strings(listed)
if strings.Join(served, ",") != strings.Join(listed, ",") {
t.Fatalf("served %v, listed %v", served, listed)
}
if _, ok := m.Replaces["zfs_health"]; ok {
t.Errorf("the health check replaces no command")
}
if len(m.Claims) != 0 {
t.Errorf("the module claims no seat: %v", m.Claims)
}
if len(m.Emits) != 0 {
t.Errorf("the events are asked for by template in requires: emits %v", m.Emits)
}
if hit := regexp.MustCompile(`\b(g14|novox|ace|shanks|jochen)\b|/home/`).FindString(strings.ReplaceAll(raw, "novox/hq", "")); hit != "" {
t.Errorf("the manifest says %q", hit)
}
}
// manifestOf is the whole manifest as a tree, for the tests that read a field the typed view does not.
func manifestOf(t *testing.T) (map[string]any, map[string]map[string]any, map[string]int) {
t.Helper()
_, raw := readManifest(t)
var tree map[string]any
if err := json.Unmarshal([]byte(raw), &tree); err != nil {
t.Fatal(err)
}
byID, order := map[string]map[string]any{}, map[string]int{}
for i, r := range tree["resources"].([]any) {
res := r.(map[string]any)
byID[res["id"].(string)] = res
order[res["id"].(string)] = i
}
return tree, byID, order
}
// conditions are the `if-config` of a resource, as written.
func conditions(r map[string]any) string {
var out []string
l, _ := r["if-config"].([]any)
for _, c := range l {
out = append(out, c.(string))
}
return strings.Join(out, ",")
}
// §7, §8 (A-1), §11 (A-14): the two packages are declared and the debug one is not; `pool` renames `scrub-pool`; each new value is
// not managed until set (no default, not required), of the form and trust §11 lists.
func TestTheManifestDeclaresThePackagesAndEveryValueAsSections7To11Say(t *testing.T) {
tree, byID, _ := manifestOf(t)
for id, pkg := range map[string]string{"utils": "zfs-utils", "dkms": "zfs-dkms"} {
if r := byID[id]; r == nil || r["type"] != "package" || r["package"] != pkg || r["state"] != nil {
t.Errorf("%s: %v", id, r)
}
}
for _, r := range byID {
if r["package"] == "zfs-utils-debug" {
t.Errorf("the debug package is not the module's: %v", r)
}
}
cfg := tree["config"].(map[string]any)
if _, old := cfg["scrub-pool"]; old {
t.Errorf("scrub-pool is declared; it is only in renamed-from")
}
if p := cfg["pool"].(map[string]any); p["renamed-from"] != "scrub-pool" || p["type"] != "string" || p["trusted"] != true || p["required"] != true || p["default"] != nil {
t.Errorf("pool: %v", p)
}
for k, want := range map[string]string{"scrub-cadence": "string/false", "scrub-day": "string/false", "scrub-time": "string/false"} {
if v := cfg[k].(map[string]any); v["default"] == nil || fmt.Sprint(v["type"]) != strings.Split(want, "/")[0] {
t.Errorf("%s keeps its default: %v", k, v)
}
}
new := map[string]string{"arc-max": "number/false", "arc-min": "number/false", "module-options": "list/true", "hostid": "string/true", "zed": "string/false",
"zed-handlers": "list/true", "trim-schedule": "string/false", "trim-day": "string/false", "trim-time": "string/false", "cache-devices": "list/true",
"log-devices": "list/true", "log-mirror": "boolean/true", "pool-properties": "object/true", "dataset-properties": "object/true", "snapshots": "object/true"}
for k, want := range new {
v, ok := cfg[k].(map[string]any)
if !ok {
t.Errorf("%s is not declared", k)
continue
}
if got := fmt.Sprint(v["type"], "/", v["trusted"]); got != want || v["default"] != nil || v["required"] != nil {
t.Errorf("%s: %s with default %v required %v, want %s not managed until set", k, got, v["default"], v["required"], want)
}
}
if len(cfg) != len(new)+4 {
t.Errorf("%d values declared, want %d", len(cfg), len(new)+4)
}
if d := fmt.Sprint(cfg["hostid"].(map[string]any)["description"]); !strings.Contains(d, "check of the pool's import at the next boot") {
t.Errorf("hostid's description does not say the change needs a check of the pool's import: %s", d)
}
}
// §9 (A-15, A-23), §12, §14 (A-2, A-7), §15: the config file is `${config-json}` and not trusted; the options file, ZED's folder, rc,
// functions, handlers and drop-in each under their condition; no link and no resource or template for /etc/hostid.
func TestTheManifestsFilesAreAsSections9To15Say(t *testing.T) {
_, byID, _ := manifestOf(t)
c := byID["config"]
if c["path"] != ConfigFile || c["mode"] != "0644" || c["trusted"] != false || c["owner"] != nil || strings.TrimSpace(templateText(t, c)) != "${config-json}" {
t.Errorf("the config file: %v", c)
}
if byID["scrub-config"] != nil {
t.Errorf("scrub-config is gone: the node-engine removes the old file as a resource that left")
}
zedFiles := map[string]string{"zed-rc": "/etc/zfs/mesh-zed.d/zed.rc", "zed-functions": "/etc/zfs/mesh-zed.d/zed-functions.sh",
"zed-handler-all-syslog": "/etc/zfs/mesh-zed.d/all-syslog.sh", "zed-handler-scrub-after-resilver": "/etc/zfs/mesh-zed.d/resilver_finish-start-scrub.sh",
"zed-drop-in": "/etc/systemd/system/zfs-zed.service.d/mesh.conf"}
for id, path := range zedFiles {
r := byID[id]
want := "zed=on"
mode := "0644"
switch id {
case "zed-handler-all-syslog":
want, mode = "zed-handlers=all-syslog", "0755"
case "zed-handler-scrub-after-resilver":
want, mode = "zed-handlers=resilver_finish-start-scrub", "0755"
}
if r == nil || r["type"] != "file" || r["path"] != path || conditions(r) != want || r["mode"] != mode {
t.Errorf("%s: %v", id, r)
}
}
if d := byID["zed-dir"]; d["type"] != "directory" || d["path"] != "/etc/zfs/mesh-zed.d" || d["unknown"] != "module" || d["mode"] != "0755" || conditions(d) != "zed=on" {
t.Errorf("the folder: %v", d)
}
if o := byID["options"]; o["path"] != "/etc/modprobe.d/zfs.conf" || o["mode"] != "0644" || conditions(o) != "arc-max,arc-min,module-options" {
t.Errorf("the options file: %v", o)
}
if d := byID["run-dir"]; d["path"] != "/run/zfs-tools" || d["unknown"] != "program" || filepath.Dir(ApplyLock) != d["path"] {
t.Errorf("the lock's folder: %v", d)
}
for id, r := range byID {
if r["type"] == "symlink" || r["type"] == "link" || r["target"] != nil {
t.Errorf("%s is a link: the mesh creates none", id)
}
if r["path"] == "/etc/hostid" {
t.Errorf("%s places /etc/hostid: the apply writes it with ZFS's own writer", id)
}
if r["type"] == "file" && strings.Contains(templateText(t, r), "/etc/hostid") {
t.Errorf("%s's template names /etc/hostid", id)
}
}
}
// §6, §13 (A-3), §18 (A-16): the run's resource precedes the resources that stop the scrub timers, with its schedule;
// each unit has the state and condition §13 lists; the trim timers are held stopped under none, weekly and monthly; the apply is
// run-once and starts again on the config file's change.
func TestTheManifestsUnitsAndTheRunAreAsSections6And13Say(t *testing.T) {
_, byID, order := manifestOf(t)
run := byID["run"]
if run["type"] != "process" || run["name"] != "zfs-mesh-run" || run["schedule"] != "0,15,30,45 * * * *" || run["user"] != nil || strings.Contains(fmt.Sprint(run["schedule"]), "/") {
t.Errorf("the scheduled run: %v", run)
}
if args, _ := run["run"].([]any); len(args) != 2 || args[1] != "run" {
t.Errorf("the run's command: %v", run["run"])
}
if RunTimer != "zfs-mesh-run.timer" || RunService != "zfs-mesh-run.service" {
t.Errorf("the tools read %s and %s", RunTimer, RunService)
}
for _, id := range []string{"distribution-weekly", "distribution-monthly", "distribution-trim-weekly", "distribution-trim-monthly"} {
if r := byID[id]; r["state"] != "stopped" || r["boot"] != "disabled" || order["run"] > order[id] {
t.Errorf("%s: %v (the run is at %d, it at %d)", id, r, order["run"], order[id])
}
}
for c, unit := range map[string]string{"distribution-weekly": "zfs-scrub-weekly@${config:pool}.timer", "distribution-monthly": "zfs-scrub-monthly@${config:pool}.timer",
"distribution-trim-weekly": "zfs-trim-weekly@${config:pool}.timer", "distribution-trim-monthly": "zfs-trim-monthly@${config:pool}.timer"} {
want := ""
if strings.Contains(c, "trim") {
want = "trim-schedule=none,trim-schedule=weekly,trim-schedule=monthly"
}
if byID[c]["unit"] != unit || conditions(byID[c]) != want {
t.Errorf("%s: %v", c, byID[c])
}
}
units := map[string][3]string{"import-cache": {"zfs-import-cache.service", "running", "enabled"}, "mount": {"zfs-mount.service", "running", "enabled"},
"target": {"zfs.target", "running", "enabled"}, "import-scan": {"zfs-import-scan.service", "", ""}, "share": {"zfs-share.service", "", ""},
"volume-wait": {"zfs-volume-wait.service", "", ""}, "import-target": {"zfs-import.target", "", ""}}
for id, u := range units {
r := byID[id]
got := [3]string{fmt.Sprint(r["unit"]), fmt.Sprint(orNone(r["state"])), fmt.Sprint(orNone(r["boot"]))}
if r == nil || got != u || conditions(r) != "" {
t.Errorf("%s: %v, want %v", id, got, u)
}
}
on, off := byID["zed-on"], byID["zed-off"]
if on["unit"] != "zfs-zed.service" || on["state"] != "running" || on["boot"] != "enabled" || conditions(on) != "zed=on" ||
off["unit"] != "zfs-zed.service" || off["state"] != "stopped" || off["boot"] != "disabled" || conditions(off) != "zed=off" {
t.Errorf("zed: %v %v", on, off)
}
if got := fmt.Sprint(on["restart-on"]); !strings.Contains(got, "zed-rc") || !strings.Contains(got, "zed-drop-in") || !strings.Contains(got, "zed-handler-all-syslog") {
t.Errorf("zed restarts on %v", got)
}
a := byID["apply"]
if a["type"] != "process" || a["name"] != "zfs-config-apply" || a["run-once"] != true || fmt.Sprint(a["restart-on"]) != "[config]" || a["schedule"] != nil || a["user"] != nil {
t.Errorf("the apply is run-once and starts on the config file's change: %v", a)
}
if order["config"] > order["apply"] || order["utils"] > order["config"] || order["options"] > order["apply"] || order["apply"] > order["run"] {
t.Errorf("the order: packages, config file and options before the apply, the apply before the run: %v", order)
}
for _, r := range byID {
if p, _ := r["path"].(string); strings.HasPrefix(p, "/etc/systemd/system/") && !strings.HasPrefix(p, "/etc/systemd/system/zfs-zed.service.d") {
t.Errorf("the module writes a unit file of its own: %s", p)
}
}
}
func orNone(v any) any {
if v == nil {
return ""
}
return v
}
// ADR 0240: zfs_health on the watch, which runs as the operator account; the follower judged by its unit.
// The scheduled run is not long-running and declares none.
func TestTheHealthChecksAndWhoRunsWhat(t *testing.T) {
_, byID, _ := manifestOf(t)
w := byID["watch"]
if h, _ := w["health"].(map[string]any); h["kind"] != "tool" || h["tool"] != "zfs_health" || w["user"] != "${machine:account}" {
t.Errorf("the watch runs as the operator account and carries zfs_health: %v", w)
}
e := byID["events"]
if h, _ := e["health"].(map[string]any); h["kind"] != "unit" || e["user"] != nil {
t.Errorf("the follower runs as root, judged by its unit: %v", e)
}
if d := byID["state-dir"]; d["path"] != "/var/lib/zfs-tools" || d["owner"] != "${machine:account}" || filepath.Dir(ErrorsFile) != d["path"] {
t.Errorf("the state the account keeps: %v", d)
}
// The scheduled run's state is root's alone, apart from the account's (third review).
if d := byID["scrub-state-dir"]; d["path"] != filepath.Dir(ServedFile) || d["mode"] != "0700" || d["owner"] != nil {
t.Errorf("the state the run keeps: %v", d)
}
}
// §19, ADR 0679 §7, §17: every command the bundle runs goes through ask, AsRoot, Escalated, Run, exec.Command or
// exec.CommandContext with an argument list from one of the functions below, each held to its program, its verb and its whole
// form; a call site that builds its own list fails this test. A new verb needs a decision first, and then a line here.
func TestTheBundleRunsOnlyTheFormsOfADR0679(t *testing.T) {
allowed := map[string][2]string{
"DatasetsArgv": {"zfs", "list"}, "SnapshotsArgv": {"zfs", "list"}, "PropertiesArgv": {"zfs", "get"},
"SnapshotArgv": {"zfs", "snapshot"},
"PoolsArgv": {"zpool", "list"}, "StatusArgv": {"zpool", "status"}, "IOArgv": {"zpool", "iostat"},
"EventsArgv": {"zpool", "events"}, "HistoryArgv": {"zpool", "history"}, "FollowArgv": {"zpool", "events"},
"ScrubArgv": {"zpool", "scrub"}, "TrimArgv": {"zpool", "trim"},
"TimerShowArgv": {"systemctl", "show"}, "ServiceShowArgv": {"systemctl", "show"},
"PoolGetArgv": {"zpool", "get"}, "PoolSetArgv": {"zpool", "set"}, "DatasetGetArgv": {"zfs", "get"},
"DatasetSetArgv": {"zfs", "set"}, "DatasetInheritArgv": {"zfs", "inherit"},
"DeviceStatusArgv": {"zpool", "status"}, "AllRealStatusArgv": {"zpool", "status"}, "RemoveArgv": {"zpool", "remove"},
"AddArgv": {"zpool", "add"}, "WipefsArgv": {"wipefs", "--no-act"}, "ZdbLabelArgv": {"zdb", "-l"},
"UdevArgv": {"udevadm", "monitor"}, "SnapshotSetArgv": {"zfs", "set"}, "StatusPathsArgv": {"zpool", "status"}, "HoldsArgv": {"zfs", "holds"}, "SnapshotPropertyArgv": {"zfs", "get"}, "DestroyArgv": {"zfs", "destroy"}, "ZdbConfigArgv": {"zdb", "-C"}, "ZgenhostidArgv": {"zgenhostid", "-f"},
}
verbs := map[string][]string{"DatasetsArgv": DatasetsArgv(), "SnapshotsArgv": SnapshotsArgv(), "PoolsArgv": PoolsArgv(),
"StatusArgv": StatusArgv(), "IOArgv": IOArgv(3), "EventsArgv": EventsArgv(), "HistoryArgv": HistoryArgv("tank"),
"PropertiesArgv": PropertiesArgv("tank"), "SnapshotArgv": SnapshotArgv("tank", "x"), "FollowArgv": FollowArgv(),
"TimerShowArgv": TimerShowArgv("x.timer"), "ServiceShowArgv": ServiceShowArgv("x.service"), "TrimArgv": TrimArgv("tank"),
"PoolGetArgv": PoolGetArgv("tank", "autotrim"), "PoolSetArgv": PoolSetArgv("tank", "autotrim", "on"),
"DatasetGetArgv": DatasetGetArgv("tank", "atime"), "DatasetSetArgv": DatasetSetArgv("tank", "atime", "on"),
"DatasetInheritArgv": DatasetInheritArgv("tank", "atime"),
"DeviceStatusArgv": DeviceStatusArgv("tank", true), "AllRealStatusArgv": AllRealStatusArgv(),
"RemoveArgv": RemoveArgv("tank", "x"), "AddArgv": AddArgv("tank", "cache", false, "/dev/x"),
"WipefsArgv": WipefsArgv("/dev/x"), "ZdbLabelArgv": ZdbLabelArgv("/dev/x"),
"HoldsArgv": HoldsArgv("tank@x"), "SnapshotPropertyArgv": SnapshotPropertyArgv("tank@x", "clones"), "DestroyArgv": DestroyArgv("tank", "x"),
"StatusPathsArgv": StatusPathsArgv(), "UdevArgv": UdevArgv(),
"SnapshotSetArgv": SnapshotSetArgv("tank@mesh-hourly-x", "hourly"), "ZdbConfigArgv": ZdbConfigArgv("tank"), "ZgenhostidArgv": ZgenhostidArgv("00bab10c")}
for fn, argv := range verbs {
for _, a := range argv {
if fn != "FollowArgv" && fn != "ZgenhostidArgv" && (a == "-f" || a == "-r" || a == "-R" || a == "-d") {
t.Errorf("%s runs %q", fn, a)
}
}
if argv[0] != allowed[fn][1] {
t.Errorf("%s runs %q, not %q", fn, argv[0], allowed[fn][1])
}
}
// The whole form of each root form §19 pins, and never a forbidden verb or flag.
pinned := map[string][]string{
"AddArgv cache": AddArgv("tank", "cache", false, "/dev/a", "/dev/b"),
"AddArgv log": AddArgv("tank", "log", true, "/dev/a", "/dev/b"),
"RemoveArgv": RemoveArgv("tank", "/dev/a"),
"PoolSetArgv": PoolSetArgv("tank", "autotrim", "on"),
"DatasetSetArgv": DatasetSetArgv("tank/a", "atime", "off"),
"DatasetInherit": DatasetInheritArgv("tank/a", "atime"),
"SnapshotArgv": SnapshotArgv("tank/a", "x"),
"SnapshotSetArgv": SnapshotSetArgv("tank/a@mesh-hourly-x", "hourly"),
"DestroyArgv": DestroyArgv("tank/a", "mesh-hourly-x"),
"TrimArgv": TrimArgv("tank"),
"WipefsArgv": WipefsArgv("/dev/a"),
"ZdbLabelArgv": ZdbLabelArgv("/dev/a"),
"ZdbConfigArgv": ZdbConfigArgv("tank"),
"ZgenhostidArgv": ZgenhostidArgv("0a0b0c0d"),
"UdevArgv": UdevArgv(),
}
forms := map[string]string{
"AddArgv cache": "add -- tank cache /dev/a /dev/b", "AddArgv log": "add -- tank log mirror /dev/a /dev/b", "RemoveArgv": "remove -- tank /dev/a",
"PoolSetArgv": "set autotrim=on -- tank", "DatasetSetArgv": "set atime=off -- tank/a", "DatasetInherit": "inherit atime -- tank/a",
"SnapshotArgv": "snapshot -- tank/a@x", "SnapshotSetArgv": "set mesh:schedule=hourly -- tank/a@mesh-hourly-x",
"DestroyArgv": "destroy -- tank/a@mesh-hourly-x", "TrimArgv": "trim -- tank", "WipefsArgv": "--no-act /dev/a", "ZdbLabelArgv": "-l /dev/a",
"ZdbConfigArgv": "-C tank", "ZgenhostidArgv": "-f 0a0b0c0d", "UdevArgv": "monitor --udev --subsystem-match=block",
}
for name, argv := range pinned {
if got := strings.Join(argv, " "); got != forms[name] {
t.Errorf("%s is %q, the pinned form is %q", name, got, forms[name])
}
for _, a := range argv {
if (a == "-f" && name != "ZgenhostidArgv") || a == "-r" || a == "-R" || a == "-d" {
t.Errorf("%s holds %s", name, a)
}
}
}
// No argument function of a zpool or zfs program runs a verb ADR 0679 does not allow.
never := map[string]bool{"upgrade": true, "rollback": true, "rename": true, "clear": true, "send": true, "receive": true, "create": true,
"import": true, "export": true, "attach": true, "detach": true, "offline": true, "online": true, "replace": true, "labelclear": true,
"split": true, "reguid": true, "promote": true, "clone": true, "destroy-pool": true, "reopen": true, "sync": true, "checkpoint": true}
for fn, pv := range allowed {
if (pv[0] == "zpool" || pv[0] == "zfs") && never[pv[1]] {
t.Errorf("%s runs %s %s", fn, pv[0], pv[1])
}
if pv[1] == "destroy" && fn != "DestroyArgv" || pv[0] == "zdb" && fn != "ZdbLabelArgv" && fn != "ZdbConfigArgv" || pv[0] == "zgenhostid" && fn != "ZgenhostidArgv" {
t.Errorf("%s runs %s %s", fn, pv[0], pv[1])
}
}
// zgenhostid only with the value the parse validated; a device only as cache or log; a destroy only of a snapshot.
files, _ := filepath.Glob("*.go")
for _, file := range files {
if strings.HasSuffix(file, "_test.go") {
continue
}
b, _ := os.ReadFile(file)
for i, l := range strings.Split(string(b), "\n") {
trimmed := strings.TrimSpace(l)
if strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "func ") {
continue
}
if strings.Contains(l, "ZgenhostidArgv(") && !strings.Contains(l, "ZgenhostidArgv(c.HostID)") {
t.Errorf("%s:%d runs zgenhostid with a value the parse did not validate: %s", file, i+1, trimmed)
}
if m := regexp.MustCompile(`AddArgv\(\w+(?:\.\w+)?, ("[a-z]+"|\w+),`).FindStringSubmatch(l); m != nil && m[1] != `"cache"` && m[1] != `"log"` {
t.Errorf("%s:%d adds a device that is neither cache nor log: %s", file, i+1, trimmed)
}
}
}
for _, act := range []string{"start", "pause", "stop", "anything"} {
argv := ScrubArgv(act, "tank")
if argv[0] != "scrub" || argv[len(argv)-2] != "--" || argv[len(argv)-1] != "tank" || len(argv) > 4 {
t.Errorf("%s: %v", act, argv)
}
if len(argv) == 4 && argv[1] != "-p" && argv[1] != "-s" {
t.Errorf("%s: %v", act, argv)
}
}
call := regexp.MustCompile(`(\.(ask|AsRoot|Escalated|Run|Start|Output|CombinedOutput)\(|exec\.Command(Context)?\(|os\.StartProcess\(|syscall\.(Exec|ForkExec)\()`)
site := regexp.MustCompile(`(?:\.(?:ask|AsRoot|Escalated)|exec\.Command)\((?:\w+, )?"(zfs|zpool|systemctl|wipefs|zdb|zgenhostid|udevadm)", (\w+)\(`)
// The bodies that run what they are given, each once.
bodies := map[string]bool{
"machine.go: r := m.Run(ctx, name, args...)": true,
"machine.go: r := m.Run(ctx, program, argv...)": true,
"machine.go: program, argv := m.Escalated(name, args...)": true,
"machine.go: cmd := exec.Command(name, args...)": true,
"machine.go: if err := cmd.Start(); err != nil {": true,
"follow.go: if err := cmd.Start(); err != nil {": true,
"follow_wake.go: if err := cmd.Start(); err != nil {": true,
// The findings writer's start, which runs no command (design 47 G A-339).
"findings_writer.go: return w.Start(ctx)": true,
}
files, _ = filepath.Glob("*.go")
sites := 0
for _, file := range files {
if strings.HasSuffix(file, "_test.go") {
continue
}
b, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
for i, l := range strings.Split(string(b), "\n") {
trimmed := strings.TrimSpace(l)
if !call.MatchString(l) || strings.HasPrefix(trimmed, "//") || bodies[file+":"+l] {
continue
}
m := site.FindStringSubmatch(l)
if m == nil {
t.Errorf("%s:%d runs a command not built by an argument function: %s", file, i+1, trimmed)
continue
}
want, ok := allowed[m[2]]
if !ok {
t.Errorf("%s:%d runs %s's list, which is not held here", file, i+1, m[2])
} else if want[0] != m[1] {
t.Errorf("%s:%d runs %s with %s's list", file, i+1, m[1], m[2])
}
sites++
}
}
if sites < 14 {
t.Errorf("found %d call sites; the test no longer sees them", sites)
}
}
// §25, §26 (A-19, A-36), A-37, A-38, §28: one events stream by template, 30 days, every kind of §25; the states applied,
// schedule and findings; the ledger snapshots of A-36 with a fixed cap and no index; the keeper alone holds the process bus, as a user of
// the module's own, naming the module's events and states and invoking the three; the three root processes declare no bus and
// reach the keeper's socket; the module invokes the service manager's start; the tools list holds §28's.
func TestTheManifestsBusStateKeeperAndTools(t *testing.T) {
tree, byID, _ := manifestOf(t)
reqs := map[string]map[string]any{}
for _, r := range tree["requires"].([]any) {
e := r.(map[string]any)
reqs[e["name"].(string)] = e
}
ev := reqs["events"]
opts, _ := ev["options"].(map[string]any)
var kinds []string
for _, k := range opts["events"].([]any) {
kinds = append(kinds, k.(string))
}
if ev["storage"] != "stream" || ev["template"] != "events" || opts["max-age-days"] != float64(30) || strings.Join(kinds, ",") != strings.Join(EventKinds, ",") {
t.Errorf("the events stream: %v", ev)
}
// The state findings too, in G's form (design 47 G §529, §577): asked for by template in requires, never in the old
// `state` list, which only a foundation part keeps; the keeper reads no observer's state (G §383).
for _, n := range []string{"applied", "schedule", "findings"} {
if e := reqs[n]; e == nil || e["storage"] != "key-value" || e["template"] != "state" {
t.Errorf("the state %s: %v", n, e)
}
}
if tree["state"] != nil {
t.Errorf("the module keeps a state in the old state list: %v", tree["state"])
}
// And its finding stream, by template, with the writer's three events (G §383, §387, A-339).
fe, _ := reqs["finding-events"]["options"].(map[string]any)
if reqs["finding-events"]["template"] != "events" || fmt.Sprint(fe["events"]) != "[finding-written.* finding-cleared.* findings-stated.*]" ||
fmt.Sprint(tree["consumes"]) != "[mesh-conditions.findings-asked]" {
t.Errorf("the finding stream %v and consumes %v", reqs["finding-events"], tree["consumes"])
}
l := reqs[LedgerName]
lo, _ := l["options"].(map[string]any)
types, _ := l["types"].([]any)
if l["storage"] != "ledger" || l["template"] != "ledger" || lo["max-mib"] == nil || len(types) != 1 {
t.Fatalf("the ledger: %v", l)
}
ty := types[0].(map[string]any)
ops, _ := json.Marshal(ty["operations"])
if ty["name"] != "snapshot" || ty["indexes"] != nil ||
string(ops) != `[{"effect":"set-entry","fields":["dataset","name","kind","at"],"into":"taken","key":"name","name":"taken"},{"effect":"remove","into":"taken","key":"name","name":"removed"}]` {
t.Errorf("the ledger's type: %v", ty)
}
k := byID["keeper"]
bus, _ := k["bus"].(map[string]any)
acct := byID["keeper-account"]
if k["user"] != "zfs-keeper" || acct["type"] != "user" || acct["name"] != "zfs-keeper" || bus == nil ||
fmt.Sprint(bus["state"]) != "[applied schedule findings]" ||
fmt.Sprint(bus["invokes"]) != "["+ToolRecord+" "+ToolSchedule+" seat:node-service-manager.start]" {
t.Errorf("the keeper: %v %v", k, acct)
}
if got := fmt.Sprint(bus["emits"]); strings.Contains(got, ScrubEvent) || !strings.Contains(got, EventApplied) || !strings.Contains(got, EventTakenSchedule) {
t.Errorf("the keeper's events: %v", got)
}
for _, id := range []string{"apply", "events", "run"} {
r := byID[id]
if r["bus"] != nil || fmt.Sprint(r["env"]) != "map["+KeeperSocketEnv+":${dir:keeper-state}/keeper.sock]" {
t.Errorf("%s declares a bus or does not reach the keeper's socket: %v", id, r)
}
}
for _, id := range []string{"watch", "keeper-state"} {
if byID[id]["bus"] != nil {
t.Errorf("%s", id)
}
}
if byID["keeper-state"]["owner"] != "zfs-keeper" {
t.Errorf("the keeper's folder: %v", byID["keeper-state"])
}
if fmt.Sprint(tree["invokes"]) != "[seat:node-service-manager.start]" {
t.Errorf("invokes: %v", tree["invokes"])
}
m, _ := readManifest(t)
for _, tool := range []string{"zfs_config", "zfs_config_apply", "zfs_snapshots_schedule", "zfs_snapshots_run_now", "zfs_snapshots_prune_now", "zfs_snapshots_pause",
"zfs_snapshots_resume", "zfs_module_events", "zfs_snapshots_check", "zfs_findings", "zfs_snapshots_record"} {
found := false
for _, x := range m.Tools {
found = found || x == tool
}
if !found {
t.Errorf("%s is not in tools", tool)
}
}
}
// §31, the test budget: the module stays within 500 unit tests and adds no integration test (its live tests are the main design's).
func TestTheModuleStaysWithinItsTestBudget(t *testing.T) {
files, _ := filepath.Glob("*_test.go")
n := 0
for _, f := range files {
b, _ := os.ReadFile(f)
if strings.Contains(string(b), "//go:"+"build integration") {
t.Errorf("%s is an integration test: the module adds none", f)
}
n += len(regexp.MustCompile(`(?m)^func Test`).FindAllString(string(b), -1))
}
if n > 500 {
t.Errorf("%d unit tests, over the budget of 500", n)
}
}