package main import ( "encoding/json" "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). 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" { want = "(a)" } if !strings.HasSuffix(tl.Description, want) { t.Errorf("%s does not end with %s", tl.Name, want) } if tl.Name != "zfs_health" && 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 strings.Join(m.Emits, ",") != SnapshotEvent { t.Errorf("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) } } // ADR 0240: the health is zfs_health on the watch, beside the event follower's unit, which the module does // not judge itself; the module installs nothing, so it never puts ZFS on a machine. func TestTheHealthIsTheWatchsToolCheckBesideTheFollowersUnit(t *testing.T) { m, raw := readManifest(t) byID := map[string]map[string]any{} for _, r := range m.Resources { byID[r["id"].(string)] = r } if len(byID) != 2 { t.Fatalf("resources: %v", m.Resources) } for id, want := range map[string][2]string{"watch": {"tool", "watch"}, "events": {"unit", "events"}} { r := byID[id] h, _ := r["health"].(map[string]any) run, _ := r["run"].([]any) if r["type"] != "process" || h["kind"] != want[0] || len(run) != 2 || run[1] != want[1] { t.Errorf("%s: %v", id, r) } } if h := byID["watch"]["health"].(map[string]any); h["tool"] != "zfs_health" { t.Errorf("the watch is judged by zfs_health: %v", h) } if strings.Contains(raw, `"package"`) { t.Errorf("the module installs no package: ZFS is the machine's, and its kernel module is built against its kernel") } } // No destroy, rollback, set, send or any other change but a snapshot. Every command the bundle runs goes // through ask, AsRoot or Escalated with an argument list from one of the functions below, each of which is // held to its verb here; a call site that builds its own list fails this test. A new verb needs a decision // first, and then a line here. func TestTheBundleRunsOnlyReadingVerbsAndSnapshot(t *testing.T) { allowed := map[string]string{"DatasetsArgv": "list", "SnapshotsArgv": "list", "PoolsArgv": "list", "StatusArgv": "status", "IOArgv": "iostat", "EventsArgv": "events", "HistoryArgv": "history", "PropertiesArgv": "get", "SnapshotArgv": "snapshot", "FollowArgv": "events"} 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", true), "FollowArgv": FollowArgv()} for fn, argv := range verbs { if argv[0] != allowed[fn] { t.Errorf("%s runs %q, not %q", fn, argv[0], allowed[fn]) } } call := regexp.MustCompile(`\.(ask|AsRoot|Escalated|Command|CommandContext)\(`) site := regexp.MustCompile(`\.(?:ask|AsRoot|Escalated|CommandContext)\((?:\w+, )?"(zfs|zpool)", (\w+)\(`) 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") { if !call.MatchString(l) || strings.HasPrefix(strings.TrimSpace(l), "func ") || strings.HasPrefix(strings.TrimSpace(l), "//") { continue } if strings.Contains(l, "m.Escalated(name, args...)") || (file == "machine.go" && strings.Contains(l, "exec.CommandContext(ctx, name, args...)")) { continue // AsRoot's and the runner's own bodies } m := site.FindStringSubmatch(l) if m == nil { t.Errorf("%s:%d runs a command not built by an argument function: %s", file, i+1, strings.TrimSpace(l)) continue } if _, ok := allowed[m[2]]; !ok { t.Errorf("%s:%d runs %s's list, which is not held here", file, i+1, m[2]) } sites++ } } if sites < 10 { t.Errorf("found %d call sites; the test no longer sees them", sites) } }