blob: a122d994afe38bd6559caa8cd4511831143abbb4 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5"""Tests for the query2ast module."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import datetime
11import time
12import unittest
13import mock
14
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010015from mrproto import ast_pb2
16from mrproto import tracker_pb2
Copybara854996b2021-09-07 19:36:02 +000017from search import query2ast
18from tracker import tracker_bizobj
19
20BOOL = query2ast.BOOL
21DATE = query2ast.DATE
22NUM = query2ast.NUM
23TXT = query2ast.TXT
24
25SUBQUERY = query2ast.SUBQUERY
26LEFT_PAREN = query2ast.LEFT_PAREN
27RIGHT_PAREN = query2ast.RIGHT_PAREN
28OR = query2ast.OR
29
30BUILTIN_ISSUE_FIELDS = query2ast.BUILTIN_ISSUE_FIELDS
31ANY_FIELD = query2ast.BUILTIN_ISSUE_FIELDS['any_field']
32
33EQ = query2ast.EQ
34NE = query2ast.NE
35LT = query2ast.LT
36GT = query2ast.GT
37LE = query2ast.LE
38GE = query2ast.GE
39TEXT_HAS = query2ast.TEXT_HAS
40NOT_TEXT_HAS = query2ast.NOT_TEXT_HAS
41IS_DEFINED = query2ast.IS_DEFINED
42IS_NOT_DEFINED = query2ast.IS_NOT_DEFINED
43KEY_HAS = query2ast.KEY_HAS
44
45MakeCond = ast_pb2.MakeCond
46NOW = 1277762224
47
48
49class QueryParsingUnitTest(unittest.TestCase):
50
51 def setUp(self):
52 self.project_id = 789
53 self.default_config = tracker_bizobj.MakeDefaultProjectIssueConfig(
54 self.project_id)
55
56 def testParseUserQuery_OrClause(self):
57 # an "OR" query, which should look like two separate simple querys
58 # joined together by a pipe.
59 ast = query2ast.ParseUserQuery(
60 'ham OR fancy', '', BUILTIN_ISSUE_FIELDS, self.default_config)
61 conj1 = ast.conjunctions[0]
62 conj2 = ast.conjunctions[1]
63 self.assertEqual([MakeCond(TEXT_HAS, [ANY_FIELD], ['ham'], [])],
64 conj1.conds)
65 self.assertEqual([MakeCond(TEXT_HAS, [ANY_FIELD], ['fancy'], [])],
66 conj2.conds)
67
68 def testParseUserQuery_Words(self):
69 # an "ORTerm" is actually anything appearing on either side of an
70 # "OR" operator. So this could be thought of as "simple" query parsing.
71
72 # a simple query with no spaces
73 ast = query2ast.ParseUserQuery(
74 'hamfancy', '', BUILTIN_ISSUE_FIELDS, self.default_config)
75 fulltext_cond = ast.conjunctions[0].conds[0]
76 self.assertEqual(
77 MakeCond(TEXT_HAS, [ANY_FIELD], ['hamfancy'], []), fulltext_cond)
78
79 # negative word
80 ast = query2ast.ParseUserQuery(
81 '-hamfancy', '', BUILTIN_ISSUE_FIELDS, self.default_config)
82 fulltext_cond = ast.conjunctions[0].conds[0]
83 self.assertEqual(
84 # note: not NOT_TEXT_HAS.
85 MakeCond(NOT_TEXT_HAS, [ANY_FIELD], ['hamfancy'], []),
86 fulltext_cond)
87
88 # invalid fulltext term
89 ast = query2ast.ParseUserQuery(
90 'ham=fancy\\', '', BUILTIN_ISSUE_FIELDS, self.default_config)
91 self.assertEqual([], ast.conjunctions[0].conds)
92
93 # an explicit "AND" query in the "featured" context
94 warnings = []
95 query2ast.ParseUserQuery(
96 'ham AND fancy', 'label:featured', BUILTIN_ISSUE_FIELDS,
97 self.default_config, warnings=warnings)
98 self.assertEqual(
99 ['The only supported boolean operator is OR (all capitals).'],
100 warnings)
101
102 # an implicit "AND" query
103 ast = query2ast.ParseUserQuery(
104 'ham fancy', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
105 self.default_config)
106 scope_cond1, ft_cond1, ft_cond2 = ast.conjunctions[0].conds
107 self.assertEqual(
108 MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
109 ['deprecated'], []),
110 scope_cond1)
111 self.assertEqual(
112 MakeCond(TEXT_HAS, [ANY_FIELD], ['ham'], []), ft_cond1)
113 self.assertEqual(
114 MakeCond(TEXT_HAS, [ANY_FIELD], ['fancy'], []), ft_cond2)
115
116 # Use word with non-operator prefix.
117 word_with_non_op_prefix = '%stest' % query2ast.NON_OP_PREFIXES[0]
118 ast = query2ast.ParseUserQuery(
119 word_with_non_op_prefix, '', BUILTIN_ISSUE_FIELDS, self.default_config)
120 fulltext_cond = ast.conjunctions[0].conds[0]
121 self.assertEqual(
122 MakeCond(TEXT_HAS, [ANY_FIELD], ['"%s"' % word_with_non_op_prefix], []),
123 fulltext_cond)
124
125 # mix positive and negative words
126 ast = query2ast.ParseUserQuery(
127 'ham -fancy', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
128 self.default_config)
129 scope_cond1, ft_cond1, ft_cond2 = ast.conjunctions[0].conds
130 self.assertEqual(
131 MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
132 ['deprecated'], []),
133 scope_cond1)
134 self.assertEqual(
135 MakeCond(TEXT_HAS, [ANY_FIELD], ['ham'], []), ft_cond1)
136 self.assertEqual(
137 MakeCond(NOT_TEXT_HAS, [ANY_FIELD], ['fancy'], []), ft_cond2)
138
139 # converts terms to lower case
140 ast = query2ast.ParseUserQuery(
141 'AmDude', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
142 self.default_config)
143 scope_cond1, fulltext_cond = ast.conjunctions[0].conds
144 self.assertEqual(
145 MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
146 ['deprecated'], []),
147 scope_cond1)
148 self.assertEqual(
149 MakeCond(TEXT_HAS, [ANY_FIELD], ['amdude'], []), fulltext_cond)
150
151 def testParseUserQuery_Phrases(self):
152 # positive phrases
153 ast = query2ast.ParseUserQuery(
154 '"one two"', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
155 self.default_config)
156 scope_cond1, fulltext_cond = ast.conjunctions[0].conds
157 self.assertEqual(
158 MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
159 ['deprecated'], []),
160 scope_cond1)
161 self.assertEqual(
162 MakeCond(TEXT_HAS, [ANY_FIELD], ['"one two"'], []), fulltext_cond)
163
164 # negative phrases
165 ast = query2ast.ParseUserQuery(
166 '-"one two"', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
167 self.default_config)
168 scope_cond1, fulltext_cond = ast.conjunctions[0].conds
169 self.assertEqual(
170 MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
171 ['deprecated'], []),
172 scope_cond1)
173 self.assertEqual(
174 MakeCond(NOT_TEXT_HAS, [ANY_FIELD], ['"one two"'], []), fulltext_cond)
175
176 # multiple phrases
177 ast = query2ast.ParseUserQuery(
178 '-"a b" "x y"', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
179 self.default_config)
180 scope_cond1, ft_cond1, ft_cond2 = ast.conjunctions[0].conds
181 self.assertEqual(
182 MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
183 ['deprecated'], []),
184 scope_cond1)
185 self.assertEqual(
186 MakeCond(NOT_TEXT_HAS, [ANY_FIELD], ['"a b"'], []), ft_cond1)
187 self.assertEqual(
188 MakeCond(TEXT_HAS, [ANY_FIELD], ['"x y"'], []), ft_cond2)
189
190 def testParseUserQuery_CodeSyntaxThatWeNeedToCopeWith(self):
191 # positive phrases
192 ast = query2ast.ParseUserQuery(
193 'Base::Tuple', '', BUILTIN_ISSUE_FIELDS, self.default_config)
194 cond = ast.conjunctions[0].conds[0]
195 self.assertEqual(
196 MakeCond(TEXT_HAS, [ANY_FIELD],
197 ['"base::tuple"'], []),
198 cond)
199
200 # stuff we just ignore
201 ast = query2ast.ParseUserQuery(
202 ':: - -- .', '', BUILTIN_ISSUE_FIELDS, self.default_config)
203 self.assertEqual([], ast.conjunctions[0].conds)
204
205 def testParseUserQuery_IsOperator(self):
206 """Test is:open, is:spam, and is:blocked."""
207 for keyword in ['open', 'spam', 'blocked']:
208 ast = query2ast.ParseUserQuery(
209 'is:' + keyword, '', BUILTIN_ISSUE_FIELDS, self.default_config)
210 cond1 = ast.conjunctions[0].conds[0]
211 self.assertEqual(
212 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS[keyword]], [], []),
213 cond1)
214 ast = query2ast.ParseUserQuery(
215 '-is:' + keyword, '', BUILTIN_ISSUE_FIELDS, self.default_config)
216 cond1 = ast.conjunctions[0].conds[0]
217 self.assertEqual(
218 MakeCond(NE, [BUILTIN_ISSUE_FIELDS[keyword]], [], []),
219 cond1)
220
221 def testParseUserQuery_HasOperator(self):
222 # Search for issues with at least one attachment
223 ast = query2ast.ParseUserQuery(
224 'has:attachment', '', BUILTIN_ISSUE_FIELDS, self.default_config)
225 cond1 = ast.conjunctions[0].conds[0]
226 self.assertEqual(
227 MakeCond(IS_DEFINED, [BUILTIN_ISSUE_FIELDS['attachment']], [], []),
228 cond1)
229
230 ast = query2ast.ParseUserQuery(
231 '-has:attachment', '', BUILTIN_ISSUE_FIELDS, self.default_config)
232 cond1 = ast.conjunctions[0].conds[0]
233 self.assertEqual(
234 MakeCond(IS_NOT_DEFINED, [BUILTIN_ISSUE_FIELDS['attachment']], [], []),
235 cond1)
236
237 ast = query2ast.ParseUserQuery(
238 'has=attachment', '', BUILTIN_ISSUE_FIELDS, self.default_config)
239 cond1 = ast.conjunctions[0].conds[0]
240 self.assertEqual(
241 MakeCond(IS_DEFINED, [BUILTIN_ISSUE_FIELDS['attachment']], [], []),
242 cond1)
243
244 ast = query2ast.ParseUserQuery(
245 '-has=attachment', '', BUILTIN_ISSUE_FIELDS, self.default_config)
246 cond1 = ast.conjunctions[0].conds[0]
247 self.assertEqual(
248 MakeCond(IS_NOT_DEFINED, [BUILTIN_ISSUE_FIELDS['attachment']], [], []),
249 cond1)
250
251 # Search for numeric fields for searches with 'has' prefix
252 ast = query2ast.ParseUserQuery(
253 'has:attachments', '', BUILTIN_ISSUE_FIELDS, self.default_config)
254 cond1 = ast.conjunctions[0].conds[0]
255 self.assertEqual(
256 MakeCond(IS_DEFINED, [BUILTIN_ISSUE_FIELDS['attachments']], [], []),
257 cond1)
258
259 ast = query2ast.ParseUserQuery(
260 '-has:attachments', '', BUILTIN_ISSUE_FIELDS, self.default_config)
261 cond1 = ast.conjunctions[0].conds[0]
262 self.assertEqual(
263 MakeCond(IS_NOT_DEFINED, [BUILTIN_ISSUE_FIELDS['attachments']],
264 [], []),
265 cond1)
266
267 # If it is not a field, look for any key-value label.
268 ast = query2ast.ParseUserQuery(
269 'has:Size', '', BUILTIN_ISSUE_FIELDS, self.default_config)
270 cond1 = ast.conjunctions[0].conds[0]
271 self.assertEqual(
272 MakeCond(IS_DEFINED, [BUILTIN_ISSUE_FIELDS['label']], ['size'], []),
273 cond1)
274
275 def testParseUserQuery_Phase(self):
276 ast = query2ast.ParseUserQuery(
277 'gate:Canary,Stable', '', BUILTIN_ISSUE_FIELDS, self.default_config)
278 cond1 = ast.conjunctions[0].conds[0]
279 self.assertEqual(
280 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['gate']],
281 ['canary', 'stable'], []),
282 cond1)
283
284 ast = query2ast.ParseUserQuery(
285 '-gate:Canary,Stable', '', BUILTIN_ISSUE_FIELDS, self.default_config)
286 cond1 = ast.conjunctions[0].conds[0]
287 self.assertEqual(
288 MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['gate']],
289 ['canary', 'stable'], []),
290 cond1)
291
292 def testParseUserQuery_Components(self):
293 """Parse user queries for components"""
294 ast = query2ast.ParseUserQuery(
295 'component:UI', '', BUILTIN_ISSUE_FIELDS, self.default_config)
296 cond1 = ast.conjunctions[0].conds[0]
297 self.assertEqual(
298 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['component']],
299 ['ui'], []),
300 cond1)
301
302 ast = query2ast.ParseUserQuery(
303 'Component:UI>AboutBox', '', BUILTIN_ISSUE_FIELDS, self.default_config)
304 cond1 = ast.conjunctions[0].conds[0]
305 self.assertEqual(
306 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['component']],
307 ['ui>aboutbox'], []),
308 cond1)
309
310 def testParseUserQuery_OwnersReportersAndCc(self):
311 """Parse user queries for owner:, reporter: and cc:."""
312 ast = query2ast.ParseUserQuery(
313 'owner:user', '', BUILTIN_ISSUE_FIELDS, self.default_config)
314 cond1 = ast.conjunctions[0].conds[0]
315 self.assertEqual(
316 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['owner']],
317 ['user'], []),
318 cond1)
319
320 ast = query2ast.ParseUserQuery(
321 'owner:user@example.com', '', BUILTIN_ISSUE_FIELDS, self.default_config)
322 cond1 = ast.conjunctions[0].conds[0]
323 self.assertEqual(
324 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['owner']],
325 ['user@example.com'], []),
326 cond1)
327
328 ast = query2ast.ParseUserQuery(
329 'owner=user@example.com', '', BUILTIN_ISSUE_FIELDS, self.default_config)
330 cond1 = ast.conjunctions[0].conds[0]
331 self.assertEqual(
332 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['owner']],
333 ['user@example.com'], []),
334 cond1)
335
336 ast = query2ast.ParseUserQuery(
337 '-reporter=user@example.com', '', BUILTIN_ISSUE_FIELDS,
338 self.default_config)
339 cond1 = ast.conjunctions[0].conds[0]
340 self.assertEqual(
341 MakeCond(NE, [BUILTIN_ISSUE_FIELDS['reporter']],
342 ['user@example.com'], []),
343 cond1)
344
345 ast = query2ast.ParseUserQuery(
346 'cc=user@example.com,user2@example.com', '', BUILTIN_ISSUE_FIELDS,
347 self.default_config)
348 cond1 = ast.conjunctions[0].conds[0]
349 self.assertEqual(
350 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['cc']],
351 ['user@example.com', 'user2@example.com'], []),
352 cond1)
353
354 ast = query2ast.ParseUserQuery(
355 'cc:user,user2', '', BUILTIN_ISSUE_FIELDS, self.default_config)
356 cond1 = ast.conjunctions[0].conds[0]
357 self.assertEqual(
358 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['cc']],
359 ['user', 'user2'], []),
360 cond1)
361
362 def testParseUserQuery_SearchWithinFields(self):
363 # Search for issues with certain filenames
364 ast = query2ast.ParseUserQuery(
365 'attachment:filename', '', BUILTIN_ISSUE_FIELDS, self.default_config)
366 cond1 = ast.conjunctions[0].conds[0]
367 self.assertEqual(
368 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['attachment']],
369 ['filename'], []),
370 cond1)
371
372 ast = query2ast.ParseUserQuery(
373 '-attachment:filename', '', BUILTIN_ISSUE_FIELDS,
374 self.default_config)
375 cond1 = ast.conjunctions[0].conds[0]
376 self.assertEqual(
377 MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['attachment']],
378 ['filename'], []),
379 cond1)
380
381 # Search for issues with a certain number of attachments
382 ast = query2ast.ParseUserQuery(
383 'attachments:2', '', BUILTIN_ISSUE_FIELDS, self.default_config)
384 cond1 = ast.conjunctions[0].conds[0]
385 self.assertEqual(
386 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['attachments']],
387 ['2'], [2]),
388 cond1)
389
390 # Searches with '=' syntax
391 ast = query2ast.ParseUserQuery(
392 'attachment=filename', '', BUILTIN_ISSUE_FIELDS, self.default_config)
393 cond1 = ast.conjunctions[0].conds[0]
394 self.assertEqual(
395 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['attachment']],
396 ['filename'], []),
397 cond1)
398
399 ast = query2ast.ParseUserQuery(
400 '-attachment=filename', '', BUILTIN_ISSUE_FIELDS, self.default_config)
401 cond1 = ast.conjunctions[0].conds[0]
402 self.assertEqual(
403 MakeCond(NE, [BUILTIN_ISSUE_FIELDS['attachment']],
404 ['filename'], []),
405 cond1)
406
407 ast = query2ast.ParseUserQuery(
408 'milestone=2009', '', BUILTIN_ISSUE_FIELDS, self.default_config)
409 cond1 = ast.conjunctions[0].conds[0]
410 self.assertEqual(
411 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['label']], ['milestone-2009'], []),
412 cond1)
413
414 ast = query2ast.ParseUserQuery(
415 '-milestone=2009', '', BUILTIN_ISSUE_FIELDS, self.default_config)
416 cond1 = ast.conjunctions[0].conds[0]
417 self.assertEqual(
418 MakeCond(NE, [BUILTIN_ISSUE_FIELDS['label']], ['milestone-2009'], []),
419 cond1)
420
421 ast = query2ast.ParseUserQuery(
422 'milestone=2009-Q1', '', BUILTIN_ISSUE_FIELDS, self.default_config)
423 cond1 = ast.conjunctions[0].conds[0]
424 self.assertEqual(
425 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['label']],
426 ['milestone-2009-q1'], []),
427 cond1)
428
429 ast = query2ast.ParseUserQuery(
430 '-milestone=2009-Q1', '', BUILTIN_ISSUE_FIELDS,
431 self.default_config)
432 cond1 = ast.conjunctions[0].conds[0]
433 self.assertEqual(
434 MakeCond(NE, [BUILTIN_ISSUE_FIELDS['label']],
435 ['milestone-2009-q1'], []),
436 cond1)
437
438 # Searches with ':' syntax
439 ast = query2ast.ParseUserQuery(
440 'summary:foo', '', BUILTIN_ISSUE_FIELDS, self.default_config)
441 cond1 = ast.conjunctions[0].conds[0]
442 self.assertEqual(
443 MakeCond(TEXT_HAS,
444 [BUILTIN_ISSUE_FIELDS['summary']], ['foo'], []),
445 cond1)
446
447 ast = query2ast.ParseUserQuery(
448 'summary:"greetings programs"', '', BUILTIN_ISSUE_FIELDS,
449 self.default_config)
450 cond1 = ast.conjunctions[0].conds[0]
451 self.assertEqual(
452 MakeCond(TEXT_HAS,
453 [BUILTIN_ISSUE_FIELDS['summary']], ['greetings programs'], []),
454 cond1)
455
456 ast = query2ast.ParseUserQuery(
457 'summary:"Ӓ"', '', BUILTIN_ISSUE_FIELDS,
458 self.default_config)
459 cond1 = ast.conjunctions[0].conds[0]
460 self.assertEqual(
461 MakeCond(TEXT_HAS,
462 [BUILTIN_ISSUE_FIELDS['summary']], ['Ӓ'], []),
463 cond1)
464
465 ast = query2ast.ParseUserQuery(
466 'priority:high', '', BUILTIN_ISSUE_FIELDS, self.default_config)
467 cond1 = ast.conjunctions[0].conds[0]
468 self.assertEqual(
469 MakeCond(KEY_HAS,
470 [BUILTIN_ISSUE_FIELDS['label']], ['priority-high'], []),
471 cond1)
472
473 ast = query2ast.ParseUserQuery(
474 'type:security', '', BUILTIN_ISSUE_FIELDS, self.default_config)
475 cond1 = ast.conjunctions[0].conds[0]
476 self.assertEqual(
477 MakeCond(KEY_HAS,
478 [BUILTIN_ISSUE_FIELDS['label']], ['type-security'], []),
479 cond1)
480
481 ast = query2ast.ParseUserQuery(
482 'label:priority-high', '', BUILTIN_ISSUE_FIELDS, self.default_config)
483 cond1 = ast.conjunctions[0].conds[0]
484 self.assertEqual(
485 MakeCond(TEXT_HAS,
486 [BUILTIN_ISSUE_FIELDS['label']], ['priority-high'], []),
487 cond1)
488
489 ast = query2ast.ParseUserQuery(
490 'blockedon:other:123', '', BUILTIN_ISSUE_FIELDS, self.default_config)
491 cond1 = ast.conjunctions[0].conds[0]
492 self.assertEqual(
493 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['blockedon']],
494 ['other:123'], []),
495 cond1)
496
497 ast = query2ast.ParseUserQuery(
498 'cost=-2', '', BUILTIN_ISSUE_FIELDS, self.default_config)
499 cond1 = ast.conjunctions[0].conds[0]
500 self.assertEqual(
501 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['label']],
502 ['cost--2'], []),
503 cond1)
504
505 # Searches with ':' and an email domain only.
506 ast = query2ast.ParseUserQuery(
507 'reporter:@google.com', '', BUILTIN_ISSUE_FIELDS, self.default_config)
508 cond1 = ast.conjunctions[0].conds[0]
509 self.assertEqual(
510 MakeCond(TEXT_HAS,
511 [BUILTIN_ISSUE_FIELDS['reporter']], ['@google.com'], []),
512 cond1)
513
514 # Search for issues in certain user hotlists.
515 ast = query2ast.ParseUserQuery(
516 'hotlist=gatsby@chromium.org:Hotlist1', '',
517 BUILTIN_ISSUE_FIELDS, self.default_config)
518 cond1 = ast.conjunctions[0].conds[0]
519 self.assertEqual(
520 MakeCond(
521 EQ, [BUILTIN_ISSUE_FIELDS['hotlist']],
522 ['gatsby@chromium.org:hotlist1'], []),
523 cond1)
524
525 # Search for 'Hotlist' labels.
526 ast = query2ast.ParseUserQuery(
527 'hotlist:sublabel', '', BUILTIN_ISSUE_FIELDS, self.default_config)
528 cond1 = ast.conjunctions[0].conds[0]
529 self.assertEqual(
530 MakeCond(KEY_HAS, [BUILTIN_ISSUE_FIELDS['label']],
531 ['hotlist-sublabel'], []),
532 cond1)
533
534 def testParseUserQuery_SearchWithinCustomFields(self):
535 """Enums are treated as labels, other fields are kept as fields."""
536 fd1 = tracker_bizobj.MakeFieldDef(
537 1, self.project_id, 'Size', tracker_pb2.FieldTypes.ENUM_TYPE,
538 'applic', 'applic', False, False, False, None, None, None, False, None,
539 None, None, 'no_action', 'doc', False)
540 fd2 = tracker_bizobj.MakeFieldDef(
541 1, self.project_id, 'EstDays', tracker_pb2.FieldTypes.INT_TYPE,
542 'applic', 'applic', False, False, False, None, None, None, False, None,
543 None, None, 'no_action', 'doc', False)
544 self.default_config.field_defs.extend([fd1, fd2])
545 ast = query2ast.ParseUserQuery(
546 'Size:Small EstDays>3', '', BUILTIN_ISSUE_FIELDS, self.default_config)
547 cond1 = ast.conjunctions[0].conds[0]
548 cond2 = ast.conjunctions[0].conds[1]
549 self.assertEqual(
550 MakeCond(KEY_HAS, [BUILTIN_ISSUE_FIELDS['label']],
551 ['size-small'], []),
552 cond1)
553 self.assertEqual(
554 MakeCond(GT, [fd2], ['3'], [3]),
555 cond2)
556
557 @mock.patch('time.time', return_value=NOW)
558 def testParseUserQuery_Approvals(self, _mock_time):
559 """Test approval queries are parsed correctly."""
560 fd1 = tracker_bizobj.MakeFieldDef(
561 1, self.project_id, 'UIReview', tracker_pb2.FieldTypes.APPROVAL_TYPE,
562 'applic', 'applic', False, False, False, None, None, None, False, None,
563 None, None, 'no_action', 'doc', False)
564 fd2 = tracker_bizobj.MakeFieldDef(
565 2, self.project_id, 'EstDays', tracker_pb2.FieldTypes.INT_TYPE,
566 'applic', 'applic', False, False, False, None, None, None, False, None,
567 None, None, 'no_action', 'doc', False)
568 fd3 = tracker_bizobj.MakeFieldDef(
569 3, self.project_id, 'UXReview', tracker_pb2.FieldTypes.APPROVAL_TYPE,
570 'applic', 'applic', False, False, False, None, None, None, False, None,
571 None, None, 'no_action', 'doc', False)
572 self.default_config.field_defs.extend([fd1, fd2, fd3])
573 ast = query2ast.ParseUserQuery(
574 'UXReview-approver:user1@mail.com,user2@mail.com UIReview:Approved '
575 'UIReview-on>today-7', '', BUILTIN_ISSUE_FIELDS, self.default_config)
576 cond1 = ast.conjunctions[0].conds[0]
577 cond2 = ast.conjunctions[0].conds[1]
578 cond3 = ast.conjunctions[0].conds[2]
579 self.assertEqual(MakeCond(TEXT_HAS, [fd3],
580 ['user1@mail.com', 'user2@mail.com'], [],
581 key_suffix='-approver'), cond1)
582 self.assertEqual(MakeCond(TEXT_HAS, [fd1], ['approved'], []), cond2)
583 self.assertEqual(
584 cond3,
585 MakeCond(
586 GT, [fd1], [], [query2ast._CalculatePastDate(7, NOW)],
587 key_suffix='-on'))
588
589 def testParseUserQuery_PhaseFields(self):
590 fd = tracker_bizobj.MakeFieldDef(
591 1, self.project_id, 'EstDays', tracker_pb2.FieldTypes.INT_TYPE,
592 'applic', 'applic', False, False, False, None, None, None, False, None,
593 None, None, 'no_action', 'doc', False, is_phase_field=True)
594 self.default_config.field_defs.append(fd)
595 ast = query2ast.ParseUserQuery(
596 'UXReview.EstDays>3', '', BUILTIN_ISSUE_FIELDS, self.default_config)
597 cond1 = ast.conjunctions[0].conds[0]
598 self.assertEqual(
599 MakeCond(GT, [fd], ['3'], [3], phase_name='uxreview'),
600 cond1)
601
602 def testParseUserQuery_QuickOr(self):
603 # quick-or searches
604 ast = query2ast.ParseUserQuery(
605 'milestone:2008,2009,2010', '', BUILTIN_ISSUE_FIELDS,
606 self.default_config)
607 cond1 = ast.conjunctions[0].conds[0]
608 self.assertEqual(
609 MakeCond(KEY_HAS, [BUILTIN_ISSUE_FIELDS['label']],
610 ['milestone-2008', 'milestone-2009', 'milestone-2010'], []),
611 cond1)
612
613 ast = query2ast.ParseUserQuery(
614 'label:milestone-2008,milestone-2009,milestone-2010', '',
615 BUILTIN_ISSUE_FIELDS, self.default_config)
616 cond1 = ast.conjunctions[0].conds[0]
617 self.assertEqual(
618 MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
619 ['milestone-2008', 'milestone-2009', 'milestone-2010'], []),
620 cond1)
621
622 ast = query2ast.ParseUserQuery(
623 'milestone=2008,2009,2010', '', BUILTIN_ISSUE_FIELDS,
624 self.default_config)
625 cond1 = ast.conjunctions[0].conds[0]
626 self.assertEqual(
627 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['label']],
628 ['milestone-2008', 'milestone-2009', 'milestone-2010'], []),
629 cond1)
630
631 # Duplicated and trailing commas are ignored.
632 ast = query2ast.ParseUserQuery(
633 'milestone=2008,,2009,2010,', '', BUILTIN_ISSUE_FIELDS,
634 self.default_config)
635 cond1 = ast.conjunctions[0].conds[0]
636 self.assertEqual(
637 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['label']],
638 ['milestone-2008', 'milestone-2009', 'milestone-2010'], []),
639 cond1)
640
641 def testParseUserQuery_Dates(self):
642 # query with a daterange
643 ast = query2ast.ParseUserQuery(
644 'modified>=2009-5-12', '', BUILTIN_ISSUE_FIELDS,
645 self.default_config)
646 cond1 = ast.conjunctions[0].conds[0]
647 ts1 = int(time.mktime(datetime.datetime(2009, 5, 12).timetuple()))
648 self.assertEqual(
649 MakeCond(GE, [BUILTIN_ISSUE_FIELDS['modified']], [], [ts1]), cond1)
650
651 # query with quick-or
652 ast = query2ast.ParseUserQuery(
653 'modified=2009-5-12,2009-5-13', '', BUILTIN_ISSUE_FIELDS,
654 self.default_config)
655 cond1 = ast.conjunctions[0].conds[0]
656 ts1 = int(time.mktime(datetime.datetime(2009, 5, 12).timetuple()))
657 ts2 = int(time.mktime(datetime.datetime(2009, 5, 13).timetuple()))
658 self.assertEqual(
659 MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['modified']], [], [ts1, ts2]), cond1)
660
661 # query with multiple dateranges
662 ast = query2ast.ParseUserQuery(
663 'modified>=2009-5-12 opened<2008/1/1', '',
664 BUILTIN_ISSUE_FIELDS, self.default_config)
665 cond1, cond2 = ast.conjunctions[0].conds
666 ts1 = int(time.mktime(datetime.datetime(2009, 5, 12).timetuple()))
667 self.assertEqual(
668 MakeCond(GE, [BUILTIN_ISSUE_FIELDS['modified']], [], [ts1]), cond1)
669 ts2 = int(time.mktime(datetime.datetime(2008, 1, 1).timetuple()))
670 self.assertEqual(
671 MakeCond(LT, [BUILTIN_ISSUE_FIELDS['opened']], [], [ts2]), cond2)
672
673 # query with multiple dateranges plus a search term
674 ast = query2ast.ParseUserQuery(
675 'one two modified>=2009-5-12 opened<2008/1/1', '',
676 BUILTIN_ISSUE_FIELDS, self.default_config)
677 ft_cond1, ft_cond2, cond1, cond2 = ast.conjunctions[0].conds
678 ts1 = int(time.mktime(datetime.datetime(2009, 5, 12).timetuple()))
679 self.assertEqual(
680 MakeCond(TEXT_HAS, [ANY_FIELD], ['one'], []), ft_cond1)
681 self.assertEqual(
682 MakeCond(TEXT_HAS, [ANY_FIELD], ['two'], []), ft_cond2)
683 self.assertEqual(
684 MakeCond(GE, [BUILTIN_ISSUE_FIELDS['modified']], [], [ts1]), cond1)
685 ts2 = int(time.mktime(datetime.datetime(2008, 1, 1).timetuple()))
686 self.assertEqual(
687 MakeCond(LT, [BUILTIN_ISSUE_FIELDS['opened']], [], [ts2]), cond2)
688
689 # query with a date field compared to "today"
690 ast = query2ast.ParseUserQuery(
691 'modified<today', '', BUILTIN_ISSUE_FIELDS,
692 self.default_config, now=NOW)
693 cond1 = ast.conjunctions[0].conds[0]
694 ts1 = query2ast._CalculatePastDate(0, now=NOW)
695 self.assertEqual(MakeCond(LT, [BUILTIN_ISSUE_FIELDS['modified']],
696 [], [ts1]),
697 cond1)
698
699 # query with a daterange using today-N alias
700 ast = query2ast.ParseUserQuery(
701 'modified>=today-13', '', BUILTIN_ISSUE_FIELDS,
702 self.default_config, now=NOW)
703 cond1 = ast.conjunctions[0].conds[0]
704 ts1 = query2ast._CalculatePastDate(13, now=NOW)
705 self.assertEqual(MakeCond(GE, [BUILTIN_ISSUE_FIELDS['modified']],
706 [], [ts1]),
707 cond1)
708
709 ast = query2ast.ParseUserQuery(
710 'modified>today-13', '', BUILTIN_ISSUE_FIELDS, self.default_config,
711 now=NOW)
712 cond1 = ast.conjunctions[0].conds[0]
713 ts1 = query2ast._CalculatePastDate(13, now=NOW)
714 self.assertEqual(MakeCond(GT, [BUILTIN_ISSUE_FIELDS['modified']],
715 [], [ts1]),
716 cond1)
717
718 # query with multiple old date query terms.
719 ast = query2ast.ParseUserQuery(
720 'modified-after:2009-5-12 opened-before:2008/1/1 '
721 'closed-after:2007-2-1', '',
722 BUILTIN_ISSUE_FIELDS, self.default_config)
723 cond1, cond2, cond3 = ast.conjunctions[0].conds
724 ts1 = int(time.mktime(datetime.datetime(2009, 5, 12).timetuple()))
725 self.assertEqual(
726 MakeCond(GT, [BUILTIN_ISSUE_FIELDS['modified']], [], [ts1]), cond1)
727 ts2 = int(time.mktime(datetime.datetime(2008, 1, 1).timetuple()))
728 self.assertEqual(
729 MakeCond(LT, [BUILTIN_ISSUE_FIELDS['opened']], [], [ts2]), cond2)
730 ts3 = int(time.mktime(datetime.datetime(2007, 2, 1).timetuple()))
731 self.assertEqual(
732 MakeCond(GT, [BUILTIN_ISSUE_FIELDS['closed']], [], [ts3]), cond3)
733
734 def testCalculatePastDate(self):
735 ts1 = query2ast._CalculatePastDate(0, now=NOW)
736 self.assertEqual(NOW, ts1)
737
738 ts2 = query2ast._CalculatePastDate(13, now=NOW)
739 self.assertEqual(ts2, NOW - 13 * 24 * 60 * 60)
740
741 # Try it once with time.time() instead of a known timestamp.
742 ts_system_clock = query2ast._CalculatePastDate(13)
743 self.assertTrue(ts_system_clock < int(time.time()))
744
745 def testParseUserQuery_BadDates(self):
746 bad_dates = ['today-13h', 'yesterday', '2/2', 'm/y/d',
747 '99/99/1999', '0-0-0']
748 for val in bad_dates:
749 with self.assertRaises(query2ast.InvalidQueryError) as cm:
750 query2ast.ParseUserQuery(
751 'modified>=' + val, '', BUILTIN_ISSUE_FIELDS,
752 self.default_config)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100753 self.assertEqual('Could not parse date: ' + val, str(cm.exception))
Copybara854996b2021-09-07 19:36:02 +0000754
755 def testQueryToSubqueries_BasicQuery(self):
756 self.assertEqual(['owner:me'], query2ast.QueryToSubqueries('owner:me'))
757
758 def testQueryToSubqueries_EmptyQuery(self):
759 self.assertEqual([''], query2ast.QueryToSubqueries(''))
760
761 def testQueryToSubqueries_UnmatchedParenthesesThrowsError(self):
762 with self.assertRaises(query2ast.InvalidQueryError):
763 self.assertEqual(['Pri=1'], query2ast.QueryToSubqueries('Pri=1))'))
764 with self.assertRaises(query2ast.InvalidQueryError):
765 self.assertEqual(
766 ['label:Hello'], query2ast.QueryToSubqueries('((label:Hello'))
767
768 with self.assertRaises(query2ast.InvalidQueryError):
769 self.assertEqual(
770 ['owner:me'], query2ast.QueryToSubqueries('((((owner:me)))'))
771
772 with self.assertRaises(query2ast.InvalidQueryError):
773 self.assertEqual(
774 ['test=What'], query2ast.QueryToSubqueries('(((test=What))))'))
775
776 def testQueryToSubqueries_IgnoresEmptyGroups(self):
777 self.assertEqual([''], query2ast.QueryToSubqueries('()(()(()))()()'))
778
779 self.assertEqual(
780 ['owner:me'], query2ast.QueryToSubqueries('()(()owner:me)()()'))
781
782 def testQueryToSubqueries_BasicOr(self):
783 self.assertEqual(
784 ['owner:me', 'status:New', 'Pri=1'],
785 query2ast.QueryToSubqueries('owner:me OR status:New OR Pri=1'))
786
787 def testQueryToSubqueries_OrAtStartOrEnd(self):
788 self.assertEqual(
789 ['owner:me OR'], query2ast.QueryToSubqueries('owner:me OR'))
790
791 self.assertEqual(
792 ['OR owner:me'], query2ast.QueryToSubqueries('OR owner:me'))
793
794 def testQueryToSubqueries_BasicParentheses(self):
795 self.assertEqual(
796 ['owner:me status:New'],
797 query2ast.QueryToSubqueries('owner:me (status:New)'))
798
799 self.assertEqual(
800 ['owner:me status:New'],
801 query2ast.QueryToSubqueries('(owner:me) status:New'))
802
803 self.assertEqual(
804 ['owner:me status:New'],
805 query2ast.QueryToSubqueries('((owner:me) (status:New))'))
806
807 def testQueryToSubqueries_ParenthesesWithOr(self):
808 self.assertEqual(
809 ['Pri=1 owner:me', 'Pri=1 status:New'],
810 query2ast.QueryToSubqueries('Pri=1 (owner:me OR status:New)'))
811
812 self.assertEqual(
813 ['owner:me component:OhNo', 'status:New component:OhNo'],
814 query2ast.QueryToSubqueries('(owner:me OR status:New) component:OhNo'))
815
816 def testQueryToSubqueries_ParenthesesWithOr_Multiple(self):
817 self.assertEqual(
818 [
819 'Pri=1 test owner:me', 'Pri=1 test status:New',
820 'Pri=2 test owner:me', 'Pri=2 test status:New'
821 ],
822 query2ast.QueryToSubqueries(
823 '(Pri=1 OR Pri=2)(test (owner:me OR status:New))'))
824
825 def testQueryToSubqueries_OrNextToParentheses(self):
826 self.assertEqual(['A', 'B'], query2ast.QueryToSubqueries('(A) OR (B)'))
827
828 self.assertEqual(
829 ['A B', 'A C E', 'A D E'],
830 query2ast.QueryToSubqueries('A (B OR (C OR D) E)'))
831
832 self.assertEqual(
833 ['A B C', 'A B D', 'A E'],
834 query2ast.QueryToSubqueries('A (B (C OR D) OR E)'))
835
836 def testQueryToSubqueries_ExtraSpaces(self):
837 self.assertEqual(
838 ['A', 'B'], query2ast.QueryToSubqueries(' ( A ) OR ( B ) '))
839
840 self.assertEqual(
841 ['A B', 'A C E', 'A D E'],
842 query2ast.QueryToSubqueries(' A ( B OR ( C OR D ) E )'))
843
844 def testQueryToSubqueries_OrAtEndOfParentheses(self):
845 self.assertEqual(['A B'], query2ast.QueryToSubqueries('(A OR )(B)'))
846 self.assertEqual(
847 ['A B', 'A C'], query2ast.QueryToSubqueries('( OR A)(B OR C)'))
848 self.assertEqual(
849 ['A B', 'A C'], query2ast.QueryToSubqueries(' OR A (B OR C)'))
850
851 def testQueryToSubqueries_EmptyOrGroup(self):
852 self.assertEqual(
853 ['A C', 'C', 'B C'], query2ast.QueryToSubqueries('(A OR OR B)(C)'))
854
855 def testParseQuery_Basic(self):
856 self.assertEqual(
857 [
858 'owner:me',
859 ],
860 query2ast._ParseQuery(
861 query2ast.PeekIterator(
862 [ast_pb2.QueryToken(token_type=SUBQUERY, value='owner:me')])))
863
864 def testParseQuery_Complex(self):
865 self.assertEqual(
866 [
867 'owner:me',
868 'Pri=1',
869 'label=test',
870 ],
871 query2ast._ParseQuery(
872 query2ast.PeekIterator(
873 [
874 ast_pb2.QueryToken(token_type=SUBQUERY, value='owner:me'),
875 ast_pb2.QueryToken(token_type=OR),
876 ast_pb2.QueryToken(token_type=LEFT_PAREN),
877 ast_pb2.QueryToken(token_type=SUBQUERY, value='Pri=1'),
878 ast_pb2.QueryToken(token_type=RIGHT_PAREN),
879 ast_pb2.QueryToken(token_type=OR),
880 ast_pb2.QueryToken(token_type=SUBQUERY, value='label=test'),
881 ])))
882
883 def testParseOrGroup_Basic(self):
884 self.assertEqual(
885 [
886 'owner:me',
887 ],
888 query2ast._ParseOrGroup(
889 query2ast.PeekIterator(
890 [ast_pb2.QueryToken(token_type=SUBQUERY, value='owner:me')])))
891
892 def testParseOrGroup_TwoAdjacentAndGroups(self):
893 self.assertEqual(
894 [
895 'owner:me Pri=1',
896 'owner:me label=test',
897 ],
898 query2ast._ParseOrGroup(
899 query2ast.PeekIterator(
900 [
901 ast_pb2.QueryToken(token_type=SUBQUERY, value='owner:me'),
902 ast_pb2.QueryToken(token_type=LEFT_PAREN),
903 ast_pb2.QueryToken(token_type=SUBQUERY, value='Pri=1'),
904 ast_pb2.QueryToken(token_type=OR),
905 ast_pb2.QueryToken(token_type=SUBQUERY, value='label=test'),
906 ast_pb2.QueryToken(token_type=RIGHT_PAREN),
907 ])))
908
909 def testParseAndGroup_Subquery(self):
910 self.assertEqual(
911 [
912 'owner:me',
913 ],
914 query2ast._ParseAndGroup(
915 query2ast.PeekIterator(
916 [ast_pb2.QueryToken(token_type=SUBQUERY, value='owner:me')])))
917
918 def testParseAndGroup_ParenthesesGroup(self):
919 self.assertEqual(
920 [
921 'owner:me',
922 'Pri=1',
923 ],
924 query2ast._ParseAndGroup(
925 query2ast.PeekIterator(
926 [
927 ast_pb2.QueryToken(token_type=LEFT_PAREN),
928 ast_pb2.QueryToken(token_type=SUBQUERY, value='owner:me'),
929 ast_pb2.QueryToken(token_type=OR),
930 ast_pb2.QueryToken(token_type=SUBQUERY, value='Pri=1'),
931 ast_pb2.QueryToken(token_type=RIGHT_PAREN),
932 ])))
933
934 def testParseAndGroup_Empty(self):
935 self.assertEqual([], query2ast._ParseAndGroup(query2ast.PeekIterator([])))
936
937 def testParseAndGroup_InvalidTokens(self):
938 with self.assertRaises(query2ast.InvalidQueryError):
939 query2ast._ParseAndGroup(
940 query2ast.PeekIterator(
941 [
942 ast_pb2.QueryToken(token_type=OR),
943 ast_pb2.QueryToken(token_type=SUBQUERY, value='Pri=1'),
944 ast_pb2.QueryToken(token_type=RIGHT_PAREN),
945 ]))
946
947 with self.assertRaises(query2ast.InvalidQueryError):
948 query2ast._ParseAndGroup(
949 query2ast.PeekIterator(
950 [
951 ast_pb2.QueryToken(token_type=RIGHT_PAREN),
952 ast_pb2.QueryToken(token_type=OR),
953 ast_pb2.QueryToken(token_type=SUBQUERY, value='Pri=1'),
954 ]))
955
956 def testValidateAndTokenizeQuery_Basic(self):
957 self.assertEqual(
958 [
959 ast_pb2.QueryToken(token_type=LEFT_PAREN),
960 ast_pb2.QueryToken(token_type=SUBQUERY, value='owner:me'),
961 ast_pb2.QueryToken(token_type=OR),
962 ast_pb2.QueryToken(token_type=SUBQUERY, value='Pri=1'),
963 ast_pb2.QueryToken(token_type=RIGHT_PAREN),
964 ], query2ast._ValidateAndTokenizeQuery('(owner:me OR Pri=1)'))
965
966 def testValidateAndTokenizeQuery_UnmatchedParentheses(self):
967 with self.assertRaises(query2ast.InvalidQueryError):
968 query2ast._ValidateAndTokenizeQuery('(owner:me')
969
970 with self.assertRaises(query2ast.InvalidQueryError):
971 query2ast._ValidateAndTokenizeQuery('owner:me)')
972
973 with self.assertRaises(query2ast.InvalidQueryError):
974 query2ast._ValidateAndTokenizeQuery('(()owner:me))')
975
976 with self.assertRaises(query2ast.InvalidQueryError):
977 query2ast._ValidateAndTokenizeQuery('(()owner:me)())')
978
979 def testTokenizeSubqueryOnOr_NoOrOperator(self):
980 self.assertEqual(
981 [ast_pb2.QueryToken(token_type=SUBQUERY, value='owner:me')],
982 query2ast._TokenizeSubqueryOnOr('owner:me'))
983
984 def testTokenizeSubqueryOnOr_BasicOrOperator(self):
985 self.assertEqual(
986 [
987 ast_pb2.QueryToken(token_type=SUBQUERY, value='A'),
988 ast_pb2.QueryToken(token_type=OR),
989 ast_pb2.QueryToken(token_type=SUBQUERY, value='B'),
990 ast_pb2.QueryToken(token_type=OR),
991 ast_pb2.QueryToken(token_type=SUBQUERY, value='C'),
992 ], query2ast._TokenizeSubqueryOnOr('A OR B OR C'))
993
994 def testTokenizeSubqueryOnOr_EmptyOrOperator(self):
995 self.assertEqual(
996 [ast_pb2.QueryToken(token_type=OR)],
997 query2ast._TokenizeSubqueryOnOr(' OR '))
998
999 self.assertEqual(
1000 [
1001 ast_pb2.QueryToken(token_type=SUBQUERY, value='A'),
1002 ast_pb2.QueryToken(token_type=OR),
1003 ], query2ast._TokenizeSubqueryOnOr('A OR '))
1004
1005 def testMultiplySubqueries_Basic(self):
1006 self.assertEqual(
1007 ['owner:me Pri=1', 'owner:me Pri=2', 'test Pri=1', 'test Pri=2'],
1008 query2ast._MultiplySubqueries(['owner:me', 'test'], ['Pri=1', 'Pri=2']))
1009
1010 def testMultiplySubqueries_OneEmpty(self):
1011 self.assertEqual(
1012 ['Pri=1', 'Pri=2'],
1013 query2ast._MultiplySubqueries([], ['Pri=1', 'Pri=2']))
1014 self.assertEqual(
1015 ['Pri=1', 'Pri=2'],
1016 query2ast._MultiplySubqueries([''], ['Pri=1', 'Pri=2']))
1017
1018 self.assertEqual(
1019 ['Pri=1', 'Pri=2'],
1020 query2ast._MultiplySubqueries(['Pri=1', 'Pri=2'], []))
1021 self.assertEqual(
1022 ['Pri=1', 'Pri=2'],
1023 query2ast._MultiplySubqueries(['Pri=1', 'Pri=2'], ['']))
1024
1025 def testPeekIterator_Basic(self):
1026 iterator = query2ast.PeekIterator([1, 2, 3])
1027
1028 self.assertEqual(1, iterator.peek())
1029 self.assertEqual(1, iterator.next())
1030
1031 self.assertEqual(2, iterator.next())
1032
1033 self.assertEqual(3, iterator.peek())
1034 self.assertEqual(3, iterator.next())
1035
1036 with self.assertRaises(StopIteration):
1037 iterator.peek()
1038
1039 with self.assertRaises(StopIteration):
1040 iterator.next()