Files
mesh-catalog/modules/zfs/cmd/zfs-tools/snapshots_test.go
T
2026-11-13 00:43:09 +01:00

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)
}
}