Files
uopi/internal/dsp/coverage_test.go
T
Martino Ferrari c0f7e662be Testing
2026-06-24 01:39:15 +02:00

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")
}
}