package main import ( "encoding/binary" "math" "testing" ) // ── frame builders (mirror the StreamHub wire format) ─────────────────────── func putf(b []byte, v float64) []byte { var t [8]byte binary.LittleEndian.PutUint64(t[:], math.Float64bits(v)) return append(b, t[:]...) } func putSignals(b []byte, sigs map[string]points) []byte { var n [4]byte binary.LittleEndian.PutUint32(n[:], uint32(len(sigs))) b = append(b, n[:]...) for k, p := range sigs { var kl [2]byte binary.LittleEndian.PutUint16(kl[:], uint16(len(k))) b = append(b, kl[:]...) b = append(b, []byte(k)...) var cnt [4]byte binary.LittleEndian.PutUint32(cnt[:], uint32(len(p.T))) b = append(b, cnt[:]...) for _, t := range p.T { b = putf(b, t) } for _, v := range p.V { b = putf(b, v) } } return b } func buildPush(srcID string, sigs map[string]points) []byte { b := []byte{1, byte(len(srcID))} b = append(b, []byte(srcID)...) return putSignals(b, sigs) } func buildCapture(trigTime, preSec, postSec float64, sigs map[string]points) []byte { b := []byte{2} b = putf(b, trigTime) b = putf(b, preSec) b = putf(b, postSec) return putSignals(b, sigs) } // ── tests ─────────────────────────────────────────────────────────────────── func TestParsePushRoundTrip(t *testing.T) { in := map[string]points{"Sine": {T: []float64{0, 0.1, 0.2}, V: []float64{1, 2, 3}}} f, err := parsePush(buildPush("src", in)) if err != nil { t.Fatalf("parsePush: %v", err) } if f.sourceID != "src" { t.Errorf("sourceID = %q, want src", f.sourceID) } got := f.signals["Sine"] if len(got.T) != 3 || got.T[2] != 0.2 || got.V[1] != 2 { t.Errorf("bad payload: %+v", got) } } func TestParsePushRejectsWrongVersion(t *testing.T) { if _, err := parsePush([]byte{2, 0, 0}); err == nil { t.Error("expected error on non-v1 frame") } } func TestParsePushTruncated(t *testing.T) { full := buildPush("src", map[string]points{"A": {T: []float64{0}, V: []float64{1}}}) if _, err := parsePush(full[:len(full)-4]); err == nil { t.Error("expected truncation error") } } func TestParseCaptureRoundTrip(t *testing.T) { in := map[string]points{"Sig": {T: []float64{1, 2}, V: []float64{9, 8}}} f, err := parseCapture(buildCapture(100.5, 0.02, 0.08, in)) if err != nil { t.Fatalf("parseCapture: %v", err) } if f.trigTime != 100.5 || f.preSec != 0.02 || f.postSec != 0.08 { t.Errorf("header = %v/%v/%v", f.trigTime, f.preSec, f.postSec) } if g := f.signals["Sig"]; len(g.T) != 2 || g.V[0] != 9 { t.Errorf("bad capture payload: %+v", g) } } func TestParseCaptureRejectsWrongVersion(t *testing.T) { if _, err := parseCapture([]byte{1, 0, 0, 0}); err == nil { t.Error("expected error on non-v2 frame") } } func TestMergedSortsAndDedups(t *testing.T) { c := &client{ configs: map[string][]signalInfo{}, zooms: map[uint32]map[string]points{}, } // two pushes, out of order, with one duplicate timestamp c.pushes = []*pushFrame{ {sourceID: "s", signals: map[string]points{"A": {T: []float64{0.2, 0.1}, V: []float64{2, 1}}}}, {sourceID: "s", signals: map[string]points{"A": {T: []float64{0.1, 0.3}, V: []float64{1, 3}}}}, } m := c.merged() got := m["s:A"] want := []float64{0.1, 0.2, 0.3} if len(got.T) != len(want) { t.Fatalf("len = %d, want %d (%+v)", len(got.T), len(want), got) } for i := range want { if got.T[i] != want[i] { t.Errorf("t[%d] = %v, want %v", i, got.T[i], want[i]) } } } func TestEdgeCrosses(t *testing.T) { rising := points{T: []float64{0, 1, 2}, V: []float64{-1, 1, 2}} if !edgeCrosses(rising, 0.5, "rising", 0.0) { t.Error("rising cross not detected") } if edgeCrosses(rising, 0.5, "falling", 0.0) { t.Error("false falling detection on rising data") } falling := points{T: []float64{0, 1}, V: []float64{1, -1}} if !edgeCrosses(falling, 0.5, "falling", 0.0) { t.Error("falling cross not detected") } if !edgeCrosses(falling, 0.5, "both", 0.0) { t.Error("both should match a falling edge") } } func TestHas(t *testing.T) { if !has("live,zoom,trigger", "zoom") { t.Error("has should find zoom") } if has("live, window", "trigger") { t.Error("has should not find trigger") } if !has("live, window", "window") { t.Error("has should trim spaces") } }