package ext
import (
"fmt"
"strings"
"testing"
"github.com/google/cel-go/cel"
)
func TestMath(t *testing.T) {
mathTests := []struct {
expr string
in any
}{
{expr: "math.least(-0.5) == -0.5"},
{expr: "math.least(-1) == -1"},
{expr: "math.least(1u) == 1u"},
{expr: "math.least(42.0, -0.5) == -0.5"},
{expr: "math.least(-1, 0) == -1"},
{expr: "math.least(-1, -1) == -1"},
{expr: "math.least(1u, 42u) == 1u"},
{expr: "math.least(42.0, -0.5, -0.25) == -0.5"},
{expr: "math.least(-1, 0, 1) == -1"},
{expr: "math.least(-1, -1, -1) == -1"},
{expr: "math.least(1u, 42u, 0u) == 0u"},
{expr: "math.least(1, 1.0) == 1"},
{expr: "math.least(1, -2.0) == -2.0"},
{expr: "math.least(2, 1u) == 1u"},
{expr: "math.least(1.5, 2) == 1.5"},
{expr: "math.least(1.5, -2) == -2"},
{expr: "math.least(2.5, 1u) == 1u"},
{expr: "math.least(1u, 2) == 1u"},
{expr: "math.least(1u, -2) == -2"},
{expr: "math.least(2u, 2.5) == 2u"},
{expr: "math.least(1u, dyn(42)) == 1"},
{expr: "math.least(1u, dyn(42), dyn(0.0)) == 0u"},
{expr: "math.least([1u, 42u, 0u]) == 0u"},
{
expr: "math.least(a, b) == a",
in: map[string]any{
"a": 1,
"b": 2,
},
},
{
expr: "math.least(numbers) == dyn(a)",
in: map[string]any{
"a": -21,
"numbers": []float64{-21.0, -10.5, 1.0},
},
},
{expr: "math.greatest(-0.5) == -0.5"},
{expr: "math.greatest(-1) == -1"},
{expr: "math.greatest(1u) == 1u"},
{expr: "math.greatest(42.0, -0.5) == 42.0"},
{expr: "math.greatest(-1, 0) == 0"},
{expr: "math.greatest(-1, -1) == -1"},
{expr: "math.greatest(1u, 42u) == 42u"},
{expr: "math.greatest(42.0, -0.5, -0.25) == 42.0"},
{expr: "math.greatest(-1, 0, 1) == 1"},
{expr: "math.greatest(-1, -1, -1) == -1"},
{expr: "math.greatest(1u, 42u, 0u) == 42u"},
{expr: "math.greatest(1, 1.0) == 1"},
{expr: "math.greatest(1, -2.0) == 1"},
{expr: "math.greatest(2, 1u) == 2"},
{expr: "math.greatest(1.5, 2) == 2"},
{expr: "math.greatest(1.5, -2) == 1.5"},
{expr: "math.greatest(2.5, 1u) == 2.5"},
{expr: "math.greatest(1u, 2) == 2"},
{expr: "math.greatest(1u, -2) == 1u"},
{expr: "math.greatest(2u, 2.5) == 2.5"},
{expr: "math.greatest(1u, dyn(42)) == 42.0"},
{expr: "math.greatest(1u, dyn(0.0), 0u) == 1"},
{expr: "math.greatest([1u, dyn(0.0), 0u]) == 1"},
{
expr: "math.greatest(a, b) == b",
in: map[string]any{
"a": 1,
"b": 2,
},
},
{
expr: "math.greatest(numbers) == dyn(a)",
in: map[string]any{
"a": 1,
"numbers": []float64{-21.0, -10.5, 1.0},
},
},
}
env := testMathEnv(t,
cel.Variable("a", cel.IntType),
cel.Variable("b", cel.IntType),
cel.Variable("numbers", cel.ListType(cel.DoubleType)),
)
for i, tst := range mathTests {
tc := tst
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
var asts []*cel.Ast
pAst, iss := env.Parse(tc.expr)
if iss.Err() != nil {
t.Fatalf("env.Parse(%v) failed: %v", tc.expr, iss.Err())
}
asts = append(asts, pAst)
cAst, iss := env.Check(pAst)
if iss.Err() != nil {
t.Fatalf("env.Check(%v) failed: %v", tc.expr, iss.Err())
}
asts = append(asts, cAst)
for _, ast := range asts {
prg, err := env.Program(ast)
if err != nil {
t.Fatalf("env.Program() failed: %v", err)
}
in := tc.in
if in == nil {
in = cel.NoVars()
}
out, _, err := prg.Eval(in)
if err != nil {
t.Fatalf("prg.Eval() failed: %v", err)
}
if out.Value() != true {
t.Errorf("prg.Eval() got %v, wanted true for expr: %s", out.Value(), tc.expr)
}
}
})
}
}
func TestMathStaticErrors(t *testing.T) {
mathTests := []struct {
expr string
err string
}{
{
expr: "math.least()",
err: "math.least() requires at least one argument",
},
{
expr: "math.least('hello')",
err: "math.least() invalid single argument value",
},
{
expr: "math.least({})",
err: "math.least() invalid single argument value",
},
{
expr: "math.least(1, true)",
err: "math.least() simple literal arguments must be numeric",
},
{
expr: "math.least(1, 2, true)",
err: "math.least() simple literal arguments must be numeric",
},
{
expr: "math.greatest()",
err: "math.greatest() requires at least one argument",
},
{
expr: "math.greatest(true)",
err: "math.greatest() invalid single argument value",
},
{
expr: "math.greatest([])",
err: "math.greatest() invalid single argument value",
},
{
expr: "math.greatest([1, true])",
err: "math.greatest() invalid single argument value",
},
{
expr: "math.greatest(1, true)",
err: "math.greatest() simple literal arguments must be numeric",
},
{
expr: "math.greatest(1, 2, true)",
err: "math.greatest() simple literal arguments must be numeric",
},
}
env := testMathEnv(t)
for i, tst := range mathTests {
tc := tst
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
_, iss := env.Compile(tc.expr)
if iss.Err() == nil || !strings.Contains(iss.Err().Error(), tc.err) {
t.Errorf("env.Compile(%q) got %v, wanted error %v", tc.expr, iss.Err(), tc.err)
}
})
}
}
func TestMathRuntimeErrors(t *testing.T) {
mathTests := []struct {
expr string
err string
in any
}{
{
expr: "math.least(a, b)",
err: "no such overload: math.@min",
in: map[string]any{
"a": []int{},
"b": 1,
},
},
{
expr: "math.least(b, a)",
err: "no such overload: math.@min",
in: map[string]any{
"a": []int{},
"b": 1,
},
},
{
expr: "math.least(a)",
err: "math.@min(list) argument must not be empty",
in: map[string]any{
"a": []int{},
},
},
{
expr: "math.least(a)",
err: "no such overload: math.@min",
in: map[string]any{
"a": []any{"hello"},
},
},
{
expr: "math.least(a)",
err: "no such overload: math.@min",
in: map[string]any{
"a": []any{[]int{}, []int{}},
},
},
{
expr: "math.least(a)",
err: "no such overload: math.@min",
in: map[string]any{
"a": []any{1, true, 2},
},
},
{
expr: "math.least(dyn('string'))",
err: "no such overload: math.@min",
},
{
expr: "math.greatest(a, b)",
err: "no such overload: math.@max",
in: map[string]any{
"a": []int{},
"b": 1,
},
},
{
expr: "math.greatest(b, a)",
err: "no such overload: math.@max",
in: map[string]any{
"a": []int{},
"b": 1,
},
},
{
expr: "math.greatest(a)",
err: "math.@max(list) argument must not be empty",
in: map[string]any{
"a": []int{},
},
},
{
expr: "math.greatest(a)",
err: "no such overload: math.@max",
in: map[string]any{
"a": []any{true},
},
},
{
expr: "math.greatest(a)",
err: "no such overload: math.@max",
in: map[string]any{
"a": []any{1, true, 2},
},
},
{
expr: "math.greatest(dyn('string'))",
err: "no such overload: math.@max",
},
}
env := testMathEnv(t,
cel.Variable("a", cel.DynType),
cel.Variable("b", cel.IntType),
cel.Variable("numbers", cel.ListType(cel.DoubleType)),
)
for i, tst := range mathTests {
tc := tst
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
ast, iss := env.Compile(tc.expr)
if iss.Err() != nil {
t.Fatalf("env.Compile(%q) failed with error %v", tc.expr, iss.Err())
}
prg, err := env.Program(ast)
if err != nil {
t.Fatalf("env.Program(ast) failed: %v", err)
}
in := tc.in
if in == nil {
in = cel.NoVars()
}
_, _, err = prg.Eval(in)
if err == nil || !strings.Contains(err.Error(), tc.err) {
t.Errorf("prg.Eval() got %v, wanted %v", err, tc.err)
}
})
}
}
func TestMathNonMatch(t *testing.T) {
var mathTests = []struct {
expr string
}{
{
expr: `100.least(42) == 42`,
},
{
expr: `100.greatest(42) == 100`,
},
}
env := testMathEnv(t,
cel.Function("greatest",
cel.MemberOverload("int_greatest_int", []*cel.Type{cel.IntType, cel.IntType}, cel.IntType,
cel.BinaryBinding(maxPair))),
cel.Function("least",
cel.MemberOverload("int_least_int", []*cel.Type{cel.IntType, cel.IntType}, cel.IntType,
cel.BinaryBinding(minPair))),
)
for i, tst := range mathTests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
var asts []*cel.Ast
pAst, iss := env.Parse(tc.expr)
if iss.Err() != nil {
t.Fatalf("env.Parse(%v) failed: %v", tc.expr, iss.Err())
}
asts = append(asts, pAst)
cAst, iss := env.Check(pAst)
if iss.Err() != nil {
t.Fatalf("env.Check(%v) failed: %v", tc.expr, iss.Err())
}
asts = append(asts, cAst)
for _, ast := range asts {
prg, err := env.Program(ast)
if err != nil {
t.Fatalf("env.Program() failed: %v", err)
}
out, _, err := prg.Eval(cel.NoVars())
if err != nil {
t.Fatalf("prg.Eval() failed: %v", err)
}
if out.Value() != true {
t.Errorf("prg.Eval() got %v, wanted true for expr: %s", out.Value(), tc.expr)
}
}
})
}
}
func TestMathWithExtension(t *testing.T) {
env := testMathEnv(t)
_, err := env.Extend(Math())
if err != nil {
t.Fatalf("env.Extend(Math()) failed: %v", err)
}
_, iss := env.Compile("math.least(0, 1, 2) == 0")
if iss.Err() != nil {
t.Errorf("env.Compile() failed: %v", iss.Err())
}
}
func testMathEnv(t *testing.T, opts ...cel.EnvOption) *cel.Env {
t.Helper()
baseOpts := []cel.EnvOption{Math(), cel.EnableMacroCallTracking()}
env, err := cel.NewEnv(append(baseOpts, opts...)...)
if err != nil {
t.Fatalf("cel.NewEnv(Math()) failed: %v", err)
}
return env
}