| 1 | package main |
| 2 | |
| 3 | // Tests for meshstats.go: the band thresholds must match |
| 4 | // deploy/status-publish.sh's pct_band/load_word bit-for-bit (both sides |
| 5 | // compute the same word for the same box, whichever process does it), the |
| 6 | // whimsical label algorithm must match status-publish.sh's box_label |
| 7 | // bit-for-bit too (TestBoxLabelMatchesBash pins a real bash-computed |
| 8 | // example), and computeBoxStats must derive asleep from awake (never probe |
| 9 | // them separately, so the two counts can't disagree). |
| 10 | |
| 11 | import ( |
| 12 | "os" |
| 13 | "path/filepath" |
| 14 | "strconv" |
| 15 | "testing" |
| 16 | "time" |
| 17 | ) |
| 18 | |
| 19 | func TestPctBandThresholds(t *testing.T) { |
| 20 | cases := []struct { |
| 21 | pct int |
| 22 | want int |
| 23 | }{ |
| 24 | {0, 0}, {69, 0}, {70, 1}, {90, 1}, {91, 2}, {100, 2}, |
| 25 | } |
| 26 | for _, c := range cases { |
| 27 | if got := pctBand(c.pct); got != c.want { |
| 28 | t.Errorf("pctBand(%d) = %d, want %d", c.pct, got, c.want) |
| 29 | } |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func TestLoadWord(t *testing.T) { |
| 34 | if loadWord(0) != "comfortable" || loadWord(1) != "busy" || loadWord(2) != "critical" { |
| 35 | t.Fatal("loadWord vocabulary must match status-publish.sh's load_word exactly") |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | func TestMemLoadWord(t *testing.T) { |
| 40 | dir := t.TempDir() |
| 41 | write := func(total, avail int64) { |
| 42 | content := "" |
| 43 | if total > 0 { |
| 44 | content += "MemTotal: " + strconv.FormatInt(total, 10) + " kB\n" |
| 45 | } |
| 46 | if avail > 0 { |
| 47 | content += "MemAvailable: " + strconv.FormatInt(avail, 10) + " kB\n" |
| 48 | } |
| 49 | if err := os.WriteFile(filepath.Join(dir, "meminfo"), []byte(content), 0o644); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | } |
| 53 | orig := meminfoPath |
| 54 | meminfoPath = filepath.Join(dir, "meminfo") |
| 55 | defer func() { meminfoPath = orig }() |
| 56 | |
| 57 | write(1000, 500) // 50% used -> comfortable |
| 58 | if w, ok := memLoadWord(); !ok || w != "comfortable" { |
| 59 | t.Fatalf("50%% used: got %q, %v", w, ok) |
| 60 | } |
| 61 | write(1000, 250) // 75% used -> busy |
| 62 | if w, ok := memLoadWord(); !ok || w != "busy" { |
| 63 | t.Fatalf("75%% used: got %q, %v", w, ok) |
| 64 | } |
| 65 | write(1000, 50) // 95% used -> critical |
| 66 | if w, ok := memLoadWord(); !ok || w != "critical" { |
| 67 | t.Fatalf("95%% used: got %q, %v", w, ok) |
| 68 | } |
| 69 | |
| 70 | meminfoPath = filepath.Join(dir, "does-not-exist") |
| 71 | if _, ok := memLoadWord(); ok { |
| 72 | t.Fatal("unreadable meminfo should report ok=false, never guess") |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // Cross-checked against the real bash script: running status-publish.sh's own |
| 77 | // box_label() function against the same seed input produces the identical |
| 78 | // "phileas-juggling" — pinning that the two word lists and hash-index math |
| 79 | // haven't drifted apart. |
| 80 | func TestBoxLabelMatchesBash(t *testing.T) { |
| 81 | dir := t.TempDir() |
| 82 | idFile := filepath.Join(dir, "machine-id") |
| 83 | if err := os.WriteFile(idFile, []byte("test-machine-id-fixture-12345"), 0o644); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | orig := machineIDPath |
| 87 | machineIDPath = idFile |
| 88 | defer func() { machineIDPath = orig }() |
| 89 | |
| 90 | if got, want := boxLabel(), "phileas-juggling"; got != want { |
| 91 | t.Fatalf("boxLabel() = %q, want %q (bash's box_label for the same seed)", got, want) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | // computeBoxStats must derive asleep = hibernating total - awake, never |
| 96 | // probe them independently, and count only actAwake (not a mid-wake/mid-stop |
| 97 | // transient) as "awake". |
| 98 | func TestComputeBoxStats(t *testing.T) { |
| 99 | reg, err := openRegistry(filepath.Join(t.TempDir(), "registry.db")) |
| 100 | if err != nil { |
| 101 | t.Fatalf("open registry: %v", err) |
| 102 | } |
| 103 | for _, h := range []string{"a.11ch.at", "b.11ch.at", "c.11ch.at"} { |
| 104 | if err := reg.upsert(Route{Host: h, Kind: "instance", Socket: "/tmp/" + h + ".sock", Hibernate: true}); err != nil { |
| 105 | t.Fatal(err) |
| 106 | } |
| 107 | } |
| 108 | // A non-hibernating route must not count toward the hibernating total. |
| 109 | if err := reg.upsert(Route{Host: "systemd.11ch.at", Kind: "instance", Socket: "/tmp/x.sock", Hibernate: false}); err != nil { |
| 110 | t.Fatal(err) |
| 111 | } |
| 112 | |
| 113 | act := newActivator(hibernateConfig{Bucket: "test-bucket"}, &fakeBackend{}, reg) |
| 114 | act.entries["a.11ch.at"] = &actEntry{host: "a.11ch.at", state: actAwake} |
| 115 | act.entries["b.11ch.at"] = &actEntry{host: "b.11ch.at", state: actWaking} // not yet awake |
| 116 | |
| 117 | stats := computeBoxStats(reg, act, nil) |
| 118 | if stats.Label == "" { |
| 119 | t.Fatal("Label must be populated") |
| 120 | } |
| 121 | if stats.Awake == nil || *stats.Awake != 1 { |
| 122 | t.Fatalf("Awake = %v, want 1 (only actAwake counts)", stats.Awake) |
| 123 | } |
| 124 | if stats.Asleep == nil || *stats.Asleep != 2 { |
| 125 | t.Fatalf("Asleep = %v, want 2 (3 hibernating - 1 awake)", stats.Asleep) |
| 126 | } |
| 127 | if stats.Region != "" { |
| 128 | t.Fatalf("Region = %q, want empty (LCHAT_BOX_REGION unset)", stats.Region) |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | // boxRegion is an explicit-override-only read of LCHAT_BOX_REGION — absent |
| 133 | // env means empty (never guessed), and computeBoxStats must carry whatever |
| 134 | // it resolves straight into BoxStats.Region. |
| 135 | func TestBoxRegionExplicitOverride(t *testing.T) { |
| 136 | t.Setenv("LCHAT_BOX_REGION", "") |
| 137 | if got := boxRegion(); got != "" { |
| 138 | t.Fatalf("boxRegion() = %q, want empty when LCHAT_BOX_REGION is unset", got) |
| 139 | } |
| 140 | |
| 141 | t.Setenv("LCHAT_BOX_REGION", "UK · London") |
| 142 | if got, want := boxRegion(), "UK · London"; got != want { |
| 143 | t.Fatalf("boxRegion() = %q, want %q", got, want) |
| 144 | } |
| 145 | |
| 146 | reg, err := openRegistry(filepath.Join(t.TempDir(), "registry.db")) |
| 147 | if err != nil { |
| 148 | t.Fatalf("open registry: %v", err) |
| 149 | } |
| 150 | act := newActivator(hibernateConfig{Bucket: "test-bucket"}, &fakeBackend{}, reg) |
| 151 | if got := computeBoxStats(reg, act, nil).Region; got != "UK · London" { |
| 152 | t.Fatalf("computeBoxStats().Region = %q, want %q", got, "UK · London") |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | // Two boxes that both know about the same mesh-replicated routes must never |
| 157 | // both count the same route as hibernating in their own published snapshot — |
| 158 | // that's exactly the "same instance reads as asleep on two hosts" confusion |
| 159 | // the Fleet page must not show. computeBoxStats filters to routes THIS box |
| 160 | // currently HRW-owns, so each box's own count is a genuine, non-overlapping |
| 161 | // slice of the fleet: every in-scope route is owned by exactly one live box, |
| 162 | // and an out-of-scope route (never replicated anywhere else) always counts |
| 163 | // locally regardless of ownership. |
| 164 | func TestComputeBoxStatsPartitionsOwnershipAcrossBoxes(t *testing.T) { |
| 165 | clk := &fakeClock{t: time.Now()} |
| 166 | store := newFakeMeshStore(clk.now) |
| 167 | domains := []string{"test"} |
| 168 | |
| 169 | meshA := newMesh("http://box-a", store, domains) |
| 170 | meshB := newMesh("http://box-b", store, domains) |
| 171 | meshA.now, meshB.now = clk.now, clk.now |
| 172 | meshA.beat() |
| 173 | meshB.beat() |
| 174 | meshA.beat() // refresh A's view so it now also sees B as live |
| 175 | |
| 176 | boxes := []string{meshA.self, meshB.self} |
| 177 | var ownedByA, ownedByB []string |
| 178 | for i := 0; len(ownedByA) < 2 || len(ownedByB) < 2; i++ { |
| 179 | h := "space-" + strconv.Itoa(i) + ".test" |
| 180 | switch hrwOwner(h, boxes) { |
| 181 | case meshA.self: |
| 182 | if len(ownedByA) < 2 { |
| 183 | ownedByA = append(ownedByA, h) |
| 184 | } |
| 185 | case meshB.self: |
| 186 | if len(ownedByB) < 2 { |
| 187 | ownedByB = append(ownedByB, h) |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | regA, err := openRegistry(filepath.Join(t.TempDir(), "a.db")) |
| 193 | if err != nil { |
| 194 | t.Fatalf("open registry A: %v", err) |
| 195 | } |
| 196 | regB, err := openRegistry(filepath.Join(t.TempDir(), "b.db")) |
| 197 | if err != nil { |
| 198 | t.Fatalf("open registry B: %v", err) |
| 199 | } |
| 200 | // Full replication: both boxes know about all 4 in-scope routes, exactly |
| 201 | // like mesh-seed/fanOutMeshRoute leave them today. Plus one out-of-scope |
| 202 | // route that only box A ever knew about (never replicated anywhere). |
| 203 | for _, reg := range []*Registry{regA, regB} { |
| 204 | for _, h := range append(append([]string{}, ownedByA...), ownedByB...) { |
| 205 | if err := reg.upsert(Route{Host: h, Kind: "instance", Socket: "/tmp/" + h + ".sock", Hibernate: true}); err != nil { |
| 206 | t.Fatal(err) |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | if err := regA.upsert(Route{Host: "local-only.example", Kind: "instance", Socket: "/tmp/local.sock", Hibernate: true}); err != nil { |
| 211 | t.Fatal(err) |
| 212 | } |
| 213 | |
| 214 | actA := newActivator(hibernateConfig{Bucket: "test-bucket"}, &fakeBackend{}, regA) |
| 215 | actB := newActivator(hibernateConfig{Bucket: "test-bucket"}, &fakeBackend{}, regB) |
| 216 | |
| 217 | statsA := computeBoxStats(regA, actA, meshA) |
| 218 | statsB := computeBoxStats(regB, actB, meshB) |
| 219 | |
| 220 | // A owns its 2 + the 1 out-of-scope local-only route = 3; B owns its 2. |
| 221 | if statsA.Asleep == nil || *statsA.Asleep != 3 { |
| 222 | t.Fatalf("box A asleep = %v, want 3 (its 2 owned in-scope + 1 out-of-scope, never B's)", statsA.Asleep) |
| 223 | } |
| 224 | if statsB.Asleep == nil || *statsB.Asleep != 2 { |
| 225 | t.Fatalf("box B asleep = %v, want 2 (only its own 2 owned in-scope routes)", statsB.Asleep) |
| 226 | } |
| 227 | // The whole point: summed across both boxes' own reports, no route is |
| 228 | // ever double-counted, even though both registries physically hold all 4 |
| 229 | // in-scope rows. |
| 230 | if got, want := *statsA.Asleep+*statsB.Asleep, 5; got != want { |
| 231 | t.Fatalf("fleet-wide total = %d, want %d (5 unique routes, no double count)", got, want) |
| 232 | } |
| 233 | } |
| 234 | |