The safety review of the first version found it safe for the pool's data and asked for these fixes before it goes on the home server: - zfs_snapshot: no recursive. Every answer gives the dataset's free space and the pool's capacity, and says that the snapshot holds every block deleted or overwritten until it is destroyed by hand. - A command that runs out of time is ended with its whole process group. An act that timed out is answered from the list, or as "outcome unknown", never as "NOT DONE". - The watch runs as the operator account. - The event follower reads ZFS's queue from its start, with a cursor per boot, so the pool's import and any downtime reach the journal once. It writes "dropped N events", kills zpool before waiting for it, and exits non-zero after five failed follows in a row. - Past error counts are a warning, and counts that rise are unhealthy. - The guard test catches .Run(, exec.Command(, exec.CommandContext( and process starts. - The fixtures and the parser follow OpenZFS 2.4.4's own printers: human scan sizes under -p, no iostat section lines, error scrubs, per-vdev rebuild resilvers, paused scrubs. The operator also asked the module to own the scrub. A timer and a service of the module's own run zpool scrub -w -- <pool> from the scrub-pool, scrub-cadence, scrub-day and scrub-time settings. In the same declaration, the distribution's weekly and monthly timers for that pool are held stopped and disabled, so exactly one schedule scrubs it. zfs_scrub shows the schedule and the next run, with any resilver's progress. zfs_scrub_start, zfs_scrub_pause and zfs_scrub_stop act with why. The health check warns of a late or failed scrub and of a running resilver, and is unhealthy for a resilver that finished with errors.
404 lines
16 KiB
Go
404 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDatasetsAreReadFromZfsListWithEverySizeInBytes(t *testing.T) {
|
|
ds, err := ParseDatasets(fixture(t, "zfs-list.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(ds) != 4 {
|
|
t.Fatalf("datasets: %d", len(ds))
|
|
}
|
|
media := find(ds, "tank/media")
|
|
if media == nil || media.Mountpoint != "/storage/media" || media.Used != 27487790694400 || media.Available != 11203942195200 ||
|
|
media.Referenced != 26938034880512 || media.UsedBySnapshots != 549755813888 || media.ShareNFS != "rw=@192.0.2.0/24" ||
|
|
media.CompressRatio != 1.00 || media.Pool != "tank" || media.Compression != "lz4" {
|
|
t.Errorf("tank/media: %+v", media)
|
|
}
|
|
if media.Quota != nil || media.Reservation != nil {
|
|
t.Errorf("a quota of 0 under -p is none: %+v", media)
|
|
}
|
|
dl := find(ds, "tank/downloads")
|
|
if dl.Quota == nil || *dl.Quota != 2199023255552 {
|
|
t.Errorf("tank/downloads has a 2 TiB quota: %+v", dl.Quota)
|
|
}
|
|
vm := find(ds, "tank/vm")
|
|
if vm.Type != "volume" || vm.Mountpoint != "" || vm.ShareNFS != "" || vm.CompressRatio != 1.85 ||
|
|
vm.RefReservation == nil || *vm.RefReservation != 4398046511 || vm.UsedByRefreservation != 2250562863 || vm.Quota != nil {
|
|
t.Errorf("a volume has no mountpoint or sharenfs, and its refreservation is read: %+v", vm)
|
|
}
|
|
}
|
|
|
|
func TestALineWithoutEveryColumnIsRefusedNotSkipped(t *testing.T) {
|
|
if _, err := ParseDatasets("tank\tfilesystem\t/storage\n"); err == nil || !strings.Contains(err.Error(), "3 columns") {
|
|
t.Errorf("a short line: %v", err)
|
|
}
|
|
if _, err := ParseDatasets(strings.Replace(fixture(t, "zfs-list.txt"), "27487790694400", "25.0T", 1)); err == nil {
|
|
t.Errorf("a size that is not whole bytes is refused (the command lost its -p)")
|
|
}
|
|
if _, err := ParsePools("tank\t1\t2\n"); err == nil {
|
|
t.Errorf("a short pool line is refused")
|
|
}
|
|
}
|
|
|
|
func TestSnapshotsCarryTheirCreationTimeAndTheirOwnSpace(t *testing.T) {
|
|
ss, err := ParseSnapshots(fixture(t, "zfs-list-snapshots.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(ss) != 4 {
|
|
t.Fatalf("snapshots: %d", len(ss))
|
|
}
|
|
s := ss[0]
|
|
if s.Dataset != "tank/media" || s.Snap != "2026-08-01" || !s.Created.Equal(time.Unix(1785542400, 0)) ||
|
|
s.Used != 274877906944 || s.Referenced != 26388279066624 {
|
|
t.Errorf("first snapshot: %+v", s)
|
|
}
|
|
}
|
|
|
|
func TestPoolsAreReadFromZpoolList(t *testing.T) {
|
|
ps, err := ParsePools(fixture(t, "zpool-list.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(ps) != 2 || ps[0].Name != "tank" || ps[0].Capacity != 60 || ps[0].Fragmentation == nil || *ps[0].Fragmentation != 3 ||
|
|
ps[0].Health != "ONLINE" || ps[0].Size != 47982397226598 || ps[0].Free != 19184805568512 {
|
|
t.Fatalf("pools: %+v", ps)
|
|
}
|
|
if ps[1].Fragmentation != nil {
|
|
t.Errorf("a pool whose fragmentation is '-' has none said: %v", *ps[1].Fragmentation)
|
|
}
|
|
}
|
|
|
|
func TestStatusGivesEachPoolsLayoutWithSectionsAndTheLastScrub(t *testing.T) {
|
|
st, err := ParseStatus(fixture(t, "zpool-status.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(st) != 2 || st[0].Name != "rpool" || st[1].Name != "tank" {
|
|
t.Fatalf("pools: %+v", st)
|
|
}
|
|
r := st[0]
|
|
if !strings.HasPrefix(r.Status, "Some supported and requested features") || !strings.HasSuffix(r.Status, "some features are unavailable.") {
|
|
t.Errorf("a status of two lines is one sentence: %q", r.Status)
|
|
}
|
|
tank := st[1]
|
|
if tank.State != "ONLINE" || tank.Errors != "No known data errors" || tank.Root == nil || tank.Root.Kind != "pool" {
|
|
t.Fatalf("tank: %+v", tank)
|
|
}
|
|
kids := tank.Root.Children
|
|
if len(kids) != 4 {
|
|
t.Fatalf("tank's top level is raidz2-0, logs, cache and spares: %+v", kids)
|
|
}
|
|
if kids[0].Name != "raidz2-0" || kids[0].Kind != "vdev" || kids[0].Role != "data" || len(kids[0].Children) != 4 ||
|
|
kids[0].Children[0].Name != "ata-WDC_WD120EFBX-68B0EN0_D7JA0001" || kids[0].Children[0].Kind != "device" {
|
|
t.Errorf("raidz2-0: %+v", kids[0])
|
|
}
|
|
if kids[1].Kind != "section" || kids[1].Role != "log" || len(kids[1].Children) != 1 || kids[1].Children[0].Name != "mirror-1" ||
|
|
len(kids[1].Children[0].Children) != 2 || kids[1].Children[0].Children[1].Role != "log" {
|
|
t.Errorf("logs: %+v", kids[1])
|
|
}
|
|
if kids[2].Role != "cache" || kids[2].Children[0].Kind != "device" || kids[2].Children[0].State != "ONLINE" {
|
|
t.Errorf("cache: %+v", kids[2])
|
|
}
|
|
if kids[3].Role != "spare" || kids[3].Children[0].State != "AVAIL" || kids[3].Children[0].Note != "" {
|
|
t.Errorf("spares: %+v", kids[3].Children[0])
|
|
}
|
|
s := tank.Scrub()
|
|
end := time.Date(2026, 9, 14, 2, 34, 6, 0, time.Local)
|
|
if s.Kind != "scrub" || s.State != "finished" || s.Took != "1 days 02:10:05" || s.Repaired != "0B" || deref(s.Errors) != 0 ||
|
|
s.Ended == nil || !s.Ended.Equal(end) || s.Started == nil || !s.Started.Equal(end.Add(-26*time.Hour-10*time.Minute-5*time.Second)) {
|
|
t.Errorf("tank's scrub: %+v", s)
|
|
}
|
|
}
|
|
|
|
func TestADegradedPoolSaysItsFaultedDeviceItsErrorsAndTheResilver(t *testing.T) {
|
|
st, err := ParseStatus(fixture(t, "zpool-status-degraded.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := st[0]
|
|
if p.State != "DEGRADED" || p.Action != "Wait for the resilver to complete." || p.Errors != "2 data errors, use '-v' for a list" {
|
|
t.Fatalf("pool: %+v", p)
|
|
}
|
|
spare := p.Root.Children[0].Children[1]
|
|
if spare.Name != "spare-1" || spare.Kind != "vdev" || len(spare.Children) != 2 {
|
|
t.Fatalf("spare-1 holds the failed disk and its spare: %+v", spare)
|
|
}
|
|
bad := spare.Children[0]
|
|
if bad.State != "FAULTED" || bad.Read != 12 || bad.Write != 107 || bad.Checksum != 3 || bad.Note != "too many errors" {
|
|
t.Errorf("the faulted disk: %+v", bad)
|
|
}
|
|
if spare.Children[1].Note != "(resilvering)" {
|
|
t.Errorf("the spare: %+v", spare.Children[1])
|
|
}
|
|
if in := p.Root.Children[1].Children[0]; in.State != "INUSE" || in.Note != "currently in use" {
|
|
t.Errorf("the spare in use: %+v", in)
|
|
}
|
|
s := p.Resilvers()[0]
|
|
if s.Kind != "resilver" || s.State != "running" || s.Done == nil || *s.Done != 22.72 || s.ToGo != "08:25:13" ||
|
|
s.Started == nil || !s.Started.Equal(time.Date(2026, 10, 8, 21, 2, 17, 0, time.Local)) {
|
|
t.Errorf("the resilver: %+v", s)
|
|
}
|
|
}
|
|
|
|
func TestARunningScrubSaysItsProgress(t *testing.T) {
|
|
st, err := ParseStatus(fixture(t, "zpool-status-scrubbing.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := st[0].Scrub()
|
|
if s.Kind != "scrub" || s.State != "running" || *s.Done != 29.17 || s.ToGo != "06:49:34" || s.Repaired != "0B" {
|
|
t.Errorf("scrub: %+v", s)
|
|
}
|
|
for _, said := range []string{"none requested", "scrub canceled on Sun Oct 4 01:00:00 2026", "scrub paused since Sun Oct 4 01:00:00 2026"} {
|
|
if got := parseScan("scan", said); got.State == "unknown" {
|
|
t.Errorf("%q not read: %+v", said, got)
|
|
}
|
|
}
|
|
if got := parseScan("scan", "resilvered 1.01G in 03:22:11 with 0 errors on Fri Oct 9 06:00:00 2026"); got.Kind != "resilver" || got.State != "finished" || got.Repaired != "1.01G" {
|
|
t.Errorf("a finished resilver: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestTheHistoryGivesTheLastFinishedScrubAndResilverNotACanceledOne(t *testing.T) {
|
|
scrub, resilver := ParseHistory(fixture(t, "zpool-history.txt"))
|
|
if scrub == nil || !scrub.Ended.Equal(time.Date(2026, 7, 13, 2, 30, 0, 0, time.Local)) || deref(scrub.Errors) != 0 {
|
|
t.Errorf("the last finished scrub is July's; September's was canceled: %+v", scrub)
|
|
}
|
|
if resilver == nil || !resilver.Ended.Equal(time.Date(2026, 8, 20, 19, 40, 2, 0, time.Local)) {
|
|
t.Errorf("the last finished resilver: %+v", resilver)
|
|
}
|
|
}
|
|
|
|
func TestIOSampleNamesEachLinesPoolAndKindFromTheLayout(t *testing.T) {
|
|
st, _ := ParseStatus(fixture(t, "zpool-status.txt"))
|
|
rows, err := ParseIO(fixture(t, "zpool-iostat.txt"), st)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]string{"tank": "pool", "raidz2-0": "vdev", "ata-WDC_WD120EFBX-68B0EN0_D7JA0001": "device",
|
|
"mirror-1": "vdev", "rpool": "pool", "nvme-Samsung_SSD_970_EVO_Plus_500GB_S4EVNX0R200001-part2": "device"}
|
|
for _, r := range rows {
|
|
if k, ok := want[r.Name]; ok && r.Kind != k {
|
|
t.Errorf("%s is a %s, said %s", r.Name, k, r.Kind)
|
|
}
|
|
}
|
|
last := rows[len(rows)-1]
|
|
if last.Pool != "rpool" || last.WriteOps != 25 || last.WriteBytes != 1310720 {
|
|
t.Errorf("the last line is rpool's disk: %+v", last)
|
|
}
|
|
if rows[2].Allocated != nil || rows[0].ReadBytes != 221249536 {
|
|
t.Errorf("a disk of a raidz has no allocation of its own: %+v / %+v", rows[2], rows[0])
|
|
}
|
|
}
|
|
|
|
func TestTheArcIsReadFromArcstats(t *testing.T) {
|
|
f := newFake(t)
|
|
f.files[ArcStats] = fixture(t, "arcstats.txt")
|
|
got, err := f.machine().Arc()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := got.(ArcAnswer)
|
|
if a.Size != 16798000128 || a.Target != 16823304192 || a.Max != 33646608384 || a.HitRatio != 90.5 ||
|
|
a.DemandHitRatio == nil || *a.DemandHitRatio != 93.0 || a.L2 == nil || a.L2.HitRatio != 25.0 || a.L2.Size != 412316860416 {
|
|
t.Errorf("arc: %+v %+v", a, a.L2)
|
|
}
|
|
f.files[ArcStats] = fixture(t, "arcstats-nol2.txt")
|
|
got, _ = f.machine().Arc()
|
|
if a := got.(ArcAnswer); a.L2 != nil || a.HitRatio != 90 || !strings.Contains(strings.Join(a.Said, " "), "no L2ARC") {
|
|
t.Errorf("without a cache device: %+v", a)
|
|
}
|
|
}
|
|
|
|
func TestEventsAreErrorsAndChangesOfStateNewestFirstAsRoot(t *testing.T) {
|
|
f := newFake(t)
|
|
f.on("sudo -n zpool events -v", fixture(t, "zpool-events.txt"))
|
|
got, err := f.machine().Events(context.Background(), false, 50)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := got.(EventsAnswer)
|
|
if a.Kept != 5 || a.Shown != 3 {
|
|
t.Fatalf("kept %d, shown %d: %+v", a.Kept, a.Shown, a.Events)
|
|
}
|
|
if a.Events[0].Class != "sysevent.fs.zfs.resilver_start" || a.Events[1].Class != "resource.fs.zfs.statechange" ||
|
|
a.Events[2].Class != "ereport.fs.zfs.io" {
|
|
t.Errorf("newest first, history and config_sync left out: %+v", a.Events)
|
|
}
|
|
sc := a.Events[1].Details
|
|
if sc["vdev_state"] != "FAULTED" || sc["vdev_laststate"] != "ONLINE" || sc["pool"] != "tank" ||
|
|
sc["vdev_path"] != "/dev/disk/by-id/ata-WDC_WD120EFBX-68B0EN0_D7JA0002-part1" {
|
|
t.Errorf("the state change: %+v", sc)
|
|
}
|
|
if _, kept := a.Events[2].Details["ena"]; kept {
|
|
t.Errorf("internal identifiers are left out: %+v", a.Events[2].Details)
|
|
}
|
|
got, _ = f.machine().Events(context.Background(), true, 2)
|
|
if a := got.(EventsAnswer); a.Shown != 2 || a.Events[1].Class != "sysevent.fs.zfs.config_sync" {
|
|
t.Errorf("all, limited to 2: %+v", a.Events)
|
|
}
|
|
}
|
|
|
|
func TestPropertiesWithholdWhereTheKeyIsAndValidateTheDataset(t *testing.T) {
|
|
f := withZFS(t)
|
|
f.on(line("zfs", PropertiesArgv("tank/media")), fixture(t, "zfs-get.txt"))
|
|
got, err := f.machine().PropertiesOf(context.Background(), "tank/media")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := got.(PropertiesAnswer)
|
|
by := map[string]Property{}
|
|
for _, p := range a.Properties {
|
|
by[p.Name] = p
|
|
}
|
|
if strings.Contains(by["keylocation"].Value, "s3cret") || by["encryption"].Value != "aes-256-gcm" || by["keystatus"].Value != "available" ||
|
|
by["tank:purpose"].Value != "the library" || by["recordsize"].Source != "local" {
|
|
t.Errorf("properties: %+v", by)
|
|
}
|
|
said := strings.Join(a.Said, "\n")
|
|
if !strings.Contains(said, "compression = lz4 (inherited from tank)") || strings.Contains(said, "s3cret") {
|
|
t.Errorf("said: %s", said)
|
|
}
|
|
if _, err := f.machine().PropertiesOf(context.Background(), "tank/nope"); err == nil {
|
|
t.Errorf("an unknown dataset is refused")
|
|
}
|
|
if n := len(ran(f, "zfs get")); n != 1 {
|
|
t.Errorf("zfs get runs only for a dataset that exists: %d", n)
|
|
}
|
|
if argv := PropertiesArgv("tank/media"); argv[len(argv)-2] != "--" {
|
|
t.Errorf("the dataset follows --: %v", argv)
|
|
}
|
|
}
|
|
|
|
func TestDatasetsCountEachOnesSnapshots(t *testing.T) {
|
|
got, err := withZFS(t).machine().DatasetsNow(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := got.(DatasetsAnswer)
|
|
if a.Count != 4 || a.Datasets[1].Name != "tank/media" || a.Datasets[1].Snapshots != 3 || a.Datasets[2].Snapshots != 1 ||
|
|
a.Datasets[0].Snapshots != 0 || a.Datasets[1].UsedBySnapshots != 549755813888 {
|
|
t.Errorf("datasets: %+v", a.Datasets)
|
|
}
|
|
if !strings.Contains(a.Said[1], "3 snapshots holding 512.0 GiB") || !strings.Contains(a.Said[1], "sharenfs rw=@192.0.2.0/24") {
|
|
t.Errorf("said: %s", a.Said[1])
|
|
}
|
|
}
|
|
|
|
func TestSpaceIsBiggestFirstWithTheBreakdownAndTheSavings(t *testing.T) {
|
|
got, err := withZFS(t).machine().Space(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := got.(SpaceAnswer)
|
|
if len(a.Pools) != 2 || a.Pools[0].Capacity != 60 {
|
|
t.Errorf("pools: %+v", a.Pools)
|
|
}
|
|
if a.Datasets[0].Name != "tank" || a.Datasets[1].Name != "tank/media" || a.Datasets[3].Name != "tank/vm" {
|
|
t.Errorf("biggest first: %v, %v, %v", a.Datasets[0].Name, a.Datasets[1].Name, a.Datasets[3].Name)
|
|
}
|
|
media := a.Datasets[1]
|
|
if media.Saved != 27500000000000-27487790694400 || media.UsedBySnapshots != 549755813888 {
|
|
t.Errorf("media: %+v", media)
|
|
}
|
|
if dl := a.Datasets[2]; dl.Quota == nil || !strings.Contains(dl.Said, "quota 2.0 TiB") {
|
|
t.Errorf("downloads: %+v", dl)
|
|
}
|
|
}
|
|
|
|
func TestSnapshotsOfOneDatasetAreItsOwnOnly(t *testing.T) {
|
|
f := withZFS(t)
|
|
got, err := f.machine().SnapshotsOf(context.Background(), "tank/media")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := got.(SnapshotsAnswer)
|
|
if a.Count != 3 || a.Snapshots[2].Name != "tank/media@before-rename" || a.Unique != 274877906944+137438953472+1048576 {
|
|
t.Errorf("snapshots: %+v", a)
|
|
}
|
|
if _, err := f.machine().SnapshotsOf(context.Background(), "tank/med"); err == nil {
|
|
t.Errorf("an unknown dataset is refused")
|
|
}
|
|
got, _ = f.machine().SnapshotsOf(context.Background(), "")
|
|
if got.(SnapshotsAnswer).Count != 4 {
|
|
t.Errorf("every snapshot: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestLayoutAndPoolsSayDeviceErrorsByName(t *testing.T) {
|
|
f := newFake(t)
|
|
f.on(line("zpool", PoolsArgv()), fixture(t, "zpool-list-full.txt"))
|
|
f.on(line("zpool", StatusArgv()), fixture(t, "zpool-status-degraded.txt"))
|
|
got, err := f.machine().Layout(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
said := strings.Join(got.(LayoutAnswer).Said, "\n")
|
|
if !strings.Contains(said, "ata-WDC_WD120EFBX-68B0EN0_D7JA0002 FAULTED — ERRORS read 12, write 107, checksum 3 too many errors") {
|
|
t.Errorf("layout: %s", said)
|
|
}
|
|
got, err = f.machine().PoolsNow(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := got.(PoolsAnswer).Pools[0]
|
|
if p.State != "DEGRADED" || p.DeviceErrs != 122 || p.Capacity != 91 || !strings.HasPrefix(p.Scan, "resilver in progress") {
|
|
t.Errorf("pool: %+v", p)
|
|
}
|
|
}
|
|
|
|
func TestIOAndScrubAnswerFromFakes(t *testing.T) {
|
|
f := withZFS(t)
|
|
f.on(line("zpool", IOArgv(3)), fixture(t, "zpool-iostat.txt"))
|
|
got, err := f.machine().IO(context.Background(), 3)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a := got.(IOAnswer); len(a.Rows) != 10 || a.Seconds != 3 {
|
|
t.Errorf("io: %+v", a)
|
|
}
|
|
if argv := IOArgv(3); strings.Join(argv, " ") != "iostat -y -Hpv 3 1" {
|
|
t.Errorf("argv: %v", argv)
|
|
}
|
|
got, err = f.machine().Scrubs(context.Background(), false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := got.(ScrubAnswer)
|
|
rp, tank := a.Pools[0], a.Pools[1]
|
|
if tank.DaysSince == nil || *tank.DaysSince != 25.4 || tank.Warning != "" {
|
|
t.Errorf("tank was scrubbed 25 days ago, and has no schedule here: %+v", tank)
|
|
}
|
|
if rp.Warning == "" || !strings.Contains(strings.Join(rp.Said, " "), "WARNING") {
|
|
t.Errorf("rpool was scrubbed in July, more than 35 days ago: %+v", rp)
|
|
}
|
|
if len(ran(f, "sudo")) != 0 {
|
|
t.Errorf("without history nothing runs as root: %v", f.calls)
|
|
}
|
|
}
|
|
|
|
func TestScrubWithHistoryReadsItAsRootAndFindsTheScrubStatusNoLongerShows(t *testing.T) {
|
|
f := newFake(t)
|
|
f.on(line("zpool", StatusArgv()), fixture(t, "zpool-status-degraded.txt"))
|
|
f.on("sudo -n zpool history -i tank", fixture(t, "zpool-history.txt"))
|
|
got, err := f.machine().Scrubs(context.Background(), true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := got.(ScrubAnswer).Pools[0]
|
|
if r.LastScrub == nil || !r.LastScrub.Ended.Equal(time.Date(2026, 7, 13, 2, 30, 0, 0, time.Local)) || r.Warning == "" {
|
|
t.Errorf("the last scrub, from the history, is 88 days old: %+v", r)
|
|
}
|
|
if r.LastResilver == nil || r.Scans[0].State != "running" {
|
|
t.Errorf("the resilver: %+v", r)
|
|
}
|
|
}
|