package main import ( "context" "encoding/json" "os" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/health" ) type memStore struct{ m map[string]json.RawMessage } func (s *memStore) Get(k string) (json.RawMessage, error) { return s.m[k], nil } func (s *memStore) Put(k string, v any) error { raw, _ := json.Marshal(v) s.m[k] = raw return nil } func (s *memStore) Keys() ([]string, error) { var out []string for k := range s.m { out = append(out, k) } return out, nil } // A round measures what the data file declares but caches, keeps the readings, writes nothing the second time, and a // walk is not repeated within a day unless asked. func TestARoundMeasuresTheDeclaredItemsAndWritesWhatChanged(t *testing.T) { text := "# pg\nitem data valuable /srv/pg /srv/pg backup walk\nitem tmp cache /srv/tmp - none\n# bad-line-module\nitem logs rebuildable /srv/logs - none shallow\n" placed, err := catalogue.ParseDataItems(text) if err != nil { t.Fatal(err) } walks := 0 run := func(_ context.Context, name string, args ...string) (string, error) { switch { case name == "test": return "", nil case name == "findmnt": return "", os.ErrNotExist case name == "bash" && strings.Contains(args[1], "-maxdepth 1"): return "1760000000 2\n", nil case name == "bash": walks++ return "4096 1760000000 3\n0\n", nil } return "", os.ErrNotExist } now := time.Date(2026, 10, 29, 12, 0, 0, 0, time.UTC) store := &memStore{m: map[string]json.RawMessage{}} k := &Keeper{Node: "ace", Items: func(context.Context) (map[string][]catalogue.PlacedDataItem, error) { return placed, nil }, Store: store, Say: func(string, ...any) {}, Measurer: Measurer{Run: run, Now: func() time.Time { return now }}} w, kept, err := k.Round(context.Background(), false) if err != nil || w != 2 || kept != 0 || len(store.m) != 2 { t.Fatalf("first round: %d written %d kept %v, keys %d", w, kept, err, len(store.m)) } now = now.Add(time.Hour) if w, kept, _ := k.Round(context.Background(), false); w != 0 || kept != 2 || walks != 1 { t.Fatalf("second round: %d written, %d kept, %d walks; wanted none written and the walk not repeated", w, kept, walks) } if _, _, err := k.MeasureNow(context.Background(), "pg", "data"); err != nil || walks != 2 { t.Fatalf("measure now: %v, %d walks", err, walks) } // An item no longer declared is kept, retired with when and why; declared again, it is not retired any more. delete(placed, "pg") if _, _, err := k.Round(context.Background(), false); err != nil { t.Fatal(err) } gone, _ := health.DecodeReading(store.m["ace.pg.data"]) if gone.RetiredAt.IsZero() || !strings.Contains(gone.RetiredWhy, "no longer declared on ace") || gone.FirstSeen.IsZero() { t.Fatalf("a reading of an item no longer declared: %+v", gone) } placed["pg"] = []catalogue.PlacedDataItem{{Module: "pg", ID: "data", Class: "valuable", Path: "/srv/pg", Measure: catalogue.MeasureWalk}} if _, _, err := k.Round(context.Background(), false); err != nil { t.Fatal(err) } if back, _ := health.DecodeReading(store.m["ace.pg.data"]); !back.RetiredAt.IsZero() { t.Fatalf("a reading declared again is still retired: %+v", back) } if _, _, err := k.MeasureNow(context.Background(), "pg", "tmp"); err == nil { t.Fatal("a cache was measured") } } // The redundant storage under an item is read: a ZFS pool healthy, then degraded; an md array with a member missing; and // plain storage, which is no redundancy at all. A dataset is measured from the filesystem's counters and a shallow item // from its top-level entries only; a walk that meets more than its bound stops and says so. func TestTheStorageUnderAnItemAndTheLargeMethodsAreRead(t *testing.T) { poolHealth, statusX, fs := "ONLINE\n", "pool 'storage' is healthy\n", "storage/media zfs\n" run := func(_ context.Context, name string, args ...string) (string, error) { line := name + " " + strings.Join(args, " ") switch { case name == "test": return "", nil case name == "findmnt": return fs, nil case name == "zpool" && args[0] == "list": return poolHealth, nil case name == "zpool": return statusX, nil case name == "cat": return "Personalities : [raid1]\nmd127 : active raid1 sdb1[1] sda1[0]\n 976630464 blocks super 1.2 [2/1] [U_]\n\nunused devices: \n", nil case name == "zfs" && args[len(args)-1] == "/storage/media/movies": return "storage/media\t97067690722560\n", nil case name == "zfs": return "", os.ErrNotExist case name == "bash" && strings.Contains(line, "-maxdepth 1"): return "1759744800 12673\n", nil case name == "bash": return "123 1759700000 2000000\n141\n", nil } return "", nil } m := Measurer{Run: run, Now: time.Now} ctx := context.Background() got := m.Measure(ctx, catalogue.PlacedDataItem{Module: "media", ID: "movies", Path: "/storage/media/movies", Measure: catalogue.MeasureDataset}) if got.SizeBytes == nil || *got.SizeBytes != 97067690722560 || !strings.HasPrefix(got.Precision, "dataset storage/media") || got.LastWrite == nil || got.Redundancy == nil || got.Redundancy.Kind != "zfs" || got.Redundancy.Healthy == nil || !*got.Redundancy.Healthy { t.Fatalf("the library from its dataset: %+v", got) } poolHealth, statusX = "DEGRADED\n", " pool: storage\n state: DEGRADED\nstatus: One or more devices has been removed\n" if r := m.redundancyOf(ctx, "/storage/media/movies"); r.Healthy == nil || *r.Healthy || !strings.Contains(r.Said, "DEGRADED") { t.Fatalf("a degraded pool: %+v", r) } fs = "/dev/md127 ext4\n" if r := m.redundancyOf(ctx, "/srv/x"); r == nil || r.Kind != "md" || r.Healthy == nil || *r.Healthy { t.Fatalf("an array missing a member: %+v", r) } fs = "/dev/nvme0n1p3 ext4\n" if r := m.redundancyOf(ctx, "/var/lib/x"); r != nil { t.Fatalf("plain storage read as redundant: %+v", r) } if s := m.Measure(ctx, catalogue.PlacedDataItem{Path: "/mnt/plex/config", Measure: catalogue.MeasureShallow}); s.SizeBytes != nil || !strings.HasPrefix(s.Precision, "no size") { t.Fatalf("a shallow item: %+v", s) } if w := m.Measure(ctx, catalogue.PlacedDataItem{Path: "/srv/big", Measure: catalogue.MeasureWalk}); !strings.HasPrefix(w.Precision, "partial") { t.Fatalf("a walk stopped at its bound: %+v", w) } }