added trigger
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
package wshub
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseSignalKey(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
base string
|
||||
idx int
|
||||
}{
|
||||
{"src:sig", "src:sig", -1},
|
||||
{"src:sig[0]", "src:sig", 0},
|
||||
{"src:sig[3]", "src:sig", 3},
|
||||
{"src:sig[x]", "src:sig[x]", -1},
|
||||
{"src:sig]", "src:sig]", -1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
base, idx := parseSignalKey(c.in)
|
||||
if base != c.base || idx != c.idx {
|
||||
t.Errorf("parseSignalKey(%q) = (%q,%d), want (%q,%d)",
|
||||
c.in, base, idx, c.base, c.idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func armed(key, edge string, thr float64) *triggerEngine {
|
||||
te := newTriggerEngine()
|
||||
te.SetConfig(trigConfig{signalKey: key, edge: edge, threshold: thr,
|
||||
windowSec: 1, prePercent: 20, mode: "normal"})
|
||||
te.Arm()
|
||||
return te
|
||||
}
|
||||
|
||||
func TestFeedRisingEdge(t *testing.T) {
|
||||
te := armed("src:sig", "rising", 0.5)
|
||||
te.feed("src:sig", 1, []float64{1, 2, 3, 4}, []float64{0, 0.2, 0.9, 1.0})
|
||||
if te.State() != trigCollecting {
|
||||
t.Fatalf("state = %q, want collecting", te.State())
|
||||
}
|
||||
// Fires at the sample that crossed, i.e. t=3.
|
||||
trigTime, pre, post, ok := te.dueCapture(1e9)
|
||||
if !ok || trigTime != 3 {
|
||||
t.Fatalf("dueCapture = (%v,%v), want trigTime 3", trigTime, ok)
|
||||
}
|
||||
if pre != 0.2 || post != 0.8 {
|
||||
t.Errorf("pre/post = %v/%v, want 0.2/0.8", pre, post)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedFallingEdgeIgnoresRising(t *testing.T) {
|
||||
te := armed("src:sig", "falling", 0.5)
|
||||
te.feed("src:sig", 1, []float64{1, 2, 3}, []float64{0, 0.9, 1.0})
|
||||
if te.State() != trigArmed {
|
||||
t.Fatalf("state = %q, want armed (no falling edge)", te.State())
|
||||
}
|
||||
te.feed("src:sig", 1, []float64{4, 5}, []float64{0.6, 0.1})
|
||||
if te.State() != trigCollecting {
|
||||
t.Fatalf("state = %q, want collecting", te.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedIgnoresOtherSignals(t *testing.T) {
|
||||
te := armed("src:sig", "rising", 0.5)
|
||||
te.feed("src:other", 1, []float64{1, 2}, []float64{0, 1})
|
||||
if te.State() != trigArmed {
|
||||
t.Fatalf("state = %q, want armed", te.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedArrayElementSelection(t *testing.T) {
|
||||
// 2-element signal, element-major: [e0,e1, e0,e1, ...]. Only element 1
|
||||
// crosses the threshold.
|
||||
te := armed("src:sig[1]", "rising", 0.5)
|
||||
tt := []float64{1, 1, 2, 2}
|
||||
vv := []float64{0, 0, 0, 1}
|
||||
te.feed("src:sig", 2, tt, vv)
|
||||
if te.State() != trigCollecting {
|
||||
t.Fatalf("state = %q, want collecting", te.State())
|
||||
}
|
||||
|
||||
// Element 0 never crosses, so a config on [0] must not fire.
|
||||
te2 := armed("src:sig[0]", "rising", 0.5)
|
||||
te2.feed("src:sig", 2, tt, vv)
|
||||
if te2.State() != trigArmed {
|
||||
t.Fatalf("state = %q, want armed", te2.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestForceUsesLastSampleTime(t *testing.T) {
|
||||
te := newTriggerEngine()
|
||||
te.SetConfig(trigConfig{signalKey: "src:sig", edge: "rising", threshold: 1e9,
|
||||
windowSec: 2, prePercent: 50, mode: "single"})
|
||||
te.Arm()
|
||||
te.feed("src:sig", 1, []float64{10, 11, 12}, []float64{0, 0, 0})
|
||||
if te.State() != trigArmed {
|
||||
t.Fatalf("state = %q, want armed (threshold unreachable)", te.State())
|
||||
}
|
||||
te.Force()
|
||||
trigTime, pre, post, ok := te.dueCapture(1e9)
|
||||
if !ok || trigTime != 12 || pre != 1 || post != 1 {
|
||||
t.Fatalf("dueCapture = (%v,%v,%v,%v), want (12,1,1,true)",
|
||||
trigTime, pre, post, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForceFromIdle(t *testing.T) {
|
||||
te := newTriggerEngine()
|
||||
te.SetConfig(trigConfig{signalKey: "src:sig", edge: "rising",
|
||||
windowSec: 1, prePercent: 20, mode: "normal"})
|
||||
te.Force()
|
||||
if te.State() != trigCollecting {
|
||||
t.Fatalf("state = %q, want collecting", te.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureMarginDelaysExtraction(t *testing.T) {
|
||||
te := armed("src:sig", "rising", 0.5)
|
||||
te.feed("src:sig", 1, []float64{0, 1}, []float64{0, 1}) // fires at t=1
|
||||
// post = 0.8 s; capture is due at 1 + 0.8 + 0.15.
|
||||
if _, _, _, ok := te.dueCapture(1.9); ok {
|
||||
t.Error("capture extracted before the margin elapsed")
|
||||
}
|
||||
if _, _, _, ok := te.dueCapture(1.96); !ok {
|
||||
t.Error("capture not extracted after the margin elapsed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoRearmNormalMode(t *testing.T) {
|
||||
te := armed("src:sig", "rising", 0.5)
|
||||
te.feed("src:sig", 1, []float64{0, 1}, []float64{0, 1})
|
||||
te.markTriggered(100)
|
||||
if te.State() != trigTriggered {
|
||||
t.Fatalf("state = %q, want triggered", te.State())
|
||||
}
|
||||
if te.dueRearm(100.1) {
|
||||
t.Error("rearmed before the delay elapsed")
|
||||
}
|
||||
if !te.dueRearm(100.3) {
|
||||
t.Error("did not rearm after the delay elapsed")
|
||||
}
|
||||
if te.dueRearm(200) {
|
||||
t.Error("rearm was not consumed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoAutoRearmInSingleMode(t *testing.T) {
|
||||
te := newTriggerEngine()
|
||||
te.SetConfig(trigConfig{signalKey: "src:sig", edge: "rising", threshold: 0.5,
|
||||
windowSec: 1, prePercent: 20, mode: "single"})
|
||||
te.Arm()
|
||||
te.feed("src:sig", 1, []float64{0, 1}, []float64{0, 1})
|
||||
te.markTriggered(100)
|
||||
if te.dueRearm(200) {
|
||||
t.Error("single mode must not auto-rearm")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoppedSuppressesRearm(t *testing.T) {
|
||||
te := armed("src:sig", "rising", 0.5)
|
||||
te.feed("src:sig", 1, []float64{0, 1}, []float64{0, 1})
|
||||
te.SetStopped(true)
|
||||
te.markTriggered(100)
|
||||
if te.dueRearm(200) {
|
||||
t.Error("stopped engine must not rearm")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetConfigClamps(t *testing.T) {
|
||||
te := newTriggerEngine()
|
||||
te.SetConfig(trigConfig{signalKey: "s:x", windowSec: 100, prePercent: 500})
|
||||
if cfg := te.Config(); cfg.windowSec != 10 || cfg.prePercent != 100 {
|
||||
t.Errorf("upper clamp = %v/%v, want 10/100", cfg.windowSec, cfg.prePercent)
|
||||
}
|
||||
te.SetConfig(trigConfig{signalKey: "s:x", windowSec: 0, prePercent: -5})
|
||||
if cfg := te.Config(); cfg.windowSec != 1e-4 || cfg.prePercent != 0 {
|
||||
t.Errorf("lower clamp = %v/%v, want 1e-4/0", cfg.windowSec, cfg.prePercent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveTracksConfiguredSignal(t *testing.T) {
|
||||
te := newTriggerEngine()
|
||||
if te.Active() {
|
||||
t.Error("a fresh engine must not be active")
|
||||
}
|
||||
te.SetConfig(trigConfig{signalKey: "src:sig", windowSec: 1})
|
||||
if !te.Active() {
|
||||
t.Error("engine must be active once a signal is configured")
|
||||
}
|
||||
// Rings must keep filling after a capture completes, not just while armed.
|
||||
te.Disarm()
|
||||
if !te.Active() {
|
||||
t.Error("engine must stay active after disarm while a signal is set")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user