blob: 9b479c3438da5c98e7c665425864509f8b0f9226 [file]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package api
import (
"errors"
"go/ast"
"go/parser"
"net/http"
"reflect"
"strings"
"testing"
)
func parseExpr(t *testing.T, s string) ast.Expr {
t.Helper()
expr, err := parser.ParseExpr(s)
if err != nil {
t.Fatalf("ParseExpr(%q) failed: %v", s, err)
}
return expr
}
func TestEvalBase(t *testing.T) {
env := map[string]any{
"x": 10,
"s": "hello",
"identity": func(v any) any {
return v
},
"add1": func(v int) int {
return v + 1
},
"greet": func(name string) (string, error) {
if name == "" {
return "", errors.New("empty name")
}
return "Hello " + name, nil
},
"panicFunc": func() {
panic("intentional panic")
},
}
tests := []struct {
expr string
want any
wantErr bool
}{
// Literals
{expr: `1`, want: 1},
{expr: `"hello"`, want: "hello"},
{expr: `true`, want: true},
{expr: `false`, want: false},
{expr: `nil`, want: nil},
// Variables
{expr: `x`, want: 10},
{expr: `s`, want: "hello"},
{expr: `y`, wantErr: true}, // undefined
// Arithmetic operators
{expr: `1 + 2`, want: 3},
{expr: `5 - 3`, want: 2},
{expr: `2 * 3`, want: 6},
{expr: `6 / 2`, want: 3},
{expr: `5 % 2`, want: 1},
{expr: `x + 5`, want: 15},
{expr: `-5`, want: -5},
{expr: `-x`, want: -10},
// String concatenation
{expr: `s + " world"`, want: "hello world"},
{expr: `"a" + "b"`, want: "ab"},
// Arithmetic errors
{expr: `6 / 0`, wantErr: true}, // division by zero
{expr: `5 % 0`, wantErr: true}, // division by zero
{expr: `x + s`, wantErr: true}, // type mismatch (int + string)
{expr: `s - "a"`, wantErr: true}, // invalid op for string
{expr: `-s`, wantErr: true}, // invalid unary minus for string
// Comparison operators (int)
{expr: `1 < 2`, want: true},
{expr: `2 < 1`, want: false},
{expr: `1 < 1`, want: false},
{expr: `1 > 2`, want: false},
{expr: `2 > 1`, want: true},
{expr: `1 > 1`, want: false},
{expr: `1 <= 2`, want: true},
{expr: `2 <= 1`, want: false},
{expr: `1 <= 1`, want: true},
{expr: `1 >= 2`, want: false},
{expr: `2 >= 1`, want: true},
{expr: `1 >= 1`, want: true},
{expr: `1 == 1`, want: true},
{expr: `1 == 2`, want: false},
{expr: `1 != 1`, want: false},
{expr: `1 != 2`, want: true},
// Comparison operators (string)
{expr: `"a" < "b"`, want: true},
{expr: `"b" < "a"`, want: false},
{expr: `"a" == "a"`, want: true},
{expr: `"a" == "b"`, want: false},
{expr: `"a" != "b"`, want: true},
// Comparison operators (bool)
{expr: `true == true`, want: true},
{expr: `true == false`, want: false},
{expr: `true != false`, want: true},
// Comparison with nil
{expr: `nil == nil`, want: true},
{expr: `nil != nil`, want: false},
{expr: `identity(nil) == nil`, want: true},
{expr: `identity(1) == nil`, want: false},
{expr: `s == nil`, want: false},
// Comparison errors
{expr: `1 < "a"`, wantErr: true}, // type mismatch
{expr: `true == 1`, want: false}, // type mismatch
{expr: `true < false`, wantErr: true}, // invalid op for bool
// Logical operators
{expr: `true && true`, want: true},
{expr: `true && false`, want: false},
{expr: `false && true`, want: false},
{expr: `false && false`, want: false},
{expr: `true || true`, want: true},
{expr: `true || false`, want: true},
{expr: `false || true`, want: true},
{expr: `false || false`, want: false},
{expr: `!true`, want: false},
{expr: `!false`, want: true},
// Short-circuiting verification
{expr: `false && panicFunc()`, want: false}, // short-circuit &&
{expr: `true || panicFunc()`, want: true}, // short-circuit ||
{expr: `false && greet("") == ""`, want: false}, // short-circuit && with error-returning func
// Logical errors
{expr: `true && 1`, wantErr: true}, // type mismatch
{expr: `false || "a"`, wantErr: true}, // type mismatch
{expr: `!1`, wantErr: true}, // invalid type for !
// Function calls
{expr: `identity(1)`, want: 1},
{expr: `identity("a")`, want: "a"},
{expr: `add1(x)`, want: 11},
{expr: `greet(s)`, want: "Hello hello"},
{expr: `greet("")`, wantErr: true}, // error return
{expr: `panicFunc()`, wantErr: true}, // panic recovery
{expr: `identity()`, wantErr: true}, // arg mismatch (0 instead of 1)
{expr: `x()`, wantErr: true}, // not a function
}
for _, tc := range tests {
t.Run(tc.expr, func(t *testing.T) {
astExpr := parseExpr(t, tc.expr)
got, err := evaluate(astExpr, env)
if (err != nil) != tc.wantErr {
t.Errorf("evaluate(%s) error = %v, wantErr %v", tc.expr, err, tc.wantErr)
return
}
if tc.wantErr {
return
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("evaluate(%s) = %v, want %v", tc.expr, got, tc.want)
}
})
}
}
func TestEvalComprehensive(t *testing.T) {
env := map[string]any{
"x": 10,
"s": "hello",
"greet": func(name string) (string, error) {
return "Hello " + name, nil
},
}
exprStr := `x > 5 && greet(s) == "Hello hello" && (1 + 2 * 3) == 7`
astExpr := parseExpr(t, exprStr)
got, err := evaluate(astExpr, env)
if err != nil {
t.Fatalf("evaluate(%s) failed: %v", exprStr, err)
}
want := true
if got != want {
t.Errorf("evaluate(%s) = %v, want %v", exprStr, got, want)
}
}
func TestEvalFilter(t *testing.T) {
sym := Symbol{
Name: "Eval",
Kind: "func",
Synopsis: "evaluates expressions",
Parent: "",
}
tests := []struct {
filter string
want bool
wantErr bool
}{
{filter: `name == "Eval"`, want: true},
{filter: `kind == "func"`, want: true},
{filter: `parent == ""`, want: true},
{filter: `kind == "func" && name == "Eval"`, want: true},
{filter: `kind == "var" || name == "Eval"`, want: true},
{filter: `kind == "func" && parent != ""`, want: false},
{filter: `name == "Evaluate"`, want: false},
{filter: `synopsis == "evaluates expressions"`, want: true},
{filter: `name == 1`, want: false}, // different types == is false
{filter: `name != 1`, want: true}, // different types != is true
// contains and matches
{filter: `contains(synopsis, "eval")`, want: true},
{filter: `contains(synopsis, "invalid")`, want: false},
{filter: `matches(name, "^Ev")`, want: true},
{filter: `matches(name, "val$")`, want: true},
{filter: "matches(name, `^a`)", want: false},
// Errors
{filter: `kind < 1`, wantErr: true}, // type mismatch for <
{filter: `nonexistent == ""`, wantErr: true}, // undefined identifier
{filter: `matches(name,"[invalid")`, wantErr: true}, // invalid regex
}
for _, tc := range tests {
t.Run(tc.filter, func(t *testing.T) {
list := []Symbol{sym}
got, err := filterStruct(list, tc.filter)
if (err != nil) != tc.wantErr {
t.Errorf("filter2(%+v, %q) error = %v, wantErr %v", list, tc.filter, err, tc.wantErr)
return
}
if tc.wantErr {
return
}
if tc.want {
if len(got) != 1 || got[0] != sym {
t.Errorf("filter2(%+v, %q) = %v, want [%+v]", list, tc.filter, got, sym)
}
} else {
if len(got) != 0 {
t.Errorf("filter2(%+v, %q) = %v, want empty", list, tc.filter, got)
}
}
})
}
}
func TestFilterErrors(t *testing.T) {
type badFuncStruct struct {
Func func() (int, int)
}
type manyFuncStruct struct {
Func func() (int, int, int)
}
type variadicFuncStruct struct {
Func func(...int) bool
}
type intStruct struct {
X int
}
type panicFuncStruct struct {
Func func() bool
}
tests := []struct {
name string
run func() error
wantBad bool // true if we expect a BadRequest error (*Error)
wantSubstr string // substring in error message
}{
{
name: "filterString empty varName",
run: func() error {
_, err := filterString([]string{"a"}, "true", "")
return err
},
wantBad: false,
wantSubstr: "string filter must have varName",
},
{
name: "filterStruct non-struct",
run: func() error {
_, err := filterStruct([]int{1}, "true")
return err
},
wantBad: false,
wantSubstr: "need struct or pointer to struct",
},
{
name: "parse error",
run: func() error {
_, err := filterString([]string{"a"}, "invalid go expr", "x")
return err
},
wantBad: true,
wantSubstr: "parsing filter",
},
{
name: "eval error undefined identifier",
run: func() error {
_, err := filterString([]string{"a"}, "unknown_var == 'a'", "x")
return err
},
wantBad: true,
wantSubstr: "undefined identifier",
},
{
name: "eval error regex compile",
run: func() error {
_, err := filterString([]string{"a"}, `matches(x, "[invalid")`, "x")
return err
},
wantBad: true,
wantSubstr: "parsing regexp",
},
{
name: "eval error arg count mismatch",
run: func() error {
_, err := filterString([]string{"a"}, "contains(x)", "x")
return err
},
wantBad: true,
wantSubstr: "argument count mismatch",
},
{
name: "eval error not a function",
run: func() error {
_, err := filterString([]string{"a"}, "x()", "x")
return err
},
wantBad: true,
wantSubstr: "not a function",
},
{
name: "eval error division by zero",
run: func() error {
_, err := filterString([]string{"a"}, "1/0 == 1", "x")
return err
},
wantBad: true,
wantSubstr: "division by zero",
},
{
name: "eval error invalid type for +",
run: func() error {
_, err := filterString([]string{"a"}, `(x == "a") + 1`, "x")
return err
},
wantBad: true,
wantSubstr: "invalid type for +",
},
{
name: "non-bool result",
run: func() error {
_, err := filterString([]string{"a"}, "1 + 1", "x")
return err
},
wantBad: true,
wantSubstr: "did not evaluate to bool",
},
{
name: "eval error second return value must be error",
run: func() error {
list := []badFuncStruct{{Func: func() (int, int) { return 1, 2 }}}
_, err := filterStruct(list, "Func() == 1")
return err
},
wantBad: true,
wantSubstr: "second return value must be error, got int",
},
{
name: "eval error function has too many return values",
run: func() error {
list := []manyFuncStruct{{Func: func() (int, int, int) { return 1, 2, 3 }}}
_, err := filterStruct(list, "Func() == 1")
return err
},
wantBad: true,
wantSubstr: "function has too many return values: 3",
},
{
name: "eval error variadic functions are not supported",
run: func() error {
list := []variadicFuncStruct{{Func: func(x ...int) bool { return true }}}
_, err := filterStruct(list, "Func(1, 2)")
return err
},
wantBad: true,
wantSubstr: "variadic functions are not supported",
},
{
name: "eval error unsupported basic lit kind",
run: func() error {
_, err := filterString([]string{"a"}, "'a' == 'a'", "x")
return err
},
wantBad: true,
wantSubstr: "unsupported basic lit kind",
},
{
name: "eval error unsupported unary operator",
run: func() error {
list := []intStruct{{X: 1}}
_, err := filterStruct(list, "^X == 1")
return err
},
wantBad: true,
wantSubstr: "unsupported unary operator",
},
{
name: "eval error invalid type for <",
run: func() error {
_, err := filterString([]string{"a"}, `(x == "a") < 1`, "x")
return err
},
wantBad: true,
wantSubstr: "invalid type for <",
},
{
name: "eval error unsupported binary operator",
run: func() error {
list := []intStruct{{X: 1}}
_, err := filterStruct(list, "(X & 1) == 1")
return err
},
wantBad: true,
wantSubstr: "unsupported binary operator",
},
{
name: "eval error expected type T, got T",
run: func() error {
_, err := filterString([]string{"a"}, "!x", "x")
return err
},
wantBad: true,
wantSubstr: "expected type bool, got string",
},
{
name: "eval error panic during function call",
run: func() error {
list := []panicFuncStruct{{Func: func() bool { panic("aaaaa") }}}
_, err := filterStruct(list, "Func()")
return err
},
wantBad: true,
wantSubstr: "panic during function call: aaaaa",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.run()
if err == nil {
t.Fatal("expected error, got nil")
}
var apiErr *Error
isBad := errors.As(err, &apiErr)
if tc.wantBad {
if !isBad {
t.Errorf("expected BadRequest (*Error), got %T (%v)", err, err)
} else if apiErr.Code != http.StatusBadRequest {
t.Errorf("expected Code %d, got %d", http.StatusBadRequest, apiErr.Code)
}
} else {
if isBad {
t.Errorf("expected plain Go error, got BadRequest (*Error): %v", err)
}
}
if tc.wantSubstr != "" {
if !strings.Contains(err.Error(), tc.wantSubstr) {
t.Errorf("expected error to contain %q, got %q", tc.wantSubstr, err.Error())
}
}
})
}
}