Phase 2/4 done, working on phase 3/5

This commit is contained in:
Martino Ferrari
2026-04-24 15:46:04 +02:00
parent 9aa89cc0cf
commit 8b548ba1c2
43 changed files with 6800 additions and 156 deletions
+351
View File
@@ -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())
}
}
}