package wshub import "testing" func heldRamp(t0, dt float64, n int) sigData { sd := sigData{T: make([]float64, n), V: make([]float64, n)} for i := range sd.T { sd.T[i] = t0 + float64(i)*dt sd.V[i] = float64(i) } return sd } func TestCaptureHoldServesRangesInsideTheWindow(t *testing.T) { var ch captureHold ch.publish(0, 10, map[string]sigData{"s1:sig": heldRamp(0, 0.1, 101)}) gt, gv, ok := ch.slice("s1:sig", 2, 3) if !ok { t.Fatal("held capture declined a range inside its window") } if gt[0] < 2 || gt[len(gt)-1] > 3 { t.Fatalf("range %v..%v escapes the request 2..3", gt[0], gt[len(gt)-1]) } if len(gt) != len(gv) { t.Fatalf("t/v length mismatch: %d vs %d", len(gt), len(gv)) } if gv[0] != 20 { t.Fatalf("first value %v, want the sample at t=2", gv[0]) } } // A range poking outside the capture is a live zoom: only the rings still track // the stream, so the hold must stand aside rather than answer a clipped range. func TestCaptureHoldDeclinesRangesOutsideTheWindow(t *testing.T) { var ch captureHold ch.publish(0, 10, map[string]sigData{"s1:sig": heldRamp(0, 0.1, 101)}) for _, r := range [][2]float64{{-1, 5}, {5, 11}, {20, 30}, {-5, -1}} { if _, _, ok := ch.slice("s1:sig", r[0], r[1]); ok { t.Fatalf("held capture answered %v..%v, which is not inside 0..10", r[0], r[1]) } } if _, _, ok := ch.slice("other:sig", 2, 3); ok { t.Fatal("held capture answered for a signal it does not hold") } } func TestCaptureHoldZeroValueAndClearDecline(t *testing.T) { var ch captureHold if _, _, ok := ch.slice("s1:sig", 0, 1); ok { t.Fatal("empty hold answered a request") } ch.publish(0, 10, map[string]sigData{"s1:sig": heldRamp(0, 0.1, 101)}) ch.clear() if _, _, ok := ch.slice("s1:sig", 2, 3); ok { t.Fatal("cleared hold still answered a request") } } // The point of the double buffer: the window a client is exploring survives the // re-armed acquisition rolling the rings past it, and is replaced only when the // next shot completes. func TestZoomIntoACaptureSurvivesTheRingRollingPast(t *testing.T) { h := NewHub() rb := newSigRing(4000) h.rings["s1:sig"] = rb // 2 s of 1 kSps, then fire a trigger over [0.5, 1.5]. ts, vs := make([]float64, 2000), make([]float64, 2000) for i := range ts { ts[i], vs[i] = float64(i)*1e-3, float64(i) } rb.write(ts, vs) if msg := h.buildTriggerCapture(1.0, 0.5, 0.5); msg == nil { t.Fatal("buildTriggerCapture produced no frame") } // The trigger re-arms and the stream runs on until the captured window has // been overwritten several times over. for pass := 0; pass < 5; pass++ { for i := range ts { ts[i] += 2.0 } rb.write(ts, vs) } if rt, _ := rb.slice(0.5, 1.5); len(rt) != 0 { t.Fatalf("ring still holds %d points of the captured window; the test is not exercising the hold", len(rt)) } got := h.zoomSlice(0.8, 0.9, []string{"s1:sig"}, 1<<30) sd, ok := got["s1:sig"] if !ok { t.Fatal("zoom into the held capture returned nothing") } if len(sd.T) != 101 { t.Fatalf("zoom returned %d points, want the 101 samples in 0.8..0.9", len(sd.T)) } if sd.V[0] != 800 || sd.V[len(sd.V)-1] != 900 { t.Fatalf("zoom returned values %v..%v, want 800..900", sd.V[0], sd.V[len(sd.V)-1]) } // A live zoom outside the held window still reaches the rings. if live := h.zoomSlice(11.0, 11.1, []string{"s1:sig"}, 1<<30); len(live["s1:sig"].T) == 0 { t.Fatal("live zoom outside the capture was swallowed by the hold") } } // A shot that yields nothing must not blank the window already on screen. func TestEmptyCaptureKeepsThePreviousHold(t *testing.T) { h := NewHub() rb := newSigRing(4000) h.rings["s1:sig"] = rb ts, vs := make([]float64, 2000), make([]float64, 2000) for i := range ts { ts[i], vs[i] = float64(i)*1e-3, float64(i) } rb.write(ts, vs) h.buildTriggerCapture(1.0, 0.5, 0.5) // A window the rings have no samples for at all. if msg := h.buildTriggerCapture(500.0, 0.5, 0.5); msg != nil { t.Fatal("capture of an empty window produced a frame") } if _, _, ok := h.capture.slice("s1:sig", 0.8, 0.9); !ok { t.Fatal("empty capture dropped the previously held window") } }