package dsp import ( "math" "testing" ) func TestGainNode(t *testing.T) { n := &GainNode{Gain: 3.0} state := map[string]any{} got, err := n.Process([]float64{2.0}, state) if err != nil { t.Fatal(err) } if got != 6.0 { t.Errorf("GainNode: want 6.0, got %v", got) } } func TestGainNodeNoInputs(t *testing.T) { n := &GainNode{Gain: 1.0} _, err := n.Process(nil, map[string]any{}) if err == nil { t.Error("GainNode: expected error with no inputs") } } func TestOffsetNode(t *testing.T) { n := &OffsetNode{Offset: 5.0} state := map[string]any{} got, err := n.Process([]float64{3.0}, state) if err != nil { t.Fatal(err) } if got != 8.0 { t.Errorf("OffsetNode: want 8.0, got %v", got) } } func TestAddNode(t *testing.T) { n := &AddNode{} state := map[string]any{} got, err := n.Process([]float64{1.0, 2.0, 3.0}, state) if err != nil { t.Fatal(err) } if got != 6.0 { t.Errorf("AddNode: want 6.0, got %v", got) } } func TestSubtractNode(t *testing.T) { tests := []struct { name string inputs []float64 want float64 errOk bool }{ {"basic", []float64{10.0, 3.0}, 7.0, false}, {"negative result", []float64{3.0, 10.0}, -7.0, false}, {"too few inputs", []float64{1.0}, 0, true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { n := &SubtractNode{} got, err := n.Process(tc.inputs, map[string]any{}) if tc.errOk { if err == nil { t.Error("expected error") } return } if err != nil { t.Fatal(err) } if got != tc.want { t.Errorf("SubtractNode: want %v, got %v", tc.want, got) } }) } } func TestMultiplyNode(t *testing.T) { n := &MultiplyNode{} got, err := n.Process([]float64{2.0, 3.0, 4.0}, map[string]any{}) if err != nil { t.Fatal(err) } if got != 24.0 { t.Errorf("MultiplyNode: want 24.0, got %v", got) } } func TestDivideNode(t *testing.T) { tests := []struct { name string inputs []float64 want float64 errOk bool }{ {"basic", []float64{10.0, 2.0}, 5.0, false}, {"zero denominator", []float64{10.0, 0.0}, 0.0, false}, {"too few inputs", []float64{1.0}, 0, true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { n := &DivideNode{} got, err := n.Process(tc.inputs, map[string]any{}) if tc.errOk { if err == nil { t.Error("expected error") } return } if err != nil { t.Fatal(err) } if got != tc.want { t.Errorf("DivideNode: want %v, got %v", tc.want, got) } }) } } func TestMovingAverageNode(t *testing.T) { n := &MovingAverageNode{Window: 3} state := map[string]any{} // Feed 1, 2, 3; window fills up values := []float64{1, 2, 3} wants := []float64{1, 1.5, 2.0} for i, v := range values { got, err := n.Process([]float64{v}, state) if err != nil { t.Fatalf("step %d: %v", i, err) } if math.Abs(got-wants[i]) > 1e-9 { t.Errorf("step %d: want %v, got %v", i, wants[i], got) } } // Feed 4; window slides: [2, 3, 4] → avg 3.0 got, err := n.Process([]float64{4}, state) if err != nil { t.Fatal(err) } if math.Abs(got-3.0) > 1e-9 { t.Errorf("sliding window: want 3.0, got %v", got) } } func TestRMSNode(t *testing.T) { n := &RMSNode{Window: 2} state := map[string]any{} // Feed 3.0 and 4.0; RMS over [3,4] = sqrt((9+16)/2) = sqrt(12.5) n.Process([]float64{3.0}, state) got, err := n.Process([]float64{4.0}, state) if err != nil { t.Fatal(err) } want := math.Sqrt(12.5) if math.Abs(got-want) > 1e-9 { t.Errorf("RMSNode: want %v, got %v", want, got) } } func TestDerivativeNode(t *testing.T) { n := &DerivativeNode{} state := map[string]any{} // First call should return 0 got, err := n.Process([]float64{1.0}, state) if err != nil { t.Fatal(err) } if got != 0.0 { t.Errorf("DerivativeNode first call: want 0, got %v", got) } // Second call should return a non-zero derivative got, err = n.Process([]float64{2.0}, state) if err != nil { t.Fatal(err) } // dt is very small (nanoseconds), so derivative should be large and positive if got <= 0 { t.Errorf("DerivativeNode second call: expected positive derivative, got %v", got) } } func TestClampNode(t *testing.T) { tests := []struct { input float64 min float64 max float64 want float64 }{ {5.0, 0.0, 10.0, 5.0}, {-5.0, 0.0, 10.0, 0.0}, {15.0, 0.0, 10.0, 10.0}, } for _, tc := range tests { n := &ClampNode{Min: tc.min, Max: tc.max} got, err := n.Process([]float64{tc.input}, map[string]any{}) if err != nil { t.Fatal(err) } if got != tc.want { t.Errorf("ClampNode(%v): want %v, got %v", tc.input, tc.want, got) } } } func TestThresholdNode(t *testing.T) { n := &ThresholdNode{Threshold: 5.0, High: 1.0, Low: 0.0} tests := []struct { input float64 want float64 }{ {3.0, 0.0}, // below threshold {5.0, 1.0}, // at threshold (outputs High) {10.0, 1.0}, // above threshold } for _, tc := range tests { got, err := n.Process([]float64{tc.input}, map[string]any{}) if err != nil { t.Fatal(err) } if got != tc.want { t.Errorf("ThresholdNode(%v): want %v, got %v", tc.input, tc.want, got) } } } func TestExprNode(t *testing.T) { tests := []struct { name string expr string inputs []float64 want float64 errOk bool }{ {"addition", "a + b", []float64{3, 4}, 7.0, false}, {"multiplication", "a * b", []float64{3, 4}, 12.0, false}, {"complex", "(a + b) * c", []float64{1, 2, 3}, 9.0, false}, {"division", "a / b", []float64{10, 2}, 5.0, false}, {"subtraction", "a - b", []float64{10, 3}, 7.0, false}, {"literal", "2 + 3", []float64{}, 5.0, false}, {"negative factor", "-a + b", []float64{3, 5}, 2.0, false}, {"nested parens", "(a + (b * c))", []float64{1, 2, 3}, 7.0, false}, {"invalid var", "x + 1", []float64{1}, 0, true}, {"invalid syntax", "a ++ b", []float64{1, 2}, 0, true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { n := &ExprNode{Expr: tc.expr} got, err := n.Process(tc.inputs, map[string]any{}) if tc.errOk { if err == nil { t.Errorf("ExprNode(%q): expected error", tc.expr) } return } if err != nil { t.Fatalf("ExprNode(%q): %v", tc.expr, err) } if math.Abs(got-tc.want) > 1e-9 { t.Errorf("ExprNode(%q): want %v, got %v", tc.expr, tc.want, got) } }) } } func TestLuaNode(t *testing.T) { tests := []struct { name string script string inputs []float64 want float64 errOk bool }{ {"multiply input", "return a * 2", []float64{3.0}, 6.0, false}, {"add two inputs", "return a + b", []float64{3.0, 4.0}, 7.0, false}, {"constant", "return 42", []float64{}, 42.0, false}, {"math lib", "return math.sqrt(a)", []float64{4.0}, 2.0, false}, {"no return", "local x = a + 1", []float64{1.0}, 0, true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { n := &LuaNode{Script: tc.script} state := map[string]any{} got, err := n.Process(tc.inputs, state) if tc.errOk { if err == nil { t.Errorf("LuaNode(%q): expected error", tc.script) } return } if err != nil { t.Fatalf("LuaNode(%q): %v", tc.script, err) } if math.Abs(got-tc.want) > 1e-9 { t.Errorf("LuaNode(%q): want %v, got %v", tc.script, tc.want, got) } }) } } func TestLuaNodeStateReuse(t *testing.T) { // Verify the Lua VM is reused across calls (stateful counter example) n := &LuaNode{Script: ` count = (count or 0) + 1 return count `} state := map[string]any{} for i := 1; i <= 3; i++ { got, err := n.Process(nil, state) if err != nil { t.Fatalf("call %d: %v", i, err) } if int(got) != i { t.Errorf("call %d: want %d, got %v", i, i, got) } } } func TestNodeTypes(t *testing.T) { nodes := []Node{ &GainNode{}, &OffsetNode{}, &AddNode{}, &SubtractNode{}, &MultiplyNode{}, &DivideNode{}, &MovingAverageNode{}, &RMSNode{}, &DerivativeNode{}, &ClampNode{}, &ThresholdNode{}, &ExprNode{}, &LuaNode{}, } types := []string{ "gain", "offset", "add", "subtract", "multiply", "divide", "moving_average", "rms", "derivative", "clamp", "threshold", "expr", "lua", } for i, n := range nodes { if n.Type() != types[i] { t.Errorf("node %T: want type %q, got %q", n, types[i], n.Type()) } } }