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