package storage import ( "testing" "time" ) func TestSnapTimeReadsZfSnapNames(t *testing.T) { // the real thing off grid taken, exp, ok := SnapTime("tank@2026-09-24_02.30.01--6w") if !ok { t.Fatal("zfSnap's own name must parse") } if taken.Format("2006-01-02 15:04:05") != "2026-09-24 02:30:01" { t.Fatalf("taken = %v", taken) } if got := exp.Sub(taken); got != 6*7*24*time.Hour { t.Fatalf("ttl = %v, want 6w", got) } // a labelled one from a policy with a prefix if _, e, ok := SnapTime("tank/apps@nightly-2026-09-24_02.30.01--30d"); !ok || e.IsZero() { t.Fatal("a prefixed name must parse and expire") } // no TTL: never expires if _, e, ok := SnapTime("tank@2026-09-24_02.30.01"); !ok || !e.IsZero() { t.Fatal("a name without a TTL must parse and never expire") } // things that are not ours for _, n := range []string{"tank@before-upgrade", "tank@manual", "tank", "tank@2026-13-45_99.99.99--6w"} { if _, _, ok := SnapTime(n); ok { t.Errorf("%q must not parse as a scheduled snapshot", n) } } } func TestScheduleIsDueAndNext(t *testing.T) { now := time.Date(2026, 9, 24, 9, 15, 0, 0, time.Local) daily := &Policy{Every: "daily", At: "02:30", Enabled: true} if d := daily.lastDue(now); d.Hour() != 2 || d.Day() != 24 { t.Fatalf("daily lastDue = %v", d) } if !daily.Due(now) { t.Fatal("never run: must be due") } daily.LastRun = now.Add(-time.Hour) // ran at 08:15 today, after 02:30 if daily.Due(now) { t.Fatal("already ran since the window: not due") } if n := daily.NextRun(now); n.Day() != 25 || n.Hour() != 2 { t.Fatalf("daily NextRun = %v", n) } // a machine that was off through its window runs once when it comes back, not never daily.LastRun = now.AddDate(0, 0, -2) if !daily.Due(now) { t.Fatal("a missed window must still be due") } // disabled never runs daily.Enabled = false if daily.Due(now) { t.Fatal("disabled must never be due") } weekly := &Policy{Every: "weekly", At: "04:00", Weekday: int(time.Sunday), Enabled: true} if d := weekly.lastDue(now); d.Weekday() != time.Sunday || d.After(now) { t.Fatalf("weekly lastDue = %v (%v)", d, d.Weekday()) } hourly := &Policy{Every: "hourly", At: "00:30", Enabled: true} if d := hourly.lastDue(now); d.Minute() != 30 || d.Hour() != 8 { t.Fatalf("hourly lastDue at 09:15 with minute 30 = %v, want 08:30", d) } } func TestValidateRefusesNonsense(t *testing.T) { ok := Policy{Dataset: "tank/apps", Every: "daily", At: "02:30", KeepFor: "6w"} if err := ok.Validate(); err != nil { t.Fatalf("a sane policy: %v", err) } bad := []Policy{ {Dataset: "", Every: "daily", At: "02:30", KeepFor: "6w"}, {Dataset: "tank", Every: "yearly", At: "02:30", KeepFor: "6w"}, {Dataset: "tank", Every: "daily", At: "25:00", KeepFor: "6w"}, {Dataset: "tank", Every: "daily", At: "02:30"}, // no retention {Dataset: "tank", Every: "daily", At: "02:30", KeepFor: "6 weeks"}, // unparseable {Dataset: "tank", Every: "monthly", At: "02:30", Day: 31, KeepFor: "6w"}, // not every month has one {Dataset: "tank", Every: "daily", At: "02:30", KeepFor: "6w", Prefix: "a--b"}, } for i, p := range bad { if err := p.Validate(); err == nil { t.Errorf("bad policy %d was accepted: %+v", i, p) } } } func TestParseAge(t *testing.T) { for s, want := range map[string]time.Duration{ "6w": 6 * 7 * 24 * time.Hour, "30d": 30 * 24 * time.Hour, "12h": 12 * time.Hour, "90m": 90 * time.Minute, } { if got, err := ParseAge(s); err != nil || got != want { t.Errorf("ParseAge(%q) = %v, %v; want %v", s, got, err, want) } } for _, s := range []string{"", "6", "w", "-1d", "0w", "6y", "six weeks"} { if _, err := ParseAge(s); err == nil { t.Errorf("ParseAge(%q) should fail", s) } } }