1 package filtering
2
3 import (
4 "testing"
5 "time"
6
7 expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
8 "google.golang.org/protobuf/testing/protocmp"
9 "gotest.tools/v3/assert"
10 )
11
12 func TestParser(t *testing.T) {
13 t.Parallel()
14 for _, tt := range []struct {
15 filter string
16 expected *expr.Expr
17 errorContains string
18 }{
19 {
20 filter: "New York Giants",
21 expected: Sequence(Text("New"), Text("York"), Text("Giants")),
22 },
23
24 {
25 filter: "New York Giants OR Yankees",
26 expected: Sequence(Text("New"), Text("York"), Or(Text("Giants"), Text("Yankees"))),
27 },
28
29 {
30 filter: "New York (Giants OR Yankees)",
31 expected: Sequence(Text("New"), Text("York"), Or(Text("Giants"), Text("Yankees"))),
32 },
33
34 {
35 filter: "a b AND c AND d",
36 expected: And(
37 Sequence(Text("a"), Text("b")),
38 Text("c"),
39 Text("d"),
40 ),
41 },
42
43 {
44 filter: "(a b) AND c AND d",
45 expected: And(
46 Sequence(Text("a"), Text("b")),
47 Text("c"),
48 Text("d"),
49 ),
50 },
51
52 {
53 filter: "a < 10 OR a >= 100",
54 expected: Or(
55 LessThan(Text("a"), Int(10)),
56 GreaterEquals(Text("a"), Int(100)),
57 ),
58 },
59 {
60 filter: "a OR b OR c",
61 expected: Or(
62 Text("a"),
63 Text("b"),
64 Text("c"),
65 ),
66 },
67
68 {
69 filter: "NOT (a OR b)",
70 expected: Not(Or(Text("a"), Text("b"))),
71 },
72
73 {
74 filter: `-file:".java"`,
75 expected: Not(Has(Text("file"), String(".java"))),
76 },
77
78 {
79 filter: "-30",
80 expected: Int(-30),
81 },
82
83 {
84 filter: "package=com.google",
85 expected: Equals(Text("package"), Member(Text("com"), "google")),
86 },
87
88 {
89 filter: `msg != 'hello'`,
90 expected: NotEquals(Text("msg"), String("hello")),
91 },
92
93 {
94 filter: `1 > 0`,
95 expected: GreaterThan(Int(1), Int(0)),
96 },
97
98 {
99 filter: `2.5 >= 2.4`,
100 expected: GreaterEquals(Float(2.5), Float(2.4)),
101 },
102
103 {
104 filter: `foo >= -2.4`,
105 expected: GreaterEquals(Text("foo"), Float(-2.4)),
106 },
107
108 {
109 filter: `foo >= (-2.4)`,
110 expected: GreaterEquals(Text("foo"), Float(-2.4)),
111 },
112
113 {
114 filter: `-2.5 >= -2.4`,
115 expected: Not(GreaterEquals(Float(2.5), Float(-2.4))),
116 },
117
118 {
119 filter: `yesterday < request.time`,
120 expected: LessThan(Text("yesterday"), Member(Text("request"), "time")),
121 },
122
123 {
124 filter: `experiment.rollout <= cohort(request.user)`,
125 expected: LessEquals(
126 Member(Text("experiment"), "rollout"),
127 Function("cohort", Member(Text("request"), "user")),
128 ),
129 },
130
131 {
132 filter: `prod`,
133 expected: Text("prod"),
134 },
135
136 {
137 filter: `expr.type_map.1.type`,
138 expected: Member(Member(Member(Text("expr"), "type_map"), "1"), "type"),
139 },
140
141 {
142 filter: `regex(m.key, '^.*prod.*$')`,
143 expected: Function("regex", Member(Text("m"), "key"), String("^.*prod.*$")),
144 },
145
146 {
147 filter: `math.mem('30mb')`,
148 expected: Function("math.mem", String("30mb")),
149 },
150
151 {
152 filter: `(msg.endsWith('world') AND retries < 10)`,
153 expected: And(
154 Function("msg.endsWith", String("world")),
155 LessThan(Text("retries"), Int(10)),
156 ),
157 },
158
159 {
160 filter: `(endsWith(msg, 'world') AND retries < 10)`,
161 expected: And(
162 Function("endsWith", Text("msg"), String("world")),
163 LessThan(Text("retries"), Int(10)),
164 ),
165 },
166
167 {
168 filter: "time.now()",
169 expected: Function("time.now"),
170 },
171
172 {
173 filter: `timestamp("2012-04-21T11:30:00-04:00")`,
174 expected: Timestamp(
175 time.Date(2012, time.April, 21, 11, 30, 0, 0, time.FixedZone("EST", -4*int(time.Hour.Seconds()))),
176 ),
177 },
178
179 {
180 filter: `duration("32s")`,
181 expected: Duration(32 * time.Second),
182 },
183
184 {
185 filter: `duration("4h0m0s")`,
186 expected: Duration(4 * time.Hour),
187 },
188
189 {
190 filter: `
191 start_time > timestamp("2006-01-02T15:04:05+07:00") AND
192 (driver = "driver1" OR start_driver = "driver1" OR end_driver = "driver1")
193 `,
194 expected: And(
195 GreaterThan(
196 Text("start_time"),
197 Timestamp(
198 time.Date(
199 2006, time.January, 2, 15, 4, 5, 0,
200 time.FixedZone("EST", 7*int(time.Hour.Seconds())),
201 ),
202 ),
203 ),
204 Or(
205 Equals(Text("driver"), String("driver1")),
206 Equals(Text("start_driver"), String("driver1")),
207 Equals(Text("end_driver"), String("driver1")),
208 ),
209 ),
210 },
211
212 {
213 filter: `annotations:schedule`,
214 expected: Has(Text("annotations"), String("schedule")),
215 },
216
217 {
218 filter: `annotations.schedule = "test"`,
219 expected: Equals(Member(Text("annotations"), "schedule"), String("test")),
220 },
221
222 {
223 filter: "<",
224 errorContains: "unexpected token <",
225 },
226
227 {
228 filter: `(-2.5) >= -2.4`,
229 errorContains: "unexpected token >=",
230 },
231
232 {
233 filter: `a = "foo`,
234 errorContains: "unterminated string",
235 },
236
237 {
238 filter: "invalid = foo\xa0\x01bar",
239 errorContains: "invalid UTF-8",
240 },
241 } {
242 tt := tt
243 t.Run(tt.filter, func(t *testing.T) {
244 t.Parallel()
245 var parser Parser
246 parser.Init(tt.filter)
247 actual, err := parser.Parse()
248 if tt.errorContains != "" {
249 if actual != nil {
250 t.Log(actual.GetExpr().String())
251 }
252 assert.ErrorContains(t, err, tt.errorContains)
253 } else {
254 assert.NilError(t, err)
255 assert.DeepEqual(
256 t,
257 tt.expected,
258 actual.GetExpr(),
259 protocmp.Transform(),
260 protocmp.IgnoreFields(&expr.Expr{}, "id"),
261 )
262 assertUniqueExprIDs(t, actual.GetExpr())
263 }
264 })
265 }
266 }
267
268 func assertUniqueExprIDs(t *testing.T, exp *expr.Expr) {
269 t.Helper()
270 seenIDs := make(map[int64]struct{})
271 Walk(func(currExpr, _ *expr.Expr) bool {
272 if _, ok := seenIDs[currExpr.GetId()]; ok {
273 t.Fatalf("duplicate expression ID '%d' for expr %v", currExpr.GetId(), currExpr)
274 }
275 seenIDs[currExpr.GetId()] = struct{}{}
276 return true
277 }, exp)
278 }
279
View as plain text