package main import ( "bytes" "context" "strings" "testing" "time" ) func TestOnlinePoolsWithoutErrorsAreHealthyAndAnOldScrubIsOnlyAWarning(t *testing.T) { h := withZFS(t).machine().Health(context.Background()) if !h.Healthy || len(h.Found) != 0 { t.Fatalf("health: %+v", h) } if len(h.Warnings) != 1 || !strings.Contains(h.Warnings[0], "Pool rpool was last scrubbed 89 days ago, more than 35") { t.Errorf("rpool's July scrub is a warning; tank's September one is not: %v", h.Warnings) } if !strings.Contains(h.Why, "tank ONLINE, 60 % full") { t.Errorf("why: %s", h.Why) } } func TestADegradedFullPoolIsUnhealthyWithEveryReason(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")) h := f.machine().Health(context.Background()) if h.Healthy { t.Fatalf("healthy: %+v", h) } for _, want := range []string{ "Pool tank is DEGRADED: One or more devices is currently being resilvered", "Action: Wait for the resilver to complete", "Pool tank is 91 % full, past 90 %", "device ata-WDC_WD120EFBX-68B0EN0_D7JA0002 in pool tank has errors: read 12, write 107, checksum 3", "device ata-WDC_WD120EFBX-68B0EN0_D7JA0002 in pool tank is FAULTED", "Pool tank: 2 data errors", } { if !strings.Contains(h.Why, want) { t.Errorf("why lacks %q: %s", want, h.Why) } } if strings.Contains(h.Why, "D7JA0009 in pool tank is") { t.Errorf("a spare in use is not a finding: %s", h.Why) } } func TestCapacityPastEightyIsAWarningAndPastNinetyUnhealthy(t *testing.T) { st, _ := ParseStatus(fixture(t, "zpool-status-scrubbing.txt")) now := newFake(t).now p := Pool{Name: "tank", Capacity: 85, Health: "ONLINE"} if h := Judge([]Pool{p}, st, now); !h.Healthy || len(h.Warnings) != 1 || !strings.Contains(h.Warnings[0], "85 % full, past 80 %") { t.Errorf("85 %%: %+v", h) } p.Capacity = 80 if h := Judge([]Pool{p}, st, now); !h.Healthy || len(h.Warnings) != 0 { t.Errorf("80 %% is not past 80: %+v", h) } p.Capacity = 92 if h := Judge([]Pool{p}, st, now); h.Healthy { t.Errorf("92 %%: %+v", h) } } func TestFaultedAndNoPoolAreUnhealthy(t *testing.T) { now := newFake(t).now if h := Judge(nil, nil, now); h.Healthy || !strings.Contains(h.Why, "no pool is imported") { t.Errorf("no pool: %+v", h) } st := []PoolStatus{{Name: "tank", State: "FAULTED", Status: "One or more devices could not be opened.", Errors: "No known data errors", Root: &Device{Name: "tank", Kind: "pool", State: "FAULTED"}}} if h := Judge([]Pool{{Name: "tank", Health: "FAULTED"}}, st, now); h.Healthy || !strings.Contains(h.Why, "Pool tank is FAULTED: One or more devices could not be opened") { t.Errorf("faulted: %+v", h) } } func TestTheWatchSaysOnlyChanges(t *testing.T) { f := withZFS(t) var out bytes.Buffer w := &Watch{M: f.machine(), Out: &out} w.Look(context.Background()) w.Look(context.Background()) if n := strings.Count(out.String(), "\n"); n != 1 || !strings.Contains(out.String(), "healthy: Every pool is online") { t.Errorf("one line for one state: %q", out.String()) } f.on(line("zpool", StatusArgv()), fixture(t, "zpool-status-degraded.txt")) w.Look(context.Background()) if !strings.Contains(out.String(), "UNHEALTHY: ") { t.Errorf("the change is said: %q", out.String()) } } func TestTheFollowerWritesEachNotableEventFromItsStartOnce(t *testing.T) { var out bytes.Buffer since := time.Date(2026, 10, 8, 21, 0, 0, 0, time.Local) if err := Follow(strings.NewReader(fixture(t, "zpool-events.txt")), &out, since); err != nil { t.Fatal(err) } got := strings.Split(strings.TrimSpace(out.String()), "\n") if len(got) != 3 || !strings.Contains(got[0], "ereport.fs.zfs.io") || !strings.Contains(got[1], "vdev_state=FAULTED") || !strings.Contains(got[2], "resilver_start") { t.Errorf("lines: %q", got) } out.Reset() Follow(strings.NewReader(fixture(t, "zpool-events.txt")), &out, time.Date(2026, 10, 8, 21, 2, 10, 0, time.Local)) if got := strings.TrimSpace(out.String()); strings.Count(got, "\n") != 0 || !strings.Contains(got, "resilver_start") { t.Errorf("only what came after since: %q", got) } if strings.Join(FollowArgv(), " ") != "events -f -v" { t.Errorf("argv: %v", FollowArgv()) } }