352 lines
8.0 KiB
Go
352 lines
8.0 KiB
Go
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())
|
|
}
|
|
}
|
|
}
|