Implemented and fixed many issues
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package wshub
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// dump returns the ring's contents oldest-first, which is what every reader
|
||||
// sees through slice() but is easier to assert on directly.
|
||||
func dump(rb *sigRing) ([]float64, []float64) {
|
||||
return rb.slice(math.Inf(-1), math.Inf(1))
|
||||
}
|
||||
|
||||
func TestRingBucketStoresMinMaxPairsInTimeOrder(t *testing.T) {
|
||||
rb := newSigRing(100)
|
||||
rb.setBucket(4)
|
||||
// Two buckets. In the first the minimum comes before the maximum, in the
|
||||
// second the order is reversed, so the emitted pairs must not be sorted by
|
||||
// value — a ring whose timestamps are not monotonic breaks slice()'s
|
||||
// binary search.
|
||||
ts := []float64{0, 1, 2, 3, 4, 5, 6, 7}
|
||||
vs := []float64{-5, 0, 0, 9, 9, 0, 0, -5}
|
||||
rb.write(ts, vs)
|
||||
|
||||
gotT, gotV := dump(rb)
|
||||
wantT := []float64{0, 3, 4, 7}
|
||||
wantV := []float64{-5, 9, 9, -5}
|
||||
if len(gotT) != len(wantT) {
|
||||
t.Fatalf("stored %d points, want %d", len(gotT), len(wantT))
|
||||
}
|
||||
for i := range wantT {
|
||||
if gotT[i] != wantT[i] || gotV[i] != wantV[i] {
|
||||
t.Fatalf("point %d = (%v,%v), want (%v,%v)", i, gotT[i], gotV[i], wantT[i], wantV[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRingBucketExtendsTheSpanAFixedCapacityCovers(t *testing.T) {
|
||||
const cap = 200
|
||||
// 2000 samples at 1 kHz is 2 s, ten times what the capacity holds verbatim.
|
||||
ts := make([]float64, 2000)
|
||||
vs := make([]float64, 2000)
|
||||
for i := range ts {
|
||||
ts[i] = float64(i) * 1e-3
|
||||
vs[i] = math.Sin(float64(i))
|
||||
}
|
||||
|
||||
full := newSigRing(cap)
|
||||
full.write(ts, vs)
|
||||
if _, span := full.stats(); span > 0.25 {
|
||||
t.Fatalf("full-rate ring spans %.3f s, expected ~0.2 s", span)
|
||||
}
|
||||
|
||||
// bucket 20 turns 20 samples into 2 points, so the same capacity reaches
|
||||
// 10x further: 200/2*20 = 2000 samples = 2 s.
|
||||
bucketed := newSigRing(cap)
|
||||
bucketed.setBucket(20)
|
||||
bucketed.write(ts, vs)
|
||||
count, span := bucketed.stats()
|
||||
if count != cap {
|
||||
t.Fatalf("bucketed ring holds %d points, want the full %d", count, cap)
|
||||
}
|
||||
if span < 1.9 {
|
||||
t.Fatalf("bucketed ring spans %.3f s, want the whole ~2 s", span)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRingSourceRateIsUnaffectedByBucketing(t *testing.T) {
|
||||
rb := newSigRing(1000)
|
||||
rb.setBucket(50)
|
||||
ts := make([]float64, 5000)
|
||||
vs := make([]float64, 5000)
|
||||
for i := range ts {
|
||||
ts[i] = float64(i) * 1e-4 // 10 kHz
|
||||
}
|
||||
rb.write(ts, vs)
|
||||
|
||||
got := rb.sourceRate()
|
||||
if math.Abs(got-10000) > 10 {
|
||||
t.Fatalf("sourceRate = %.1f, want ~10000", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetBucketFlushesThePartialBucket(t *testing.T) {
|
||||
rb := newSigRing(100)
|
||||
rb.setBucket(10)
|
||||
// Three samples: not enough to close a bucket of 10, so nothing is stored
|
||||
// yet and they would be silently dropped by a re-bucket that just reset the
|
||||
// accumulator.
|
||||
rb.write([]float64{0, 1, 2}, []float64{7, -7, 0})
|
||||
if n, _ := rb.stats(); n != 0 {
|
||||
t.Fatalf("partial bucket already emitted %d points", n)
|
||||
}
|
||||
rb.setBucket(2)
|
||||
gotT, gotV := dump(rb)
|
||||
if len(gotT) != 2 || gotT[0] != 0 || gotV[0] != 7 || gotT[1] != 1 || gotV[1] != -7 {
|
||||
t.Fatalf("flushed pair = %v/%v, want t=[0 1] v=[7 -7]", gotT, gotV)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveWindowSecFallsBackToTheDefault(t *testing.T) {
|
||||
h := NewHub()
|
||||
if got := h.activeWindowSec(); got != defaultLiveWindowSec {
|
||||
t.Fatalf("activeWindowSec with no clients = %v, want %v", got, defaultLiveWindowSec)
|
||||
}
|
||||
}
|
||||
|
||||
// Clients disagree about how far back they are plotting, and a buffer sized for
|
||||
// the narrowest one leaves the others with nothing to zoom into.
|
||||
func TestActiveWindowSecTakesTheWidestClientWindow(t *testing.T) {
|
||||
h := NewHub()
|
||||
narrow, wide, silent := &wsClient{}, &wsClient{}, &wsClient{}
|
||||
narrow.setDisplayWindowSec(1)
|
||||
wide.setDisplayWindowSec(120)
|
||||
h.clients[narrow], h.clients[wide], h.clients[silent] = true, true, true
|
||||
|
||||
if got := h.activeWindowSec(); got != 120 {
|
||||
t.Fatalf("activeWindowSec = %v, want the widest 120", got)
|
||||
}
|
||||
}
|
||||
|
||||
// An armed trigger owns the window: its pre-window has to be in the buffer
|
||||
// before the trigger fires or the capture has nothing to back-fill from.
|
||||
func TestActiveWindowSecPrefersTheArmedTrigger(t *testing.T) {
|
||||
h := NewHub()
|
||||
c := &wsClient{}
|
||||
c.setDisplayWindowSec(1)
|
||||
h.clients[c] = true
|
||||
h.trigger.SetConfig(trigConfig{signalKey: "s1:sig", windowSec: 45, mode: "normal"})
|
||||
|
||||
if got := h.activeWindowSec(); got != 45 {
|
||||
t.Fatalf("activeWindowSec = %v, want the trigger's 45", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetBucketToOneRestoresVerbatimStorage(t *testing.T) {
|
||||
rb := newSigRing(100)
|
||||
rb.setBucket(4)
|
||||
rb.setBucket(1)
|
||||
ts := []float64{0, 1, 2, 3}
|
||||
vs := []float64{1, 2, 3, 4}
|
||||
rb.write(ts, vs)
|
||||
gotT, _ := dump(rb)
|
||||
if len(gotT) != 4 {
|
||||
t.Fatalf("stored %d points, want all 4", len(gotT))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user