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