package wshub import ( "math" "testing" ) // A scope's envelope must not lose a spike, however narrow, and must stay in // time order so it can be plotted as a single trace. func TestMinMaxDecimateKeepsExtremes(t *testing.T) { const n = 10000 ts := make([]float64, n) vs := make([]float64, n) for i := range ts { ts[i] = float64(i) * 1e-6 vs[i] = math.Sin(float64(i) * 0.01) } // A one-sample spike in each direction: exactly what plain decimation drops. vs[4321] = 12.5 vs[6789] = -9.75 dt, dv := minMaxDecimate(ts, vs, 200) if len(dt) > 200 || len(dt) != len(dv) { t.Fatalf("got %d t / %d v points, want <= 200 of each", len(dt), len(dv)) } hiSeen, loSeen := false, false for i := range dv { switch dv[i] { case 12.5: hiSeen = true if dt[i] != ts[4321] { t.Errorf("spike kept at t=%v, want %v: timestamps must be the real ones", dt[i], ts[4321]) } case -9.75: loSeen = true } if i > 0 && dt[i] < dt[i-1] { t.Fatalf("output is not time-ordered at %d: %v after %v", i, dt[i], dt[i-1]) } } if !hiSeen || !loSeen { t.Errorf("envelope lost a spike (max kept=%v, min kept=%v)", hiSeen, loSeen) } } func TestMinMaxDecimatePassesShortInputThrough(t *testing.T) { ts := []float64{1, 2, 3} vs := []float64{4, 5, 6} dt, dv := minMaxDecimate(ts, vs, 200) if len(dt) != 3 || dv[2] != 6 { t.Errorf("input below the budget was altered: %v / %v", dt, dv) } // A flat bucket contributes one point, not two: nothing is invented. flatT := make([]float64, 100) flatV := make([]float64, 100) for i := range flatT { flatT[i] = float64(i) } if ft, _ := minMaxDecimate(flatT, flatV, 10); len(ft) != 5 { t.Errorf("flat input decimated to %d points, want 5 (one per bucket)", len(ft)) } } func TestZoomPoints(t *testing.T) { cases := []struct { n int present bool want int }{ {0, false, 2400}, // absent → default budget {2400, true, 2400}, // explicit budget honoured {0, true, 1 << 30}, // 0 → every sample in range {-1, true, 1 << 30}, // negative → every sample in range {5, true, 2400}, // implausibly small → default budget } for _, c := range cases { if got := zoomPoints(c.n, c.present); got != c.want { t.Errorf("zoomPoints(%d,%v) = %d, want %d", c.n, c.present, got, c.want) } } } func TestZoomSliceReturnsFullResolution(t *testing.T) { h := NewHub() rb := newSigRing(1000) ts := make([]float64, 500) vs := make([]float64, 500) for i := range ts { ts[i] = float64(i) * 0.001 // 1 kHz vs[i] = float64(i) } rb.write(ts, vs) h.rings["s1:sig"] = rb // A budget larger than the range must return every sample untouched. res := h.zoomSlice(0.100, 0.199, []string{"s1:sig"}, 1<<30) sd, ok := res["s1:sig"] if !ok { t.Fatal("signal missing from zoom result") } if len(sd.T) != 100 { t.Fatalf("got %d points, want 100", len(sd.T)) } if sd.V[0] != 100 || sd.V[99] != 199 { t.Errorf("value range = %v..%v, want 100..199", sd.V[0], sd.V[99]) } // A small budget decimates but keeps the endpoints. dec := h.zoomSlice(0.100, 0.199, []string{"s1:sig"}, 20) if len(dec["s1:sig"].T) != 20 { t.Errorf("decimated to %d points, want 20", len(dec["s1:sig"].T)) } } func TestZoomSliceUnknownSignal(t *testing.T) { h := NewHub() if res := h.zoomSlice(0, 1, []string{"nope", ""}, 100); len(res) != 0 { t.Errorf("got %d entries, want 0", len(res)) } }