blob: a68c279bd96b2dac3fc9cf4a997780ee079f777d [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"""Unit tests for filterrules_helpers feature."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import mock
12import unittest
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020013from six.moves import urllib
14from six.moves.urllib.parse import parse_qs
Copybara854996b2021-09-07 19:36:02 +000015
16import settings
17from features import filterrules_helpers
18from framework import cloud_tasks_helpers
19from framework import framework_constants
20from framework import template_helpers
21from framework import urls
22from proto import ast_pb2
23from proto import tracker_pb2
24from search import query2ast
25from services import service_manager
26from testing import fake
27from tracker import tracker_bizobj
28
29
30ORIG_SUMMARY = 'this is the orginal summary'
31ORIG_LABELS = ['one', 'two']
32
33# Fake user id mapping
34TEST_ID_MAP = {
35 'mike.j.parent': 1,
36 'jrobbins': 2,
37 'ningerso': 3,
38 'ui@example.com': 4,
39 'db@example.com': 5,
40 'ui-db@example.com': 6,
41 }
42
43TEST_LABEL_IDS = {
44 'i18n': 1,
45 'l10n': 2,
46 'Priority-High': 3,
47 'Priority-Medium': 4,
48 }
49
50
51class RecomputeAllDerivedFieldsTest(unittest.TestCase):
52
53 BLOCK = filterrules_helpers.BLOCK
54
55 def setUp(self):
56 self.features = fake.FeaturesService()
57 self.user = fake.UserService()
58 self.services = service_manager.Services(
59 features=self.features,
60 user=self.user,
61 issue=fake.IssueService())
62 self.project = fake.Project(project_name='proj')
63 self.config = 'fake config'
64 self.cnxn = 'fake cnxn'
65
66
67 def testRecomputeDerivedFields_Disabled(self):
68 """Servlet should just call RecomputeAllDerivedFieldsNow with no bounds."""
69 saved_flag = settings.recompute_derived_fields_in_worker
70 settings.recompute_derived_fields_in_worker = False
71
72 filterrules_helpers.RecomputeAllDerivedFields(
73 self.cnxn, self.services, self.project, self.config)
74 self.assertTrue(self.services.issue.get_all_issues_in_project_called)
75 self.assertTrue(self.services.issue.update_issues_called)
76 self.assertTrue(self.services.issue.enqueue_issues_called)
77
78 settings.recompute_derived_fields_in_worker = saved_flag
79
80 def testRecomputeDerivedFields_DisabledNextIDSet(self):
81 """Servlet should just call RecomputeAllDerivedFields with no bounds."""
82 saved_flag = settings.recompute_derived_fields_in_worker
83 settings.recompute_derived_fields_in_worker = False
84 self.services.issue.next_id = 1234
85
86 filterrules_helpers.RecomputeAllDerivedFields(
87 self.cnxn, self.services, self.project, self.config)
88 self.assertTrue(self.services.issue.get_all_issues_in_project_called)
89 self.assertTrue(self.services.issue.enqueue_issues_called)
90
91 settings.recompute_derived_fields_in_worker = saved_flag
92
93 def testRecomputeDerivedFields_NoIssues(self):
94 """Servlet should not call because there is no work to do."""
95 saved_flag = settings.recompute_derived_fields_in_worker
96 settings.recompute_derived_fields_in_worker = True
97
98 filterrules_helpers.RecomputeAllDerivedFields(
99 self.cnxn, self.services, self.project, self.config)
100 self.assertFalse(self.services.issue.get_all_issues_in_project_called)
101 self.assertFalse(self.services.issue.update_issues_called)
102 self.assertFalse(self.services.issue.enqueue_issues_called)
103
104 settings.recompute_derived_fields_in_worker = saved_flag
105
106 @mock.patch('framework.cloud_tasks_helpers._get_client')
107 def testRecomputeDerivedFields_SomeIssues(self, get_client_mock):
108 """Servlet should enqueue one work item rather than call directly."""
109 saved_flag = settings.recompute_derived_fields_in_worker
110 settings.recompute_derived_fields_in_worker = True
111 self.services.issue.next_id = 1234
112 num_calls = (self.services.issue.next_id // self.BLOCK + 1)
113
114 filterrules_helpers.RecomputeAllDerivedFields(
115 self.cnxn, self.services, self.project, self.config)
116 self.assertFalse(self.services.issue.get_all_issues_in_project_called)
117 self.assertFalse(self.services.issue.update_issues_called)
118 self.assertFalse(self.services.issue.enqueue_issues_called)
119
120 get_client_mock().queue_path.assert_any_call(
121 settings.app_id, settings.CLOUD_TASKS_REGION, 'recomputederivedfields')
122 self.assertEqual(get_client_mock().queue_path.call_count, num_calls)
123 self.assertEqual(get_client_mock().create_task.call_count, num_calls)
124
125 parent = get_client_mock().queue_path()
126 highest_id = self.services.issue.GetHighestLocalID(
127 self.cnxn, self.project.project_id)
128 steps = list(range(1, highest_id + 1, self.BLOCK))
129 steps.reverse()
130 shard_id = 0
131 for step in steps:
132 params = {
133 'project_id': self.project.project_id,
134 'lower_bound': step,
135 'upper_bound': min(step + self.BLOCK, highest_id + 1),
136 'shard_id': shard_id,
137 }
138 task = {
139 'app_engine_http_request':
140 {
141 'relative_uri': urls.RECOMPUTE_DERIVED_FIELDS_TASK + '.do',
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200142 'body': urllib.parse.urlencode(params),
Copybara854996b2021-09-07 19:36:02 +0000143 'headers':
144 {
145 'Content-type': 'application/x-www-form-urlencoded'
146 }
147 }
148 }
149 get_client_mock().create_task.assert_any_call(
150 parent, task, retry=cloud_tasks_helpers._DEFAULT_RETRY)
151 shard_id = (shard_id + 1) % settings.num_logical_shards
152
153 settings.recompute_derived_fields_in_worker = saved_flag
154
155 @mock.patch('framework.cloud_tasks_helpers._get_client')
156 def testRecomputeDerivedFields_LotsOfIssues(self, get_client_mock):
157 """Servlet should enqueue multiple work items."""
158 saved_flag = settings.recompute_derived_fields_in_worker
159 settings.recompute_derived_fields_in_worker = True
160 self.services.issue.next_id = 12345
161
162 filterrules_helpers.RecomputeAllDerivedFields(
163 self.cnxn, self.services, self.project, self.config)
164
165 self.assertFalse(self.services.issue.get_all_issues_in_project_called)
166 self.assertFalse(self.services.issue.update_issues_called)
167 self.assertFalse(self.services.issue.enqueue_issues_called)
168 num_calls = (self.services.issue.next_id // self.BLOCK + 1)
169 get_client_mock().queue_path.assert_any_call(
170 settings.app_id, settings.CLOUD_TASKS_REGION, 'recomputederivedfields')
171 self.assertEqual(get_client_mock().queue_path.call_count, num_calls)
172 self.assertEqual(get_client_mock().create_task.call_count, num_calls)
173
174 ((_parent, called_task),
175 _kwargs) = get_client_mock().create_task.call_args_list[0]
176 relative_uri = called_task.get('app_engine_http_request').get(
177 'relative_uri')
178 self.assertEqual(relative_uri, urls.RECOMPUTE_DERIVED_FIELDS_TASK + '.do')
179 encoded_params = called_task.get('app_engine_http_request').get('body')
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200180 params = {k: v[0] for k, v in parse_qs(encoded_params).items()}
Copybara854996b2021-09-07 19:36:02 +0000181 self.assertEqual(params['project_id'], str(self.project.project_id))
182 self.assertEqual(
183 params['lower_bound'], str(12345 // self.BLOCK * self.BLOCK + 1))
184 self.assertEqual(params['upper_bound'], str(12345))
185
186 ((_parent, called_task), _kwargs) = get_client_mock().create_task.call_args
187 relative_uri = called_task.get('app_engine_http_request').get(
188 'relative_uri')
189 self.assertEqual(relative_uri, urls.RECOMPUTE_DERIVED_FIELDS_TASK + '.do')
190 encoded_params = called_task.get('app_engine_http_request').get('body')
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200191 params = {k: v[0] for k, v in parse_qs(encoded_params).items()}
Copybara854996b2021-09-07 19:36:02 +0000192 self.assertEqual(params['project_id'], str(self.project.project_id))
193 self.assertEqual(params['lower_bound'], str(1))
194 self.assertEqual(params['upper_bound'], str(self.BLOCK + 1))
195
196 settings.recompute_derived_fields_in_worker = saved_flag
197
198 @mock.patch(
199 'features.filterrules_helpers.ApplyGivenRules', return_value=(True, {}))
200 def testRecomputeAllDerivedFieldsNow(self, apply_mock):
201 """Servlet should reapply all filter rules to project's issues."""
202 self.services.issue.next_id = 12345
203 test_issue_1 = fake.MakeTestIssue(
204 project_id=self.project.project_id, local_id=1, issue_id=1001,
205 summary='sum1', owner_id=100, status='New')
206 test_issue_1.assume_stale = False # We will store this issue.
207 test_issue_2 = fake.MakeTestIssue(
208 project_id=self.project.project_id, local_id=2, issue_id=1002,
209 summary='sum2', owner_id=100, status='New')
210 test_issue_2.assume_stale = False # We will store this issue.
211 test_issues = [test_issue_1, test_issue_2]
212 self.services.issue.TestAddIssue(test_issue_1)
213 self.services.issue.TestAddIssue(test_issue_2)
214
215 filterrules_helpers.RecomputeAllDerivedFieldsNow(
216 self.cnxn, self.services, self.project, self.config)
217
218 self.assertTrue(self.services.issue.get_all_issues_in_project_called)
219 self.assertTrue(self.services.issue.update_issues_called)
220 self.assertTrue(self.services.issue.enqueue_issues_called)
221 self.assertEqual(test_issues, self.services.issue.updated_issues)
222 self.assertEqual([issue.issue_id for issue in test_issues],
223 self.services.issue.enqueued_issues)
224 self.assertEqual(apply_mock.call_count, 2)
225 for test_issue in test_issues:
226 apply_mock.assert_any_call(
227 self.cnxn, self.services, test_issue, self.config, [], [])
228
229
230class FilterRulesHelpersTest(unittest.TestCase):
231
232 def setUp(self):
233 self.cnxn = 'fake cnxn'
234 self.services = service_manager.Services(
235 user=fake.UserService(),
236 project=fake.ProjectService(),
237 issue=fake.IssueService(),
238 config=fake.ConfigService())
239 self.project = self.services.project.TestAddProject('proj', project_id=789)
240 self.other_project = self.services.project.TestAddProject(
241 'otherproj', project_id=890)
242 for email, user_id in TEST_ID_MAP.items():
243 self.services.user.TestAddUser(email, user_id)
244 self.services.config.TestAddLabelsDict(TEST_LABEL_IDS)
245
246 def testApplyRule(self):
247 cnxn = 'fake sql connection'
248 issue = fake.MakeTestIssue(
249 789, 1, ORIG_SUMMARY, 'New', 111, labels=ORIG_LABELS)
250 config = tracker_pb2.ProjectIssueConfig(project_id=self.project.project_id)
251 # Empty label set cannot satisfy rule looking for labels.
252 pred = 'label:a label:b'
253 rule = filterrules_helpers.MakeRule(
254 pred, default_owner_id=1, default_status='S')
255 predicate_ast = query2ast.ParseUserQuery(
256 pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
257 self.assertEqual(
258 (None, None, [], [], [], None, None),
259 filterrules_helpers._ApplyRule(
260 cnxn, self.services, rule, predicate_ast, issue, set(), config))
261
262 pred = 'label:a -label:b'
263 rule = filterrules_helpers.MakeRule(
264 pred, default_owner_id=1, default_status='S')
265 predicate_ast = query2ast.ParseUserQuery(
266 pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
267 self.assertEqual(
268 (None, None, [], [], [], None, None),
269 filterrules_helpers._ApplyRule(
270 cnxn, self.services, rule, predicate_ast, issue, set(), config))
271
272 # Empty label set will satisfy rule looking for missing labels.
273 pred = '-label:a -label:b'
274 rule = filterrules_helpers.MakeRule(
275 pred, default_owner_id=1, default_status='S')
276 predicate_ast = query2ast.ParseUserQuery(
277 pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
278 self.assertEqual(
279 (1, 'S', [], [], [], None, None),
280 filterrules_helpers._ApplyRule(
281 cnxn, self.services, rule, predicate_ast, issue, set(), config))
282
283 # Label set has the needed labels.
284 pred = 'label:a label:b'
285 rule = filterrules_helpers.MakeRule(
286 pred, default_owner_id=1, default_status='S')
287 predicate_ast = query2ast.ParseUserQuery(
288 pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
289 self.assertEqual(
290 (1, 'S', [], [], [], None, None),
291 filterrules_helpers._ApplyRule(
292 cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
293 config))
294
295 # Label set has the needed labels with test for unicode.
296 pred = 'label:a label:b'
297 rule = filterrules_helpers.MakeRule(
298 pred, default_owner_id=1, default_status='S')
299 predicate_ast = query2ast.ParseUserQuery(
300 pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
301 self.assertEqual(
302 (1, 'S', [], [], [], None, None),
303 filterrules_helpers._ApplyRule(
304 cnxn, self.services, rule, predicate_ast, issue, {u'a', u'b'},
305 config))
306
307 # Label set has the needed labels, capitalization irrelevant.
308 pred = 'label:A label:B'
309 rule = filterrules_helpers.MakeRule(
310 pred, default_owner_id=1, default_status='S')
311 predicate_ast = query2ast.ParseUserQuery(
312 pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
313 self.assertEqual(
314 (1, 'S', [], [], [], None, None),
315 filterrules_helpers._ApplyRule(
316 cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
317 config))
318
319 # Label set has a label, the rule negates.
320 pred = 'label:a -label:b'
321 rule = filterrules_helpers.MakeRule(
322 pred, default_owner_id=1, default_status='S')
323 predicate_ast = query2ast.ParseUserQuery(
324 pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
325 self.assertEqual(
326 (None, None, [], [], [], None, None),
327 filterrules_helpers._ApplyRule(
328 cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
329 config))
330
331 # Consequence is to add a warning.
332 pred = 'label:a'
333 rule = filterrules_helpers.MakeRule(
334 pred, warning='Hey look out')
335 predicate_ast = query2ast.ParseUserQuery(
336 pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
337 self.assertEqual(
338 (None, None, [], [], [], 'Hey look out', None),
339 filterrules_helpers._ApplyRule(
340 cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
341 config))
342
343 # Consequence is to add an error.
344 pred = 'label:a'
345 rule = filterrules_helpers.MakeRule(
346 pred, error='We cannot allow that')
347 predicate_ast = query2ast.ParseUserQuery(
348 pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
349 self.assertEqual(
350 (None, None, [], [], [], None, 'We cannot allow that'),
351 filterrules_helpers._ApplyRule(
352 cnxn, self.services, rule, predicate_ast, issue, {'a', 'b'},
353 config))
354
355 def testComputeDerivedFields_Components(self):
356 cnxn = 'fake sql connection'
357 rules = []
358 component_defs = [
359 tracker_bizobj.MakeComponentDef(
360 10, 789, 'DB', 'database', False, [],
361 [TEST_ID_MAP['db@example.com'],
362 TEST_ID_MAP['ui-db@example.com']],
363 0, 0,
364 label_ids=[TEST_LABEL_IDS['i18n'],
365 TEST_LABEL_IDS['Priority-High']]),
366 tracker_bizobj.MakeComponentDef(
367 20, 789, 'Install', 'installer', False, [],
368 [], 0, 0),
369 tracker_bizobj.MakeComponentDef(
370 30, 789, 'UI', 'doc', False, [],
371 [TEST_ID_MAP['ui@example.com'],
372 TEST_ID_MAP['ui-db@example.com']],
373 0, 0,
374 label_ids=[TEST_LABEL_IDS['i18n'],
375 TEST_LABEL_IDS['l10n'],
376 TEST_LABEL_IDS['Priority-Medium']]),
377 ]
378 excl_prefixes = ['Priority', 'type', 'milestone']
379 config = tracker_pb2.ProjectIssueConfig(
380 exclusive_label_prefixes=excl_prefixes,
381 component_defs=component_defs)
382 predicate_asts = filterrules_helpers.ParsePredicateASTs(rules, config, [])
383
384 # No components.
385 issue = fake.MakeTestIssue(
386 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS)
387 self.assertEqual(
388 (0, '', [], [], [], {}, [], []),
389 filterrules_helpers._ComputeDerivedFields(
390 cnxn, self.services, issue, config, rules, predicate_asts))
391
392 # One component, no CCs or labels added
393 issue.component_ids = [20]
394 issue = fake.MakeTestIssue(
395 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS)
396 self.assertEqual(
397 (0, '', [], [], [], {}, [], []),
398 filterrules_helpers._ComputeDerivedFields(
399 cnxn, self.services, issue, config, rules, predicate_asts))
400
401 # One component, some CCs and labels added
402 issue = fake.MakeTestIssue(
403 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS,
404 component_ids=[10])
405 traces = {
406 (tracker_pb2.FieldID.CC, TEST_ID_MAP['db@example.com']):
407 'Added by component DB',
408 (tracker_pb2.FieldID.CC, TEST_ID_MAP['ui-db@example.com']):
409 'Added by component DB',
410 (tracker_pb2.FieldID.LABELS, 'i18n'):
411 'Added by component DB',
412 (tracker_pb2.FieldID.LABELS, 'Priority-High'):
413 'Added by component DB',
414 }
415 self.assertEqual(
416 (
417 0, '', [
418 TEST_ID_MAP['db@example.com'], TEST_ID_MAP['ui-db@example.com']
419 ], ['i18n', 'Priority-High'], [], traces, [], []),
420 filterrules_helpers._ComputeDerivedFields(
421 cnxn, self.services, issue, config, rules, predicate_asts))
422
423 # One component, CCs and labels not added because of labels on the issue.
424 issue = fake.MakeTestIssue(
425 789, 1, ORIG_SUMMARY, 'New', 0, labels=['Priority-Low', 'i18n'],
426 component_ids=[10])
427 issue.cc_ids = [TEST_ID_MAP['db@example.com']]
428 traces = {
429 (tracker_pb2.FieldID.CC, TEST_ID_MAP['ui-db@example.com']):
430 'Added by component DB',
431 }
432 self.assertEqual(
433 (0, '', [TEST_ID_MAP['ui-db@example.com']], [], [], traces, [], []),
434 filterrules_helpers._ComputeDerivedFields(
435 cnxn, self.services, issue, config, rules, predicate_asts))
436
437 # Multiple components, added CCs treated as a set, exclusive labels in later
438 # components take priority over earlier ones.
439 issue = fake.MakeTestIssue(
440 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS,
441 component_ids=[10, 30])
442 traces = {
443 (tracker_pb2.FieldID.CC, TEST_ID_MAP['db@example.com']):
444 'Added by component DB',
445 (tracker_pb2.FieldID.CC, TEST_ID_MAP['ui-db@example.com']):
446 'Added by component DB',
447 (tracker_pb2.FieldID.LABELS, 'i18n'):
448 'Added by component DB',
449 (tracker_pb2.FieldID.LABELS, 'Priority-High'):
450 'Added by component DB',
451 (tracker_pb2.FieldID.CC, TEST_ID_MAP['ui@example.com']):
452 'Added by component UI',
453 (tracker_pb2.FieldID.LABELS, 'Priority-Medium'):
454 'Added by component UI',
455 (tracker_pb2.FieldID.LABELS, 'l10n'):
456 'Added by component UI',
457 }
458 self.assertEqual(
459 (
460 0, '', [
461 TEST_ID_MAP['db@example.com'], TEST_ID_MAP['ui-db@example.com'],
462 TEST_ID_MAP['ui@example.com']
463 ], ['i18n', 'l10n', 'Priority-Medium'], [], traces, [], []),
464 filterrules_helpers._ComputeDerivedFields(
465 cnxn, self.services, issue, config, rules, predicate_asts))
466
467 def testComputeDerivedFields_Rules(self):
468 cnxn = 'fake sql connection'
469 rules = [
470 filterrules_helpers.MakeRule(
471 'label:HasWorkaround', add_labels=['Priority-Low']),
472 filterrules_helpers.MakeRule(
473 'label:Security', add_labels=['Private']),
474 filterrules_helpers.MakeRule(
475 'label:Security', add_labels=['Priority-High'],
476 add_notify=['jrobbins@chromium.org']),
477 filterrules_helpers.MakeRule(
478 'Priority=High label:Regression', add_labels=['Urgent']),
479 filterrules_helpers.MakeRule(
480 'Size=L', default_owner_id=444),
481 filterrules_helpers.MakeRule(
482 'Size=XL', warning='It will take too long'),
483 filterrules_helpers.MakeRule(
484 'Size=XL', warning='It will cost too much'),
485 ]
486 excl_prefixes = ['Priority', 'type', 'milestone']
487 config = tracker_pb2.ProjectIssueConfig(
488 exclusive_label_prefixes=excl_prefixes,
489 project_id=self.project.project_id)
490 predicate_asts = filterrules_helpers.ParsePredicateASTs(rules, config, [])
491
492 # No rules fire.
493 issue = fake.MakeTestIssue(
494 789, 1, ORIG_SUMMARY, 'New', 0, labels=ORIG_LABELS)
495 self.assertEqual(
496 (0, '', [], [], [], {}, [], []),
497 filterrules_helpers._ComputeDerivedFields(
498 cnxn, self.services, issue, config, rules, predicate_asts))
499
500 issue = fake.MakeTestIssue(
501 789, 1, ORIG_SUMMARY, 'New', 0, labels=['foo', 'bar'])
502 self.assertEqual(
503 (0, '', [], [], [], {}, [], []),
504 filterrules_helpers._ComputeDerivedFields(
505 cnxn, self.services, issue, config, rules, predicate_asts))
506
507 # One rule fires.
508 issue = fake.MakeTestIssue(
509 789, 1, ORIG_SUMMARY, 'New', 0, labels=['Size-L'])
510 traces = {
511 (tracker_pb2.FieldID.OWNER, 444):
512 'Added by rule: IF Size=L THEN SET DEFAULT OWNER',
513 }
514 self.assertEqual(
515 (444, '', [], [], [], traces, [], []),
516 filterrules_helpers._ComputeDerivedFields(
517 cnxn, self.services, issue, config, rules, predicate_asts))
518
519 # One rule fires, but no effect because of explicit fields.
520 issue = fake.MakeTestIssue(
521 789, 1, ORIG_SUMMARY, 'New', 0,
522 labels=['HasWorkaround', 'Priority-Critical'])
523 traces = {}
524 self.assertEqual(
525 (0, '', [], [], [], traces, [], []),
526 filterrules_helpers._ComputeDerivedFields(
527 cnxn, self.services, issue, config, rules, predicate_asts))
528
529 # One rule fires, another has no effect because of explicit exclusive label.
530 issue = fake.MakeTestIssue(
531 789, 1, ORIG_SUMMARY, 'New', 0,
532 labels=['Security', 'Priority-Critical'])
533 traces = {
534 (tracker_pb2.FieldID.LABELS, 'Private'):
535 'Added by rule: IF label:Security THEN ADD LABEL',
536 }
537 self.assertEqual(
538 (0, '', [], ['Private'], ['jrobbins@chromium.org'], traces, [], []),
539 filterrules_helpers._ComputeDerivedFields(
540 cnxn, self.services, issue, config, rules, predicate_asts))
541
542 # Multiple rules have cumulative effect.
543 issue = fake.MakeTestIssue(
544 789, 1, ORIG_SUMMARY, 'New', 0, labels=['HasWorkaround', 'Size-L'])
545 traces = {
546 (tracker_pb2.FieldID.LABELS, 'Priority-Low'):
547 'Added by rule: IF label:HasWorkaround THEN ADD LABEL',
548 (tracker_pb2.FieldID.OWNER, 444):
549 'Added by rule: IF Size=L THEN SET DEFAULT OWNER',
550 }
551 self.assertEqual(
552 (444, '', [], ['Priority-Low'], [], traces, [], []),
553 filterrules_helpers._ComputeDerivedFields(
554 cnxn, self.services, issue, config, rules, predicate_asts))
555
556 # Multiple rules have cumulative warnings.
557 issue = fake.MakeTestIssue(
558 789, 1, ORIG_SUMMARY, 'New', 0, labels=['Size-XL'])
559 traces = {
560 (tracker_pb2.FieldID.WARNING, 'It will take too long'):
561 'Added by rule: IF Size=XL THEN ADD WARNING',
562 (tracker_pb2.FieldID.WARNING, 'It will cost too much'):
563 'Added by rule: IF Size=XL THEN ADD WARNING',
564 }
565 self.assertEqual(
566 (
567 0, '', [], [], [], traces,
568 ['It will take too long', 'It will cost too much'], []),
569 filterrules_helpers._ComputeDerivedFields(
570 cnxn, self.services, issue, config, rules, predicate_asts))
571
572 # Two rules fire, second overwrites the first.
573 issue = fake.MakeTestIssue(
574 789, 1, ORIG_SUMMARY, 'New', 0, labels=['HasWorkaround', 'Security'])
575 traces = {
576 (tracker_pb2.FieldID.LABELS, 'Priority-Low'):
577 'Added by rule: IF label:HasWorkaround THEN ADD LABEL',
578 (tracker_pb2.FieldID.LABELS, 'Priority-High'):
579 'Added by rule: IF label:Security THEN ADD LABEL',
580 (tracker_pb2.FieldID.LABELS, 'Private'):
581 'Added by rule: IF label:Security THEN ADD LABEL',
582 }
583 self.assertEqual(
584 (
585 0, '', [], ['Private', 'Priority-High'], ['jrobbins@chromium.org'],
586 traces, [], []),
587 filterrules_helpers._ComputeDerivedFields(
588 cnxn, self.services, issue, config, rules, predicate_asts))
589
590 # Two rules fire, second triggered by the first.
591 issue = fake.MakeTestIssue(
592 789, 1, ORIG_SUMMARY, 'New', 0, labels=['Security', 'Regression'])
593 traces = {
594 (tracker_pb2.FieldID.LABELS, 'Priority-High'):
595 'Added by rule: IF label:Security THEN ADD LABEL',
596 (tracker_pb2.FieldID.LABELS, 'Urgent'):
597 'Added by rule: IF Priority=High label:Regression THEN ADD LABEL',
598 (tracker_pb2.FieldID.LABELS, 'Private'):
599 'Added by rule: IF label:Security THEN ADD LABEL',
600 }
601 self.assertEqual(
602 (
603 0, '', [], ['Private', 'Priority-High', 'Urgent'],
604 ['jrobbins@chromium.org'], traces, [], []),
605 filterrules_helpers._ComputeDerivedFields(
606 cnxn, self.services, issue, config, rules, predicate_asts))
607
608 # Two rules fire, each one wants to add the same CC: only add once.
609 rules.append(filterrules_helpers.MakeRule('Watch', add_cc_ids=[111]))
610 rules.append(filterrules_helpers.MakeRule('Monitor', add_cc_ids=[111]))
611 config = tracker_pb2.ProjectIssueConfig(
612 exclusive_label_prefixes=excl_prefixes,
613 project_id=self.project.project_id)
614 predicate_asts = filterrules_helpers.ParsePredicateASTs(rules, config, [])
615 traces = {
616 (tracker_pb2.FieldID.CC, 111):
617 'Added by rule: IF Watch THEN ADD CC',
618 }
619 issue = fake.MakeTestIssue(
620 789, 1, ORIG_SUMMARY, 'New', 111, labels=['Watch', 'Monitor'])
621 self.assertEqual(
622 (0, '', [111], [], [], traces, [], []),
623 filterrules_helpers._ComputeDerivedFields(
624 cnxn, self.services, issue, config, rules, predicate_asts))
625
626 def testCompareComponents_Trivial(self):
627 config = tracker_pb2.ProjectIssueConfig()
628 self.assertTrue(filterrules_helpers._CompareComponents(
629 config, ast_pb2.QueryOp.IS_DEFINED, [], [123]))
630 self.assertFalse(filterrules_helpers._CompareComponents(
631 config, ast_pb2.QueryOp.IS_NOT_DEFINED, [], [123]))
632 self.assertFalse(filterrules_helpers._CompareComponents(
633 config, ast_pb2.QueryOp.IS_DEFINED, [], []))
634 self.assertTrue(filterrules_helpers._CompareComponents(
635 config, ast_pb2.QueryOp.IS_NOT_DEFINED, [], []))
636 self.assertFalse(filterrules_helpers._CompareComponents(
637 config, ast_pb2.QueryOp.EQ, [123], []))
638
639 def testCompareComponents_Normal(self):
640 config = tracker_pb2.ProjectIssueConfig()
641 config.component_defs.append(tracker_bizobj.MakeComponentDef(
642 100, 789, 'UI', 'doc', False, [], [], 0, 0))
643 config.component_defs.append(tracker_bizobj.MakeComponentDef(
644 110, 789, 'UI>Help', 'doc', False, [], [], 0, 0))
645 config.component_defs.append(tracker_bizobj.MakeComponentDef(
646 200, 789, 'Networking', 'doc', False, [], [], 0, 0))
647
648 # Check if the issue is in a specified component or subcomponent.
649 self.assertTrue(filterrules_helpers._CompareComponents(
650 config, ast_pb2.QueryOp.EQ, ['UI'], [100]))
651 self.assertTrue(filterrules_helpers._CompareComponents(
652 config, ast_pb2.QueryOp.EQ, ['UI>Help'], [110]))
653 self.assertTrue(filterrules_helpers._CompareComponents(
654 config, ast_pb2.QueryOp.EQ, ['UI'], [100, 110]))
655 self.assertFalse(filterrules_helpers._CompareComponents(
656 config, ast_pb2.QueryOp.EQ, ['UI'], []))
657 self.assertFalse(filterrules_helpers._CompareComponents(
658 config, ast_pb2.QueryOp.EQ, ['UI'], [110]))
659 self.assertFalse(filterrules_helpers._CompareComponents(
660 config, ast_pb2.QueryOp.EQ, ['UI'], [200]))
661 self.assertFalse(filterrules_helpers._CompareComponents(
662 config, ast_pb2.QueryOp.EQ, ['UI>Help'], [100]))
663 self.assertFalse(filterrules_helpers._CompareComponents(
664 config, ast_pb2.QueryOp.EQ, ['Networking'], [100]))
665
666 self.assertTrue(filterrules_helpers._CompareComponents(
667 config, ast_pb2.QueryOp.NE, ['UI'], []))
668 self.assertFalse(filterrules_helpers._CompareComponents(
669 config, ast_pb2.QueryOp.NE, ['UI'], [100]))
670 self.assertTrue(filterrules_helpers._CompareComponents(
671 config, ast_pb2.QueryOp.NE, ['Networking'], [100]))
672
673 # Exact vs non-exact.
674 self.assertFalse(filterrules_helpers._CompareComponents(
675 config, ast_pb2.QueryOp.EQ, ['Help'], [110]))
676 self.assertTrue(filterrules_helpers._CompareComponents(
677 config, ast_pb2.QueryOp.TEXT_HAS, ['UI'], [110]))
678 self.assertFalse(filterrules_helpers._CompareComponents(
679 config, ast_pb2.QueryOp.TEXT_HAS, ['Help'], [110]))
680 self.assertFalse(filterrules_helpers._CompareComponents(
681 config, ast_pb2.QueryOp.NOT_TEXT_HAS, ['UI'], [110]))
682 self.assertTrue(filterrules_helpers._CompareComponents(
683 config, ast_pb2.QueryOp.NOT_TEXT_HAS, ['Help'], [110]))
684
685 # Multivalued issues and Quick-OR notation
686 self.assertTrue(filterrules_helpers._CompareComponents(
687 config, ast_pb2.QueryOp.EQ, ['Networking'], [200]))
688 self.assertFalse(filterrules_helpers._CompareComponents(
689 config, ast_pb2.QueryOp.EQ, ['Networking'], [100, 110]))
690 self.assertTrue(filterrules_helpers._CompareComponents(
691 config, ast_pb2.QueryOp.EQ, ['UI', 'Networking'], [100]))
692 self.assertFalse(filterrules_helpers._CompareComponents(
693 config, ast_pb2.QueryOp.EQ, ['UI', 'Networking'], [110]))
694 self.assertTrue(filterrules_helpers._CompareComponents(
695 config, ast_pb2.QueryOp.EQ, ['UI', 'Networking'], [200]))
696 self.assertTrue(filterrules_helpers._CompareComponents(
697 config, ast_pb2.QueryOp.EQ, ['UI', 'Networking'], [110, 200]))
698 self.assertTrue(filterrules_helpers._CompareComponents(
699 config, ast_pb2.QueryOp.TEXT_HAS, ['UI', 'Networking'], [110, 200]))
700 self.assertTrue(filterrules_helpers._CompareComponents(
701 config, ast_pb2.QueryOp.EQ, ['UI>Help', 'Networking'], [110, 200]))
702
703 def testCompareIssueRefs_Trivial(self):
704 self.assertTrue(filterrules_helpers._CompareIssueRefs(
705 self.cnxn, self.services, self.project,
706 ast_pb2.QueryOp.IS_DEFINED, [], [123]))
707 self.assertFalse(filterrules_helpers._CompareIssueRefs(
708 self.cnxn, self.services, self.project,
709 ast_pb2.QueryOp.IS_NOT_DEFINED, [], [123]))
710 self.assertFalse(filterrules_helpers._CompareIssueRefs(
711 self.cnxn, self.services, self.project,
712 ast_pb2.QueryOp.IS_DEFINED, [], []))
713 self.assertTrue(filterrules_helpers._CompareIssueRefs(
714 self.cnxn, self.services, self.project,
715 ast_pb2.QueryOp.IS_NOT_DEFINED, [], []))
716 self.assertFalse(filterrules_helpers._CompareIssueRefs(
717 self.cnxn, self.services, self.project,
718 ast_pb2.QueryOp.EQ, ['1'], []))
719
720 def testCompareIssueRefs_Normal(self):
721 self.services.issue.TestAddIssue(fake.MakeTestIssue(
722 789, 1, 'summary', 'New', 0, issue_id=123))
723 self.services.issue.TestAddIssue(fake.MakeTestIssue(
724 789, 2, 'summary', 'New', 0, issue_id=124))
725 self.services.issue.TestAddIssue(fake.MakeTestIssue(
726 890, 1, 'other summary', 'New', 0, issue_id=125))
727
728 # EQ and NE, implict references to the current project.
729 self.assertTrue(filterrules_helpers._CompareIssueRefs(
730 self.cnxn, self.services, self.project,
731 ast_pb2.QueryOp.EQ, ['1'], [123]))
732 self.assertFalse(filterrules_helpers._CompareIssueRefs(
733 self.cnxn, self.services, self.project,
734 ast_pb2.QueryOp.NE, ['1'], [123]))
735
736 # EQ and NE, explicit project references.
737 self.assertTrue(filterrules_helpers._CompareIssueRefs(
738 self.cnxn, self.services, self.project,
739 ast_pb2.QueryOp.EQ, ['proj:1'], [123]))
740 self.assertTrue(filterrules_helpers._CompareIssueRefs(
741 self.cnxn, self.services, self.project,
742 ast_pb2.QueryOp.EQ, ['otherproj:1'], [125]))
743
744 # Inequalities
745 self.assertTrue(filterrules_helpers._CompareIssueRefs(
746 self.cnxn, self.services, self.project,
747 ast_pb2.QueryOp.GE, ['1'], [123]))
748 self.assertTrue(filterrules_helpers._CompareIssueRefs(
749 self.cnxn, self.services, self.project,
750 ast_pb2.QueryOp.GE, ['1'], [124]))
751 self.assertTrue(filterrules_helpers._CompareIssueRefs(
752 self.cnxn, self.services, self.project,
753 ast_pb2.QueryOp.GE, ['2'], [124]))
754 self.assertFalse(filterrules_helpers._CompareIssueRefs(
755 self.cnxn, self.services, self.project,
756 ast_pb2.QueryOp.GT, ['2'], [124]))
757
758 def testCompareUsers(self):
759 pass # TODO(jrobbins): Add this test.
760
761 def testCompareUserIDs(self):
762 pass # TODO(jrobbins): Add this test.
763
764 def testCompareEmails(self):
765 pass # TODO(jrobbins): Add this test.
766
767 def testCompare(self):
768 pass # TODO(jrobbins): Add this test.
769
770 def testParseOneRuleAddLabels(self):
771 cnxn = 'fake SQL connection'
772 error_list = []
773 rule_pb = filterrules_helpers._ParseOneRule(
774 cnxn, 'label:lab1 label:lab2', 'add_labels', 'hot cOld, ', None, 1,
775 error_list)
776 self.assertEqual('label:lab1 label:lab2', rule_pb.predicate)
777 self.assertEqual(error_list, [])
778 self.assertEqual(len(rule_pb.add_labels), 2)
779 self.assertEqual(rule_pb.add_labels[0], 'hot')
780 self.assertEqual(rule_pb.add_labels[1], 'cOld')
781
782 rule_pb = filterrules_helpers._ParseOneRule(
783 cnxn, '', 'default_status', 'hot cold', None, 1, error_list)
784 self.assertEqual(len(rule_pb.predicate), 0)
785 self.assertEqual(error_list, [])
786
787 def testParseOneRuleDefaultOwner(self):
788 cnxn = 'fake SQL connection'
789 error_list = []
790 rule_pb = filterrules_helpers._ParseOneRule(
791 cnxn, 'label:lab1, label:lab2 ', 'default_owner', 'jrobbins',
792 self.services.user, 1, error_list)
793 self.assertEqual(error_list, [])
794 self.assertEqual(rule_pb.default_owner_id, TEST_ID_MAP['jrobbins'])
795
796 def testParseOneRuleDefaultStatus(self):
797 cnxn = 'fake SQL connection'
798 error_list = []
799 rule_pb = filterrules_helpers._ParseOneRule(
800 cnxn, 'label:lab1', 'default_status', 'InReview',
801 None, 1, error_list)
802 self.assertEqual(error_list, [])
803 self.assertEqual(rule_pb.default_status, 'InReview')
804
805 def testParseOneRuleAddCcs(self):
806 cnxn = 'fake SQL connection'
807 error_list = []
808 rule_pb = filterrules_helpers._ParseOneRule(
809 cnxn, 'label:lab1', 'add_ccs', 'jrobbins, mike.j.parent',
810 self.services.user, 1, error_list)
811 self.assertEqual(error_list, [])
812 self.assertEqual(rule_pb.add_cc_ids[0], TEST_ID_MAP['jrobbins'])
813 self.assertEqual(rule_pb.add_cc_ids[1], TEST_ID_MAP['mike.j.parent'])
814 self.assertEqual(len(rule_pb.add_cc_ids), 2)
815
816 def testParseRulesNone(self):
817 cnxn = 'fake SQL connection'
818 post_data = {}
819 rules = filterrules_helpers.ParseRules(
820 cnxn, post_data, None, template_helpers.EZTError())
821 self.assertEqual(rules, [])
822
823 def testParseRules(self):
824 cnxn = 'fake SQL connection'
825 post_data = {
826 'predicate1': 'a, b c',
827 'action_type1': 'default_status',
828 'action_value1': 'Reviewed',
829 'predicate2': 'a, b c',
830 'action_type2': 'default_owner',
831 'action_value2': 'jrobbins',
832 'predicate3': 'a, b c',
833 'action_type3': 'add_ccs',
834 'action_value3': 'jrobbins, mike.j.parent',
835 'predicate4': 'a, b c',
836 'action_type4': 'add_labels',
837 'action_value4': 'hot, cold',
838 }
839 errors = template_helpers.EZTError()
840 rules = filterrules_helpers.ParseRules(
841 cnxn, post_data, self.services.user, errors)
842 self.assertEqual(rules[0].predicate, 'a, b c')
843 self.assertEqual(rules[0].default_status, 'Reviewed')
844 self.assertEqual(rules[1].default_owner_id, TEST_ID_MAP['jrobbins'])
845 self.assertEqual(rules[2].add_cc_ids[0], TEST_ID_MAP['jrobbins'])
846 self.assertEqual(rules[2].add_cc_ids[1], TEST_ID_MAP['mike.j.parent'])
847 self.assertEqual(rules[3].add_labels[0], 'hot')
848 self.assertEqual(rules[3].add_labels[1], 'cold')
849 self.assertEqual(len(rules), 4)
850 self.assertFalse(errors.AnyErrors())
851
852 def testOwnerCcsInvolvedInFilterRules(self):
853 rules = [
854 tracker_pb2.FilterRule(add_cc_ids=[111, 333], default_owner_id=999),
855 tracker_pb2.FilterRule(default_owner_id=888),
856 tracker_pb2.FilterRule(add_cc_ids=[999, 777]),
857 tracker_pb2.FilterRule(),
858 ]
859 actual_user_ids = filterrules_helpers.OwnerCcsInvolvedInFilterRules(rules)
860 self.assertItemsEqual([111, 333, 777, 888, 999], actual_user_ids)
861
862 def testBuildFilterRuleStrings(self):
863 rules = [
864 tracker_pb2.FilterRule(
865 predicate='label:machu', add_cc_ids=[111, 333, 999]),
866 tracker_pb2.FilterRule(predicate='label:pichu', default_owner_id=222),
867 tracker_pb2.FilterRule(
868 predicate='owner:farmer@test.com',
869 add_labels=['cows-farting', 'chicken', 'machu-pichu']),
870 tracker_pb2.FilterRule(predicate='label:beach', default_status='New'),
871 tracker_pb2.FilterRule(
872 predicate='label:rainforest',
873 add_notify_addrs=['cake@test.com', 'pie@test.com']),
874 ]
875 emails_by_id = {
876 111: 'cow@test.com', 222: 'fox@test.com', 333: 'llama@test.com'}
877 rule_strs = filterrules_helpers.BuildFilterRuleStrings(rules, emails_by_id)
878
879 self.assertItemsEqual(
880 rule_strs, [
881 'if label:machu '
882 'then add cc(s): cow@test.com, llama@test.com, user not found',
883 'if label:pichu then set default owner: fox@test.com',
884 'if owner:farmer@test.com '
885 'then add label(s): cows-farting, chicken, machu-pichu',
886 'if label:beach then set default status: New',
887 'if label:rainforest then notify: cake@test.com, pie@test.com',
888 ])
889
890 def testBuildRedactedFilterRuleStrings(self):
891 rules_by_project = {
892 16: [
893 tracker_pb2.FilterRule(
894 predicate='label:machu', add_cc_ids=[111, 333, 999]),
895 tracker_pb2.FilterRule(
896 predicate='label:pichu', default_owner_id=222)],
897 19: [
898 tracker_pb2.FilterRule(
899 predicate='owner:farmer@test.com',
900 add_labels=['cows-farting', 'chicken', 'machu-pichu']),
901 tracker_pb2.FilterRule(
902 predicate='label:rainforest',
903 add_notify_addrs=['cake@test.com', 'pie@test.com'])],
904 }
905 deleted_emails = ['farmer@test.com', 'pie@test.com', 'fox@test.com']
906 self.services.user.TestAddUser('cow@test.com', 111)
907 self.services.user.TestAddUser('fox@test.com', 222)
908 self.services.user.TestAddUser('llama@test.com', 333)
909 actual = filterrules_helpers.BuildRedactedFilterRuleStrings(
910 self.cnxn, rules_by_project, self.services.user, deleted_emails)
911
912 self.assertItemsEqual(
913 actual,
914 {16: [
915 'if label:machu '
916 'then add cc(s): cow@test.com, llama@test.com, user not found',
917 'if label:pichu '
918 'then set default owner: %s' %
919 framework_constants.DELETED_USER_NAME],
920 19: [
921 'if owner:%s '
922 'then add label(s): cows-farting, chicken, machu-pichu' %
923 framework_constants.DELETED_USER_NAME,
924 'if label:rainforest '
925 'then notify: cake@test.com, %s' %
926 framework_constants.DELETED_USER_NAME],
927 })