Files
mesh-catalog/modules/zfs/cmd/zfs-tools/snapshot_test.go
T
jochen c86f055384
mesh/merge-gate pass: builds new: modules/zfs, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Give the test snapshot a creation time apart from the fake clock in every zone
On the build node, in UTC, the fixture's creation time was the fake clock's own instant, so the test
could not tell the list's time from the clock's. A day and a half earlier is apart from it everywhere.
2026-10-09 00:16:29 +02:00

209 lines
8.0 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", false)...)
if got := p + " " + strings.Join(argv, " "); got != takeLine {
t.Errorf("ran %q", got)
}
p, argv = m.Escalated("zfs", SnapshotArgv("tank", "x", true)...)
if got := p + " " + strings.Join(argv, " "); got != "sudo -n zfs snapshot -r -- tank@x" {
t.Errorf("recursive: %q", got)
}
root := &Machine{UID: 0}
if p, argv := root.Escalated("zfs", SnapshotArgv("tank", "x", false)...); 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", false)
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"] != SnapshotEvent || 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", false)
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", false)
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", false)
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", false)
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", "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, false)
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 TestRecursiveIsRefusedWhenAChildHasTheNameAlready(t *testing.T) {
f := withZFS(t)
_, err := f.machine().TakeSnapshot(context.Background(), "tank", "weekly", "why", true)
if err == nil || !strings.Contains(err.Error(), "tank/downloads@weekly already exists") {
t.Fatalf("err: %v", err)
}
// Without recursive, only tank's own name counts.
f = withZFS(t)
snapshotting(f, "sudo -n zfs snapshot -- tank@weekly", "tank@weekly\t1791400000\t0\t1")
if a, err := f.machine().TakeSnapshot(context.Background(), "tank", "weekly", "why", false); err != nil || len(a.Made) != 1 {
t.Errorf("tank@weekly alone: %v %+v", err, a)
}
}
func TestRecursiveAnswersEveryChildsSnapshot(t *testing.T) {
f := withZFS(t)
snapshotting(f, "sudo -n zfs snapshot -r -- tank@all", "tank@all\t1\t0\t1", "tank/media@all\t1\t0\t1",
"tank/downloads@all\t1\t0\t1", "tank/vm@all\t1\t0\t1")
a, err := f.machine().TakeSnapshot(context.Background(), "tank", "all", "why", true)
if err != nil || len(a.Made) != 4 || !a.Recursive {
t.Fatalf("%v %+v", err, a)
}
}
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" {
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)
}
}