blob: 878954c7530c9de6488a7783422318b4016fed19 [file]
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ext
import (
"fmt"
"strings"
"testing"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
)
func TestMath(t *testing.T) {
mathTests := []struct {
expr string
in any
}{
// Tests for math.least
{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"},
// math.least two arg overloads across type.
{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"},
// math.least with dynamic values across type.
{expr: "math.least(1u, dyn(42)) == 1"},
{expr: "math.least(1u, dyn(42), dyn(0.0)) == 0u"},
// math.least with a list literal.
{expr: "math.least([1u, 42u, 0u]) == 0u"},
// math.least with expression arguments.
{
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},
},
},
// Tests for math.greatest
{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"},
// math.greatest two arg overloads across type.
{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"},
// math.greatest with dynamic values across type.
{expr: "math.greatest(1u, dyn(42)) == 42.0"},
{expr: "math.greatest(1u, dyn(0.0), 0u) == 1"},
// math.greatest with a list literal
{expr: "math.greatest([1u, dyn(0.0), 0u]) == 1"},
// math.greatest with expression arguments.
{
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},
},
},
// Tests for math bitwise operators
// Signed bitwise ops
{expr: "math.bitAnd(1, 2) == 0"},
{expr: "math.bitAnd(1, -1) == 1"},
{expr: "math.bitAnd(1, 3) == 1"},
{expr: "math.bitOr(1, 2) == 3"},
{expr: "math.bitXor(1, 3) == 2"},
{expr: "math.bitXor(3, 5) == 6"},
{expr: "math.bitNot(1) == -2"},
{expr: "math.bitNot(0) == -1"},
{expr: "math.bitNot(-1) == 0"},
{expr: "math.bitShiftLeft(1, 2) == 4"},
{expr: "math.bitShiftLeft(1, 200) == 0"},
{expr: "math.bitShiftLeft(-1, 200) == 0"},
{expr: "math.bitShiftRight(1024, 2) == 256"},
{expr: "math.bitShiftRight(1024, 64) == 0"},
{expr: "math.bitShiftRight(-1024, 3) == 2305843009213693824"},
{expr: "math.bitShiftRight(-1024, 64) == 0"},
// Unsigned bitwise ops
{expr: "math.bitAnd(1u, 2u) == 0u"},
{expr: "math.bitAnd(1u, 3u) == 1u"},
{expr: "math.bitOr(1u, 2u) == 3u"},
{expr: "math.bitXor(1u, 3u) == 2u"},
{expr: "math.bitXor(3u, 5u) == 6u"},
{expr: "math.bitNot(1u) == 18446744073709551614u"},
{expr: "math.bitNot(0u) == 18446744073709551615u"},
{expr: "math.bitShiftLeft(1u, 2) == 4u"},
{expr: "math.bitShiftLeft(1u, 200) == 0u"},
{expr: "math.bitShiftRight(1024u, 2) == 256u"},
{expr: "math.bitShiftRight(1024u, 64) == 0u"},
// Tests for floating point helpers
{expr: "math.isNaN(0.0/0.0)"},
{expr: "!math.isNaN(1.0/0.0)"},
{expr: "math.isFinite(1.0/1.5)"},
{expr: "!math.isFinite(1.0/0.0)"},
{expr: "math.isInf(1.0/0.0)"},
// Tests for rounding functions
{expr: "math.ceil(1.2) == 2.0"},
{expr: "math.ceil(-1.2) == -1.0"},
{expr: "math.floor(1.2) == 1.0"},
{expr: "math.floor(-1.2) == -2.0"},
{expr: "math.round(1.2) == 1.0"},
{expr: "math.round(1.5) == 2.0"},
{expr: "math.round(-1.5) == -2.0"},
{expr: "math.isNaN(math.round(0.0/0.0))"},
{expr: "math.round(-1.2) == -1.0"},
{expr: "math.trunc(-1.3) == -1.0"},
{expr: "math.trunc(1.3) == 1.0"},
// Tests for signedness related functions
{expr: "math.sign(-42) == -1"},
{expr: "math.sign(0) == 0"},
{expr: "math.sign(42) == 1"},
{expr: "math.sign(0u) == 0u"},
{expr: "math.sign(42u) == 1u"},
{expr: "math.sign(-0.3) == -1.0"},
{expr: "math.sign(0.0) == 0.0"},
{expr: "math.isNaN(math.sign(0.0/0.0))"},
{expr: "math.sign(1.0/0.0) == 1.0"},
{expr: "math.sign(-1.0/0.0) == -1.0"},
{expr: "math.sign(0.3) == 1.0"},
{expr: "math.abs(-1) == 1"},
{expr: "math.abs(1) == 1"},
{expr: "math.abs(-234.5) == 234.5"},
{expr: "math.abs(234.5) == 234.5"},
// Tests for Square root function
{expr: "math.sqrt(49.0) == 7.0"},
{expr: "math.sqrt(0) == 0.0"},
{expr: "math.sqrt(1) == 1.0"},
{expr: "math.sqrt(25u) == 5.0"},
{expr: "math.sqrt(82) == 9.055385138137417"},
{expr: "math.sqrt(985.25) == 31.388692231439016"},
{expr: "math.isNaN(math.sqrt(-15.34))"},
}
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
}{
// Tests for math.least
{
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",
},
// Tests for math.greatest
{
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
}{
// Tests for math.least
{
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",
},
// Tests for math.greatest
{
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",
},
{
expr: "math.bitShiftLeft(1, -2) == 4",
err: "math.bitShiftLeft() negative offset",
},
{
expr: "math.bitShiftLeft(1u, -2) == 0u",
err: "math.bitShiftLeft() negative offset",
},
{
expr: "math.bitShiftRight(-1024, -3) == -128",
err: "math.bitShiftRight() negative offset",
},
{
expr: "math.bitShiftRight(1024u, -4) == 1u",
err: "math.bitShiftRight() negative offset",
},
{
expr: "math.abs(-9223372036854775808)",
err: "overflow",
},
{
expr: "math.bitOr(dyn(1.2), 1)",
err: "no such overload: math.bitOr(double, int)",
},
{
expr: "math.bitAnd(2u, dyn(''))",
err: "no such overload: math.bitAnd(uint, string)",
},
{
expr: "math.bitXor(dyn(1), dyn(1u))",
err: "no such overload: math.bitXor(int, uint)",
},
{
expr: "math.bitXor(dyn([]), dyn([1]))",
err: "no such overload: math.bitXor(list, list)",
},
{
expr: "math.bitNot(dyn([1]))",
err: "no such overload: math.bitNot(list)",
},
{
expr: "math.bitShiftLeft(dyn([1]), 1)",
err: "no such overload: math.bitShiftLeft(list, int)",
},
{
expr: "math.bitShiftRight(dyn({}), 1)",
err: "no such overload: math.bitShiftRight(map, int)",
},
{
expr: "math.isInf(dyn(1u))",
err: "no such overload: math.isInf(uint)",
},
{
expr: "math.isFinite(dyn(1u))",
err: "no such overload: math.isFinite(uint)",
},
{
expr: "math.isNaN(dyn(1u))",
err: "no such overload: math.isNaN(uint)",
},
{
expr: "math.sign(dyn(''))",
err: "no such overload: math.sign(string)",
},
{
expr: "math.abs(dyn(''))",
err: "no such overload: math.abs(string)",
},
{
expr: "math.ceil(dyn(''))",
err: "no such overload: math.ceil(string)",
},
{
expr: "math.floor(dyn(''))",
err: "no such overload: math.floor(string)",
},
{
expr: "math.round(dyn(1))",
err: "no such overload: math.round(int)",
},
{
expr: "math.trunc(dyn(1u))",
err: "no such overload: math.trunc(uint)",
},
{
expr: "math.sqrt(dyn(''))",
err: "no such overload: math.sqrt(string)",
},
}
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
}{
// Even though 'least' is the macro, the call is left unexpanded since the operand is not 'math'.
{
expr: `100.least(42) == 42`,
},
// Even though 'greatest' is the macro, the call is left unexpanded since the operand is not 'math'.
{
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 TestMathVersions(t *testing.T) {
versionCases := []struct {
version uint32
supportedFunctions map[string]string
}{
{
version: 0,
supportedFunctions: map[string]string{
"greatest": `math.greatest(1, 2) == 2`,
"least": `math.least(2.1, -1.0) == -1.0`,
},
},
{
version: 1,
supportedFunctions: map[string]string{
"ceil": `math.ceil(1.5) == 2.0`,
"floor": `math.floor(1.2) == 1.0`,
"round": `math.round(1.5) == 2.0`,
"trunc": `math.trunc(1.222) == 1.0`,
"isInf": `!math.isInf(0.0)`,
"isNaN": `math.isNaN(0.0/0.0)`,
"isFinite": `math.isFinite(0.0)`,
"abs": `math.abs(1.2) == 1.2`,
"sign": `math.sign(-1) == -1`,
"bitAnd": `math.bitAnd(1, 2) == 0`,
"bitOr": `math.bitOr(1, 2) == 3`,
"bitXor": `math.bitXor(1, 3) == 2`,
"bitNot": `math.bitNot(-1) == 0`,
"bitShiftLeft": `math.bitShiftLeft(4, 2) == 16`,
"bitShiftRight": `math.bitShiftRight(4, 2) == 1`,
},
},
{
version: 2,
supportedFunctions: map[string]string{
"sqrt": `math.sqrt(25) == 5.0`,
},
},
}
for _, lib := range versionCases {
env, err := cel.NewEnv(Math(MathVersion(lib.version)))
if err != nil {
t.Fatalf("cel.NewEnv(Math(MathVersion(%d))) failed: %v", lib.version, err)
}
t.Run(fmt.Sprintf("version=%d", lib.version), func(t *testing.T) {
for _, tc := range versionCases {
for name, expr := range tc.supportedFunctions {
supported := lib.version >= tc.version
t.Run(fmt.Sprintf("%s-supported=%t", name, supported), func(t *testing.T) {
ast, iss := env.Compile(expr)
if supported {
if iss.Err() != nil {
t.Errorf("unexpected error: %v", iss.Err())
}
} else {
if iss.Err() == nil || !strings.Contains(iss.Err().Error(), "undeclared reference") {
t.Errorf("got error %v, wanted error %s for expr: %s, version: %d", iss.Err(), "undeclared reference", expr, tc.version)
}
return
}
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 != types.True {
t.Errorf("prg.Eval() got %v, wanted true", out)
}
})
}
}
})
}
}
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
}