Files
mesh-catalog/modules/zfs/cmd/zfs-tools/snapshot_test.go
T
2026-11-13 00:41:21 +01:00

182 lines
6.9 KiB
Go

package main
import (
"context"
"errors"
"strings"
"testing"
"time"
)
const takeLine = "sudo -n zfs snapshot -- tank/media@before-move"
// snapshotting makes the fake's snapshot command add a snapshot to what the list then answers, with a
// creation time of its own: the answer must carry the list's time, not the clock's.
func snapshotting(f *fake, cmd string, added ...string) {
f.on(cmd, "")
f.after[cmd] = func(f *fake) {
key := line("zfs", SnapshotsArgv())
r := f.answers[key]
for _, a := range added {
r.Out += a + "\n"
}
f.answers[key] = r
}
}
func TestTheSnapshotCommandIsFixedWordsThroughSudoWithTheNameAfterDashDash(t *testing.T) {
m := &Machine{UID: 1000}
p, argv := m.Escalated("zfs", SnapshotArgv("tank/media", "before-move")...)
if got := p + " " + strings.Join(argv, " "); got != takeLine {
t.Errorf("ran %q", got)
}
root := &Machine{UID: 0}
if p, argv := root.Escalated("zfs", SnapshotArgv("tank", "x")...); p != "zfs" || argv[0] != "snapshot" {
t.Errorf("as root, no sudo: %s %v", p, argv)
}
}
func TestASnapshotIsAnsweredFromTheListAfterwardsAndRecorded(t *testing.T) {
f := withZFS(t)
snapshotting(f, takeLine, "tank/media@before-move\t1791400000\t0\t26938034880512")
a, err := f.machine().TakeSnapshot(context.Background(), "tank/media", "before-move", "before moving the library", "tester")
if err != nil {
t.Fatal(err)
}
if !a.Done || len(a.Made) != 1 || a.Made[0].Name != "tank/media@before-move" || a.Made[0].Referenced != 26938034880512 {
t.Fatalf("answer: %+v", a)
}
if a.Made[0].Created != time.Unix(1791400000, 0).Local().Format(time.RFC3339) || a.Made[0].Created == f.now.Format(time.RFC3339) {
t.Errorf("the creation time is the list's: %s", a.Made[0].Created)
}
if len(a.Others) != 0 || !strings.Contains(a.Said, "No other snapshot came or went") || !strings.Contains(a.Said, "Why: before moving the library") {
t.Errorf("said: %s / %v", a.Said, a.Others)
}
if strings.Join(a.Ran, " ") != takeLine || len(ran(f, "sudo")) != 1 {
t.Errorf("ran: %v; every call: %v", a.Ran, f.calls)
}
if len(f.emitted) != 1 || f.emitted[0]["_type"] != EventTakenPerson || f.emitted[0]["why"] != "before moving the library" ||
f.emitted[0]["done"] != true || f.emitted[0]["made"] != "tank/media@before-move" || !a.Recorded {
t.Errorf("recorded: %+v", f.emitted)
}
}
func TestASnapshotTheListDoesNotShowIsNotAnsweredAsMade(t *testing.T) {
f := withZFS(t)
f.on(takeLine, "") // zfs exits 0, and the list does not change
a, err := f.machine().TakeSnapshot(context.Background(), "tank/media", "before-move", "why", "tester")
if err == nil || a.Done || !strings.Contains(err.Error(), "the list shows 0 of the 1") {
t.Fatalf("err %v, answer %+v", err, a)
}
if len(f.emitted) != 1 || f.emitted[0]["done"] != false {
t.Errorf("the failed act is recorded too: %+v", f.emitted)
}
}
func TestARefusedSudoIsSaidAndRecorded(t *testing.T) {
f := withZFS(t)
f.answers[takeLine] = Ran{Code: 1, Err: "sudo: a password is required"}
_, err := f.machine().TakeSnapshot(context.Background(), "tank/media", "before-move", "why", "tester")
if err == nil || !strings.Contains(err.Error(), "NOT DONE") || !strings.Contains(err.Error(), "sudo refused the operator account") {
t.Fatalf("err: %v", err)
}
if len(f.emitted) != 1 || f.emitted[0]["done"] != false {
t.Errorf("recorded: %+v", f.emitted)
}
}
func TestAnUnrecordedActSaysSoLoudly(t *testing.T) {
f := withZFS(t)
snapshotting(f, takeLine, "tank/media@before-move\t1791400000\t0\t1")
f.emitErr = errors.New("the bus did not accept it")
a, err := f.machine().TakeSnapshot(context.Background(), "tank/media", "before-move", "why", "tester")
if err != nil {
t.Fatal(err)
}
if a.Recorded || !strings.Contains(a.Said, "NOT RECORDED on the bus: the bus did not accept it") {
t.Errorf("said: %s", a.Said)
}
}
func TestAnotherSnapshotMeanwhileIsSaidNotHidden(t *testing.T) {
f := withZFS(t)
snapshotting(f, takeLine, "tank/media@before-move\t1791400000\t0\t1", "tank/downloads@auto-1200\t1791547201\t0\t1")
a, err := f.machine().TakeSnapshot(context.Background(), "tank/media", "before-move", "why", "tester")
if err != nil {
t.Fatal(err)
}
if len(a.Others) != 1 || a.Others[0] != "appeared: tank/downloads@auto-1200" || !strings.Contains(a.Said, "not by this act") {
t.Errorf("others: %v; said %s", a.Others, a.Said)
}
}
func TestEveryRefusalRunsNoSnapshotCommand(t *testing.T) {
cases := []struct{ dataset, name, why, says string }{
{"tank/nope", "x", "why", "no dataset"},
{"", "x", "why", "name the dataset"},
{"tank/media@x", "y", "why", "is not a dataset"},
{"tank/media", "", "why", "name the snapshot"},
{"tank/media", "mesh-hourly-20261109T1200Z", "why", "may not begin"},
{"tank/media", "a b", "why", "may hold only"},
{"tank/media", "x;rm -rf /", "why", "may hold only"},
{"tank/media", "$(reboot)", "why", "may hold only"},
{"tank/media", "a/b", "why", "may hold only"},
{"tank/media", "a@b", "why", "may hold only"},
{"tank/media", "-r", "why", ""}, // a name may start with '-': it follows -- and the dataset's @
{"tank/media", strings.Repeat("x", 250), "why", "longer than"},
{"tank/media", "before-rename", "why", "already exists"},
{"tank/media", "x", "", "say why"},
{"tank/media", "x", "one\ntwo", "one line"},
}
for _, c := range cases {
f := withZFS(t)
_, err := f.machine().TakeSnapshot(context.Background(), c.dataset, c.name, c.why, "tester")
if c.says == "" {
if len(ran(f, "sudo -n zfs snapshot -- tank/media@-r")) != 1 {
t.Errorf("%q: the name follows -- and runs as a name: %v", c.name, f.calls)
}
continue
}
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%q@%q why %q: %v", c.dataset, c.name, c.why, err)
}
if n := len(ran(f, "sudo")); n != 0 {
t.Errorf("%q@%q: a refusal ran %v", c.dataset, c.name, f.calls)
}
}
}
func TestAMachineWithoutZFSSaysTheModuleDoesNotApply(t *testing.T) {
f := newFake(t) // every command: executable file not found
m := f.machine()
for _, tool := range Tools(m) {
if tool.Name == "zfs_health" || handTools[tool.Name] {
continue
}
args := map[string]any{"dataset": "tank", "name": "x", "why": "y"}
got, err := tool.Run(args)
if err != nil {
t.Errorf("%s: %v", tool.Name, err)
continue
}
na, ok := got.(NotApplicable)
if !ok || na.Applicable || !strings.Contains(na.Said, "does not apply here") {
t.Errorf("%s: %+v", tool.Name, got)
}
}
h := m.Health(context.Background())
if h.Healthy || !strings.Contains(h.Why, "not installed") || !strings.Contains(h.Why, "Unassign") {
t.Errorf("health: %+v", h)
}
if n := len(ran(f, "sudo -n zfs snapshot")); n != 0 {
t.Errorf("no snapshot is tried: %v", f.calls)
}
}
func TestAMissingKernelModuleIsNotApplicableToo(t *testing.T) {
r := Ran{Code: 1, Err: "The ZFS modules cannot be auto-loaded.\nTry running 'modprobe zfs' as root to manually load them."}
if why := absent(r); !strings.Contains(why, "kernel module is not loaded") {
t.Errorf("absent: %q", why)
}
}