diff --git a/pkg/deepcopy/deepcopy.go b/pkg/deepcopy/deepcopy.go new file mode 100644 index 0000000..75c2c4b --- /dev/null +++ b/pkg/deepcopy/deepcopy.go @@ -0,0 +1,158 @@ +package deepcopy + +import ( + "fmt" + + "go.xrstf.de/rudi/pkg/lang/ast" +) + +func Clone[T any](val T) (T, error) { + cloned, err := clone(val) + if err != nil { + empty := new(T) + return *empty, err + } + + if cloned == nil { + empty := new(T) + return *empty, nil + } + + return cloned.(T), nil +} + +func MustClone[T any](val T) T { + cloned, err := Clone(val) + if err != nil { + panic(err) + } + + return cloned +} + +func clonePtr[T any](ptr *T) *T { + if ptr == nil { + return nil + } + + cloned, _ := Clone(*ptr) + return &cloned +} + +//nolint:gocyclo +func clone(val any) (any, error) { + switch asserted := val.(type) { + // Go native types + + case nil: + return asserted, nil + case bool: + return asserted, nil + case int: + return asserted, nil + case int32: + return asserted, nil + case int64: + return asserted, nil + case float32: + return asserted, nil + case float64: + return asserted, nil + case string: + return asserted, nil + case map[string]any: + return cloneObject(asserted) + case []any: + return cloneVector(asserted) + + // pointer to Go types + + case *bool: + return clonePtr(asserted), nil + case *int: + return clonePtr(asserted), nil + case *int32: + return clonePtr(asserted), nil + case *int64: + return clonePtr(asserted), nil + case *float32: + return clonePtr(asserted), nil + case *float64: + return clonePtr(asserted), nil + case *string: + return clonePtr(asserted), nil + case *map[string]any: + return clonePtr(asserted), nil + case *[]any: + return clonePtr(asserted), nil + + // AST literals + + case ast.Null: + return ast.Null{}, nil + case ast.Bool: + return asserted, nil + case ast.Number: + return ast.Number{Value: asserted.Value}, nil + case ast.String: + return asserted, nil + case ast.Object: + cloned, err := Clone(asserted.Data) + if err != nil { + return nil, err + } + return ast.Object{Data: cloned}, nil + case ast.Vector: + cloned, err := Clone(asserted.Data) + if err != nil { + return nil, err + } + return ast.Vector{Data: cloned}, nil + + // pointer to AST literals + + case *ast.Null: + return clonePtr(asserted), nil + case *ast.Bool: + return clonePtr(asserted), nil + case *ast.Number: + return clonePtr(asserted), nil + case *ast.String: + return clonePtr(asserted), nil + case *ast.Object: + return clonePtr(asserted), nil + case *ast.Vector: + return clonePtr(asserted), nil + + default: + return nil, fmt.Errorf("cannot deep-copy %T", val) + } +} + +func cloneVector(obj []any) ([]any, error) { + result := make([]any, len(obj)) + for i, item := range obj { + cloned, err := clone(item) + if err != nil { + return nil, err + } + + result[i] = cloned + } + + return result, nil +} + +func cloneObject(obj map[string]any) (map[string]any, error) { + result := map[string]any{} + for key, value := range obj { + cloned, err := clone(value) + if err != nil { + return nil, err + } + + result[key] = cloned + } + + return result, nil +} diff --git a/pkg/eval/builtin/comparisons_test.go b/pkg/eval/builtin/comparisons_test.go index 8d448e5..ea01ae8 100644 --- a/pkg/eval/builtin/comparisons_test.go +++ b/pkg/eval/builtin/comparisons_test.go @@ -6,35 +6,10 @@ package builtin import ( "fmt" "testing" -) -type comparisonsTestcase struct { - expr string - expected any - document any - invalid bool -} - -func (tc *comparisonsTestcase) Test(t *testing.T) { - t.Helper() - - result, err := runExpression(t, tc.expr, tc.document, nil) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run %s: %v", tc.expr, err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run %s, but got: %v", tc.expr, result) - } - - if result != tc.expected { - t.Fatalf("Expected %v (%T), but got %v (%T)", tc.expected, tc.expected, result, result) - } -} + "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" +) type flippedTestcases struct { left string @@ -44,23 +19,25 @@ type flippedTestcases struct { invalid bool } -func genFlippedExpressions(fun string, testcases []flippedTestcases) []comparisonsTestcase { - result := []comparisonsTestcase{} +func genFlippedExpressions(fun string, testcases []flippedTestcases) []testutil.Testcase { + result := []testutil.Testcase{} for _, tc := range testcases { result = append( result, - comparisonsTestcase{ - expr: fmt.Sprintf(`(%s %s %s)`, fun, tc.left, tc.right), - invalid: tc.invalid, - expected: tc.expected, - document: tc.document, + testutil.Testcase{ + Expression: fmt.Sprintf(`(%s %s %s)`, fun, tc.left, tc.right), + Invalid: tc.invalid, + Expected: tc.expected, + Document: tc.document, + ExpectedDocument: tc.document, }, - comparisonsTestcase{ - expr: fmt.Sprintf(`(%s %s %s)`, fun, tc.right, tc.left), - invalid: tc.invalid, - expected: tc.expected, - document: tc.document, + testutil.Testcase{ + Expression: fmt.Sprintf(`(%s %s %s)`, fun, tc.right, tc.left), + Invalid: tc.invalid, + Expected: tc.expected, + Document: tc.document, + ExpectedDocument: tc.document, }, ) } @@ -70,7 +47,7 @@ func genFlippedExpressions(fun string, testcases []flippedTestcases) []compariso func TestEqFunction(t *testing.T) { testDoc := map[string]any{ - "int": int(4), + "int": int64(4), "float": float64(1.2), "bool": true, "string": "foo", @@ -81,26 +58,26 @@ func TestEqFunction(t *testing.T) { }, } - syntax := []comparisonsTestcase{ + syntax := []testutil.Testcase{ { - expr: `(eq?)`, - invalid: true, + Expression: `(eq?)`, + Invalid: true, }, { - expr: `(eq? true)`, - invalid: true, + Expression: `(eq? true)`, + Invalid: true, }, { - expr: `(eq? "too" "many" "args")`, - invalid: true, + Expression: `(eq? "too" "many" "args")`, + Invalid: true, }, { - expr: `(eq? identifier "foo")`, - invalid: true, + Expression: `(eq? identifier "foo")`, + Invalid: true, }, { - expr: `(eq? "foo" identifier)`, - invalid: true, + Expression: `(eq? "foo" identifier)`, + Invalid: true, }, } @@ -108,39 +85,39 @@ func TestEqFunction(t *testing.T) { { left: `true`, right: `true`, - expected: true, + expected: ast.Bool(true), }, { left: `true`, right: `false`, - expected: false, + expected: ast.Bool(false), }, { left: `true`, right: `.bool`, - expected: true, + expected: ast.Bool(true), document: testDoc, }, { left: `1`, right: `1`, - expected: true, + expected: ast.Bool(true), }, { left: `1`, right: `2`, - expected: false, + expected: ast.Bool(false), }, { left: `.int`, right: `4`, - expected: true, + expected: ast.Bool(true), document: testDoc, }, { left: `1`, right: `1.0`, - expected: false, + expected: ast.Bool(false), }, { left: `1`, @@ -155,107 +132,108 @@ func TestEqFunction(t *testing.T) { { left: `"foo"`, right: `"foo"`, - expected: true, + expected: ast.Bool(true), }, { left: `.string`, right: `"foo"`, - expected: true, + expected: ast.Bool(true), document: testDoc, }, { left: `"foo"`, right: `"bar"`, - expected: false, + expected: ast.Bool(false), }, { left: `"foo"`, right: `"Foo"`, - expected: false, + expected: ast.Bool(false), }, { left: `"foo"`, right: `" foo"`, - expected: false, + expected: ast.Bool(false), }, { left: `[]`, right: `[]`, - expected: true, + expected: ast.Bool(true), }, { left: `[]`, right: `[1]`, - expected: false, + expected: ast.Bool(false), }, { left: `[1]`, right: `[1]`, - expected: true, + expected: ast.Bool(true), }, { left: `[1 [2] {foo "bar"}]`, right: `[1 [2] {foo "bar"}]`, - expected: true, + expected: ast.Bool(true), }, { left: `[1 [2] {foo "bar"}]`, right: `[1 [2] {foo "baz"}]`, - expected: false, + expected: ast.Bool(false), }, { left: `{}`, right: `{}`, - expected: true, + expected: ast.Bool(true), }, { left: `{}`, right: `{foo "bar"}`, - expected: false, + expected: ast.Bool(false), }, { left: `{foo "bar"}`, right: `{foo "bar"}`, - expected: true, + expected: ast.Bool(true), }, { left: `{foo "bar"}`, right: `{foo "baz"}`, - expected: false, + expected: ast.Bool(false), }, { left: `{foo "bar" l [1 2]}`, right: `{foo "bar" l [1 2]}`, - expected: true, + expected: ast.Bool(true), }, }) for _, testcase := range append(syntax, flipped...) { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestLikeFunction(t *testing.T) { - syntax := []comparisonsTestcase{ + syntax := []testutil.Testcase{ { - expr: `(like?)`, - invalid: true, + Expression: `(like?)`, + Invalid: true, }, { - expr: `(like? true)`, - invalid: true, + Expression: `(like? true)`, + Invalid: true, }, { - expr: `(like? "too" "many" "args")`, - invalid: true, + Expression: `(like? "too" "many" "args")`, + Invalid: true, }, { - expr: `(like? identifier "foo")`, - invalid: true, + Expression: `(like? identifier "foo")`, + Invalid: true, }, { - expr: `(like? "foo" identifier)`, - invalid: true, + Expression: `(like? "foo" identifier)`, + Invalid: true, }, } @@ -263,206 +241,208 @@ func TestLikeFunction(t *testing.T) { { left: `1`, right: `1`, - expected: true, + expected: ast.Bool(true), }, { left: `1`, right: `"1"`, - expected: true, + expected: ast.Bool(true), }, { left: `1`, right: `1.0`, - expected: true, + expected: ast.Bool(true), }, { left: `1`, right: `"1.0"`, - expected: true, + expected: ast.Bool(true), }, { left: `1`, right: `"2.0"`, - expected: false, + expected: ast.Bool(false), }, { left: `1`, right: `true`, - expected: true, + expected: ast.Bool(true), }, { left: `1`, right: `2`, - expected: false, + expected: ast.Bool(false), }, { left: `false`, right: `null`, - expected: true, + expected: ast.Bool(true), }, { left: `false`, right: `"null"`, - expected: false, + expected: ast.Bool(false), }, { left: `0`, right: `null`, - expected: true, + expected: ast.Bool(true), }, { left: `0.0`, right: `null`, - expected: true, + expected: ast.Bool(true), }, { left: `""`, right: `null`, - expected: true, + expected: ast.Bool(true), }, { left: `false`, right: `0`, - expected: true, + expected: ast.Bool(true), }, { left: `false`, right: `""`, - expected: true, + expected: ast.Bool(true), }, { left: `false`, right: `[]`, - expected: true, + expected: ast.Bool(true), }, { left: `false`, right: `{}`, - expected: true, + expected: ast.Bool(true), }, { left: `false`, right: `"false"`, - expected: true, + expected: ast.Bool(true), }, { left: `true`, right: `{foo "bar"}`, - expected: true, + expected: ast.Bool(true), }, { left: `"foo"`, right: `"bar"`, - expected: false, + expected: ast.Bool(false), }, { left: `true`, right: `[""]`, - expected: true, + expected: ast.Bool(true), }, { left: `{}`, right: `[]`, - expected: true, + expected: ast.Bool(true), }, { left: `{}`, right: `[1]`, - expected: false, + expected: ast.Bool(false), }, { left: `{foo "bar"}`, right: `[]`, - expected: false, + expected: ast.Bool(false), }, }) for _, testcase := range append(syntax, testcases...) { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestLtFunction(t *testing.T) { - testcases := []comparisonsTestcase{ + testcases := []testutil.Testcase{ { - expr: `(lt?)`, - invalid: true, + Expression: `(lt?)`, + Invalid: true, }, { - expr: `(lt? true)`, - invalid: true, + Expression: `(lt? true)`, + Invalid: true, }, { - expr: `(lt? "too" "many" "args")`, - invalid: true, + Expression: `(lt? "too" "many" "args")`, + Invalid: true, }, { - expr: `(lt? identifier "foo")`, - invalid: true, + Expression: `(lt? identifier "foo")`, + Invalid: true, }, { - expr: `(lt? "foo" identifier)`, - invalid: true, + Expression: `(lt? "foo" identifier)`, + Invalid: true, }, { - expr: `(lt? 3 "strings")`, - invalid: true, + Expression: `(lt? 3 "strings")`, + Invalid: true, }, { - expr: `(lt? 3 3.1)`, - invalid: true, + Expression: `(lt? 3 3.1)`, + Invalid: true, }, { - expr: `(lt? 3 [1 2 3])`, - invalid: true, + Expression: `(lt? 3 [1 2 3])`, + Invalid: true, }, { - expr: `(lt? 3 {foo "bar"})`, - invalid: true, + Expression: `(lt? 3 {foo "bar"})`, + Invalid: true, }, { - expr: `(lt? 3 3)`, - expected: false, + Expression: `(lt? 3 3)`, + Expected: ast.Bool(false), }, { - expr: `(lt? 2 (+ 1 2))`, - expected: true, + Expression: `(lt? 2 (+ 1 2))`, + Expected: ast.Bool(true), }, { - expr: `(lt? 2 3)`, - expected: true, + Expression: `(lt? 2 3)`, + Expected: ast.Bool(true), }, { - expr: `(lt? -3 2)`, - expected: true, + Expression: `(lt? -3 2)`, + Expected: ast.Bool(true), }, { - expr: `(lt? -3 -5)`, - expected: false, + Expression: `(lt? -3 -5)`, + Expected: ast.Bool(false), }, { - expr: `(lt? 3.4 3.4)`, - expected: false, + Expression: `(lt? 3.4 3.4)`, + Expected: ast.Bool(false), }, { - expr: `(lt? 2.4 (+ 1.4 2))`, - expected: true, + Expression: `(lt? 2.4 (+ 1.4 2))`, + Expected: ast.Bool(true), }, { - expr: `(lt? 2.4 3.4)`, - expected: true, + Expression: `(lt? 2.4 3.4)`, + Expected: ast.Bool(true), }, { - expr: `(lt? -3.4 2.4)`, - expected: true, + Expression: `(lt? -3.4 2.4)`, + Expected: ast.Bool(true), }, { - expr: `(lt? -3.4 -5.4)`, - expected: false, + Expression: `(lt? -3.4 -5.4)`, + Expected: ast.Bool(false), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/builtin/core.go b/pkg/eval/builtin/core.go index bd0e68e..afc2696 100644 --- a/pkg/eval/builtin/core.go +++ b/pkg/eval/builtin/core.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" + "go.xrstf.de/rudi/pkg/deepcopy" "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/eval/coalescing" "go.xrstf.de/rudi/pkg/eval/types" @@ -179,7 +180,7 @@ func tryFunction(ctx types.Context, args []ast.Expression) (any, error) { _, result, err := eval.EvalExpression(ctx, args[0]) if err != nil { if len(args) == 1 { - return nil, nil + return ast.Null{}, nil } _, result, err = eval.EvalExpression(ctx, args[1]) @@ -262,6 +263,13 @@ func (deleteFunction) Evaluate(ctx types.Context, args []ast.Expression) (any, e currentValue = ctx.GetDocument().Data() } + // we need to operate on a _copy_ of the value and then, if need be, rely on the BangHandler + // to make the actual deletion happen and stick. + currentValue, err = deepcopy.Clone(currentValue) + if err != nil { + return nil, fmt.Errorf("invalid current value: %w", err) + } + // delete the desired path in the value updatedValue, err := pathexpr.Delete(currentValue, pathexpr.FromEvaluatedPath(*pathExpr)) if err != nil { diff --git a/pkg/eval/builtin/core_test.go b/pkg/eval/builtin/core_test.go index d5c848c..a0d7130 100644 --- a/pkg/eval/builtin/core_test.go +++ b/pkg/eval/builtin/core_test.go @@ -8,439 +8,451 @@ import ( "go.xrstf.de/rudi/pkg/eval/types" "go.xrstf.de/rudi/pkg/lang/ast" - - "github.com/google/go-cmp/cmp" + "go.xrstf.de/rudi/pkg/testutil" ) -type coreTestcase struct { - expr string - expected any - document any - variables types.Variables - invalid bool -} - -func (tc *coreTestcase) Test(t *testing.T) { - t.Helper() - - result, err := runExpression(t, tc.expr, tc.document, tc.variables) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run %s: %v", tc.expr, err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run %s, but got: %v", tc.expr, result) - } - - if _, ok := tc.expected.([]any); ok { - if !cmp.Equal(tc.expected, result) { - t.Fatalf("Expected %v (%T), but got %v (%T)", tc.expected, tc.expected, result, result) - } - } else if _, ok := tc.expected.(map[string]any); ok { - if !cmp.Equal(tc.expected, result) { - t.Fatalf("Expected %+v (%T), but got %+v (%T)", tc.expected, tc.expected, result, result) - } - } else { - if result != tc.expected { - t.Fatalf("Expected %v (%T), but got %v (%T)", tc.expected, tc.expected, result, result) - } - } -} - func TestIfFunction(t *testing.T) { - testcases := []coreTestcase{ + testcases := []testutil.Testcase{ { - expr: `(if)`, - invalid: true, + Expression: `(if)`, + Invalid: true, }, { - expr: `(if true)`, - invalid: true, + Expression: `(if true)`, + Invalid: true, }, { - expr: `(if true "yes" "no" "extra")`, - invalid: true, + Expression: `(if true "yes" "no" "extra")`, + Invalid: true, }, { - expr: `(if identifier "yes")`, - invalid: true, + Expression: `(if identifier "yes")`, + Invalid: true, }, { - expr: `(if {} "yes")`, - invalid: true, + Expression: `(if {} "yes")`, + Invalid: true, }, { - expr: `(if [] "yes")`, - invalid: true, + Expression: `(if [] "yes")`, + Invalid: true, }, { - expr: `(if 1 "yes")`, - invalid: true, + Expression: `(if 1 "yes")`, + Invalid: true, }, { - expr: `(if 3.4 "yes")`, - invalid: true, + Expression: `(if 3.4 "yes")`, + Invalid: true, }, { - expr: `(if (+ 1 1) "yes")`, - invalid: true, + Expression: `(if (+ 1 1) "yes")`, + Invalid: true, }, { - expr: `(if true 3)`, - expected: int64(3), + Expression: `(if true 3)`, + Expected: ast.Number{Value: int64(3)}, }, { - expr: `(if (eq? 1 1) 3)`, - expected: int64(3), + Expression: `(if (eq? 1 1) 3)`, + Expected: ast.Number{Value: int64(3)}, }, { - expr: `(if (eq? 1 2) 3)`, - expected: nil, + Expression: `(if (eq? 1 2) 3)`, + Expected: ast.Null{}, }, { - expr: `(if (eq? 1 2) "yes" "else")`, - expected: "else", + Expression: `(if (eq? 1 2) "yes" "else")`, + Expected: ast.String("else"), }, { - expr: `(if false "yes" (+ 1 4))`, - expected: int64(5), + Expression: `(if false "yes" (+ 1 4))`, + Expected: ast.Number{Value: int64(5)}, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestSetFunction(t *testing.T) { - testObjDocument := map[string]any{ - "aString": "foo", - "aList": []any{"first", 2, "third"}, - "aBool": true, - "anObject": map[string]any{ - "key1": true, - "key2": nil, - "key3": []any{9, map[string]any{"foo": "bar"}, 7}, - }, + testObjDocument := func() any { + return map[string]any{ + "aString": "foo", + "aList": []any{"first", int64(2), "third"}, + "aBool": true, + "anObject": map[string]any{ + "key1": true, + "key2": nil, + "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, + }, + } } - testVecDocument := []any{1, 2, map[string]any{"foo": "bar"}} + testVecDocument := func() any { + return []any{int64(1), int64(2), map[string]any{"foo": "bar"}} + } - testVariables := types.Variables{ - "myvar": 42, - "obj": testObjDocument, - "vec": testVecDocument, - "astVec": ast.Vector{Data: []any{ast.String("foo")}}, + testVariables := func() types.Variables { + return types.Variables{ + "myvar": int64(42), + "obj": testObjDocument(), + "vec": testVecDocument(), + "astVec": ast.Vector{Data: []any{ast.String("foo")}}, + } } - testcases := []coreTestcase{ + testcases := []testutil.Testcase{ { - expr: `(set)`, - invalid: true, + Expression: `(set)`, + Invalid: true, }, { - expr: `(set true)`, - invalid: true, + Expression: `(set true)`, + Invalid: true, }, { - expr: `(set "foo")`, - invalid: true, + Expression: `(set "foo")`, + Invalid: true, }, { - expr: `(set 42)`, - invalid: true, + Expression: `(set 42)`, + Invalid: true, }, { - expr: `(set {foo "bar"})`, - invalid: true, + Expression: `(set {foo "bar"})`, + Invalid: true, }, { - expr: `(set $var)`, - invalid: true, + Expression: `(set $var)`, + Invalid: true, }, { - expr: `(set $var "too" "many")`, - invalid: true, + Expression: `(set $var "too" "many")`, + Invalid: true, }, { - expr: `(set $var (unknown-func))`, - invalid: true, + Expression: `(set $var (unknown-func))`, + Invalid: true, }, // return the value that was set (without a bang modifier, this doesn't actually modify anything) { - expr: `(set $var "foo")`, - expected: "foo", + Expression: `(set $var "foo")`, + Expected: ast.String("foo"), }, { - expr: `(set $var "foo") $var`, - invalid: true, + Expression: `(set $var "foo") $var`, + Invalid: true, }, { - expr: `(set $var 1)`, - expected: int64(1), + Expression: `(set $var 1)`, + Expected: ast.Number{Value: int64(1)}, }, // with bang it works as expected { - expr: `(set! $var 1)`, - expected: int64(1), + Expression: `(set! $var 1)`, + Expected: ast.Number{Value: int64(1)}, }, { - expr: `(set! $var 1) $var`, - expected: int64(1), + Expression: `(set! $var 1) $var`, + Expected: ast.Number{Value: int64(1)}, }, // can overwrite variables on the top level { - expr: `(set! $myvar 12) $myvar`, - variables: testVariables, - expected: int64(12), + Expression: `(set! $myvar 12) $myvar`, + Variables: testVariables(), + Expected: ast.Number{Value: int64(12)}, }, // can change the type { - expr: `(set! $myvar "new value") $myvar`, - variables: testVariables, - expected: "new value", + Expression: `(set! $myvar "new value") $myvar`, + Variables: testVariables(), + Expected: ast.String("new value"), }, { - expr: `(set! $obj.aList[1] "new value")`, - variables: testVariables, - expected: "new value", + Expression: `(set! $obj.aList[1] "new value")`, + Variables: testVariables(), + Expected: ast.String("new value"), }, { - expr: `(set! $obj.aList[1] "new value") $obj`, - variables: testVariables, - expected: map[string]any{ + Expression: `(set! $obj.aList[1] "new value") $obj`, + Variables: testVariables(), + Expected: ast.Object{Data: map[string]any{ "aString": "foo", - "aList": []any{"first", "new value", "third"}, + "aList": []any{"first", ast.String("new value"), "third"}, "aBool": true, "anObject": map[string]any{ "key1": true, "key2": nil, "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, }, - }, + }}, }, // set itself does not change the first argument { - expr: `(set $myvar "new value") $myvar`, - variables: testVariables, - expected: int64(42), + Expression: `(set $myvar "new value") $myvar`, + Variables: testVariables(), + Expected: ast.Number{Value: int64(42)}, }, { - expr: `(set $obj.aString "new value") $obj.aString`, - variables: testVariables, - expected: "foo", + Expression: `(set $obj.aString "new value") $obj.aString`, + Variables: testVariables(), + Expected: ast.String("foo"), }, { - expr: `(set $obj.aList[1] "new value") $obj.aList`, - variables: testVariables, - expected: []any{"first", int64(2), "third"}, + Expression: `(set $obj.aList[1] "new value") $obj.aList`, + Variables: testVariables(), + Expected: ast.Vector{Data: []any{"first", int64(2), "third"}}, }, // ...but not leak into upper scopes { - expr: `(set! $a 1) (if true (set! $a 2)) $a`, - expected: int64(1), + Expression: `(set! $a 1) (if true (set! $a 2)) $a`, + Expected: ast.Number{Value: int64(1)}, }, { - expr: `(set! $a 1) (if true (set! $b 2)) $b`, - invalid: true, + Expression: `(set! $a 1) (if true (set! $b 2)) $b`, + Invalid: true, }, // do not accidentally set a key without creating a new context { - expr: `(set! $a {foo "bar"}) (if true (set! $a.foo "updated"))`, - expected: "updated", + Expression: `(set! $a {foo "bar"}) (if true (set! $a.foo "updated"))`, + Expected: ast.String("updated"), }, { - expr: `(set! $a {foo "bar"}) (if true (set! $a.foo "updated")) $a.foo`, - expected: "bar", + Expression: `(set! $a {foo "bar"}) (if true (set! $a.foo "updated")) $a.foo`, + Expected: ast.String("bar"), }, // handle bad paths { - expr: `(set! $obj[5.6] "new value")`, - invalid: true, + Expression: `(set! $obj[5.6] "new value")`, + Invalid: true, }, // not a vector { - expr: `(set! $obj[5] "new value")`, - invalid: true, + Expression: `(set! $obj[5] "new value")`, + Invalid: true, }, { - expr: `(set! $obj.aBool[5] "new value")`, - invalid: true, + Expression: `(set! $obj.aBool[5] "new value")`, + Invalid: true, }, // update a key within an object variable { - expr: `(set! $obj.aString "new value")`, - expected: "new value", - variables: testVariables, + Expression: `(set! $obj.aString "new value")`, + Expected: ast.String("new value"), + Variables: testVariables(), }, { - expr: `(set! $obj.aString "new value") $obj.aString`, - expected: "new value", - variables: testVariables, + Expression: `(set! $obj.aString "new value") $obj.aString`, + Expected: ast.String("new value"), + Variables: testVariables(), }, // add a new sub key { - expr: `(set! $obj.newKey "new value")`, - expected: "new value", - variables: testVariables, + Expression: `(set! $obj.newKey "new value")`, + Expected: ast.String("new value"), + Variables: testVariables(), }, { - expr: `(set! $obj.newKey "new value") $obj.newKey`, - expected: "new value", - variables: testVariables, + Expression: `(set! $obj.newKey "new value") $obj.newKey`, + Expected: ast.String("new value"), + Variables: testVariables(), }, // runtime variables { - expr: `(set! $vec [1]) (set! $vec[0] 2) $vec[0]`, - expected: int64(2), + Expression: `(set! $vec [1]) (set! $vec[0] 2) $vec[0]`, + Expected: ast.Number{Value: int64(2)}, }, // replace the global document { - expr: `(set! . 1) .`, - document: testObjDocument, - expected: int64(1), + Expression: `(set! . 1) .`, + Document: testObjDocument(), + Expected: ast.Number{Value: int64(1)}, + ExpectedDocument: int64(1), }, // update keys in the global document { - expr: `(set! .aString "new-value") .aString`, - document: testObjDocument, - expected: "new-value", + Expression: `(set! .aString "new-value") .aString`, + Document: testObjDocument(), + Expected: ast.String("new-value"), + ExpectedDocument: map[string]any{ + "aString": ast.String("new-value"), + "aList": []any{"first", int64(2), "third"}, + "aBool": true, + "anObject": map[string]any{ + "key1": true, + "key2": nil, + "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, + }, + }, }, // add new keys { - expr: `(set! .newKey "new-value") .newKey`, - document: testObjDocument, - expected: "new-value", + Expression: `(set! .newKey "new-value") .newKey`, + Document: testObjDocument(), + Expected: ast.String("new-value"), + ExpectedDocument: map[string]any{ + "aString": "foo", + "aList": []any{"first", int64(2), "third"}, + "aBool": true, + "anObject": map[string]any{ + "key1": true, + "key2": nil, + "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, + }, + // TODO: Should we generally try to put native values in objects and vectors? + "newKey": ast.String("new-value"), + }, }, // update vectors { - expr: `(set! .aList[1] "new-value") .aList[1]`, - document: testObjDocument, - expected: "new-value", + Expression: `(set! .aList[1] "new-value") .aList[1]`, + Document: testObjDocument(), + Expected: ast.String("new-value"), + ExpectedDocument: map[string]any{ + "aString": "foo", + "aList": []any{"first", ast.String("new-value"), "third"}, + "aBool": true, + "anObject": map[string]any{ + "key1": true, + "key2": nil, + "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, + }, + }, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestDeleteFunction(t *testing.T) { - testObjDocument := map[string]any{ - "aString": "foo", - "aList": []any{"first", int64(2), "third"}, - "aBool": true, - "anObject": map[string]any{ - "key1": true, - "key2": nil, - "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, - }, + testObjDocument := func() map[string]any { + return map[string]any{ + "aString": "foo", + "aList": []any{"first", int64(2), "third"}, + "aBool": true, + "anObject": map[string]any{ + "key1": true, + "key2": nil, + "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, + }, + } } - testcases := []coreTestcase{ + testcases := []testutil.Testcase{ { - expr: `(delete)`, - invalid: true, + Expression: `(delete)`, + Invalid: true, }, { - expr: `(delete "too" "many")`, - invalid: true, + Expression: `(delete "too" "many")`, + Invalid: true, }, { - expr: `(delete $var)`, - invalid: true, + Expression: `(delete $var)`, + Invalid: true, }, { // TODO: This should be valid. - expr: `(delete [1 2 3][1])`, - invalid: true, + Expression: `(delete [1 2 3][1])`, + Invalid: true, }, { // TODO: This should be valid. - expr: `(delete {foo "bar"}.foo)`, - invalid: true, + Expression: `(delete {foo "bar"}.foo)`, + Invalid: true, }, // allow removing everything { - expr: `(delete .)`, - document: map[string]any{"foo": "bar"}, - expected: nil, + Expression: `(delete .)`, + Document: map[string]any{"foo": "bar"}, + Expected: ast.Null{}, + ExpectedDocument: map[string]any{"foo": "bar"}, }, { - expr: `(delete! .) .`, - document: map[string]any{"foo": "bar"}, - expected: nil, + Expression: `(delete! .) .`, + Document: map[string]any{"foo": "bar"}, + Expected: ast.Null{}, }, // delete does not update the target { - expr: `(delete .) .`, - document: map[string]any{"foo": "bar"}, - expected: map[string]any{"foo": "bar"}, + Expression: `(delete .) .`, + Document: map[string]any{"foo": "bar"}, + Expected: ast.Object{Data: map[string]any{"foo": "bar"}}, + ExpectedDocument: map[string]any{"foo": "bar"}, }, // can remove a key { - expr: `(delete .foo)`, - document: map[string]any{"foo": "bar"}, - expected: map[string]any{}, + Expression: `(delete .foo)`, + Document: map[string]any{"foo": "bar"}, + Expected: ast.Object{Data: map[string]any{}}, + ExpectedDocument: map[string]any{"foo": "bar"}, }, { - expr: `(delete .foo) .`, - document: map[string]any{"foo": "bar"}, - expected: map[string]any{"foo": "bar"}, + Expression: `(delete .foo) .`, + Document: map[string]any{"foo": "bar"}, + Expected: ast.Object{Data: map[string]any{"foo": "bar"}}, + ExpectedDocument: map[string]any{"foo": "bar"}, }, { - expr: `(delete! .foo) .`, - document: map[string]any{"foo": "bar"}, - expected: map[string]any{}, + Expression: `(delete! .foo) .`, + Document: map[string]any{"foo": "bar"}, + Expected: ast.Object{Data: map[string]any{}}, + ExpectedDocument: map[string]any{}, }, // non-existent key is okay { - expr: `(delete .bar)`, - document: map[string]any{"foo": "bar"}, - expected: map[string]any{"foo": "bar"}, + Expression: `(delete .bar)`, + Document: map[string]any{"foo": "bar"}, + Expected: ast.Object{Data: map[string]any{"foo": "bar"}}, + ExpectedDocument: map[string]any{"foo": "bar"}, }, // path must be sane though { - expr: `(delete .[1])`, - document: map[string]any{"foo": "bar"}, - invalid: true, + Expression: `(delete .[1])`, + Document: map[string]any{"foo": "bar"}, + Invalid: true, }, // can delete from array { - expr: `(delete .[1])`, - document: []any{"a", "b", "c"}, - expected: []any{"a", "c"}, + Expression: `(delete .[1])`, + Document: []any{"a", "b", "c"}, + Expected: ast.Vector{Data: []any{"a", "c"}}, + ExpectedDocument: []any{"a", "b", "c"}, }, { - expr: `(delete .[1]) .`, - document: []any{"a", "b", "c"}, - expected: []any{"a", "b", "c"}, + Expression: `(delete .[1]) .`, + Document: []any{"a", "b", "c"}, + Expected: ast.Vector{Data: []any{"a", "b", "c"}}, + ExpectedDocument: []any{"a", "b", "c"}, }, { - expr: `(delete! .[1]) .`, - document: []any{"a", "b", "c"}, - expected: []any{"a", "c"}, + Expression: `(delete! .[1]) .`, + Document: []any{"a", "b", "c"}, + Expected: ast.Vector{Data: []any{"a", "c"}}, + ExpectedDocument: []any{"a", "c"}, }, // vector bounds are checked { - expr: `(delete .[-1])`, - document: []any{"a", "b", "c"}, - invalid: true, + Expression: `(delete .[-1])`, + Document: []any{"a", "b", "c"}, + Invalid: true, }, { - expr: `(delete .[3])`, - document: []any{"a", "b", "c"}, - invalid: true, + Expression: `(delete .[3])`, + Document: []any{"a", "b", "c"}, + Invalid: true, }, // can delete sub keys { - expr: `(delete .aList[1])`, - document: testObjDocument, - expected: map[string]any{ + Expression: `(delete .aList[1])`, + Document: testObjDocument(), + Expected: ast.Object{Data: map[string]any{ "aString": "foo", "aList": []any{"first", "third"}, "aBool": true, @@ -449,17 +461,29 @@ func TestDeleteFunction(t *testing.T) { "key2": nil, "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, }, - }, + }}, + ExpectedDocument: testObjDocument(), }, { - expr: `(delete .aList[1]) .`, - document: testObjDocument, - expected: testObjDocument, + Expression: `(delete .aList[1]) .`, + Document: testObjDocument(), + Expected: ast.Object{Data: testObjDocument()}, + ExpectedDocument: testObjDocument(), }, { - expr: `(delete! .aList[1]) .`, - document: testObjDocument, - expected: map[string]any{ + Expression: `(delete! .aList[1]) .`, + Document: testObjDocument(), + Expected: ast.Object{Data: map[string]any{ + "aString": "foo", + "aList": []any{"first", "third"}, + "aBool": true, + "anObject": map[string]any{ + "key1": true, + "key2": nil, + "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, + }, + }}, + ExpectedDocument: map[string]any{ "aString": "foo", "aList": []any{"first", "third"}, "aBool": true, @@ -471,9 +495,9 @@ func TestDeleteFunction(t *testing.T) { }, }, { - expr: `(delete .anObject.key3[1].foo)`, - document: testObjDocument, - expected: map[string]any{ + Expression: `(delete .anObject.key3[1].foo)`, + Document: testObjDocument(), + Expected: ast.Object{Data: map[string]any{ "aString": "foo", "aList": []any{"first", int64(2), "third"}, "aBool": true, @@ -482,17 +506,29 @@ func TestDeleteFunction(t *testing.T) { "key2": nil, "key3": []any{int64(9), map[string]any{}, int64(7)}, }, - }, + }}, + ExpectedDocument: testObjDocument(), }, { - expr: `(delete .anObject.key3[1].foo) .`, - document: testObjDocument, - expected: testObjDocument, + Expression: `(delete .anObject.key3[1].foo) .`, + Document: testObjDocument(), + Expected: ast.Object{Data: testObjDocument()}, + ExpectedDocument: testObjDocument(), }, { - expr: `(delete! .anObject.key3[1].foo) .`, - document: testObjDocument, - expected: map[string]any{ + Expression: `(delete! .anObject.key3[1].foo) .`, + Document: testObjDocument(), + Expected: ast.Object{Data: map[string]any{ + "aString": "foo", + "aList": []any{"first", int64(2), "third"}, + "aBool": true, + "anObject": map[string]any{ + "key1": true, + "key2": nil, + "key3": []any{int64(9), map[string]any{}, int64(7)}, + }, + }}, + ExpectedDocument: map[string]any{ "aString": "foo", "aList": []any{"first", int64(2), "third"}, "aBool": true, @@ -506,489 +542,520 @@ func TestDeleteFunction(t *testing.T) { } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestDoFunction(t *testing.T) { - testcases := []coreTestcase{ + testcases := []testutil.Testcase{ { - expr: `(do)`, - invalid: true, + Expression: `(do)`, + Invalid: true, }, { - expr: `(do identifier)`, - invalid: true, + Expression: `(do identifier)`, + Invalid: true, }, { - expr: `(do 3)`, - expected: int64(3), + Expression: `(do 3)`, + Expected: ast.Number{Value: int64(3)}, }, // test that the runtime context is inherited from one step to another { - expr: `(do (set! $var "foo") $var)`, - expected: "foo", + Expression: `(do (set! $var "foo") $var)`, + Expected: ast.String("foo"), }, { - expr: `(do (set! $var "foo") $var (set! $var "new") $var)`, - expected: "new", + Expression: `(do (set! $var "foo") $var (set! $var "new") $var)`, + Expected: ast.String("new"), }, // test that the runtime context doesn't leak { - expr: `(set! $var "outer") (do (set! $var "inner")) (concat $var ["1" "2"])`, - expected: "1outer2", + Expression: `(set! $var "outer") (do (set! $var "inner")) (concat $var ["1" "2"])`, + Expected: ast.String("1outer2"), }, { - expr: `(do (set! $var "inner")) (concat $var ["1" "2"])`, - invalid: true, + Expression: `(do (set! $var "inner")) (concat $var ["1" "2"])`, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestDefaultFunction(t *testing.T) { - testcases := []coreTestcase{ + testcases := []testutil.Testcase{ { - expr: `(default)`, - invalid: true, + Expression: `(default)`, + Invalid: true, }, { - expr: `(default true)`, - invalid: true, + Expression: `(default true)`, + Invalid: true, }, { - expr: `(default null 3)`, - expected: int64(3), + Expression: `(default null 3)`, + Expected: ast.Number{Value: int64(3)}, }, // coalescing should be applied { - expr: `(default false 3)`, - expected: int64(3), + Expression: `(default false 3)`, + Expected: ast.Number{Value: int64(3)}, }, { - expr: `(default [] 3)`, - expected: int64(3), + Expression: `(default [] 3)`, + Expected: ast.Number{Value: int64(3)}, }, // errors are not swallowed { - expr: `(default (eq? 3 "foo") 3)`, - invalid: true, + Expression: `(default (eq? 3 "foo") 3)`, + Invalid: true, }, { - expr: `(default false (eq? 3 "foo"))`, - invalid: true, + Expression: `(default false (eq? 3 "foo"))`, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestTryFunction(t *testing.T) { - testcases := []coreTestcase{ + testcases := []testutil.Testcase{ { - expr: `(try)`, - invalid: true, + Expression: `(try)`, + Invalid: true, }, { - expr: `(try (+ 1 2))`, - expected: int64(3), + Expression: `(try (+ 1 2))`, + Expected: ast.Number{Value: int64(3)}, }, // coalescing should be not applied { - expr: `(try false)`, - expected: false, + Expression: `(try false)`, + Expected: ast.Bool(false), }, { - expr: `(try null)`, - expected: nil, + Expression: `(try null)`, + Expected: ast.Null{}, }, { - expr: `(try null "fallback")`, - expected: nil, + Expression: `(try null "fallback")`, + Expected: ast.Null{}, }, // swallow errors { - expr: `(try (eq? 3 "foo"))`, - expected: nil, + Expression: `(try (eq? 3 "foo"))`, + Expected: ast.Null{}, }, { - expr: `(try (eq? 3 "foo") "fallback")`, - expected: "fallback", + Expression: `(try (eq? 3 "foo") "fallback")`, + Expected: ast.String("fallback"), }, // not in the fallback though { - expr: `(try (eq? 3 "foo") (eq? 3 "foo"))`, - invalid: true, + Expression: `(try (eq? 3 "foo") (eq? 3 "foo"))`, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestIsEmptyFunction(t *testing.T) { - testcases := []coreTestcase{ + testcases := []testutil.Testcase{ { - expr: `(empty?)`, - invalid: true, + Expression: `(empty?)`, + Invalid: true, }, { - expr: `(empty? "too" "many")`, - invalid: true, + Expression: `(empty? "too" "many")`, + Invalid: true, }, { - expr: `(empty? ident)`, - invalid: true, + Expression: `(empty? ident)`, + Invalid: true, }, { - expr: `(empty? null)`, - expected: true, + Expression: `(empty? null)`, + Expected: ast.Bool(true), }, { - expr: `(empty? true)`, - expected: false, + Expression: `(empty? true)`, + Expected: ast.Bool(false), }, { - expr: `(empty? false)`, - expected: true, + Expression: `(empty? false)`, + Expected: ast.Bool(true), }, { - expr: `(empty? 0)`, - expected: true, + Expression: `(empty? 0)`, + Expected: ast.Bool(true), }, { - expr: `(empty? 0.0)`, - expected: true, + Expression: `(empty? 0.0)`, + Expected: ast.Bool(true), }, { - expr: `(empty? (+ 0 0.0))`, - expected: true, + Expression: `(empty? (+ 0 0.0))`, + Expected: ast.Bool(true), }, { - expr: `(empty? (+ 1 0.0))`, - expected: false, + Expression: `(empty? (+ 1 0.0))`, + Expected: ast.Bool(false), }, { - expr: `(empty? [])`, - expected: true, + Expression: `(empty? [])`, + Expected: ast.Bool(true), }, { - expr: `(empty? [""])`, - expected: false, + Expression: `(empty? [""])`, + Expected: ast.Bool(false), }, { - expr: `(empty? {})`, - expected: true, + Expression: `(empty? {})`, + Expected: ast.Bool(true), }, { - expr: `(empty? {foo "bar"})`, - expected: false, + Expression: `(empty? {foo "bar"})`, + Expected: ast.Bool(false), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestHasFunction(t *testing.T) { testObjDocument := map[string]any{ "aString": "foo", - "aList": []any{"first", 2, "third"}, + "aList": []any{"first", int64(2), "third"}, "aBool": true, "anObject": map[string]any{ "key1": true, "key2": nil, - "key3": []any{9, map[string]any{"foo": "bar"}, 7}, + "key3": []any{int64(9), map[string]any{"foo": "bar"}, int64(7)}, }, } - testVecDocument := []any{1, 2, map[string]any{"foo": "bar"}} + testVecDocument := []any{int64(1), int64(2), map[string]any{"foo": "bar"}} testVariables := types.Variables{ // value does not matter here, but this testcase is still meant // to ensure the missing path is detected, not detect an unknown variable - "myvar": 42, + "myvar": int64(42), "obj": testObjDocument, "vec": testVecDocument, "astVec": ast.Vector{Data: []any{ast.String("foo")}}, } - testcases := []coreTestcase{ + testcases := []testutil.Testcase{ { - expr: `(has?)`, - invalid: true, + Expression: `(has?)`, + Invalid: true, }, { - expr: `(has? "too" "many")`, - invalid: true, + Expression: `(has? "too" "many")`, + Invalid: true, }, { - expr: `(has? true)`, - invalid: true, + Expression: `(has? true)`, + Invalid: true, }, { - expr: `(has? (+ 1 2))`, - invalid: true, + Expression: `(has? (+ 1 2))`, + Invalid: true, }, { - expr: `(has? "string")`, - invalid: true, + Expression: `(has? "string")`, + Invalid: true, }, { - expr: `(has? .[5.6])`, - invalid: true, + Expression: `(has? .[5.6])`, + Invalid: true, }, { - expr: `(has? (unknown-func).bar)`, - invalid: true, + Expression: `(has? (unknown-func).bar)`, + Invalid: true, }, // access the global document { - expr: `(has? .)`, - expected: true, - document: nil, // the . always matches, no matter what the document is + Expression: `(has? .)`, + Expected: ast.Bool(true), + Document: nil, // the . always matches, no matter what the document is }, { - expr: `(has? .)`, - expected: true, - document: testObjDocument, + Expression: `(has? .)`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .nonexistingKey)`, - expected: false, - document: testObjDocument, + Expression: `(has? .nonexistingKey)`, + Expected: ast.Bool(false), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .[0])`, - expected: false, - document: testObjDocument, + Expression: `(has? .[0])`, + Expected: ast.Bool(false), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .aString)`, - expected: true, - document: testObjDocument, + Expression: `(has? .aString)`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .["aString"])`, - expected: true, - document: testObjDocument, + Expression: `(has? .["aString"])`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .[(concat "" "a" "String")])`, - expected: true, - document: testObjDocument, + Expression: `(has? .[(concat "" "a" "String")])`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .aBool)`, - expected: true, - document: testObjDocument, + Expression: `(has? .aBool)`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .aList)`, - expected: true, - document: testObjDocument, + Expression: `(has? .aList)`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .aList[0])`, - expected: true, - document: testObjDocument, + Expression: `(has? .aList[0])`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .aList[99])`, - expected: false, - document: testObjDocument, + Expression: `(has? .aList[99])`, + Expected: ast.Bool(false), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .aList.invalidObjKey)`, - expected: false, - document: testObjDocument, + Expression: `(has? .aList.invalidObjKey)`, + Expected: ast.Bool(false), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .anObject)`, - expected: true, - document: testObjDocument, + Expression: `(has? .anObject)`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .anObject[99])`, - expected: false, - document: testObjDocument, + Expression: `(has? .anObject[99])`, + Expected: ast.Bool(false), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .anObject.key1)`, - expected: true, - document: testObjDocument, + Expression: `(has? .anObject.key1)`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .anObject.key99)`, - expected: false, - document: testObjDocument, + Expression: `(has? .anObject.key99)`, + Expected: ast.Bool(false), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .anObject.key3[1].foo)`, - expected: true, - document: testObjDocument, + Expression: `(has? .anObject.key3[1].foo)`, + Expected: ast.Bool(true), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, { - expr: `(has? .anObject.key3[1].bar)`, - expected: false, - document: testObjDocument, + Expression: `(has? .anObject.key3[1].bar)`, + Expected: ast.Bool(false), + Document: testObjDocument, + ExpectedDocument: testObjDocument, }, // global document is an array { - expr: `(has? .[1])`, - expected: true, - document: testVecDocument, + Expression: `(has? .[1])`, + Expected: ast.Bool(true), + Document: testVecDocument, + ExpectedDocument: testVecDocument, }, { - expr: `(has? .key)`, - expected: false, - document: testVecDocument, + Expression: `(has? .key)`, + Expected: ast.Bool(false), + Document: testVecDocument, + ExpectedDocument: testVecDocument, }, { - expr: `(has? .[2].foo)`, - expected: true, - document: testVecDocument, + Expression: `(has? .[2].foo)`, + Expected: ast.Bool(true), + Document: testVecDocument, + ExpectedDocument: testVecDocument, }, // global document is a scalar { - expr: `(has? .)`, - expected: true, - document: "testdata", + Expression: `(has? .)`, + Expected: ast.Bool(true), + Document: "testdata", + ExpectedDocument: "testdata", }, { - expr: `(has? .)`, - expected: true, - document: nil, + Expression: `(has? .)`, + Expected: ast.Bool(true), + Document: nil, + ExpectedDocument: nil, }, { - expr: `(has? .foo)`, - expected: false, - document: "testdata", + Expression: `(has? .foo)`, + Expected: ast.Bool(false), + Document: "testdata", + ExpectedDocument: "testdata", }, { - expr: `(has? .)`, - expected: true, - document: 64, + Expression: `(has? .)`, + Expected: ast.Bool(true), + Document: 64, + ExpectedDocument: int64(64), }, { - expr: `(has? .)`, - expected: true, - document: ast.String("foo"), + Expression: `(has? .)`, + Expected: ast.Bool(true), + Document: ast.String("foo"), + ExpectedDocument: "foo", }, // follow a path expression on a variable { - expr: `(has? $myvar)`, - invalid: true, + Expression: `(has? $myvar)`, + Invalid: true, }, { // missing path expression (TODO: should this be valid?) - expr: `(has? $myvar)`, - invalid: true, - variables: testVariables, + Expression: `(has? $myvar)`, + Invalid: true, + Variables: testVariables, }, { - expr: `(has? $myvar.foo)`, - expected: false, - variables: testVariables, + Expression: `(has? $myvar.foo)`, + Expected: ast.Bool(false), + Variables: testVariables, }, { - expr: `(has? $myvar[0])`, - expected: false, - variables: testVariables, + Expression: `(has? $myvar[0])`, + Expected: ast.Bool(false), + Variables: testVariables, }, { - expr: `(has? $obj.aString)`, - expected: true, - variables: testVariables, + Expression: `(has? $obj.aString)`, + Expected: ast.Bool(true), + Variables: testVariables, }, { - expr: `(has? $obj.aList[1])`, - expected: true, - variables: testVariables, + Expression: `(has? $obj.aList[1])`, + Expected: ast.Bool(true), + Variables: testVariables, }, { - expr: `(has? $vec[1])`, - expected: true, - variables: testVariables, + Expression: `(has? $vec[1])`, + Expected: ast.Bool(true), + Variables: testVariables, }, { - expr: `(has? $astVec[0])`, - expected: true, - variables: testVariables, + Expression: `(has? $astVec[0])`, + Expected: ast.Bool(true), + Variables: testVariables, }, // follow a path expression on a vector node { - expr: `(has? [1 2 3][1])`, - expected: true, + Expression: `(has? [1 2 3][1])`, + Expected: ast.Bool(true), }, { - expr: `(has? [1 2 3][4])`, - expected: false, + Expression: `(has? [1 2 3][4])`, + Expected: ast.Bool(false), }, // follow a path expression on an object node { - expr: `(has? {foo "bar"}.foo)`, - expected: true, + Expression: `(has? {foo "bar"}.foo)`, + Expected: ast.Bool(true), }, { - expr: `(has? {foo "bar"}.bar)`, - expected: false, + Expression: `(has? {foo "bar"}.bar)`, + Expected: ast.Bool(false), }, // follow a path expression on a tuple node // (don't even need "set!" here) { - expr: `(has? (set $foo {foo "bar"}).foo)`, - expected: true, + Expression: `(has? (set $foo {foo "bar"}).foo)`, + Expected: ast.Bool(true), }, { - expr: `(has? (set $foo {foo "bar"}).bar)`, - expected: false, + Expression: `(has? (set $foo {foo "bar"}).bar)`, + Expected: ast.Bool(false), }, { - expr: `(has? (set $foo [1])[0])`, - expected: true, + Expression: `(has? (set $foo [1])[0])`, + Expected: ast.Bool(true), }, { - expr: `(has? (set $foo {foo "bar"})[0])`, - expected: false, + Expression: `(has? (set $foo {foo "bar"})[0])`, + Expected: ast.Bool(false), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/builtin/encoding_test.go b/pkg/eval/builtin/encoding_test.go index 7d21bc0..bfd9fee 100644 --- a/pkg/eval/builtin/encoding_test.go +++ b/pkg/eval/builtin/encoding_test.go @@ -5,130 +5,108 @@ package builtin import ( "testing" -) - -type encodingTestcase struct { - expr string - expected string - invalid bool -} - -func (tc *encodingTestcase) Test(t *testing.T) { - t.Helper() - - result, err := runExpression(t, tc.expr, nil, nil) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run %s: %v", tc.expr, err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run %s, but got: %v", tc.expr, result) - } - if result != tc.expected { - t.Fatalf("Expected %v (%T), but got %v (%T)", tc.expected, tc.expected, result, result) - } -} + "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" +) func TestToBase64Function(t *testing.T) { - testcases := []encodingTestcase{ + testcases := []testutil.Testcase{ { - expr: `(to-base64)`, - invalid: true, + Expression: `(to-base64)`, + Invalid: true, }, { - expr: `(to-base64 "too" "many")`, - invalid: true, + Expression: `(to-base64 "too" "many")`, + Invalid: true, }, { - expr: `(to-base64 true)`, - invalid: true, + Expression: `(to-base64 true)`, + Invalid: true, }, { - expr: `(to-base64 1)`, - invalid: true, + Expression: `(to-base64 1)`, + Invalid: true, }, { - expr: `(to-base64 null)`, - invalid: true, + Expression: `(to-base64 null)`, + Invalid: true, }, { - expr: `(to-base64 "")`, - expected: "", + Expression: `(to-base64 "")`, + Expected: ast.String(""), }, { - expr: `(to-base64 " ")`, - expected: "IA==", + Expression: `(to-base64 " ")`, + Expected: ast.String("IA=="), }, { - expr: `(to-base64 (concat "" "f" "o" "o"))`, - expected: "Zm9v", + Expression: `(to-base64 (concat "" "f" "o" "o"))`, + Expected: ast.String("Zm9v"), }, { - expr: `(to-base64 "test")`, - expected: "dGVzdA==", + Expression: `(to-base64 "test")`, + Expected: ast.String("dGVzdA=="), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestFromBase64Function(t *testing.T) { - testcases := []encodingTestcase{ + testcases := []testutil.Testcase{ { - expr: `(from-base64)`, - invalid: true, + Expression: `(from-base64)`, + Invalid: true, }, { - expr: `(from-base64 "too" "many")`, - invalid: true, + Expression: `(from-base64 "too" "many")`, + Invalid: true, }, { - expr: `(from-base64 true)`, - invalid: true, + Expression: `(from-base64 true)`, + Invalid: true, }, { - expr: `(from-base64 1)`, - invalid: true, + Expression: `(from-base64 1)`, + Invalid: true, }, { - expr: `(from-base64 null)`, - invalid: true, + Expression: `(from-base64 null)`, + Invalid: true, }, { - expr: `(from-base64 "definitely-not-base64")`, - invalid: true, + Expression: `(from-base64 "definitely-not-base64")`, + Invalid: true, }, { // should be able to recover - expr: `(try (from-base64 "definitely-not-base64") "fallback")`, - expected: "fallback", + Expression: `(try (from-base64 "definitely-not-base64") "fallback")`, + Expected: ast.String("fallback"), }, { - expr: `(from-base64 "")`, - expected: "", + Expression: `(from-base64 "")`, + Expected: ast.String(""), }, { - expr: `(from-base64 "IA==")`, - expected: " ", + Expression: `(from-base64 "IA==")`, + Expected: ast.String(" "), }, { - expr: `(from-base64 (concat "" "Z" "m" "9" "v"))`, - expected: "foo", + Expression: `(from-base64 (concat "" "Z" "m" "9" "v"))`, + Expected: ast.String("foo"), }, { - expr: `(from-base64 "dGVzdA==")`, - expected: "test", + Expression: `(from-base64 "dGVzdA==")`, + Expected: ast.String("test"), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/builtin/hashing_test.go b/pkg/eval/builtin/hashing_test.go index 5dd5e65..2951915 100644 --- a/pkg/eval/builtin/hashing_test.go +++ b/pkg/eval/builtin/hashing_test.go @@ -5,154 +5,133 @@ package builtin import ( "testing" -) - -type hashingTestcase struct { - expr string - expected string - invalid bool -} - -func (tc *hashingTestcase) Test(t *testing.T) { - t.Helper() - - result, err := runExpression(t, tc.expr, nil, nil) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run %s: %v", tc.expr, err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run %s, but got: %v", tc.expr, result) - } - if result != tc.expected { - t.Fatalf("Expected %v (%T), but got %v (%T)", tc.expected, tc.expected, result, result) - } -} + "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" +) func TestSha1Function(t *testing.T) { - testcases := []hashingTestcase{ + testcases := []testutil.Testcase{ { - expr: `(sha1)`, - invalid: true, + Expression: `(sha1)`, + Invalid: true, }, { - expr: `(sha1 "too" "many")`, - invalid: true, + Expression: `(sha1 "too" "many")`, + Invalid: true, }, { - expr: `(sha1 true)`, - invalid: true, + Expression: `(sha1 true)`, + Invalid: true, }, { - expr: `(sha1 1)`, - invalid: true, + Expression: `(sha1 1)`, + Invalid: true, }, { - expr: `(sha1 null)`, - invalid: true, + Expression: `(sha1 null)`, + Invalid: true, }, { - expr: `(sha1 "")`, - expected: "da39a3ee5e6b4b0d3255bfef95601890afd80709", + Expression: `(sha1 "")`, + Expected: ast.String("da39a3ee5e6b4b0d3255bfef95601890afd80709"), }, { - expr: `(sha1 " ")`, - expected: "b858cb282617fb0956d960215c8e84d1ccf909c6", + Expression: `(sha1 " ")`, + Expected: ast.String("b858cb282617fb0956d960215c8e84d1ccf909c6"), }, { - expr: `(sha1 (concat "" "f" "o" "o"))`, - expected: "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33", + Expression: `(sha1 (concat "" "f" "o" "o"))`, + Expected: ast.String("0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestSha256Function(t *testing.T) { - testcases := []hashingTestcase{ + testcases := []testutil.Testcase{ { - expr: `(sha256)`, - invalid: true, + Expression: `(sha256)`, + Invalid: true, }, { - expr: `(sha256 "too" "many")`, - invalid: true, + Expression: `(sha256 "too" "many")`, + Invalid: true, }, { - expr: `(sha256 true)`, - invalid: true, + Expression: `(sha256 true)`, + Invalid: true, }, { - expr: `(sha256 1)`, - invalid: true, + Expression: `(sha256 1)`, + Invalid: true, }, { - expr: `(sha256 null)`, - invalid: true, + Expression: `(sha256 null)`, + Invalid: true, }, { - expr: `(sha256 "")`, - expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + Expression: `(sha256 "")`, + Expected: ast.String("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), }, { - expr: `(sha256 " ")`, - expected: "36a9e7f1c95b82ffb99743e0c5c4ce95d83c9a430aac59f84ef3cbfab6145068", + Expression: `(sha256 " ")`, + Expected: ast.String("36a9e7f1c95b82ffb99743e0c5c4ce95d83c9a430aac59f84ef3cbfab6145068"), }, { - expr: `(sha256 (concat "" "f" "o" "o"))`, - expected: "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + Expression: `(sha256 (concat "" "f" "o" "o"))`, + Expected: ast.String("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestSha512Function(t *testing.T) { - testcases := []hashingTestcase{ + testcases := []testutil.Testcase{ { - expr: `(sha512)`, - invalid: true, + Expression: `(sha512)`, + Invalid: true, }, { - expr: `(sha512 "too" "many")`, - invalid: true, + Expression: `(sha512 "too" "many")`, + Invalid: true, }, { - expr: `(sha512 true)`, - invalid: true, + Expression: `(sha512 true)`, + Invalid: true, }, { - expr: `(sha512 1)`, - invalid: true, + Expression: `(sha512 1)`, + Invalid: true, }, { - expr: `(sha512 null)`, - invalid: true, + Expression: `(sha512 null)`, + Invalid: true, }, { - expr: `(sha512 "")`, - expected: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + Expression: `(sha512 "")`, + Expected: ast.String("cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"), }, { - expr: `(sha512 " ")`, - expected: "f90ddd77e400dfe6a3fcf479b00b1ee29e7015c5bb8cd70f5f15b4886cc339275ff553fc8a053f8ddc7324f45168cffaf81f8c3ac93996f6536eef38e5e40768", + Expression: `(sha512 " ")`, + Expected: ast.String("f90ddd77e400dfe6a3fcf479b00b1ee29e7015c5bb8cd70f5f15b4886cc339275ff553fc8a053f8ddc7324f45168cffaf81f8c3ac93996f6536eef38e5e40768"), }, { - expr: `(sha512 (concat "" "f" "o" "o"))`, - expected: "f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7", + Expression: `(sha512 (concat "" "f" "o" "o"))`, + Expected: ast.String("f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7"), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/builtin/lists.go b/pkg/eval/builtin/lists.go index f73ffb9..23d5aaf 100644 --- a/pkg/eval/builtin/lists.go +++ b/pkg/eval/builtin/lists.go @@ -183,6 +183,7 @@ func rangeFunction(ctx types.Context, args []ast.Expression) (any, error) { } var result any + result = ast.Null{} // list over vector elements if sourceVector, ok := source.(ast.Vector); ok { diff --git a/pkg/eval/builtin/lists_test.go b/pkg/eval/builtin/lists_test.go index 0685e97..d8b4eb7 100644 --- a/pkg/eval/builtin/lists_test.go +++ b/pkg/eval/builtin/lists_test.go @@ -6,515 +6,495 @@ package builtin import ( "testing" - "github.com/google/go-cmp/cmp" + "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) -type listsTestcase struct { - expr string - expected any - invalid bool -} - -func (tc *listsTestcase) Test(t *testing.T) { - t.Helper() - - result, err := runExpression(t, tc.expr, nil, nil) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run %s: %v", tc.expr, err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run %s, but got: %v", tc.expr, result) - } - - if !cmp.Equal(result, tc.expected) { - t.Fatalf("Did not receive expected output:\n%s", cmp.Diff(tc.expected, result)) - } -} - func TestLenFunction(t *testing.T) { - testcases := []listsTestcase{ + testcases := []testutil.Testcase{ { - expr: `(len)`, - invalid: true, + Expression: `(len)`, + Invalid: true, }, { - expr: `(len true)`, - invalid: true, + Expression: `(len true)`, + Invalid: true, }, { - expr: `(len 1)`, - invalid: true, + Expression: `(len 1)`, + Invalid: true, }, { - expr: `(len null)`, - invalid: true, + Expression: `(len null)`, + Invalid: true, }, { - expr: `(len [] [])`, - invalid: true, + Expression: `(len [] [])`, + Invalid: true, }, { - expr: `(len "")`, - expected: int64(0), + Expression: `(len "")`, + Expected: ast.Number{Value: int64(0)}, }, { - expr: `(len " foo ")`, - expected: int64(5), + Expression: `(len " foo ")`, + Expected: ast.Number{Value: int64(5)}, }, { - expr: `(len [])`, - expected: int64(0), + Expression: `(len [])`, + Expected: ast.Number{Value: int64(0)}, }, { - expr: `(len [1 2 3])`, - expected: int64(3), + Expression: `(len [1 2 3])`, + Expected: ast.Number{Value: int64(3)}, }, { - expr: `(len {})`, - expected: int64(0), + Expression: `(len {})`, + Expected: ast.Number{Value: int64(0)}, }, { - expr: `(len {foo "bar" hello "world"})`, - expected: int64(2), + Expression: `(len {foo "bar" hello "world"})`, + Expected: ast.Number{Value: int64(2)}, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestAppendFunction(t *testing.T) { - testcases := []listsTestcase{ + testcases := []testutil.Testcase{ { - expr: `(append)`, - invalid: true, + Expression: `(append)`, + Invalid: true, }, { - expr: `(append [])`, - invalid: true, + Expression: `(append [])`, + Invalid: true, }, { - expr: `(append true 1)`, - invalid: true, + Expression: `(append true 1)`, + Invalid: true, }, { - expr: `(append 1 1)`, - invalid: true, + Expression: `(append 1 1)`, + Invalid: true, }, { - expr: `(append null 1)`, - invalid: true, + Expression: `(append null 1)`, + Invalid: true, }, { - expr: `(append {} 1)`, - invalid: true, + Expression: `(append {} 1)`, + Invalid: true, }, { - expr: `(append {} 1)`, - invalid: true, + Expression: `(append {} 1)`, + Invalid: true, }, { - expr: `(append [] 1)`, - expected: []any{int64(1)}, + Expression: `(append [] 1)`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 1}}}, }, { - expr: `(append [1 2] 3 "foo")`, - expected: []any{int64(1), int64(2), int64(3), "foo"}, + Expression: `(append [1 2] 3 "foo")`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 1}, ast.Number{Value: 2}, ast.Number{Value: 3}, ast.String("foo")}}, }, { - expr: `(append [] [])`, - expected: []any{[]any{}}, + Expression: `(append [] [])`, + Expected: ast.Vector{Data: []any{ast.Vector{Data: []any{}}}}, }, { - expr: `(append [] "foo")`, - expected: []any{"foo"}, + Expression: `(append [] "foo")`, + Expected: ast.Vector{Data: []any{ast.String("foo")}}, }, { - expr: `(append "foo" [])`, - invalid: true, + Expression: `(append "foo" [])`, + Invalid: true, }, { - expr: `(append "foo" "bar" [])`, - invalid: true, + Expression: `(append "foo" "bar" [])`, + Invalid: true, }, { - expr: `(append "foo" "bar" "test")`, - expected: "foobartest", + Expression: `(append "foo" "bar" "test")`, + Expected: ast.String("foobartest"), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestPrependFunction(t *testing.T) { - testcases := []listsTestcase{ + testcases := []testutil.Testcase{ { - expr: `(prepend)`, - invalid: true, + Expression: `(prepend)`, + Invalid: true, }, { - expr: `(prepend [])`, - invalid: true, + Expression: `(prepend [])`, + Invalid: true, }, { - expr: `(prepend true 1)`, - invalid: true, + Expression: `(prepend true 1)`, + Invalid: true, }, { - expr: `(prepend 1 1)`, - invalid: true, + Expression: `(prepend 1 1)`, + Invalid: true, }, { - expr: `(prepend null 1)`, - invalid: true, + Expression: `(prepend null 1)`, + Invalid: true, }, { - expr: `(prepend {} 1)`, - invalid: true, + Expression: `(prepend {} 1)`, + Invalid: true, }, { - expr: `(prepend {} 1)`, - invalid: true, + Expression: `(prepend {} 1)`, + Invalid: true, }, { - expr: `(prepend [] 1)`, - expected: []any{int64(1)}, + Expression: `(prepend [] 1)`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 1}}}, }, { - expr: `(prepend [1] 2)`, - expected: []any{int64(2), int64(1)}, + Expression: `(prepend [1] 2)`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 2}, ast.Number{Value: 1}}}, }, { - expr: `(prepend [1 2] 3 "foo")`, - expected: []any{int64(3), "foo", int64(1), int64(2)}, + Expression: `(prepend [1 2] 3 "foo")`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 3}, ast.String("foo"), ast.Number{Value: 1}, ast.Number{Value: 2}}}, }, { - expr: `(prepend [] [])`, - expected: []any{[]any{}}, + Expression: `(prepend [] [])`, + Expected: ast.Vector{Data: []any{ast.Vector{Data: []any{}}}}, }, { - expr: `(prepend [] "foo")`, - expected: []any{"foo"}, + Expression: `(prepend [] "foo")`, + Expected: ast.Vector{Data: []any{ast.String("foo")}}, }, { - expr: `(prepend "foo" [])`, - invalid: true, + Expression: `(prepend "foo" [])`, + Invalid: true, }, { - expr: `(prepend "foo" "bar" [])`, - invalid: true, + Expression: `(prepend "foo" "bar" [])`, + Invalid: true, }, { - expr: `(prepend "foo" "bar" "test")`, - expected: "bartestfoo", + Expression: `(prepend "foo" "bar" "test")`, + Expected: ast.String("bartestfoo"), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestReverseFunction(t *testing.T) { - testcases := []listsTestcase{ + testcases := []testutil.Testcase{ { - expr: `(reverse)`, - invalid: true, + Expression: `(reverse)`, + Invalid: true, }, { - expr: `(reverse "too" "many")`, - invalid: true, + Expression: `(reverse "too" "many")`, + Invalid: true, }, { - expr: `(reverse 1)`, - invalid: true, + Expression: `(reverse 1)`, + Invalid: true, }, { - expr: `(reverse true)`, - invalid: true, + Expression: `(reverse true)`, + Invalid: true, }, { - expr: `(reverse null)`, - invalid: true, + Expression: `(reverse null)`, + Invalid: true, }, { - expr: `(reverse {})`, - invalid: true, + Expression: `(reverse {})`, + Invalid: true, }, { - expr: `(reverse "")`, - expected: "", + Expression: `(reverse "")`, + Expected: ast.String(""), }, { - expr: `(reverse (concat "" "f" "oo"))`, - expected: "oof", + Expression: `(reverse (concat "" "f" "oo"))`, + Expected: ast.String("oof"), }, { - expr: `(reverse "abcd")`, - expected: "dcba", + Expression: `(reverse "abcd")`, + Expected: ast.String("dcba"), }, { - expr: `(reverse (reverse "abcd"))`, - expected: "abcd", + Expression: `(reverse (reverse "abcd"))`, + Expected: ast.String("abcd"), }, { - expr: `(reverse [])`, - expected: []any{}, + Expression: `(reverse [])`, + Expected: ast.Vector{Data: []any{}}, }, { - expr: `(reverse [1])`, - expected: []any{int64(1)}, + Expression: `(reverse [1])`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 1}}}, }, { - expr: `(reverse [1 2 3])`, - expected: []any{int64(3), int64(2), int64(1)}, + Expression: `(reverse [1 2 3])`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 3}, ast.Number{Value: 2}, ast.Number{Value: 1}}}, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestRangeFunction(t *testing.T) { - testcases := []listsTestcase{ + testcases := []testutil.Testcase{ { // missing everything - expr: `(range)`, - invalid: true, + Expression: `(range)`, + Invalid: true, }, { // missing naming vector - expr: `(range [1 2 3])`, - invalid: true, + Expression: `(range [1 2 3])`, + Invalid: true, }, { // missing naming vector - expr: `(range [1 2 3] (+ 1 2))`, - invalid: true, + Expression: `(range [1 2 3] (+ 1 2))`, + Invalid: true, }, { // naming vector must be 1 or 2 elements long - expr: `(range [1 2 3] [] (+ 1 2))`, - invalid: true, + Expression: `(range [1 2 3] [] (+ 1 2))`, + Invalid: true, }, { // naming vector must be 1 or 2 elements long - expr: `(range [1 2 3] [a b c] (+ 1 2))`, - invalid: true, + Expression: `(range [1 2 3] [a b c] (+ 1 2))`, + Invalid: true, }, { // do not allow numbers in the naming vector - expr: `(range [1 2 3] [1 2] (+ 1 2))`, - invalid: true, + Expression: `(range [1 2 3] [1 2] (+ 1 2))`, + Invalid: true, }, { // do not allow strings in naming vector - expr: `(range [1 2 3] ["foo" "bar"] (+ 1 2))`, - invalid: true, + Expression: `(range [1 2 3] ["foo" "bar"] (+ 1 2))`, + Invalid: true, }, { // cannot range over non-vectors/objects - expr: `(range "invalid" [a] (+ 1 2))`, - invalid: true, + Expression: `(range "invalid" [a] (+ 1 2))`, + Invalid: true, }, { // cannot range over non-vectors/objects - expr: `(range 5 [a] (+ 1 2))`, - invalid: true, + Expression: `(range 5 [a] (+ 1 2))`, + Invalid: true, }, { // single simple expression - expr: `(range [1 2 3] [a] (+ 1 2))`, - expected: int64(3), + Expression: `(range [1 2 3] [a] (+ 1 2))`, + Expected: ast.Number{Value: int64(3)}, }, { // multiple expressions that use a common context - expr: `(range [1 2 3] [a] (set! $foo $a) (+ $foo 3))`, - expected: int64(6), + Expression: `(range [1 2 3] [a] (set! $foo $a) (+ $foo 3))`, + Expected: ast.Number{Value: int64(6)}, }, { // count iterations - expr: `(range [1 2 3] [loop-var] (set! $counter (+ (default (try $counter) 0) 1)))`, - expected: int64(3), + Expression: `(range [1 2 3] [loop-var] (set! $counter (+ (default (try $counter) 0) 1)))`, + Expected: ast.Number{Value: int64(3)}, }, { // value is bound to desired variable - expr: `(range [1 2 3] [a] $a)`, - expected: int64(3), + Expression: `(range [1 2 3] [a] $a)`, + Expected: ast.Number{Value: int64(3)}, }, { // support loop index variable - expr: `(range [1 2 3] [idx var] $idx)`, - expected: int64(2), + Expression: `(range [1 2 3] [idx var] $idx)`, + Expected: ast.Number{Value: int64(2)}, }, { // support loop index variable - expr: `(range [1 2 3] [idx var] $var)`, - expected: int64(3), + Expression: `(range [1 2 3] [idx var] $var)`, + Expected: ast.Number{Value: int64(3)}, }, { // variables do not leak outside the range - expr: `(range [1 2 3] [idx var] $idx) (+ $var 0)`, - invalid: true, + Expression: `(range [1 2 3] [idx var] $idx) (+ $var 0)`, + Invalid: true, }, { // variables do not leak outside the range - expr: `(range [1 2 3] [idx var] $idx) (+ $idx 0)`, - invalid: true, + Expression: `(range [1 2 3] [idx var] $idx) (+ $idx 0)`, + Invalid: true, }, { // support ranging over objects - expr: `(range {} [key value] $key)`, - expected: nil, + Expression: `(range {} [key value] $key)`, + Expected: ast.Null{}, }, { - expr: `(range {foo "bar"} [key value] $key)`, - expected: "foo", + Expression: `(range {foo "bar"} [key value] $key)`, + Expected: ast.String("foo"), }, { - expr: `(range {foo "bar"} [key value] $value)`, - expected: "bar", + Expression: `(range {foo "bar"} [key value] $value)`, + Expected: ast.String("bar"), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestMapFunction(t *testing.T) { - testcases := []listsTestcase{ + testcases := []testutil.Testcase{ { // missing everything - expr: `(map)`, - invalid: true, + Expression: `(map)`, + Invalid: true, }, { // missing function identifier - expr: `(map [1 2 3])`, - invalid: true, + Expression: `(map [1 2 3])`, + Invalid: true, }, { // missing naming vector - expr: `(map [1 2 3] (+ 1 2))`, - invalid: true, + Expression: `(map [1 2 3] (+ 1 2))`, + Invalid: true, }, { // naming vector must be 1 or 2 elements long - expr: `(map [1 2 3] [] (+ 1 2))`, - invalid: true, + Expression: `(map [1 2 3] [] (+ 1 2))`, + Invalid: true, }, { // naming vector must be 1 or 2 elements long - expr: `(map [1 2 3] [a b c] (+ 1 2))`, - invalid: true, + Expression: `(map [1 2 3] [a b c] (+ 1 2))`, + Invalid: true, }, { // do not allow numbers in the naming vector - expr: `(map [1 2 3] [1 2] (+ 1 2))`, - invalid: true, + Expression: `(map [1 2 3] [1 2] (+ 1 2))`, + Invalid: true, }, { // do not allow strings in naming vector - expr: `(map [1 2 3] ["foo" "bar"] (+ 1 2))`, - invalid: true, + Expression: `(map [1 2 3] ["foo" "bar"] (+ 1 2))`, + Invalid: true, }, { // cannot map non-vectors/objects - expr: `(map "invalid" [a] (+ 1 2))`, - invalid: true, + Expression: `(map "invalid" [a] (+ 1 2))`, + Invalid: true, }, { // cannot map non-vectors/objects - expr: `(map 5 [a] (+ 1 2))`, - invalid: true, + Expression: `(map 5 [a] (+ 1 2))`, + Invalid: true, }, { // single simple expression - expr: `(map ["foo" "bar"] to-upper)`, - expected: []any{"FOO", "BAR"}, + Expression: `(map ["foo" "bar"] to-upper)`, + Expected: ast.Vector{Data: []any{ast.String("FOO"), ast.String("BAR")}}, }, { - expr: `(map {foo "bar"} to-upper)`, - expected: map[string]any{"foo": "BAR"}, + Expression: `(map {foo "bar"} to-upper)`, + Expected: ast.Object{Data: map[string]any{"foo": ast.String("BAR")}}, }, { // type safety still applies - expr: `(map [1] to-upper)`, - invalid: true, + Expression: `(map [1] to-upper)`, + Invalid: true, }, { // eval expression with variable - expr: `(map [1 2 3] [val] (+ $val 3))`, - expected: []any{int64(4), int64(5), int64(6)}, + Expression: `(map [1 2 3] [val] (+ $val 3))`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 4}, ast.Number{Value: 5}, ast.Number{Value: 6}}}, }, { // eval with loop index - expr: `(map ["foo" "bar"] [idx _] $idx)`, - expected: []any{int64(0), int64(1)}, + Expression: `(map ["foo" "bar"] [idx _] $idx)`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 0}, ast.Number{Value: 1}}}, }, { // last expression controls the result - expr: `(map [1 2 3] [val] (+ $val 3) "foo")`, - expected: []any{"foo", "foo", "foo"}, + Expression: `(map [1 2 3] [val] (+ $val 3) "foo")`, + Expected: ast.Vector{Data: []any{ast.String("foo"), ast.String("foo"), ast.String("foo")}}, }, { // multiple expressions that use a common context - expr: `(map [1 2 3] [val] (set! $foo $val) (+ $foo 3))`, - expected: []any{int64(4), int64(5), int64(6)}, + Expression: `(map [1 2 3] [val] (set! $foo $val) (+ $foo 3))`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 4}, ast.Number{Value: 5}, ast.Number{Value: 6}}}, }, { // context is even shared across elements - expr: `(map ["foo" "bar"] [_] (set! $counter (+ (try $counter 0) 1)))`, - expected: []any{int64(1), int64(2)}, + Expression: `(map ["foo" "bar"] [_] (set! $counter (+ (try $counter 0) 1)))`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 1}, ast.Number{Value: 2}}}, }, { // variables do not leak outside the range - expr: `(map [1 2 3] [idx var] $idx) (+ $var 0)`, - invalid: true, + Expression: `(map [1 2 3] [idx var] $idx) (+ $var 0)`, + Invalid: true, }, { // variables do not leak outside the range - expr: `(map [1 2 3] [idx var] $idx) (+ $idx 0)`, - invalid: true, + Expression: `(map [1 2 3] [idx var] $idx) (+ $idx 0)`, + Invalid: true, }, // do not modify the source { - expr: `(set! $foo [1 2 3]) (map $foo [_ __] "bar")`, - expected: []any{"bar", "bar", "bar"}, + Expression: `(set! $foo [1 2 3]) (map $foo [_ __] "bar")`, + Expected: ast.Vector{Data: []any{ast.String("bar"), ast.String("bar"), ast.String("bar")}}, }, { - expr: `(set! $foo [1 2 3]) (map $foo [_ __] "bar") $foo`, - expected: []any{int64(1), int64(2), int64(3)}, + Expression: `(set! $foo [1 2 3]) (map $foo [_ __] "bar") $foo`, + Expected: ast.Vector{Data: []any{ast.Number{Value: 1}, ast.Number{Value: 2}, ast.Number{Value: 3}}}, }, { - expr: `(set! $foo {foo "bar"}) (map $foo [_ __] "new-value") $foo`, - expected: map[string]any{"foo": "bar"}, + Expression: `(set! $foo {foo "bar"}) (map $foo [_ __] "new-value") $foo`, + Expected: ast.Object{Data: map[string]any{"foo": ast.String("bar")}}, }, { - expr: `(set! $foo ["foo" "bar"]) (map $foo to-upper)`, - expected: []any{"FOO", "BAR"}, + Expression: `(set! $foo ["foo" "bar"]) (map $foo to-upper)`, + Expected: ast.Vector{Data: []any{ast.String("FOO"), ast.String("BAR")}}, }, { - expr: `(set! $foo ["foo" "bar"]) (map $foo to-upper) $foo`, - expected: []any{"foo", "bar"}, + Expression: `(set! $foo ["foo" "bar"]) (map $foo to-upper) $foo`, + Expected: ast.Vector{Data: []any{ast.String("foo"), ast.String("bar")}}, }, { - expr: `(set! $foo {foo "bar"}) (map $foo to-upper) $foo`, - expected: map[string]any{"foo": "bar"}, + Expression: `(set! $foo {foo "bar"}) (map $foo to-upper) $foo`, + Expected: ast.Object{Data: map[string]any{"foo": ast.String("bar")}}, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/builtin/logic_test.go b/pkg/eval/builtin/logic_test.go index 4c4d434..8b8f381 100644 --- a/pkg/eval/builtin/logic_test.go +++ b/pkg/eval/builtin/logic_test.go @@ -5,234 +5,213 @@ package builtin import ( "testing" -) - -type logicTestcase struct { - expr string - expected any - invalid bool -} - -func (tc *logicTestcase) Test(t *testing.T) { - t.Helper() - - result, err := runExpression(t, tc.expr, nil, nil) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run %s: %v", tc.expr, err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run %s, but got: %v", tc.expr, result) - } - if result != tc.expected { - t.Fatalf("Expected %v (%T), but got %v (%T)", tc.expected, tc.expected, result, result) - } -} + "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" +) func TestAndFunction(t *testing.T) { - testcases := []logicTestcase{ + testcases := []testutil.Testcase{ { - expr: `(and)`, - invalid: true, + Expression: `(and)`, + Invalid: true, }, { - expr: `(and 1)`, - invalid: true, + Expression: `(and 1)`, + Invalid: true, }, { - expr: `(and 1.1)`, - invalid: true, + Expression: `(and 1.1)`, + Invalid: true, }, { - expr: `(and null)`, - invalid: true, + Expression: `(and null)`, + Invalid: true, }, { - expr: `(and "")`, - invalid: true, + Expression: `(and "")`, + Invalid: true, }, { - expr: `(and "nonempty")`, - invalid: true, + Expression: `(and "nonempty")`, + Invalid: true, }, { - expr: `(and {})`, - invalid: true, + Expression: `(and {})`, + Invalid: true, }, { - expr: `(and {foo "bar"})`, - invalid: true, + Expression: `(and {foo "bar"})`, + Invalid: true, }, { - expr: `(and [])`, - invalid: true, + Expression: `(and [])`, + Invalid: true, }, { - expr: `(and ["bar"])`, - invalid: true, + Expression: `(and ["bar"])`, + Invalid: true, }, { - expr: `(and true)`, - expected: true, + Expression: `(and true)`, + Expected: ast.Bool(true), }, { - expr: `(and false)`, - expected: false, + Expression: `(and false)`, + Expected: ast.Bool(false), }, { - expr: `(and true false)`, - expected: false, + Expression: `(and true false)`, + Expected: ast.Bool(false), }, { - expr: `(and true true)`, - expected: true, + Expression: `(and true true)`, + Expected: ast.Bool(true), }, { - expr: `(and (eq? 1 1) true)`, - expected: true, + Expression: `(and (eq? 1 1) true)`, + Expected: ast.Bool(true), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestOrFunction(t *testing.T) { - testcases := []logicTestcase{ + testcases := []testutil.Testcase{ { - expr: `(or)`, - invalid: true, + Expression: `(or)`, + Invalid: true, }, { - expr: `(or 1)`, - invalid: true, + Expression: `(or 1)`, + Invalid: true, }, { - expr: `(or 1.1)`, - invalid: true, + Expression: `(or 1.1)`, + Invalid: true, }, { - expr: `(or null)`, - invalid: true, + Expression: `(or null)`, + Invalid: true, }, { - expr: `(or "")`, - invalid: true, + Expression: `(or "")`, + Invalid: true, }, { - expr: `(or "nonempty")`, - invalid: true, + Expression: `(or "nonempty")`, + Invalid: true, }, { - expr: `(or {})`, - invalid: true, + Expression: `(or {})`, + Invalid: true, }, { - expr: `(or {foo "bar"})`, - invalid: true, + Expression: `(or {foo "bar"})`, + Invalid: true, }, { - expr: `(or [])`, - invalid: true, + Expression: `(or [])`, + Invalid: true, }, { - expr: `(or ["bar"])`, - invalid: true, + Expression: `(or ["bar"])`, + Invalid: true, }, { - expr: `(or true)`, - expected: true, + Expression: `(or true)`, + Expected: ast.Bool(true), }, { - expr: `(or false)`, - expected: false, + Expression: `(or false)`, + Expected: ast.Bool(false), }, { - expr: `(or true false)`, - expected: true, + Expression: `(or true false)`, + Expected: ast.Bool(true), }, { - expr: `(or true true)`, - expected: true, + Expression: `(or true true)`, + Expected: ast.Bool(true), }, { - expr: `(or (eq? 1 1) true)`, - expected: true, + Expression: `(or (eq? 1 1) true)`, + Expected: ast.Bool(true), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestNotFunction(t *testing.T) { - testcases := []logicTestcase{ + testcases := []testutil.Testcase{ { - expr: `(not)`, - invalid: true, + Expression: `(not)`, + Invalid: true, }, { - expr: `(not true true)`, - invalid: true, + Expression: `(not true true)`, + Invalid: true, }, { - expr: `(not 1)`, - invalid: true, + Expression: `(not 1)`, + Invalid: true, }, { - expr: `(not 1.1)`, - invalid: true, + Expression: `(not 1.1)`, + Invalid: true, }, { - expr: `(not null)`, - invalid: true, + Expression: `(not null)`, + Invalid: true, }, { - expr: `(not "")`, - invalid: true, + Expression: `(not "")`, + Invalid: true, }, { - expr: `(not "nonempty")`, - invalid: true, + Expression: `(not "nonempty")`, + Invalid: true, }, { - expr: `(not {})`, - invalid: true, + Expression: `(not {})`, + Invalid: true, }, { - expr: `(not {foo "bar"})`, - invalid: true, + Expression: `(not {foo "bar"})`, + Invalid: true, }, { - expr: `(not [])`, - invalid: true, + Expression: `(not [])`, + Invalid: true, }, { - expr: `(not ["bar"])`, - invalid: true, + Expression: `(not ["bar"])`, + Invalid: true, }, { - expr: `(not false)`, - expected: true, + Expression: `(not false)`, + Expected: ast.Bool(true), }, { - expr: `(not true)`, - expected: false, + Expression: `(not true)`, + Expected: ast.Bool(false), }, { - expr: `(not (not (not (not true))))`, - expected: true, + Expression: `(not (not (not (not true))))`, + Expected: ast.Bool(true), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/builtin/math_test.go b/pkg/eval/builtin/math_test.go index 2d3e4fc..826f1ce 100644 --- a/pkg/eval/builtin/math_test.go +++ b/pkg/eval/builtin/math_test.go @@ -5,239 +5,219 @@ package builtin import ( "testing" -) - -type mathTestcase struct { - expr string - expected any - invalid bool -} - -func (tc *mathTestcase) Test(t *testing.T) { - t.Helper() - - result, err := runExpression(t, tc.expr, nil, nil) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run %s: %v", tc.expr, err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run %s, but got: %v", tc.expr, result) - } - if result != tc.expected { - t.Fatalf("Expected %v (%T), but got %v (%T)", tc.expected, tc.expected, result, result) - } -} + "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" +) func TestSumFunction(t *testing.T) { - testcases := []mathTestcase{ + testcases := []testutil.Testcase{ { - expr: `(+)`, - invalid: true, + Expression: `(+)`, + Invalid: true, }, { - expr: `(+ 1)`, - invalid: true, + Expression: `(+ 1)`, + Invalid: true, }, { - expr: `(+ 1 "1")`, - invalid: true, + Expression: `(+ 1 "1")`, + Invalid: true, }, { - expr: `(+ 1 "foo")`, - invalid: true, + Expression: `(+ 1 "foo")`, + Invalid: true, }, { - expr: `(+ 1 [])`, - invalid: true, + Expression: `(+ 1 [])`, + Invalid: true, }, { - expr: `(+ 1 {})`, - invalid: true, + Expression: `(+ 1 {})`, + Invalid: true, }, { - expr: `(+ 1 2)`, - expected: int64(3), + Expression: `(+ 1 2)`, + Expected: ast.Number{Value: int64(3)}, }, { - expr: `(+ 1 -2 5)`, - expected: int64(4), + Expression: `(+ 1 -2 5)`, + Expected: ast.Number{Value: int64(4)}, }, { - expr: `(+ 1 1.5)`, - expected: float64(2.5), + Expression: `(+ 1 1.5)`, + Expected: ast.Number{Value: float64(2.5)}, }, { - expr: `(+ 1 1.5 (+ 1 2))`, - expected: float64(5.5), + Expression: `(+ 1 1.5 (+ 1 2))`, + Expected: ast.Number{Value: float64(5.5)}, }, { - expr: `(+ 0 0.0 -5.6)`, - expected: float64(-5.6), + Expression: `(+ 0 0.0 -5.6)`, + Expected: ast.Number{Value: float64(-5.6)}, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestMinusFunction(t *testing.T) { - testcases := []mathTestcase{ + testcases := []testutil.Testcase{ { - expr: `(-)`, - invalid: true, + Expression: `(-)`, + Invalid: true, }, { - expr: `(- 1)`, - invalid: true, + Expression: `(- 1)`, + Invalid: true, }, { - expr: `(- 1 "foo")`, - invalid: true, + Expression: `(- 1 "foo")`, + Invalid: true, }, { - expr: `(- 1 [])`, - invalid: true, + Expression: `(- 1 [])`, + Invalid: true, }, { - expr: `(- 1 {})`, - invalid: true, + Expression: `(- 1 {})`, + Invalid: true, }, { - expr: `(- 1 2)`, - expected: int64(-1), + Expression: `(- 1 2)`, + Expected: ast.Number{Value: int64(-1)}, }, { - expr: `(- 1 -2 5)`, - expected: int64(-2), + Expression: `(- 1 -2 5)`, + Expected: ast.Number{Value: int64(-2)}, }, { - expr: `(- 1 1.5)`, - expected: float64(-0.5), + Expression: `(- 1 1.5)`, + Expected: ast.Number{Value: float64(-0.5)}, }, { - expr: `(- 1 1.5 (- 1 2))`, - expected: float64(0.5), + Expression: `(- 1 1.5 (- 1 2))`, + Expected: ast.Number{Value: float64(0.5)}, }, { - expr: `(- 0 0.0 -5.6)`, - expected: float64(5.6), + Expression: `(- 0 0.0 -5.6)`, + Expected: ast.Number{Value: float64(5.6)}, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestMultiplyFunction(t *testing.T) { - testcases := []mathTestcase{ + testcases := []testutil.Testcase{ { - expr: `(*)`, - invalid: true, + Expression: `(*)`, + Invalid: true, }, { - expr: `(* 1)`, - invalid: true, + Expression: `(* 1)`, + Invalid: true, }, { - expr: `(* 1 "foo")`, - invalid: true, + Expression: `(* 1 "foo")`, + Invalid: true, }, { - expr: `(* 1 [])`, - invalid: true, + Expression: `(* 1 [])`, + Invalid: true, }, { - expr: `(* 1 {})`, - invalid: true, + Expression: `(* 1 {})`, + Invalid: true, }, { - expr: `(* 1 2)`, - expected: int64(2), + Expression: `(* 1 2)`, + Expected: ast.Number{Value: int64(2)}, }, { - expr: `(* 1 -2 5)`, - expected: int64(-10), + Expression: `(* 1 -2 5)`, + Expected: ast.Number{Value: int64(-10)}, }, { - expr: `(* 2 -1.5)`, - expected: float64(-3.0), + Expression: `(* 2 -1.5)`, + Expected: ast.Number{Value: float64(-3.0)}, }, { - expr: `(* 1 1.5 (* 1 2))`, - expected: float64(3.0), + Expression: `(* 1 1.5 (* 1 2))`, + Expected: ast.Number{Value: float64(3.0)}, }, { - expr: `(* 0 0.0 -5.6)`, - expected: float64(0), + Expression: `(* 0 0.0 -5.6)`, + Expected: ast.Number{Value: float64(0)}, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestDivideFunction(t *testing.T) { - testcases := []mathTestcase{ + testcases := []testutil.Testcase{ { - expr: `(/)`, - invalid: true, + Expression: `(/)`, + Invalid: true, }, { - expr: `(/ 1)`, - invalid: true, + Expression: `(/ 1)`, + Invalid: true, }, { - expr: `(/ 1 "foo")`, - invalid: true, + Expression: `(/ 1 "foo")`, + Invalid: true, }, { - expr: `(/ 1 [])`, - invalid: true, + Expression: `(/ 1 [])`, + Invalid: true, }, { - expr: `(/ 1 {})`, - invalid: true, + Expression: `(/ 1 {})`, + Invalid: true, }, { - expr: `(/ 1 2)`, - expected: float64(0.5), + Expression: `(/ 1 2)`, + Expected: ast.Number{Value: float64(0.5)}, }, { - expr: `(/ 1 -2 5)`, - expected: float64(-0.1), + Expression: `(/ 1 -2 5)`, + Expected: ast.Number{Value: float64(-0.1)}, }, { - expr: `(/ 2 -1.5)`, - expected: float64(-1.33333333333333333333), + Expression: `(/ 2 -1.5)`, + Expected: ast.Number{Value: float64(-1.33333333333333333333)}, }, { - expr: `(/ 1 1.5 (/ 1 2))`, - expected: float64(1.33333333333333333333), + Expression: `(/ 1 1.5 (/ 1 2))`, + Expected: ast.Number{Value: float64(1.33333333333333333333)}, }, { - expr: `(/ 0 0.0 -5.6)`, - invalid: true, + Expression: `(/ 0 0.0 -5.6)`, + Invalid: true, }, { - expr: `(/ 1 0)`, - invalid: true, + Expression: `(/ 1 0)`, + Invalid: true, }, { - expr: `(/ 1 2 0.0)`, - invalid: true, + Expression: `(/ 1 2 0.0)`, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/builtin/strings_test.go b/pkg/eval/builtin/strings_test.go index 7a7fca1..da9840a 100644 --- a/pkg/eval/builtin/strings_test.go +++ b/pkg/eval/builtin/strings_test.go @@ -6,232 +6,220 @@ package builtin import ( "testing" - "github.com/google/go-cmp/cmp" + "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) -type stringsTestcase struct { - expr string - expected any - invalid bool -} - -func (tc *stringsTestcase) Test(t *testing.T) { - t.Helper() - - result, err := runExpression(t, tc.expr, nil, nil) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run %s: %v", tc.expr, err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run %s, but got: %v", tc.expr, result) - } - - if !cmp.Equal(result, tc.expected) { - t.Fatalf("Did not receive expected output:\n%s", cmp.Diff(tc.expected, result)) - } -} - func TestConcatFunction(t *testing.T) { - testcases := []stringsTestcase{ + testcases := []testutil.Testcase{ { - expr: `(concat)`, - invalid: true, + Expression: `(concat)`, + Invalid: true, }, { - expr: `(concat "foo")`, - invalid: true, + Expression: `(concat "foo")`, + Invalid: true, }, { - expr: `(concat [] "foo")`, - invalid: true, + Expression: `(concat [] "foo")`, + Invalid: true, }, { - expr: `(concat {} "foo")`, - invalid: true, + Expression: `(concat {} "foo")`, + Invalid: true, }, { - expr: `(concat "g" {})`, - invalid: true, + Expression: `(concat "g" {})`, + Invalid: true, }, { - expr: `(concat "g" [{}])`, - invalid: true, + Expression: `(concat "g" [{}])`, + Invalid: true, }, { - expr: `(concat "g" [["foo"]])`, - invalid: true, + Expression: `(concat "g" [["foo"]])`, + Invalid: true, }, { - expr: `(concat "-" "foo" 1)`, - invalid: true, + Expression: `(concat "-" "foo" 1)`, + Invalid: true, }, { - expr: `(concat true "foo" "bar")`, - invalid: true, + Expression: `(concat true "foo" "bar")`, + Invalid: true, }, { - expr: `(concat "g" "foo")`, - expected: "foo", + Expression: `(concat "g" "foo")`, + Expected: ast.String("foo"), }, { - expr: `(concat "-" "foo" "bar" "test")`, - expected: "foo-bar-test", + Expression: `(concat "-" "foo" "bar" "test")`, + Expected: ast.String("foo-bar-test"), }, { - expr: `(concat "" "foo" "bar")`, - expected: "foobar", + Expression: `(concat "" "foo" "bar")`, + Expected: ast.String("foobar"), }, { - expr: `(concat "" ["foo" "bar"])`, - expected: "foobar", + Expression: `(concat "" ["foo" "bar"])`, + Expected: ast.String("foobar"), }, { - expr: `(concat "-" ["foo" "bar"] "test" ["suffix"])`, - expected: "foo-bar-test-suffix", + Expression: `(concat "-" ["foo" "bar"] "test" ["suffix"])`, + Expected: ast.String("foo-bar-test-suffix"), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestSplitFunction(t *testing.T) { - testcases := []stringsTestcase{ + testcases := []testutil.Testcase{ { - expr: `(split)`, - invalid: true, + Expression: `(split)`, + Invalid: true, }, { - expr: `(split "foo")`, - invalid: true, + Expression: `(split "foo")`, + Invalid: true, }, { - expr: `(split [] "foo")`, - invalid: true, + Expression: `(split [] "foo")`, + Invalid: true, }, { - expr: `(split {} "foo")`, - invalid: true, + Expression: `(split {} "foo")`, + Invalid: true, }, { - expr: `(split "g" {})`, - invalid: true, + Expression: `(split "g" {})`, + Invalid: true, }, { - expr: `(split "g" [{}])`, - invalid: true, + Expression: `(split "g" [{}])`, + Invalid: true, }, { - expr: `(split "" "")`, - expected: []any{}, + Expression: `(split "" "")`, + Expected: ast.Vector{ + Data: []any{}, + }, }, { - expr: `(split "g" "")`, - expected: []any{""}, + Expression: `(split "g" "")`, + Expected: ast.Vector{ + Data: []any{ast.String("")}, + }, }, { - expr: `(split "g" "foo")`, - expected: []any{"foo"}, + Expression: `(split "g" "foo")`, + Expected: ast.Vector{ + Data: []any{ast.String("foo")}, + }, }, { - expr: `(split "-" "foo-bar-test-")`, - expected: []any{"foo", "bar", "test", ""}, + Expression: `(split "-" "foo-bar-test-")`, + Expected: ast.Vector{ + Data: []any{ast.String("foo"), ast.String("bar"), ast.String("test"), ast.String("")}, + }, }, { - expr: `(split "" "foobar")`, - expected: []any{"f", "o", "o", "b", "a", "r"}, + Expression: `(split "" "foobar")`, + Expected: ast.Vector{ + Data: []any{ast.String("f"), ast.String("o"), ast.String("o"), ast.String("b"), ast.String("a"), ast.String("r")}, + }, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestToUpperFunction(t *testing.T) { - testcases := []stringsTestcase{ + testcases := []testutil.Testcase{ { - expr: `(to-upper)`, - invalid: true, + Expression: `(to-upper)`, + Invalid: true, }, { - expr: `(to-upper "too" "many")`, - invalid: true, + Expression: `(to-upper "too" "many")`, + Invalid: true, }, { - expr: `(to-upper true)`, - invalid: true, + Expression: `(to-upper true)`, + Invalid: true, }, { - expr: `(to-upper [])`, - invalid: true, + Expression: `(to-upper [])`, + Invalid: true, }, { - expr: `(to-upper {})`, - invalid: true, + Expression: `(to-upper {})`, + Invalid: true, }, { - expr: `(to-upper "")`, - expected: "", + Expression: `(to-upper "")`, + Expected: ast.String(""), }, { - expr: `(to-upper " TeSt ")`, - expected: " TEST ", + Expression: `(to-upper " TeSt ")`, + Expected: ast.String(" TEST "), }, { - expr: `(to-upper " test ")`, - expected: " TEST ", + Expression: `(to-upper " test ")`, + Expected: ast.String(" TEST "), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestToLowerFunction(t *testing.T) { - testcases := []stringsTestcase{ + testcases := []testutil.Testcase{ { - expr: `(to-lower)`, - invalid: true, + Expression: `(to-lower)`, + Invalid: true, }, { - expr: `(to-lower "too" "many")`, - invalid: true, + Expression: `(to-lower "too" "many")`, + Invalid: true, }, { - expr: `(to-lower true)`, - invalid: true, + Expression: `(to-lower true)`, + Invalid: true, }, { - expr: `(to-lower [])`, - invalid: true, + Expression: `(to-lower [])`, + Invalid: true, }, { - expr: `(to-lower {})`, - invalid: true, + Expression: `(to-lower {})`, + Invalid: true, }, { - expr: `(to-lower "")`, - expected: "", + Expression: `(to-lower "")`, + Expected: ast.String(""), }, { - expr: `(to-lower " TeSt ")`, - expected: " test ", + Expression: `(to-lower " TeSt ")`, + Expected: ast.String(" test "), }, { - expr: `(to-lower " TEST ")`, - expected: " test ", + Expression: `(to-lower " TEST ")`, + Expected: ast.String(" test "), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/builtin/types_test.go b/pkg/eval/builtin/types_test.go index f4a9086..8b0f326 100644 --- a/pkg/eval/builtin/types_test.go +++ b/pkg/eval/builtin/types_test.go @@ -5,340 +5,321 @@ package builtin import ( "testing" -) - -type typesTestcase struct { - expr string - expected any - invalid bool -} - -func (tc *typesTestcase) Test(t *testing.T) { - t.Helper() - - result, err := runExpression(t, tc.expr, nil, nil) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run %s: %v", tc.expr, err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run %s, but got: %v", tc.expr, result) - } - if result != tc.expected { - t.Fatalf("Expected %v (%T), but got %v (%T)", tc.expected, tc.expected, result, result) - } -} + "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" +) func TestToStringFunction(t *testing.T) { - testcases := []typesTestcase{ + testcases := []testutil.Testcase{ { - expr: `(to-string)`, - invalid: true, + Expression: `(to-string)`, + Invalid: true, }, { - expr: `(to-string "too" "many")`, - invalid: true, + Expression: `(to-string "too" "many")`, + Invalid: true, }, { - expr: `(to-string "foo")`, - expected: "foo", + Expression: `(to-string "foo")`, + Expected: ast.String("foo"), }, { - expr: `(to-string 1)`, - expected: "1", + Expression: `(to-string 1)`, + Expected: ast.String("1"), }, { - expr: `(to-string (+ 1 3))`, - expected: "4", + Expression: `(to-string (+ 1 3))`, + Expected: ast.String("4"), }, { - expr: `(to-string 1.5)`, - expected: "1.5", + Expression: `(to-string 1.5)`, + Expected: ast.String("1.5"), }, { - expr: `(to-string 1e3)`, - expected: "1000", + Expression: `(to-string 1e3)`, + Expected: ast.String("1000"), }, { - expr: `(to-string true)`, - expected: "true", + Expression: `(to-string true)`, + Expected: ast.String("true"), }, { - expr: `(to-string null)`, - expected: "null", + Expression: `(to-string null)`, + Expected: ast.String("null"), }, { - expr: `(to-string [])`, - invalid: true, + Expression: `(to-string [])`, + Invalid: true, }, { - expr: `(to-string {})`, - invalid: true, + Expression: `(to-string {})`, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestToIntFunction(t *testing.T) { - testcases := []typesTestcase{ + testcases := []testutil.Testcase{ { - expr: `(to-int)`, - invalid: true, + Expression: `(to-int)`, + Invalid: true, }, { - expr: `(to-int "too" "many")`, - invalid: true, + Expression: `(to-int "too" "many")`, + Invalid: true, }, { - expr: `(to-int 1)`, - expected: int64(1), + Expression: `(to-int 1)`, + Expected: ast.Number{Value: int64(1)}, }, { - expr: `(to-int "42")`, - expected: int64(42), + Expression: `(to-int "42")`, + Expected: ast.Number{Value: int64(42)}, }, { - expr: `(to-int (+ 1 3))`, - expected: int64(4), + Expression: `(to-int (+ 1 3))`, + Expected: ast.Number{Value: int64(4)}, }, { - expr: `(to-int 1.5)`, - invalid: true, // should this be allowed? + Expression: `(to-int 1.5)`, + Invalid: true, }, { - expr: `(to-int "1.5")`, - invalid: true, // should this be allowed? + Expression: `(to-int "1.5")`, + Invalid: true, }, { - expr: `(to-int true)`, - expected: int64(1), + Expression: `(to-int true)`, + Expected: ast.Number{Value: int64(1)}, }, { - expr: `(to-int false)`, - expected: int64(0), + Expression: `(to-int false)`, + Expected: ast.Number{Value: int64(0)}, }, { - expr: `(to-int null)`, - expected: int64(0), + Expression: `(to-int null)`, + Expected: ast.Number{Value: int64(0)}, }, { - expr: `(to-int [])`, - invalid: true, + Expression: `(to-int [])`, + Invalid: true, }, { - expr: `(to-int {})`, - invalid: true, + Expression: `(to-int {})`, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestToFloatFunction(t *testing.T) { - testcases := []typesTestcase{ + testcases := []testutil.Testcase{ { - expr: `(to-float)`, - invalid: true, + Expression: `(to-float)`, + Invalid: true, }, { - expr: `(to-float "too" "many")`, - invalid: true, + Expression: `(to-float "too" "many")`, + Invalid: true, }, { - expr: `(to-float 1)`, - expected: float64(1), + Expression: `(to-float 1)`, + Expected: ast.Number{Value: float64(1)}, }, { - expr: `(to-float (+ 1 3))`, - expected: float64(4), + Expression: `(to-float (+ 1 3))`, + Expected: ast.Number{Value: float64(4)}, }, { - expr: `(to-float 1.5)`, - expected: float64(1.5), + Expression: `(to-float 1.5)`, + Expected: ast.Number{Value: float64(1.5)}, }, { - expr: `(to-float "3")`, - expected: float64(3), + Expression: `(to-float "3")`, + Expected: ast.Number{Value: float64(3)}, }, { - expr: `(to-float "1.5")`, - expected: float64(1.5), + Expression: `(to-float "1.5")`, + Expected: ast.Number{Value: float64(1.5)}, }, { - expr: `(to-float true)`, - expected: float64(1), + Expression: `(to-float true)`, + Expected: ast.Number{Value: float64(1)}, }, { - expr: `(to-float false)`, - expected: float64(0), + Expression: `(to-float false)`, + Expected: ast.Number{Value: float64(0)}, }, { - expr: `(to-float null)`, - expected: float64(0), + Expression: `(to-float null)`, + Expected: ast.Number{Value: float64(0)}, }, { - expr: `(to-float [])`, - invalid: true, + Expression: `(to-float [])`, + Invalid: true, }, { - expr: `(to-float {})`, - invalid: true, + Expression: `(to-float {})`, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestToBoolFunction(t *testing.T) { - testcases := []typesTestcase{ + testcases := []testutil.Testcase{ { - expr: `(to-bool)`, - invalid: true, + Expression: `(to-bool)`, + Invalid: true, }, { - expr: `(to-bool "too" "many")`, - invalid: true, + Expression: `(to-bool "too" "many")`, + Invalid: true, }, { - expr: `(to-bool 1)`, - expected: true, + Expression: `(to-bool 1)`, + Expected: ast.Bool(true), }, { - expr: `(to-bool 0)`, - expected: false, + Expression: `(to-bool 0)`, + Expected: ast.Bool(false), }, { - expr: `(to-bool (+ 1 3))`, - expected: true, + Expression: `(to-bool (+ 1 3))`, + Expected: ast.Bool(true), }, { - expr: `(to-bool 1.5)`, - expected: true, + Expression: `(to-bool 1.5)`, + Expected: ast.Bool(true), }, { - expr: `(to-bool 0.0)`, - expected: false, + Expression: `(to-bool 0.0)`, + Expected: ast.Bool(false), }, { - expr: `(to-bool "3")`, - expected: true, + Expression: `(to-bool "3")`, + Expected: ast.Bool(true), }, { - expr: `(to-bool true)`, - expected: true, + Expression: `(to-bool true)`, + Expected: ast.Bool(true), }, { - expr: `(to-bool false)`, - expected: false, + Expression: `(to-bool false)`, + Expected: ast.Bool(false), }, { - expr: `(to-bool null)`, - expected: false, + Expression: `(to-bool null)`, + Expected: ast.Bool(false), }, { - expr: `(to-bool [])`, - expected: false, + Expression: `(to-bool [])`, + Expected: ast.Bool(false), }, { - expr: `(to-bool [0])`, - expected: true, + Expression: `(to-bool [0])`, + Expected: ast.Bool(true), }, { - expr: `(to-bool {})`, - expected: false, + Expression: `(to-bool {})`, + Expected: ast.Bool(false), }, { - expr: `(to-bool {foo "bar"})`, - expected: true, + Expression: `(to-bool {foo "bar"})`, + Expected: ast.Bool(true), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } func TestTypeOfFunction(t *testing.T) { - testcases := []typesTestcase{ + testcases := []testutil.Testcase{ { - expr: `(type-of)`, - invalid: true, + Expression: `(type-of)`, + Invalid: true, }, { - expr: `(type-of "too" "many")`, - invalid: true, + Expression: `(type-of "too" "many")`, + Invalid: true, }, { - expr: `(type-of 1)`, - expected: "number", + Expression: `(type-of 1)`, + Expected: ast.String("number"), }, { - expr: `(type-of 0)`, - expected: "number", + Expression: `(type-of 0)`, + Expected: ast.String("number"), }, { - expr: `(type-of (+ 1 3))`, - expected: "number", + Expression: `(type-of (+ 1 3))`, + Expected: ast.String("number"), }, { - expr: `(type-of 1.5)`, - expected: "number", + Expression: `(type-of 1.5)`, + Expected: ast.String("number"), }, { - expr: `(type-of 0.0)`, - expected: "number", + Expression: `(type-of 0.0)`, + Expected: ast.String("number"), }, { - expr: `(type-of "3")`, - expected: "string", + Expression: `(type-of "3")`, + Expected: ast.String("string"), }, { - expr: `(type-of true)`, - expected: "bool", + Expression: `(type-of true)`, + Expected: ast.String("bool"), }, { - expr: `(type-of false)`, - expected: "bool", + Expression: `(type-of false)`, + Expected: ast.String("bool"), }, { - expr: `(type-of null)`, - expected: "null", + Expression: `(type-of null)`, + Expected: ast.String("null"), }, { - expr: `(type-of [])`, - expected: "vector", + Expression: `(type-of [])`, + Expected: ast.String("vector"), }, { - expr: `(type-of (append [] "test"))`, - expected: "vector", + Expression: `(type-of (append [] "test"))`, + Expected: ast.String("vector"), }, { - expr: `(type-of [0])`, - expected: "vector", + Expression: `(type-of [0])`, + Expected: ast.String("vector"), }, { - expr: `(type-of {})`, - expected: "object", + Expression: `(type-of {})`, + Expected: ast.String("object"), }, { - expr: `(type-of {foo "bar"})`, - expected: "object", + Expression: `(type-of {foo "bar"})`, + Expected: ast.String("object"), }, } for _, testcase := range testcases { - t.Run(testcase.expr, testcase.Test) + testcase.Functions = Functions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/builtin/util_test.go b/pkg/eval/builtin/util_test.go deleted file mode 100644 index 3c5bb81..0000000 --- a/pkg/eval/builtin/util_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Christoph Mewes -// SPDX-License-Identifier: MIT - -package builtin - -import ( - "log" - "strings" - "testing" - - "go.xrstf.de/rudi/pkg/eval" - "go.xrstf.de/rudi/pkg/eval/types" - "go.xrstf.de/rudi/pkg/lang/ast" - "go.xrstf.de/rudi/pkg/lang/parser" -) - -func runExpression(t *testing.T, expr string, document any, variables types.Variables) (any, error) { - prog := strings.NewReader(expr) - - got, err := parser.ParseReader("test.go", prog) - if err != nil { - t.Fatalf("Failed to parse %s: %v", expr, err) - } - - program, ok := got.(ast.Program) - if !ok { - t.Fatalf("Parsed result is not a ast.Program, but %T", got) - } - - doc, err := eval.NewDocument(document) - if err != nil { - log.Fatalf("Failed to create parser document: %v", err) - } - - progContext := eval.NewContext(doc, variables, Functions) - - _, result, err := eval.Run(progContext, &program) - - return result, err -} diff --git a/pkg/eval/eval_tuple.go b/pkg/eval/eval_tuple.go index 3e72c41..e180990 100644 --- a/pkg/eval/eval_tuple.go +++ b/pkg/eval/eval_tuple.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" + "go.xrstf.de/rudi/pkg/deepcopy" "go.xrstf.de/rudi/pkg/eval/types" "go.xrstf.de/rudi/pkg/lang/ast" "go.xrstf.de/rudi/pkg/pathexpr" @@ -109,6 +110,11 @@ func EvalFunctionCall(ctx types.Context, fun ast.Identifier, args []ast.Expressi currentValue = ctx.GetDocument().Data() } + currentValue, err = deepcopy.Clone(currentValue) + if err != nil { + return ctx, nil, err + } + // apply the path expression updatedValue, err = pathexpr.Set(currentValue, pathexpr.FromEvaluatedPath(*pathExpr), result) if err != nil { diff --git a/pkg/eval/test/bool_test.go b/pkg/eval/test/bool_test.go index 36697e2..90afba9 100644 --- a/pkg/eval/test/bool_test.go +++ b/pkg/eval/test/bool_test.go @@ -6,47 +6,24 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalBool(t *testing.T) { - testcases := []struct { - input ast.Bool - expected ast.Bool - }{ + testcases := []testutil.Testcase{ { - input: ast.Bool(true), - expected: ast.Bool(true), + AST: ast.Bool(true), + Expected: ast.Bool(true), }, { - input: ast.Bool(false), - expected: ast.Bool(false), + AST: ast.Bool(false), + Expected: ast.Bool(false), }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, nil) - - _, value, err := eval.EvalBool(ctx, testcase.input) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - returned, ok := value.(ast.Bool) - if !ok { - t.Fatalf("EvalBool returned unexpected type %T", value) - } - - if !returned.Equal(testcase.expected) { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/expression_test.go b/pkg/eval/test/expression_test.go index 4e0dafa..d26ac0a 100644 --- a/pkg/eval/test/expression_test.go +++ b/pkg/eval/test/expression_test.go @@ -6,39 +6,34 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/equality" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalExpression(t *testing.T) { - testcases := []struct { - input ast.Expression - expected ast.Literal - invalid bool - }{ + testcases := []testutil.Testcase{ { - input: ast.Null{}, - expected: ast.Null{}, + AST: ast.Null{}, + Expected: ast.Null{}, }, { - input: ast.Bool(true), - expected: ast.Bool(true), + AST: ast.Bool(true), + Expected: ast.Bool(true), }, { - input: ast.String("foo"), - expected: ast.String("foo"), + AST: ast.String("foo"), + Expected: ast.String("foo"), }, { - input: ast.Number{Value: 1}, - expected: ast.Number{Value: 1}, + AST: ast.Number{Value: 1}, + Expected: ast.Number{Value: 1}, }, { - input: ast.Object{Data: map[string]any{"foo": "bar"}}, - expected: ast.Object{Data: map[string]any{"foo": "bar"}}, + AST: ast.Object{Data: map[string]any{"foo": "bar"}}, + Expected: ast.Object{Data: map[string]any{"foo": "bar"}}, }, { - input: ast.ObjectNode{ + AST: ast.ObjectNode{ Data: []ast.KeyValuePair{ { Key: ast.Identifier{Name: "foo"}, @@ -46,58 +41,25 @@ func TestEvalExpression(t *testing.T) { }, }, }, - expected: ast.Object{Data: map[string]any{"foo": "bar"}}, + Expected: ast.Object{Data: map[string]any{"foo": "bar"}}, }, { - input: ast.Vector{Data: []any{"foo", 1}}, - expected: ast.Vector{Data: []any{"foo", 1}}, + AST: ast.Vector{Data: []any{"foo", 1}}, + Expected: ast.Vector{Data: []any{"foo", 1}}, }, { - input: ast.VectorNode{ + AST: ast.VectorNode{ Expressions: []ast.Expression{ ast.String("foo"), ast.Number{Value: 1}, }, }, - expected: ast.Vector{Data: []any{"foo", 1}}, + Expected: ast.Vector{Data: []any{"foo", 1}}, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, dummyFunctions) - - _, value, err := eval.EvalExpression(ctx, testcase.input) - if err != nil { - if !testcase.invalid { - t.Fatalf("Failed to run: %v", err) - } - - return - } - - if testcase.invalid { - t.Fatalf("Should not have been able to run, but got: %v (%T)", value, value) - } - - returned, ok := value.(ast.Literal) - if !ok { - t.Fatalf("EvalExpression returned unexpected type %T", value) - } - - equal, err := equality.StrictEqual(testcase.expected, returned) - if err != nil { - t.Fatalf("Could not compare result: %v", err) - } - - if !equal { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/identifier_test.go b/pkg/eval/test/identifier_test.go index cf5e9fe..90404cb 100644 --- a/pkg/eval/test/identifier_test.go +++ b/pkg/eval/test/identifier_test.go @@ -6,35 +6,24 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalIdentifier(t *testing.T) { - testcases := []struct { - input ast.Identifier - }{ + testcases := []testutil.Testcase{ { - input: ast.Identifier{Name: "foo"}, + AST: ast.Identifier{Name: "foo"}, + Invalid: true, }, { - input: ast.Identifier{Name: "foo", Bang: true}, + AST: ast.Identifier{Name: "foo", Bang: true}, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, nil) - - _, value, err := eval.EvalIdentifier(ctx, testcase.input) - if err == nil { - t.Fatalf("Evaluating identifiers should result in an error, but got %v.", value) - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/null_test.go b/pkg/eval/test/null_test.go index 10a5c39..49876f0 100644 --- a/pkg/eval/test/null_test.go +++ b/pkg/eval/test/null_test.go @@ -6,43 +6,20 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalNull(t *testing.T) { - testcases := []struct { - input ast.Null - expected ast.Null - }{ + testcases := []testutil.Testcase{ { - input: ast.Null{}, - expected: ast.Null{}, + AST: ast.Null{}, + Expected: ast.Null{}, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, nil) - - _, value, err := eval.EvalNull(ctx, testcase.input) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - returned, ok := value.(ast.Null) - if !ok { - t.Fatalf("EvalNull returned unexpected type %T", value) - } - - if !returned.Equal(testcase.expected) { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/number_test.go b/pkg/eval/test/number_test.go index 2f87b5c..a321784 100644 --- a/pkg/eval/test/number_test.go +++ b/pkg/eval/test/number_test.go @@ -6,51 +6,28 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalNumber(t *testing.T) { - testcases := []struct { - input ast.Number - expected ast.Number - }{ + testcases := []testutil.Testcase{ { - input: ast.Number{Value: 0}, - expected: ast.Number{Value: 0}, + AST: ast.Number{Value: 0}, + Expected: ast.Number{Value: 0}, }, { - input: ast.Number{Value: 1}, - expected: ast.Number{Value: 1}, + AST: ast.Number{Value: 1}, + Expected: ast.Number{Value: 1}, }, { - input: ast.Number{Value: 3.14}, - expected: ast.Number{Value: 3.14}, + AST: ast.Number{Value: 3.14}, + Expected: ast.Number{Value: 3.14}, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, nil) - - _, value, err := eval.EvalNumber(ctx, testcase.input) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - returned, ok := value.(ast.Number) - if !ok { - t.Fatalf("EvalNumber returned unexpected type %T", value) - } - - if !returned.Equal(testcase.expected) { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/object_test.go b/pkg/eval/test/object_test.go index 474325e..44283ca 100644 --- a/pkg/eval/test/object_test.go +++ b/pkg/eval/test/object_test.go @@ -6,25 +6,20 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/equality" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalObjectNode(t *testing.T) { - testcases := []struct { - input ast.ObjectNode - expected ast.Literal - invalid bool - }{ + testcases := []testutil.Testcase{ // {} { - input: ast.ObjectNode{}, - expected: ast.Object{}, + AST: ast.ObjectNode{}, + Expected: ast.Object{}, }, // {foo "bar"} { - input: ast.ObjectNode{ + AST: ast.ObjectNode{ Data: []ast.KeyValuePair{ { Key: ast.Identifier{Name: "foo"}, @@ -32,7 +27,7 @@ func TestEvalObjectNode(t *testing.T) { }, }, }, - expected: ast.Object{ + Expected: ast.Object{ Data: map[string]any{ "foo": ast.String("bar"), }, @@ -40,7 +35,7 @@ func TestEvalObjectNode(t *testing.T) { }, // {(eval "evaled") (eval "also evaled")} { - input: ast.ObjectNode{ + AST: ast.ObjectNode{ Data: []ast.KeyValuePair{ { Key: ast.Tuple{ @@ -58,7 +53,7 @@ func TestEvalObjectNode(t *testing.T) { }, }, }, - expected: ast.Object{ + Expected: ast.Object{ Data: map[string]any{ "evaled": ast.String("also evaled"), }, @@ -66,7 +61,7 @@ func TestEvalObjectNode(t *testing.T) { }, // {foo "bar"}.foo { - input: ast.ObjectNode{ + AST: ast.ObjectNode{ Data: []ast.KeyValuePair{ { Key: ast.Identifier{Name: "foo"}, @@ -79,11 +74,11 @@ func TestEvalObjectNode(t *testing.T) { }, }, }, - expected: ast.String("bar"), + Expected: ast.String("bar"), }, // {foo bar} { - input: ast.ObjectNode{ + AST: ast.ObjectNode{ Data: []ast.KeyValuePair{ { Key: ast.Identifier{Name: "foo"}, @@ -91,11 +86,11 @@ func TestEvalObjectNode(t *testing.T) { }, }, }, - invalid: true, + Invalid: true, }, // {true "bar"} { - input: ast.ObjectNode{ + AST: ast.ObjectNode{ Data: []ast.KeyValuePair{ { Key: ast.Bool(true), @@ -103,11 +98,11 @@ func TestEvalObjectNode(t *testing.T) { }, }, }, - invalid: true, + Invalid: true, }, // {null "bar"} { - input: ast.ObjectNode{ + AST: ast.ObjectNode{ Data: []ast.KeyValuePair{ { Key: ast.Null{}, @@ -115,11 +110,11 @@ func TestEvalObjectNode(t *testing.T) { }, }, }, - invalid: true, + Invalid: true, }, // {1 "bar"} { - input: ast.ObjectNode{ + AST: ast.ObjectNode{ Data: []ast.KeyValuePair{ { Key: ast.Number{Value: 1}, @@ -127,45 +122,12 @@ func TestEvalObjectNode(t *testing.T) { }, }, }, - invalid: true, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, dummyFunctions) - - _, value, err := eval.EvalObjectNode(ctx, testcase.input) - if err != nil { - if !testcase.invalid { - t.Fatalf("Failed to run: %v", err) - } - - return - } - - if testcase.invalid { - t.Fatalf("Should not have been able to run, but got: %v (%T)", value, value) - } - - returned, ok := value.(ast.Literal) - if !ok { - t.Fatalf("EvalObjectNode returned unexpected type %T", value) - } - - equal, err := equality.StrictEqual(testcase.expected, returned) - if err != nil { - t.Fatalf("Could not compare result: %v", err) - } - - if !equal { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/program_test.go b/pkg/eval/test/program_test.go index b2099d9..6eb1371 100644 --- a/pkg/eval/test/program_test.go +++ b/pkg/eval/test/program_test.go @@ -6,9 +6,8 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/equality" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func makeProgram(exprs ...ast.Expression) ast.Program { @@ -39,37 +38,33 @@ func makeVar(name string, pathExpr *ast.PathExpression) ast.Symbol { } func TestEvalProgram(t *testing.T) { - testcases := []struct { - input ast.Program - expected ast.Literal - invalid bool - }{ + testcases := []testutil.Testcase{ // (empty program) { - input: makeProgram(), - expected: ast.Null{}, + AST: makeProgram(), + Expected: ast.Null{}, }, // single statement // "foo" { - input: makeProgram( + AST: makeProgram( ast.String("foo"), ), - expected: ast.String("foo"), + Expected: ast.String("foo"), }, // program result should be the result from the last statement // "foo" "bar" { - input: makeProgram( + AST: makeProgram( ast.String("foo"), ast.String("bar"), ), - expected: ast.String("bar"), + Expected: ast.String("bar"), }, // context changes from one statement should affect the next // (set! $foo 1) $foo (set! $bar $foo) $bar { - input: makeProgram( + AST: makeProgram( makeTuple( ast.Identifier{Name: "set", Bang: true}, makeVar("foo", nil), @@ -83,12 +78,12 @@ func TestEvalProgram(t *testing.T) { ), makeVar("bar", nil), ), - expected: ast.Number{Value: 1}, + Expected: ast.Number{Value: 1}, }, // context changes from inner statements should not leak // (set! $foo (set! $bar 1)) $bar { - input: makeProgram( + AST: makeProgram( makeTuple( ast.Identifier{Name: "set", Bang: true}, makeVar("foo", nil), @@ -100,45 +95,12 @@ func TestEvalProgram(t *testing.T) { ), makeVar("bar", nil), ), - invalid: true, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, dummyFunctions) - - _, value, err := eval.EvalProgram(ctx, &testcase.input) - if err != nil { - if !testcase.invalid { - t.Fatalf("Failed to run: %v", err) - } - - return - } - - if testcase.invalid { - t.Fatalf("Should not have been able to run, but got: %v (%T)", value, value) - } - - returned, ok := value.(ast.Literal) - if !ok { - t.Fatalf("EvalProgram returned unexpected type %T", value) - } - - equal, err := equality.StrictEqual(testcase.expected, returned) - if err != nil { - t.Fatalf("Could not compare result: %v", err) - } - - if !equal { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/statement_test.go b/pkg/eval/test/statement_test.go index 10c78bb..2c33e2b 100644 --- a/pkg/eval/test/statement_test.go +++ b/pkg/eval/test/statement_test.go @@ -6,78 +6,40 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/equality" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalStatement(t *testing.T) { - testcases := []struct { - input ast.Statement - expected ast.Literal - invalid bool - }{ + testcases := []testutil.Testcase{ { - input: ast.Statement{Expression: ast.Null{}}, - expected: ast.Null{}, + AST: ast.Statement{Expression: ast.Null{}}, + Expected: ast.Null{}, }, { - input: ast.Statement{Expression: ast.Bool(true)}, - expected: ast.Bool(true), + AST: ast.Statement{Expression: ast.Bool(true)}, + Expected: ast.Bool(true), }, { - input: ast.Statement{Expression: ast.String("foo")}, - expected: ast.String("foo"), + AST: ast.Statement{Expression: ast.String("foo")}, + Expected: ast.String("foo"), }, { - input: ast.Statement{Expression: ast.Number{Value: 1}}, - expected: ast.Number{Value: 1}, + AST: ast.Statement{Expression: ast.Number{Value: 1}}, + Expected: ast.Number{Value: 1}, }, { - input: ast.Statement{Expression: ast.Object{Data: map[string]any{"foo": "bar"}}}, - expected: ast.Object{Data: map[string]any{"foo": "bar"}}, + AST: ast.Statement{Expression: ast.Object{Data: map[string]any{"foo": "bar"}}}, + Expected: ast.Object{Data: map[string]any{"foo": "bar"}}, }, { - input: ast.Statement{Expression: ast.Vector{Data: []any{"foo", 1}}}, - expected: ast.Vector{Data: []any{"foo", 1}}, + AST: ast.Statement{Expression: ast.Vector{Data: []any{"foo", 1}}}, + Expected: ast.Vector{Data: []any{"foo", 1}}, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, dummyFunctions) - - _, value, err := eval.EvalStatement(ctx, testcase.input) - if err != nil { - if !testcase.invalid { - t.Fatalf("Failed to run: %v", err) - } - - return - } - - if testcase.invalid { - t.Fatalf("Should not have been able to run, but got: %v (%T)", value, value) - } - - returned, ok := value.(ast.Literal) - if !ok { - t.Fatalf("EvalStatement returned unexpected type %T", value) - } - - equal, err := equality.StrictEqual(testcase.expected, returned) - if err != nil { - t.Fatalf("Could not compare result: %v", err) - } - - if !equal { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/string_test.go b/pkg/eval/test/string_test.go index 75b14cb..f00d624 100644 --- a/pkg/eval/test/string_test.go +++ b/pkg/eval/test/string_test.go @@ -6,47 +6,24 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalString(t *testing.T) { - testcases := []struct { - input ast.String - expected ast.String - }{ + testcases := []testutil.Testcase{ { - input: ast.String(""), - expected: ast.String(""), + AST: ast.String(""), + Expected: ast.String(""), }, { - input: ast.String("foo"), - expected: ast.String("foo"), + AST: ast.String("foo"), + Expected: ast.String("foo"), }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, nil) - - _, value, err := eval.EvalString(ctx, testcase.input) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - returned, ok := value.(ast.String) - if !ok { - t.Fatalf("EvalString returned unexpected type %T", value) - } - - if !returned.Equal(testcase.expected) { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/symbol_test.go b/pkg/eval/test/symbol_test.go index 04906bb..f62ea7a 100644 --- a/pkg/eval/test/symbol_test.go +++ b/pkg/eval/test/symbol_test.go @@ -6,10 +6,9 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/equality" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/eval/types" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func makeSymbol(name string, path *ast.PathExpression) ast.Symbol { @@ -26,63 +25,57 @@ func makeSymbol(name string, path *ast.PathExpression) ast.Symbol { } func TestEvalSymbol(t *testing.T) { - testcases := []struct { - input ast.Symbol - expected ast.Literal - variables types.Variables - document any - invalid bool - }{ + testcases := []testutil.Testcase{ // { - input: ast.Symbol{}, - invalid: true, + AST: ast.Symbol{}, + Invalid: true, }, // $undefined { - input: makeSymbol("undefined", nil), - invalid: true, + AST: makeSymbol("undefined", nil), + Invalid: true, }, // $var { - input: makeSymbol("var", nil), - variables: types.Variables{ + AST: makeSymbol("var", nil), + Variables: types.Variables{ "var": ast.String("foo"), }, - expected: ast.String("foo"), + Expected: ast.String("foo"), }, // $native { - input: makeSymbol("native", nil), - variables: types.Variables{ + AST: makeSymbol("native", nil), + Variables: types.Variables{ "native": "foo", }, - expected: ast.String("foo"), + Expected: ast.String("foo"), }, // $var.foo { - input: makeSymbol("var", &ast.PathExpression{Steps: []ast.Expression{ast.Identifier{Name: "foo"}}}), - variables: types.Variables{ + AST: makeSymbol("var", &ast.PathExpression{Steps: []ast.Expression{ast.Identifier{Name: "foo"}}}), + Variables: types.Variables{ "var": map[string]any{ "foo": ast.String("foobar"), }, }, - expected: ast.String("foobar"), + Expected: ast.String("foobar"), }, // $aVector.foo { - input: makeSymbol("aVector", &ast.PathExpression{Steps: []ast.Expression{ast.Identifier{Name: "foo"}}}), - variables: types.Variables{ + AST: makeSymbol("aVector", &ast.PathExpression{Steps: []ast.Expression{ast.Identifier{Name: "foo"}}}), + Variables: types.Variables{ "var": ast.Vector{ Data: []any{ast.String("first")}, }, }, - invalid: true, + Invalid: true, }, // $var[1] { - input: makeSymbol("var", &ast.PathExpression{Steps: []ast.Expression{ast.Number{Value: 1}}}), - variables: types.Variables{ + AST: makeSymbol("var", &ast.PathExpression{Steps: []ast.Expression{ast.Number{Value: 1}}}), + Variables: types.Variables{ "var": ast.Vector{ Data: []any{ ast.String("first"), @@ -90,58 +83,25 @@ func TestEvalSymbol(t *testing.T) { }, }, }, - expected: ast.String("second"), + Expected: ast.String("second"), }, // $aString[1] { - input: makeSymbol("aString", &ast.PathExpression{Steps: []ast.Expression{ast.Number{Value: 1}}}), - variables: types.Variables{ + AST: makeSymbol("aString", &ast.PathExpression{Steps: []ast.Expression{ast.Number{Value: 1}}}), + Variables: types.Variables{ "var": ast.String("bar"), }, - invalid: true, + Invalid: true, }, // . { - input: makeSymbol("", &ast.PathExpression{}), - expected: ast.Null{}, + AST: makeSymbol("", &ast.PathExpression{}), + Expected: ast.Null{}, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(testcase.document) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, testcase.variables, dummyFunctions) - - _, value, err := eval.EvalSymbol(ctx, testcase.input) - if err != nil { - if !testcase.invalid { - t.Fatalf("Failed to run: %v", err) - } - - return - } - - if testcase.invalid { - t.Fatalf("Should not have been able to run, but got: %v (%T)", value, value) - } - - returned, ok := value.(ast.Literal) - if !ok { - t.Fatalf("EvalSymbol returned unexpected type %T", value) - } - - equal, err := equality.StrictEqual(testcase.expected, returned) - if err != nil { - t.Fatalf("Could not compare result: %v", err) - } - - if !equal { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/tuple_test.go b/pkg/eval/test/tuple_test.go index f96c451..f5e0e0f 100644 --- a/pkg/eval/test/tuple_test.go +++ b/pkg/eval/test/tuple_test.go @@ -6,93 +6,86 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/equality" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/eval/types" "go.xrstf.de/rudi/pkg/lang/ast" - - "github.com/google/go-cmp/cmp" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalTuple(t *testing.T) { - testcases := []struct { - input ast.Tuple - expected ast.Literal - invalid bool - }{ + testcases := []testutil.Testcase{ { - input: ast.Tuple{}, - invalid: true, + AST: ast.Tuple{}, + Invalid: true, }, // (true) { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Bool(true), }, }, - invalid: true, + Invalid: true, }, // ("invalid") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.String("invalid"), }, }, - invalid: true, + Invalid: true, }, // (1) { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Number{Value: 1}, }, }, - invalid: true, + Invalid: true, }, // ((eval)) { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Tuple{Expressions: []ast.Expression{ast.Identifier{Name: "eval"}}}, }, }, - invalid: true, + Invalid: true, }, // (unknownfunc) { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "unknownfunc"}, }, }, - invalid: true, + Invalid: true, }, // (eval "too" "many") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "eval"}, ast.String("too"), ast.String("many"), }, }, - invalid: true, + Invalid: true, }, // (eval "foo") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "eval"}, ast.String("foo"), }, }, - expected: ast.String("foo"), + Expected: ast.String("foo"), }, // (eval {foo "bar"}).foo { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "eval"}, ast.ObjectNode{ @@ -110,11 +103,11 @@ func TestEvalTuple(t *testing.T) { }, }, }, - expected: ast.String("bar"), + Expected: ast.String("bar"), }, // (eval {foo "bar"})[1] { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "eval"}, ast.ObjectNode{ @@ -132,11 +125,11 @@ func TestEvalTuple(t *testing.T) { }, }, }, - invalid: true, + Invalid: true, }, // (eval [1 2])[1] { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "eval"}, ast.VectorNode{ @@ -152,11 +145,11 @@ func TestEvalTuple(t *testing.T) { }, }, }, - expected: ast.Number{Value: 2}, + Expected: ast.Number{Value: 2}, }, // (eval [1 2]).invalid { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "eval"}, ast.VectorNode{ @@ -172,93 +165,52 @@ func TestEvalTuple(t *testing.T) { }, }, }, - invalid: true, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, dummyFunctions) - - _, value, err := eval.EvalTuple(ctx, testcase.input) - if err != nil { - if !testcase.invalid { - t.Fatalf("Failed to run: %v", err) - } - - return - } - - if testcase.invalid { - t.Fatalf("Should not have been able to run, but got: %v (%T)", value, value) - } - - returned, ok := value.(ast.Literal) - if !ok { - t.Fatalf("EvalTuple returned unexpected type %T", value) - } - - equal, err := equality.StrictEqual(testcase.expected, returned) - if err != nil { - t.Fatalf("Could not compare result: %v", err) - } - - if !equal { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } func TestEvalTupleBangModifier(t *testing.T) { - testcases := []struct { - input ast.Tuple - variables types.Variables - document any - expected ast.Literal - expectedDocument any - expectedVariables types.Variables - invalid bool - }{ + testcases := []testutil.Testcase{ // (set!) { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, }, }, - invalid: true, + Invalid: true, }, // (set! "invalid" "value") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, ast.String("invalid"), ast.String("value"), }, }, - invalid: true, + Invalid: true, }, // (set! {} "value") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, ast.ObjectNode{}, ast.String("value"), }, }, - invalid: true, + Invalid: true, }, // (set! .[true] "value") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, ast.Symbol{ @@ -271,11 +223,11 @@ func TestEvalTupleBangModifier(t *testing.T) { ast.String("value"), }, }, - invalid: true, + Invalid: true, }, // (set! .[-1] "value") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, ast.Symbol{ @@ -288,49 +240,49 @@ func TestEvalTupleBangModifier(t *testing.T) { ast.String("value"), }, }, - invalid: true, + Invalid: true, }, // (set! . "value") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, ast.Symbol{PathExpression: &ast.PathExpression{}}, ast.String("value"), }, }, - expected: ast.String("value"), - expectedDocument: "value", + Expected: ast.String("value"), + ExpectedDocument: "value", }, // (set! . "value") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, ast.Symbol{PathExpression: &ast.PathExpression{}}, ast.String("value"), }, }, - expected: ast.String("value"), - expectedDocument: "value", + Expected: ast.String("value"), + ExpectedDocument: "value", }, // (set! $val "value") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, makeVar("myvar", nil), ast.String("value"), }, }, - expected: ast.String("value"), - expectedVariables: types.Variables{ + Expected: ast.String("value"), + ExpectedVariables: types.Variables{ "myvar": ast.String("value"), }, }, // (set! .hello "value") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, ast.Symbol{PathExpression: &ast.PathExpression{ @@ -341,13 +293,13 @@ func TestEvalTupleBangModifier(t *testing.T) { ast.String("value"), }, }, - document: map[string]any{"hello": "world", "hei": "verden"}, - expected: ast.String("value"), - expectedDocument: map[string]any{"hello": "value", "hei": "verden"}, + Document: map[string]any{"hello": "world", "hei": "verden"}, + Expected: ast.String("value"), + ExpectedDocument: map[string]any{"hello": ast.String("value"), "hei": "verden"}, }, // (set! $val.key "value") { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, makeVar("val", &ast.PathExpression{ @@ -358,17 +310,17 @@ func TestEvalTupleBangModifier(t *testing.T) { ast.String("value"), }, }, - variables: types.Variables{ + Variables: types.Variables{ "val": map[string]any{"foo": "bar", "key": 42}, }, - expected: ast.String("value"), - expectedVariables: types.Variables{ - "myvar": map[string]any{"foo": "bar", "key": "value"}, + Expected: ast.String("value"), + ExpectedVariables: types.Variables{ + "val": map[string]any{"foo": "bar", "key": ast.String("value")}, }, }, // (set! .hei (set! .hello "value")) { - input: ast.Tuple{ + AST: ast.Tuple{ Expressions: []ast.Expression{ ast.Identifier{Name: "set", Bang: true}, ast.Symbol{PathExpression: &ast.PathExpression{ @@ -389,58 +341,14 @@ func TestEvalTupleBangModifier(t *testing.T) { }, }, }, - document: map[string]any{"hello": "world", "hei": "verden"}, - expected: ast.String("value"), - expectedDocument: map[string]any{"hello": "value", "hei": "value"}, + Document: map[string]any{"hello": "world", "hei": "verden"}, + Expected: ast.String("value"), + ExpectedDocument: map[string]any{"hello": ast.String("value"), "hei": ast.String("value")}, }, } - for _, tc := range testcases { - t.Run(tc.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(tc.document) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, tc.variables, dummyFunctions) - - resultCtx, value, err := eval.EvalTuple(ctx, tc.input) - if err != nil { - if !tc.invalid { - t.Fatalf("Failed to run: %v", err) - } - - return - } - - if tc.invalid { - t.Fatalf("Should not have been able to run, but got: %v (%T)", value, value) - } - - // compare return value - - returned, ok := value.(ast.Literal) - if !ok { - t.Errorf("EvalTuple returned unexpected type %T", value) - } else { - equal, err := equality.StrictEqual(tc.expected, returned) - if err != nil { - t.Errorf("Could not compare result: %v", err) - } else if !equal { - t.Fatalf("Expected result value %v (%T), but got %v (%T)", tc.expected, tc.expected, value, value) - } - } - - // compare expected document - - resultDoc := resultCtx.GetDocument().Data() - - unwrappedDoc, err := types.UnwrapType(resultDoc) - if err != nil { - t.Errorf("Failed to unwrap document: %v", err) - } else if !cmp.Equal(tc.expectedDocument, unwrappedDoc) { - t.Fatalf("Expected document %v (%T), but got %v (%T)", tc.expectedDocument, tc.expectedDocument, unwrappedDoc, unwrappedDoc) - } - }) + for _, testcase := range testcases { + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/test/vector_test.go b/pkg/eval/test/vector_test.go index 7c46760..00aa61f 100644 --- a/pkg/eval/test/vector_test.go +++ b/pkg/eval/test/vector_test.go @@ -6,34 +6,29 @@ package test import ( "testing" - "go.xrstf.de/rudi/pkg/equality" - "go.xrstf.de/rudi/pkg/eval" "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/testutil" ) func TestEvalVectorNode(t *testing.T) { - testcases := []struct { - input ast.VectorNode - expected ast.Literal - invalid bool - }{ + testcases := []testutil.Testcase{ // [] { - input: ast.VectorNode{}, - expected: ast.Vector{}, + AST: ast.VectorNode{}, + Expected: ast.Vector{}, }, // [identifier] { - input: ast.VectorNode{ + AST: ast.VectorNode{ Expressions: []ast.Expression{ ast.Identifier{Name: "identifier"}, }, }, - invalid: true, + Invalid: true, }, // [true "foo" (eval "evaled")] { - input: ast.VectorNode{ + AST: ast.VectorNode{ Expressions: []ast.Expression{ ast.Bool(true), ast.String("foo"), @@ -45,7 +40,7 @@ func TestEvalVectorNode(t *testing.T) { }, }, }, - expected: ast.Vector{ + Expected: ast.Vector{ Data: []any{ true, ast.String("foo"), @@ -55,7 +50,7 @@ func TestEvalVectorNode(t *testing.T) { }, // [true "foo"][1] { - input: ast.VectorNode{ + AST: ast.VectorNode{ Expressions: []ast.Expression{ ast.Bool(true), ast.String("foo"), @@ -66,11 +61,11 @@ func TestEvalVectorNode(t *testing.T) { }, }, }, - expected: ast.String("foo"), + Expected: ast.String("foo"), }, // ["foo"][1] { - input: ast.VectorNode{ + AST: ast.VectorNode{ Expressions: []ast.Expression{ ast.String("foo"), }, @@ -80,11 +75,11 @@ func TestEvalVectorNode(t *testing.T) { }, }, }, - invalid: true, + Invalid: true, }, // ["foo"].ident { - input: ast.VectorNode{ + AST: ast.VectorNode{ Expressions: []ast.Expression{ ast.String("foo"), }, @@ -94,11 +89,11 @@ func TestEvalVectorNode(t *testing.T) { }, }, }, - invalid: true, + Invalid: true, }, // ["foo"][1.2] { - input: ast.VectorNode{ + AST: ast.VectorNode{ Expressions: []ast.Expression{ ast.String("foo"), }, @@ -108,45 +103,12 @@ func TestEvalVectorNode(t *testing.T) { }, }, }, - invalid: true, + Invalid: true, }, } for _, testcase := range testcases { - t.Run(testcase.input.String(), func(t *testing.T) { - doc, err := eval.NewDocument(nil) - if err != nil { - t.Fatalf("Failed to create test document: %v", err) - } - - ctx := eval.NewContext(doc, nil, dummyFunctions) - - _, value, err := eval.EvalVectorNode(ctx, testcase.input) - if err != nil { - if !testcase.invalid { - t.Fatalf("Failed to run: %v", err) - } - - return - } - - if testcase.invalid { - t.Fatalf("Should not have been able to run, but got: %v (%T)", value, value) - } - - returned, ok := value.(ast.Literal) - if !ok { - t.Fatalf("EvalVectorNode returned unexpected type %T", value) - } - - equal, err := equality.StrictEqual(testcase.expected, returned) - if err != nil { - t.Fatalf("Could not compare result: %v", err) - } - - if !equal { - t.Fatal("Result does not match expectation.") - } - }) + testcase.Functions = dummyFunctions + t.Run(testcase.String(), testcase.Run) } } diff --git a/pkg/eval/types/context.go b/pkg/eval/types/context.go index 76dafbd..f462434 100644 --- a/pkg/eval/types/context.go +++ b/pkg/eval/types/context.go @@ -238,46 +238,18 @@ func UnwrapType(val any) (any, error) { case float64: return asserted, nil case ast.Vector: - return unwrapVector(&asserted) + return asserted.Data, nil case *ast.Vector: - return unwrapVector(asserted) + return asserted.Data, nil case []any: - return unwrapVector(&ast.Vector{Data: asserted}) + return asserted, nil case ast.Object: - return unwrapObject(&asserted) + return asserted.Data, nil case *ast.Object: - return unwrapObject(asserted) + return asserted.Data, nil case map[string]any: - return unwrapObject(&ast.Object{Data: asserted}) + return asserted, nil default: return nil, fmt.Errorf("cannot unwrap %v (%T)", val, val) } } - -func unwrapVector(v *ast.Vector) ([]any, error) { - result := make([]any, len(v.Data)) - for i, item := range v.Data { - unwrappedItem, err := UnwrapType(item) - if err != nil { - return nil, err - } - - result[i] = unwrappedItem - } - - return result, nil -} - -func unwrapObject(o *ast.Object) (map[string]any, error) { - result := map[string]any{} - for key, value := range o.Data { - unwrappedValue, err := UnwrapType(value) - if err != nil { - return nil, err - } - - result[key] = unwrappedValue - } - - return result, nil -} diff --git a/pkg/pathexpr/set_test.go b/pkg/pathexpr/set_test.go index c6a1f16..c099cdd 100644 --- a/pkg/pathexpr/set_test.go +++ b/pkg/pathexpr/set_test.go @@ -119,7 +119,7 @@ func TestSet(t *testing.T) { dest: map[string]any{"foo": "bar", "deeper": []any{1, 2, map[string]any{"deep": "value"}}}, path: Path{"deeper", 2, "deep"}, newValue: "new-value", - expected: map[string]any{"foo": "bar", "deeper": []any{int64(1), int64(2), map[string]any{"deep": "new-value"}}}, + expected: map[string]any{"foo": "bar", "deeper": []any{1, 2, map[string]any{"deep": "new-value"}}}, }, { name: "sub slice key can be updated", @@ -133,7 +133,7 @@ func TestSet(t *testing.T) { dest: map[string]any{"foo": "bar", "deeper": []any{1, 2, map[string]any{"deep": "value"}}}, path: Path{"deeper", 2}, newValue: "new-value", - expected: map[string]any{"foo": "bar", "deeper": []any{int64(1), int64(2), "new-value"}}, + expected: map[string]any{"foo": "bar", "deeper": []any{1, 2, "new-value"}}, }, } diff --git a/pkg/testutil/testcase.go b/pkg/testutil/testcase.go new file mode 100644 index 0000000..6b05edd --- /dev/null +++ b/pkg/testutil/testcase.go @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2023 Christoph Mewes +// SPDX-License-Identifier: MIT + +package testutil + +import ( + "fmt" + "log" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "go.xrstf.de/rudi/pkg/equality" + "go.xrstf.de/rudi/pkg/eval" + "go.xrstf.de/rudi/pkg/eval/types" + "go.xrstf.de/rudi/pkg/lang/ast" + "go.xrstf.de/rudi/pkg/lang/parser" +) + +type Testcase struct { + // use either Expression or AST + Expression string + AST ast.Expression + + Document any + Variables types.Variables + Functions types.Functions + Expected any + ExpectedDocument any + ExpectedVariables types.Variables + Invalid bool +} + +func (tc *Testcase) String() string { + if tc.Expression != "" { + return tc.Expression + } + + if tc.AST != nil { + return tc.AST.String() + } + + return "" +} + +func (tc *Testcase) Run(t *testing.T) { + t.Helper() + + ctx, result, err := tc.eval(t) + if err != nil { + if !tc.Invalid { + t.Fatalf("Failed to eval %s: %v", tc.Expression, err) + } + + return + } + + if tc.Invalid { + t.Fatalf("Should not have been able to eval %s, but got: %v", tc.Expression, result) + } + + assertResultValue(t, tc.Expected, result) + assertVariables(t, tc.ExpectedVariables, ctx) + assertDocument(t, tc.ExpectedDocument, ctx) +} + +func (tc *Testcase) eval(t *testing.T) (types.Context, any, error) { + if (tc.Expression == "") == (tc.AST == nil) { + t.Fatal("Must use either AST or Expression as test input.") + } + + doc, err := eval.NewDocument(tc.Document) + if err != nil { + log.Fatalf("Failed to create parser document: %v", err) + } + + progContext := eval.NewContext(doc, tc.Variables, tc.Functions) + + if tc.Expression != "" { + prog := strings.NewReader(tc.Expression) + + got, err := parser.ParseReader("test.go", prog) + if err != nil { + t.Fatalf("Failed to parse %s: %v", tc.Expression, err) + } + + program, ok := got.(ast.Program) + if !ok { + t.Fatalf("Parsed result is not a ast.Program, but %T", got) + } + + return eval.EvalProgram(progContext, &program) + } + + // To enable tests for programs and statements, we handle them explicitly + // instead of extending EvalExpression() to handle them, as that would not + // fit the language structure. + + switch asserted := tc.AST.(type) { + case ast.Program: + return eval.EvalProgram(progContext, &asserted) + case ast.Statement: + return eval.EvalStatement(progContext, asserted) + default: + return eval.EvalExpression(progContext, tc.AST) + } +} + +func renderDiff(expected any, actual any) string { + var builder strings.Builder + + builder.WriteString(fmt.Sprintf("Expected type...: %T\n", expected)) + builder.WriteString(fmt.Sprintf("Expected value..: %#v\n", expected)) + builder.WriteString("\n") + builder.WriteString(fmt.Sprintf("Actual type.....: %T\n", actual)) + builder.WriteString(fmt.Sprintf("Actual value....: %#v\n", actual)) + + return builder.String() +} + +func assertResultValue(t *testing.T, expected any, actual any) { + if expectedNode, ok := expected.(ast.Literal); ok { + resultNode, ok := actual.(ast.Literal) + if !ok { + t.Errorf("Result has invalid type:\n%s", renderDiff(expected, actual)) + } else { + equal, err := equality.StrictEqual(expectedNode, resultNode) + if err != nil { + t.Errorf("Could not compare result with expectation: %v", err) + } else if !equal { + t.Errorf("Resulting value does not match expectation:\n\n%s\n", renderDiff(expected, actual)) + } + } + } else if !cmp.Equal(expected, actual) { + t.Errorf("Resulting value does not match expectation:\n\n%s\n", renderDiff(expected, actual)) + } +} + +func assertDocument(t *testing.T, expected any, ctx types.Context) { + resultDoc := ctx.GetDocument().Data() + + unwrappedDoc, err := types.UnwrapType(resultDoc) + if err != nil { + t.Errorf("Failed to unwrap document: %v", err) + } else if !cmp.Equal(expected, unwrappedDoc) { + t.Errorf("Resulting document does not match expectation:\n\n%s\n", renderDiff(expected, unwrappedDoc)) + } +} + +func assertVariables(t *testing.T, expected types.Variables, ctx types.Context) { + if expected == nil { + return + } + + for varName, value := range expected { + actualValue, ok := ctx.GetVariable(varName) + if !ok { + t.Errorf("Variable $%s does not exist anymore.", varName) + continue + } + + if !cmp.Equal(value, actualValue) { + t.Errorf("Variable $%s dooes not match expectation:\n\n%s\n", varName, renderDiff(value, actualValue)) + } + } +}