163 lines
6.5 KiB
Go
163 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type ledgerCalls struct {
|
|
entries []LedgerEntry
|
|
read error
|
|
record error
|
|
records []map[string]any
|
|
}
|
|
|
|
func (l *ledgerCalls) call(key string, body any) (json.RawMessage, error) {
|
|
switch key {
|
|
case ToolSchedule:
|
|
if l.read != nil {
|
|
return nil, l.read
|
|
}
|
|
return json.Marshal(map[string]any{"ledger": l.entries})
|
|
case ToolRecord:
|
|
if l.record != nil {
|
|
return nil, l.record
|
|
}
|
|
l.records = append(l.records, body.(map[string]any))
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
return nil, errors.New("not a tool of the run: " + key)
|
|
}
|
|
|
|
func snapFake(t *testing.T, keeps string, l *ledgerCalls) (*fake, *Machine, memStates) {
|
|
f := withZFS(t)
|
|
f.uid = 0
|
|
f.now = time.Date(2026, 10, 9, 12, 0, 0, 0, time.Local) // a Friday
|
|
m := f.machine()
|
|
st := memStates{}
|
|
m.States, m.Call = st, l.call
|
|
return f, m, st
|
|
}
|
|
|
|
func keepsOfJSON(t *testing.T, s string) *Config {
|
|
c, ref := ParseConfig([]byte(`{"pool":"tank","snapshots":`+s+`}`), 16000)
|
|
if len(ref) != 0 {
|
|
t.Fatal(ref)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// §21, A-20, A-30, A-31, §31: a due kind takes one ZFS snapshot per filesystem of the pool (never a volume,
|
|
// never recursive), sets mesh:schedule, records taken, and says snapshot.taken.schedule; with the bus down, the
|
|
// ledger full, the pool above 80 % or the schedule paused none is taken, and snapshot.skipped.schedule says why.
|
|
func TestADueKindTakesOneSnapshotPerFilesystemAndRecordsEach(t *testing.T) {
|
|
stamp := time.Date(2026, 10, 9, 12, 0, 0, 0, time.Local).UTC().Format(StampForm)
|
|
fresh := time.Date(2026, 10, 9, 12, 0, 0, 0, time.Local).UTC().Format(time.RFC3339)
|
|
old := time.Date(2026, 10, 9, 10, 30, 0, 0, time.Local).UTC().Format(time.RFC3339)
|
|
cases := []struct {
|
|
name string
|
|
keeps string
|
|
l *ledgerCalls
|
|
setup func(f *fake, m *Machine, st memStates)
|
|
force bool
|
|
taken int
|
|
reason string
|
|
evKind string
|
|
wantErr bool
|
|
}{
|
|
{name: "hourly due", keeps: `{"hourly":2}`, l: &ledgerCalls{entries: []LedgerEntry{{"tank", "mesh-hourly-x", "hourly", old}}}, taken: 3, evKind: EventTakenSchedule},
|
|
{name: "hourly already taken this hour", keeps: `{"hourly":2}`, l: &ledgerCalls{entries: []LedgerEntry{{"tank", "mesh-hourly-x", "hourly", fresh}}}},
|
|
{name: "weekly not on a Friday", keeps: `{"weekly":2}`, l: &ledgerCalls{}},
|
|
{name: "keep none", keeps: `{"none":true}`, l: &ledgerCalls{}},
|
|
{name: "a kind with 0 is not taken", keeps: `{"hourly":0,"daily":0}`, l: &ledgerCalls{}},
|
|
{name: "bus down", keeps: `{"hourly":2}`, l: &ledgerCalls{read: errors.New("no bus")}, reason: "ledger-unavailable", evKind: EventSkippedSched},
|
|
{name: "ledger full", keeps: `{"hourly":1}`, l: &ledgerCalls{entries: make([]LedgerEntry, 6)}, reason: "ledger-full", evKind: EventSkippedSched},
|
|
{name: "pool above 80 percent", keeps: `{"hourly":2}`, l: &ledgerCalls{}, reason: "full", evKind: EventSkippedSched,
|
|
setup: func(f *fake, _ *Machine, _ memStates) {
|
|
f.on(line("zpool", PoolsArgv()), "tank\t1000\t900\t100\t90\t1\tONLINE\t1.00\n")
|
|
}},
|
|
{name: "paused", keeps: `{"hourly":2}`, l: &ledgerCalls{}, reason: "paused", evKind: EventSkippedSched,
|
|
setup: func(_ *fake, _ *Machine, st memStates) { st.Put(StateSchedule, KeyPaused, map[string]string{}) }},
|
|
{name: "a person's act takes every kind and says .person", keeps: `{"hourly":1,"daily":1}`, l: &ledgerCalls{}, force: true, taken: 6, evKind: EventTakenPerson},
|
|
{name: "the ledger refuses the record", keeps: `{"hourly":2}`, l: &ledgerCalls{record: errors.New("full")}, wantErr: true},
|
|
}
|
|
for _, c := range cases {
|
|
f, m, st := snapFake(t, c.keeps, c.l)
|
|
if c.setup != nil {
|
|
c.setup(f, m, st)
|
|
}
|
|
cmds := map[string]bool{}
|
|
for _, ds := range []string{"tank", "tank/media", "tank/downloads"} {
|
|
for _, k := range []string{"hourly", "daily"} {
|
|
n := "mesh-" + k + "-" + stamp
|
|
f.on("zfs snapshot -- "+ds+"@"+n, "")
|
|
f.on("zfs set mesh:schedule="+k+" -- "+ds+"@"+n, "")
|
|
cmds[ds+"@"+n] = true
|
|
}
|
|
}
|
|
_, err := m.TakeScheduled(context.Background(), keepsOfJSON(t, c.keeps), c.force, "person@node")
|
|
if (err != nil) != c.wantErr {
|
|
t.Errorf("%s: error %v", c.name, err)
|
|
}
|
|
snaps := ran(f, "zfs snapshot")
|
|
if c.wantErr {
|
|
if len(snaps) == 0 {
|
|
t.Errorf("%s: nothing taken", c.name)
|
|
}
|
|
continue
|
|
}
|
|
if len(snaps) != c.taken || len(c.l.records) != c.taken || len(ran(f, "zfs set mesh:schedule=")) != c.taken {
|
|
t.Errorf("%s: %d snapshots, %d sets, %d records, want %d: %v", c.name, len(snaps), len(ran(f, "zfs set mesh:schedule=")), len(c.l.records), c.taken, f.calls)
|
|
}
|
|
for _, s := range snaps {
|
|
if strings.Contains(s, "tank/vm") || strings.Contains(s, " -r") {
|
|
t.Errorf("%s: %s", c.name, s)
|
|
}
|
|
}
|
|
if c.evKind == "" && len(f.emitted) != 0 || c.evKind != "" && (len(f.emitted) == 0 || f.emitted[0]["_type"] != c.evKind) {
|
|
t.Errorf("%s: events %v, want %s", c.name, f.emitted, c.evKind)
|
|
}
|
|
if c.reason != "" && !strings.HasPrefix(f.emitted[0]["reason"].(string), c.reason) {
|
|
t.Errorf("%s: reason %v", c.name, f.emitted[0]["reason"])
|
|
}
|
|
if c.force && f.emitted[0]["by"] != "person@node" {
|
|
t.Errorf("%s: %v", c.name, f.emitted[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
// A-20: the tool zfs_snapshots_record refuses a snapshot not named mesh- and one of another operation.
|
|
type fakeLedger struct{ applied []string }
|
|
|
|
func (l *fakeLedger) Apply(instance, op string, body any, call string) error {
|
|
l.applied = append(l.applied, op+" "+instance)
|
|
return nil
|
|
}
|
|
func (l *fakeLedger) Entries() ([]LedgerEntry, error) { return nil, nil }
|
|
|
|
func TestTheLedgerTakesOnlyTheRunsSnapshots(t *testing.T) {
|
|
l := &fakeLedger{}
|
|
m := newFake(t).machine()
|
|
m.Ledger = l
|
|
e := LedgerEntry{Dataset: "tank/media", Name: "mesh-hourly-20261009T1000Z", Kind: "hourly", At: "2026-10-09T10:00:00Z"}
|
|
if err := m.Record("taken", e, "id"); err != nil || len(l.applied) != 1 || l.applied[0] != "taken snapshot/hourly-20261009T1000Z-tank~media" {
|
|
t.Fatalf("%v %v", err, l.applied)
|
|
}
|
|
if err := m.Record("removed", e, "id"); err != nil || l.applied[1] != "removed snapshot/hourly-20261009T1000Z-tank~media" {
|
|
t.Fatal(err, l.applied)
|
|
}
|
|
for _, bad := range []LedgerEntry{{Dataset: "tank", Name: "before-move"}, {Dataset: "tank", Name: "mesh-yearly-20261009T1000Z"},
|
|
{Dataset: "", Name: "mesh-hourly-20261009T1000Z"}, {Dataset: "tank@x", Name: "mesh-hourly-20261009T1000Z"}} {
|
|
if err := m.Record("taken", bad, ""); err == nil {
|
|
t.Errorf("accepted %+v", bad)
|
|
}
|
|
}
|
|
if err := m.Record("destroy", e, ""); err == nil || len(l.applied) != 2 {
|
|
t.Errorf("another operation: %v %v", err, l.applied)
|
|
}
|
|
}
|