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