146 lines
4.3 KiB
Go
146 lines
4.3 KiB
Go
package dsp
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
// TestExprFunctions exercises every built-in function branch of parseCall plus
|
|
// the two-argument forms and the right-associative power operator.
|
|
func TestExprFunctions(t *testing.T) {
|
|
st := map[string]any{}
|
|
cases := []struct {
|
|
expr string
|
|
inputs []float64
|
|
want float64
|
|
}{
|
|
{"exp(a)", []float64{1}, math.E},
|
|
{"log(a)", []float64{math.E}, 1},
|
|
{"ln(a)", []float64{math.E}, 1},
|
|
{"log2(a)", []float64{8}, 3},
|
|
{"log10(a)", []float64{1000}, 3},
|
|
{"sqrt(a)", []float64{9}, 3},
|
|
{"abs(a)", []float64{-4}, 4},
|
|
{"sin(a)", []float64{0}, 0},
|
|
{"cos(a)", []float64{0}, 1},
|
|
{"tan(a)", []float64{0}, 0},
|
|
{"asin(a)", []float64{1}, math.Pi / 2},
|
|
{"acos(a)", []float64{1}, 0},
|
|
{"atan(a)", []float64{1}, math.Pi / 4},
|
|
{"atan2(a, b)", []float64{1, 1}, math.Pi / 4},
|
|
{"pow(a, b)", []float64{2, 10}, 1024},
|
|
{"floor(a)", []float64{2.9}, 2},
|
|
{"ceil(a)", []float64{2.1}, 3},
|
|
{"round(a)", []float64{2.5}, 3},
|
|
{"min(a, b)", []float64{3, 7}, 3},
|
|
{"max(a, b)", []float64{3, 7}, 7},
|
|
{"a ^ b", []float64{2, 3}, 8},
|
|
{"2 ^ 3 ^ 2", []float64{}, 512}, // right-associative: 2^(3^2)
|
|
{"-a ^ 2", []float64{3}, -9}, // unary minus binds outside power
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.expr, func(t *testing.T) {
|
|
n := &ExprNode{Expr: tc.expr}
|
|
got, err := n.Process(tc.inputs, st)
|
|
if err != nil {
|
|
t.Fatalf("Process(%q): %v", tc.expr, err)
|
|
}
|
|
if math.Abs(got-tc.want) > 1e-9 {
|
|
t.Errorf("Process(%q) = %v, want %v", tc.expr, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExprFunctionErrors covers the error branches of parseCall/parseFactor.
|
|
func TestExprFunctionErrors(t *testing.T) {
|
|
st := map[string]any{}
|
|
cases := []string{
|
|
"bogus(a)", // unknown function
|
|
"sqrt(a", // missing ')'
|
|
"sqrt(", // empty / unexpected end inside call
|
|
"@", // unexpected character
|
|
"(a + 1", // missing closing parenthesis
|
|
"1.2.3", // invalid number
|
|
}
|
|
for _, expr := range cases {
|
|
t.Run(expr, func(t *testing.T) {
|
|
n := &ExprNode{Expr: expr}
|
|
if _, err := n.Process([]float64{1}, st); err == nil {
|
|
t.Errorf("Process(%q): want error", expr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestArrayNodeScalarAdapters covers the legacy scalar Node interface
|
|
// (Type + Process) on the array nodes, which the array-path tests skip.
|
|
func TestArrayNodeScalarAdapters(t *testing.T) {
|
|
st := map[string]any{}
|
|
|
|
// Reductions: Process treats its float64 inputs as a single-element array,
|
|
// so each reduction over one value returns that value.
|
|
reductions := []struct {
|
|
node ArrayNode
|
|
typ string
|
|
}{
|
|
{&SumNode{}, "sum"},
|
|
{&MeanNode{}, "mean"},
|
|
{&MinNode{}, "min"},
|
|
{&MaxNode{}, "max"},
|
|
{&IndexNode{I: 0}, "index"},
|
|
}
|
|
for _, r := range reductions {
|
|
t.Run(r.typ, func(t *testing.T) {
|
|
if r.node.Type() != r.typ {
|
|
t.Errorf("Type() = %q, want %q", r.node.Type(), r.typ)
|
|
}
|
|
got, err := r.node.Process([]float64{42}, st)
|
|
if err != nil {
|
|
t.Fatalf("Process: %v", err)
|
|
}
|
|
if got != 42 {
|
|
t.Errorf("Process = %v, want 42", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
// LengthNode over a single scalar input → length 1.
|
|
ln := &LengthNode{}
|
|
if ln.Type() != "length" {
|
|
t.Errorf("LengthNode.Type() = %q", ln.Type())
|
|
}
|
|
if got, err := ln.Process([]float64{7}, st); err != nil || got != 1 {
|
|
t.Errorf("LengthNode.Process = %v, %v; want 1", got, err)
|
|
}
|
|
|
|
// SliceNode.Process returns the first element of the resulting slice.
|
|
sn := &SliceNode{Start: 0, End: 0}
|
|
if sn.Type() != "slice" {
|
|
t.Errorf("SliceNode.Type() = %q", sn.Type())
|
|
}
|
|
if got, err := sn.Process([]float64{5}, st); err != nil || got != 5 {
|
|
t.Errorf("SliceNode.Process = %v, %v; want 5", got, err)
|
|
}
|
|
|
|
// FFTNode.Process returns the first magnitude bin (DC term = the value).
|
|
fn := &FFTNode{}
|
|
if fn.Type() != "fft" {
|
|
t.Errorf("FFTNode.Type() = %q", fn.Type())
|
|
}
|
|
if got, err := fn.Process([]float64{3}, st); err != nil || math.Abs(got-3) > 1e-9 {
|
|
t.Errorf("FFTNode.Process = %v, %v; want 3", got, err)
|
|
}
|
|
}
|
|
|
|
// TestArrayNodeProcessErrors covers the error propagation through the scalar
|
|
// Process adapters.
|
|
func TestArrayNodeProcessErrors(t *testing.T) {
|
|
st := map[string]any{}
|
|
// IndexNode with an out-of-range index propagates the reduction error.
|
|
n := &IndexNode{I: 5}
|
|
if _, err := n.Process([]float64{1}, st); err == nil {
|
|
t.Error("IndexNode.Process out of range: want error")
|
|
}
|
|
}
|