mesh/merge-gate pass: builds new: modules/zfs, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestASnapshotIsAnsweredFromTheListAfterwardsAndRecorded (0.00s)
mesh/delivery superseded: a newer head of the same pull request
The home server's pool holds the operator's media library, and no mesh tool could say whether it is healthy, how full it is, when it was last scrubbed or what snapshots exist; the only way was a login. This module reads all of it with fixed zfs and zpool commands (list, get, status, iostat, events, history) and has one act, zfs_snapshot, which only adds a snapshot: refused for an unknown dataset, a name outside [A-Za-z0-9._-], an existing snapshot or no why; run as sudo -n zfs snapshot -- ds@name; answered from zfs list afterwards, with any other snapshot that came or went meanwhile; recorded as the event snapshot.taken. No destroy, rollback, set or send exists, and a test holds every command to its verb. Health (ADR 0240): zfs_health on the watch process, unhealthy for a pool not ONLINE, device or data errors, a pool past 90 % full or no ZFS at all; past 80 % and a scrub older than 35 days are warnings. Beside it, the event follower (zpool events -f, as root) keeps ZFS's errors in its journal past a reboot and is judged by its unit, the check the module does not run itself. No seat (ADR 0121), no package: ZFS is the machine's.
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.Scan
|
|
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 != "0" || 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.Scan
|
|
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].Scan
|
|
if s.Kind != "scrub" || s.State != "running" || *s.Done != 29.17 || s.ToGo != "06:49:34" || s.Repaired != "0" {
|
|
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(said); got.State == "unknown" {
|
|
t.Errorf("%q not read: %+v", said, got)
|
|
}
|
|
}
|
|
if got := parseScan("resilvered 1088516226 in 03:22:11 with 0 errors on Fri Oct 9 06:00:00 2026"); got.Kind != "resilver" || got.State != "finished" || got.Repaired != "1088516226" {
|
|
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", "logs": "section",
|
|
"mirror-1": "vdev", "cache": "section", "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) != 12 || 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: %+v", tank)
|
|
}
|
|
if rp.Warning == "" || !strings.Contains(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.Last.State != "running" {
|
|
t.Errorf("the resilver: %+v", r)
|
|
}
|
|
}
|