Phase 2/4 done, working on phase 3/5
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user