1// SPDX-License-Identifier: MIT
2
3package expr
4
5import (
6 "context"
7 "strings"
8 "testing"
9 "unicode/utf16"
10)
11
12// TestRunnerSemantics pins behaviours of the PORTING.md list that the C# runner
13// and the JavaScript reference share or differ on. Every expectation was
14// obtained by running the expression through the runner's own code
15// (gen/runner); the bulk is also covered by TestRunnerOracle.
16func TestRunnerSemantics(t *testing.T) {
17 ctx := map[string]Value{"github": mustJSON(t, `{
18 "commits": [{"id": "1"}, {"id": "2", "x": 1}, {"y": 3}],
19 "obj": {"a": {"a": 1}, "K": "v", "b": {"c": 2}},
20 "nested": [[1, 2], [3], []]}`)}
21 tests := []struct{ src, want string }{
22 // Index access: strings and numbers index arrays by their numeric value,
23 // booleans and null become 1 and 0, negative, NaN and infinite give null.
24 {"github.commits['1'].id", "String:2"},
25 {"github.commits[' 1 '].id", "String:2"},
26 {"github.commits['1.9'].id", "String:2"},
27 {"github.commits[1.9].id", "String:2"},
28 {"github.commits[true].id", "String:2"},
29 {"github.commits[null].id", "String:1"},
30 {"github.commits[-1]", "Null:"},
31 {"github.commits[NaN]", "Null:"},
32 {"github.commits[Infinity]", "Null:"},
33 {"github.commits['x']", "Null:"},
34 {"github.commits[5].id", "Null:"},
35 // On a dictionary only the string form counts, case-insensitively.
36 {"github.obj[true]", "Null:"},
37 {"github.obj[null]", "Null:"},
38 {"github.obj[1]", "Null:"},
39 {"github.obj['K']", "String:v"},
40 {"GITHUB.OBJ.K", "String:v"},
41 {"github.OBJ.k", "String:v"},
42 {"github.nope.deeper", "Null:"},
43 // Filters.
44 {"toJSON(github.obj.*.a)", "String:[\n 1\n]"},
45 {"toJSON(github.commits.*.id)", "String:[\n \"1\",\n \"2\"\n]"},
46 {"toJSON(github.commits.*.*)", "String:[\n \"1\",\n \"2\",\n 1,\n 3\n]"},
47 {"toJSON(github.nested.*.*)", "String:[\n 1,\n 2,\n 3\n]"},
48 {"toJSON(github.nested.*[0])", "String:[\n 1,\n 3\n]"},
49 {"github.commits.* == github.commits.*", "Boolean:false"},
50 {"github.obj.* == github.obj.*", "Boolean:false"},
51 {"github.commits == github.commits", "Boolean:true"},
52 {"github.obj == github.obj", "Boolean:true"},
53 // Comparison.
54 {"null < 1", "Boolean:true"},
55 {"null > -1", "Boolean:true"},
56 {"null == 0", "Boolean:true"},
57 {"null < true", "Boolean:true"},
58 {"null < ''", "Boolean:false"},
59 {"null == ''", "Boolean:true"},
60 {"NaN == NaN", "Boolean:false"},
61 {"NaN != NaN", "Boolean:true"},
62 {"NaN < 1", "Boolean:false"},
63 {"true > false", "Boolean:true"},
64 {"true > 0", "Boolean:true"},
65 // && and || return an operand.
66 {"'' || 'x'", "String:x"},
67 {"'a' && 'b'", "String:b"},
68 {"0 && 'x'", "Number:0"},
69 {"null || false", "Boolean:false"},
70 // White space may surround "." and "[" and sit between a function name
71 // and its "(" (the lexer looks ahead past it).
72 {"github . obj . K", "String:v"},
73 {"github.obj.\nK", "String:v"},
74 {"github.obj .K", "String:v"},
75 {"github [ 'obj' ].K", "String:v"},
76 {"toJSON (1)", "String:1"},
77 // White space is Char.IsWhiteSpace: U+00A0, U+000B and U+0085 are skipped.
78 {"1\u00a0", "Number:1"},
79 {"\u00a01", "Number:1"},
80 {"\v1", "Number:1"},
81 {"'a'\u0085", "String:a"},
82 }
83 for _, tc := range tests {
84 node, err := Parse(tc.src, []string{"github"}, nil)
85 if err != nil {
86 t.Errorf("Parse(%q): %v", tc.src, err)
87 continue
88 }
89 v, err := Evaluate(context.Background(), node, EvalContext{Contexts: ctx})
90 if err != nil {
91 t.Errorf("Evaluate(%q): %v", tc.src, err)
92 continue
93 }
94 if got := v.Kind().String() + ":" + CoerceString(v); got != tc.want {
95 t.Errorf("%s = %q, want %q", tc.src, got, tc.want)
96 }
97 }
98
99 // Parse errors.
100 for _, tc := range []struct{ src, want string }{
101 {"contains('a','b',)", "Unexpected symbol: ')'. Located at position 18 within expression: contains('a','b',)"},
102 {"contains(,'a')", "Unexpected symbol: ','. Located at position 10 within expression: contains(,'a')"},
103 {"[1] == [1]", "Unexpected symbol: '['. Located at position 1 within expression: [1] == [1]"},
104 {"1 \u00a0 2", "Unexpected symbol: '2'. Located at position 5 within expression: 1 \u00a0 2"},
105 } {
106 if _, err := Parse(tc.src, []string{"github"}, nil); err == nil || err.Error() != tc.want {
107 t.Errorf("Parse(%q) = %v, want %q", tc.src, err, tc.want)
108 }
109 }
110
111 // An empty expression is not an error and has no tree.
112 for _, src := range []string{"", " ", "\n", "\u00a0"} {
113 if n, err := Parse(src, nil, nil); n != nil || err != nil {
114 t.Errorf("Parse(%q) = %v, %v; want nil, nil", src, n, err)
115 }
116 }
117}
118
119// BenchmarkParseLong parses an expression of the maximum length made of many
120// short tokens spread over many lines: building the position of every token
121// must not rescan the expression.
122func BenchmarkParseLong(b *testing.B) {
123 src := strings.Repeat("1 ||\n", MaxExpressionLength/5-1) + "1"
124 if len(src) > MaxExpressionLength {
125 b.Fatal(len(src))
126 }
127 b.SetBytes(int64(len(src)))
128 for i := 0; i < b.N; i++ {
129 if _, err := Parse(src, nil, nil); err != nil {
130 b.Fatal(err)
131 }
132 }
133}
134
135func TestPositionLines(t *testing.T) {
136 src := "a\n\nbc d\n e"
137 p := &csParser{units: utf16.Encode([]rune(src))}
138 for _, tc := range []struct{ idx, line, col int }{{0, 0, 0}, {1, 0, 1}, {2, 1, 0}, {3, 2, 0}, {5, 2, 2}, {7, 2, 4}, {8, 3, 0}, {10, 3, 2}, {len(src), 3, 3}} {
139 if got := p.position(tc.idx); got.Line != tc.line || got.Column != tc.col {
140 t.Errorf("position(%d) = %v, want %d:%d", tc.idx, got, tc.line, tc.col)
141 }
142 }
143}