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.
209 lines
8.0 KiB
Go
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)
|
|
}
|
|
}
|