Files
nmon/internal/api/graph_internal_test.go
jleung f85c3ef781
Some checks failed
ci / lint (push) Has been cancelled
ci / test (ubuntu-24.04-arm) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / postgres (push) Has been cancelled
ci / openapi (push) Has been cancelled
M3: Go API per UI contract, embedded UI, openapi compat check, nmon-devtools, live gate + screenshots; M4 WP1 reachability and dependency suppression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 20:34:14 +00:00

161 lines
5.2 KiB
Go

package api
import (
"math"
"testing"
"time"
)
func TestDefaultStepIsMaxOf60sAndRangeOver1000RoundedToMinutes(t *testing.T) {
for rng, want := range map[time.Duration]time.Duration{
time.Hour: time.Minute, 16 * time.Hour: time.Minute, 17 * time.Hour: 2 * time.Minute,
24 * time.Hour: 2 * time.Minute, 30 * 24 * time.Hour: 44 * time.Minute, 365 * 24 * time.Hour: 526 * time.Minute,
} {
if got := defaultStep(rng); got != want {
t.Errorf("defaultStep(%s) = %s, want %s", rng, got, want)
}
}
}
func TestPlanGraphTiers(t *testing.T) {
now := time.Date(2026, 10, 22, 12, 0, 0, 0, time.UTC)
hotStart := now.Add(-30 * 24 * time.Hour)
cases := []struct {
name string
from, to time.Time
tiers []string
stitched bool
wantErr bool
step time.Duration
newestStep time.Duration
}{
{"1h", now.Add(-time.Hour), now, []string{"raw"}, false, false, 0, time.Minute},
{"exactly the hot window", hotStart, now, []string{"raw"}, false, false, 0, 44 * time.Minute},
{"a hair beyond hot is still raw", hotStart.Add(-20 * time.Minute), now, []string{"raw"}, false, false, 0, 44 * time.Minute},
{"straddles", now.Add(-40 * 24 * time.Hour), now, []string{"1h", "raw"}, true, false, 0, 58 * time.Minute},
{"ends inside hot start", now.Add(-60 * 24 * time.Hour), hotStart, []string{"1h"}, false, false, 0, time.Hour},
{"old", now.Add(-90 * 24 * time.Hour), now.Add(-60 * 24 * time.Hour), []string{"1h"}, false, false, 0, time.Hour},
{"too many points", now.Add(-30 * 24 * time.Hour), now, nil, false, true, time.Second, 0},
{"explicit step", now.Add(-time.Hour), now, []string{"raw"}, false, false, 5 * time.Minute, 5 * time.Minute},
}
for _, tc := range cases {
p, err := planGraph(tc.from, tc.to, hotStart, tc.step)
if (err != nil) != tc.wantErr {
t.Errorf("%s: err %v", tc.name, err)
continue
}
if tc.wantErr {
continue
}
var got []string
for _, w := range p.tiers {
got = append(got, w.tier)
}
if len(got) != len(tc.tiers) || (p.stitchedAt != nil) != tc.stitched || p.newest().step != tc.newestStep {
t.Errorf("%s: tiers %v stitched %v newest step %s, want %v %v %s", tc.name, got, p.stitchedAt, p.newest().step, tc.tiers, tc.stitched, tc.newestStep)
continue
}
for i := range got {
if got[i] != tc.tiers[i] {
t.Errorf("%s: tiers %v, want %v", tc.name, got, tc.tiers)
}
}
if tc.stitched {
join := *p.stitchedAt
if join.Before(hotStart) || join.Sub(hotStart) >= p.newest().step || join.Unix()%int64(p.newest().step/time.Second) != 0 {
t.Errorf("%s: join %v", tc.name, join)
}
if long := p.tiers[0]; long.last.Unix() >= join.Unix() || long.bound != join {
t.Errorf("%s: long tier overlaps raw: %+v", tc.name, long)
}
}
}
}
func TestFindGapsThresholdIsTwoSteps(t *testing.T) {
grid := make([]slot, 11)
for i := range grid {
grid[i] = slot{ts: int64(i * 60), step: 60}
}
has := func(missing ...int) func(int64) bool {
return func(ts int64) bool {
for _, m := range missing {
if ts == int64(m*60) {
return false
}
}
return true
}
}
for _, tc := range []struct {
name string
missing []int
want [][2]int64
}{
{"complete", nil, nil},
{"one missing sample", []int{4}, nil},
{"two missing samples", []int{4, 5}, [][2]int64{{180, 360}}},
{"leading", []int{0, 1, 2}, [][2]int64{{0, 180}}},
{"leading short", []int{0, 1}, nil},
{"trailing", []int{8, 9, 10}, [][2]int64{{420, 600}}},
{"two separate holes", []int{2, 3, 7, 8}, [][2]int64{{60, 240}, {360, 540}}},
} {
got := findGaps(grid, has(tc.missing...), 0, 600)
if len(got) != len(tc.want) {
t.Errorf("%s: %v, want %v", tc.name, got, tc.want)
continue
}
for i, g := range got {
if g.From.Unix() != tc.want[i][0] || g.To.Unix() != tc.want[i][1] {
t.Errorf("%s: gap %d is %v..%v, want %v", tc.name, i, g.From.Unix(), g.To.Unix(), tc.want[i])
}
}
}
none := func(int64) bool { return false }
if g := findGaps(grid, none, 0, 600); len(g) != 1 || g[0].From.Unix() != 0 || g[0].To.Unix() != 600 {
t.Errorf("no data: %v", g)
}
if g := findGaps(nil, none, 0, 0); g != nil {
t.Errorf("empty grid: %v", g)
}
}
func TestNearestRank(t *testing.T) {
seq := func(n int) []float64 {
out := make([]float64, n)
for i := range out {
out[i] = float64(n - i) // descending, so sorting matters
}
return out
}
for n, want := range map[int]float64{1: 1, 2: 2, 19: 19, 20: 19, 21: 20, 100: 95, 101: 96, 1000: 950} {
if got := nearestRank(seq(n), 0.95); got != want {
t.Errorf("p95 of 1..%d = %v, want %v", n, got, want)
}
}
if !math.IsNaN(nearestRank(nil, 0.95)) {
t.Error("no values should be NaN")
}
in := []float64{3, 1, 2}
nearestRank(in, 0.5)
if in[0] != 3 {
t.Error("input must not be reordered")
}
}
func TestParentCycleHelpersAndEvidence(t *testing.T) {
got := evidenceLines(`{"b":2,"a":"x","c":["y"]}`)
if len(got) != 3 || got[0] != "a: x" || got[1] != "b: 2" || got[2] != `c: ["y"]` {
t.Errorf("evidence: %q", got)
}
if got := evidenceLines(`not json`); len(got) != 1 || got[0] != "not json" {
t.Errorf("evidence fallback: %q", got)
}
if got := evidenceLines(``); got == nil || len(got) != 0 {
t.Errorf("empty evidence must be [] not nil: %#v", got)
}
if quote(`a"b\c`) != `"a\"b\\c"` {
t.Errorf("quote: %s", quote(`a"b\c`))
}
}