package main import ( "context" "strings" "testing" "time" ) const scrubConf = "pool=tank\ncadence=weekly\nday=Mon\ntime=00:00\n" // scheduled is the healthy fixtures with the module's scrub schedule for tank, its timer waiting and its // last run a success. func scheduled(t *testing.T) *fake { f := withZFS(t) f.files[ScrubConfig] = scrubConf f.on(line("systemctl", TimerShowArgv(ScrubTimer)), "ActiveState=active\nUnitFileState=enabled\nNextElapseUSecRealtime=@1791842400\nLastTriggerUSec=@1791237600\n") f.on(line("systemctl", ServiceShowArgv(ScrubService)), "ActiveState=inactive\nResult=success\nExecMainStartTimestamp=@1791237600\nExecMainExitTimestamp=@1791331200\n") return f } func TestTheScheduleIsTheConfigAndWhatTheTimerSays(t *testing.T) { f := scheduled(t) got, err := f.machine().Scrubs(context.Background(), false) if err != nil { t.Fatal(err) } a := got.(ScrubAnswer) s := a.Schedule if s == nil || s.Pool != "tank" || s.Cadence != "weekly" || s.Timer != "zfs-mesh-scrub.timer" || s.TimerState != "active" || s.Enabled != "enabled" || s.Next == nil || !s.Next.Equal(time.Date(2026, 10, 12, 0, 0, 0, 0, time.Local)) || s.LastResult != "success" { t.Fatalf("schedule: %+v", s) } if want := s.NextSlot(f.now).Format("2006-01-02 15:04"); !strings.Contains(strings.Join(a.Said, "\n"), "next scheduled scrub of tank: "+want) || want != "2026-10-12 00:00" { t.Errorf("said: %v", a.Said) } } func TestAScheduleThatIsNotSetIsSaidAndAnInvalidOneRefused(t *testing.T) { f := withZFS(t) got, _ := f.machine().Scrubs(context.Background(), false) if a := got.(ScrubAnswer); a.Schedule != nil || !strings.Contains(strings.Join(a.Said, " "), "no scrub schedule") { t.Errorf("no config: %+v", a) } for _, conf := range []string{ "pool=tank\ncadence=daily\nday=Mon\ntime=00:00\n", "pool=tank\ncadence=weekly\nday=Monday\ntime=00:00\n", "pool=tank\ncadence=weekly\nday=Mon\ntime=25:00\n", "pool=tank x\ncadence=weekly\nday=Mon\ntime=00:00\n", } { if _, err := ParseScrubConfig(conf); err == nil { t.Errorf("accepted %q", conf) } } } func TestTheScheduleWarnsOfAnOldScrubAFailedRunAndAStoppedTimer(t *testing.T) { f := scheduled(t) h := f.machine().Health(context.Background()) if !h.Healthy { t.Fatalf("%+v", h) } w := strings.Join(h.Warnings, " ") // tank's last scrub finished 2026-09-14, 25 days before the fake clock: past weekly plus its margin. if !strings.Contains(w, "Pool tank was last scrubbed 25 days ago, more than its weekly schedule allows (9 days)") { t.Errorf("warnings: %v", h.Warnings) } f.on(line("systemctl", ServiceShowArgv(ScrubService)), "ActiveState=failed\nResult=exit-code\nExecMainStartTimestamp=@1791237600\nExecMainExitTimestamp=@1791237601\n") f.on(line("systemctl", TimerShowArgv(ScrubTimer)), "ActiveState=inactive\nUnitFileState=disabled\nNextElapseUSecRealtime=\nLastTriggerUSec=@1791237600\n") w = strings.Join(f.machine().Health(context.Background()).Warnings, " ") for _, want := range []string{"The last scheduled scrub of tank failed (exit-code)", "The scrub schedule's timer zfs-mesh-scrub.timer is inactive and disabled"} { if !strings.Contains(w, want) { t.Errorf("lacks %q: %s", want, w) } } } func TestTheScrubActsAreFixedCommandsWithWhyReadBackAndRecorded(t *testing.T) { for act, cmd := range map[string]string{ "start": "sudo -n zpool scrub -- tank", "pause": "sudo -n zpool scrub -p -- tank", "stop": "sudo -n zpool scrub -s -- tank", } { f := withZFS(t) f.on(cmd, "") a, err := f.machine().ScrubAct(context.Background(), act, "tank", "the operator asked") if err != nil { t.Fatalf("%s: %v", act, err) } if strings.Join(a.Ran, " ") != cmd || a.After == "" || !a.Done { t.Errorf("%s: %+v", act, a) } if len(f.emitted) != 1 || f.emitted[0]["_type"] != ScrubEvent || f.emitted[0]["act"] != act || f.emitted[0]["why"] != "the operator asked" { t.Errorf("%s recorded: %+v", act, f.emitted) } } f := withZFS(t) for _, c := range []struct{ act, pool, why, says string }{ {"start", "nope", "why", "no pool"}, {"start", "tank", "", "say why"}, {"start", "-s", "why", "no pool"}, {"destroy", "tank", "why", "not an act"}, } { if _, err := f.machine().ScrubAct(context.Background(), c.act, c.pool, c.why); err == nil || !strings.Contains(err.Error(), c.says) { t.Errorf("%+v: %v", c, err) } } if n := len(ran(f, "sudo")); n != 0 { t.Errorf("a refusal ran %v", f.calls) } f.answers["sudo -n zpool scrub -- tank"] = Ran{Code: -1, TimedOut: true} if _, err := f.machine().ScrubAct(context.Background(), "start", "tank", "why"); err == nil || !strings.Contains(err.Error(), "outcome unknown; check zfs_scrub") { t.Errorf("timed out: %v", err) } }