blob: 44aa6f719d5e892191a4c08f391522416995acf5 [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 sorting.py functions."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import unittest
11# For convenient debugging
12import logging
13
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020014try:
15 from mox3 import mox
16except ImportError:
17 import mox
Copybara854996b2021-09-07 19:36:02 +000018
19from framework import sorting
20from framework import framework_views
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010021from mrproto import tracker_pb2
Copybara854996b2021-09-07 19:36:02 +000022from testing import fake
23from testing import testing_helpers
24from tracker import tracker_bizobj
25
26
27def MakeDescending(accessor):
28 return sorting._MaybeMakeDescending(accessor, True)
29
30
31class DescendingValueTest(unittest.TestCase):
32
33 def testMinString(self):
34 """When sorting desc, a min string will sort last instead of first."""
35 actual = sorting.DescendingValue.MakeDescendingValue(sorting.MIN_STRING)
36 self.assertEqual(sorting.MAX_STRING, actual)
37
38 def testMaxString(self):
39 """When sorting desc, a max string will sort first instead of last."""
40 actual = sorting.DescendingValue.MakeDescendingValue(sorting.MAX_STRING)
41 self.assertEqual(sorting.MIN_STRING, actual)
42
43 def testDescValues(self):
44 """The point of DescendingValue is to reverse the sort order."""
45 anti_a = sorting.DescendingValue.MakeDescendingValue('a')
46 anti_b = sorting.DescendingValue.MakeDescendingValue('b')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010047 self.assertGreater(anti_a, anti_b)
Copybara854996b2021-09-07 19:36:02 +000048
49 def testMaybeMakeDescending(self):
50 """It returns an accessor that makes DescendingValue iff arg is True."""
51 asc_accessor = sorting._MaybeMakeDescending(lambda issue: 'a', False)
52 asc_value = asc_accessor('fake issue')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010053 self.assertEqual(asc_value, 'a')
Copybara854996b2021-09-07 19:36:02 +000054
55 desc_accessor = sorting._MaybeMakeDescending(lambda issue: 'a', True)
56 print(desc_accessor)
57 desc_value = desc_accessor('fake issue')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010058 self.assertIsInstance(desc_value, sorting.DescendingValue)
Copybara854996b2021-09-07 19:36:02 +000059
60
61class SortingTest(unittest.TestCase):
62
63 def setUp(self):
64 self.mox = mox.Mox()
65 self.default_cols = 'a b c'
66 self.builtin_cols = 'a b x y z'
67 self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
68 self.config.component_defs.append(tracker_bizobj.MakeComponentDef(
69 11, 789, 'Database', 'doc', False, [], [], 0, 0))
70 self.config.component_defs.append(tracker_bizobj.MakeComponentDef(
71 22, 789, 'User Interface', 'doc', True, [], [], 0, 0))
72 self.config.component_defs.append(tracker_bizobj.MakeComponentDef(
73 33, 789, 'Installer', 'doc', False, [], [], 0, 0))
74
75 def tearDown(self):
76 self.mox.UnsetStubs()
77 self.mox.ResetAll()
78
79 def testMakeSingleSortKeyAccessor_Status(self):
80 """Sorting by status should create an accessor for that column."""
81 self.mox.StubOutWithMock(sorting, '_IndexOrLexical')
82 status_names = [wks.status for wks in self.config.well_known_statuses]
83 sorting._IndexOrLexical(status_names, 'status accessor')
84 self.mox.ReplayAll()
85
86 sorting._MakeSingleSortKeyAccessor(
87 'status', self.config, {'status': 'status accessor'}, [], {}, [])
88 self.mox.VerifyAll()
89
90 def testMakeSingleSortKeyAccessor_Component(self):
91 """Sorting by component should create an accessor for that column."""
92 self.mox.StubOutWithMock(sorting, '_IndexListAccessor')
93 component_ids = [11, 33, 22]
94 sorting._IndexListAccessor(component_ids, 'component accessor')
95 self.mox.ReplayAll()
96
97 sorting._MakeSingleSortKeyAccessor(
98 'component', self.config, {'component': 'component accessor'}, [], {}, [])
99 self.mox.VerifyAll()
100
101 def testMakeSingleSortKeyAccessor_OtherBuiltInColunms(self):
102 """Sorting a built-in column should create an accessor for that column."""
103 accessor = sorting._MakeSingleSortKeyAccessor(
104 'buildincol', self.config, {'buildincol': 'accessor'}, [], {}, [])
105 self.assertEqual('accessor', accessor)
106
107 def testMakeSingleSortKeyAccessor_WithPostProcessor(self):
108 """Sorting a built-in user column should create a user accessor."""
109 self.mox.StubOutWithMock(sorting, '_MakeAccessorWithPostProcessor')
110 users_by_id = {111: 'fake user'}
111 sorting._MakeAccessorWithPostProcessor(
112 users_by_id, 'mock owner accessor', 'mock postprocessor')
113 self.mox.ReplayAll()
114
115 sorting._MakeSingleSortKeyAccessor(
116 'owner', self.config, {'owner': 'mock owner accessor'},
117 {'owner': 'mock postprocessor'}, users_by_id, [])
118 self.mox.VerifyAll()
119
120 def testIndexOrLexical(self):
121 well_known_values = ['x-a', 'x-b', 'x-c', 'x-d']
122 art = 'this is a fake artifact'
123
124 # Case 1: accessor generates no values.
125 base_accessor = lambda art: None
126 accessor = sorting._IndexOrLexical(well_known_values, base_accessor)
127 self.assertEqual(sorting.MAX_STRING, accessor(art))
128 neg_accessor = MakeDescending(accessor)
129 self.assertEqual(sorting.DescendingValue(sorting.MAX_STRING),
130 neg_accessor(art))
131
132 # Case 2: accessor generates a value, but it is an empty value.
133 base_accessor = lambda art: ''
134 accessor = sorting._IndexOrLexical(well_known_values, base_accessor)
135 self.assertEqual(sorting.MAX_STRING, accessor(art))
136 neg_accessor = MakeDescending(accessor)
137 self.assertEqual(sorting.DescendingValue(sorting.MAX_STRING),
138 neg_accessor(art))
139
140 # Case 3: A single well-known value
141 base_accessor = lambda art: 'x-c'
142 accessor = sorting._IndexOrLexical(well_known_values, base_accessor)
143 self.assertEqual(2, accessor(art))
144 neg_accessor = MakeDescending(accessor)
145 self.assertEqual(-2, neg_accessor(art))
146
147 # Case 4: A single odd-ball value
148 base_accessor = lambda art: 'x-zzz'
149 accessor = sorting._IndexOrLexical(well_known_values, base_accessor)
150 self.assertEqual('x-zzz', accessor(art))
151 neg_accessor = MakeDescending(accessor)
152 self.assertEqual(
153 sorting.DescendingValue('x-zzz'), neg_accessor(art))
154
155 def testIndexListAccessor_SomeWellKnownValues(self):
156 """Values sort according to their position in the well-known list."""
157 well_known_values = [11, 33, 22] # These represent component IDs.
158 art = fake.MakeTestIssue(789, 1, 'sum 1', 'New', 111)
159 base_accessor = lambda issue: issue.component_ids
160 accessor = sorting._IndexListAccessor(well_known_values, base_accessor)
161
162 # Case 1: accessor generates no values.
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100163 self.assertEqual([sorting.MAX_STRING], accessor(art))
Copybara854996b2021-09-07 19:36:02 +0000164 neg_accessor = MakeDescending(accessor)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100165 self.assertEqual([sorting.MAX_STRING], neg_accessor(art))
Copybara854996b2021-09-07 19:36:02 +0000166
167 # Case 2: A single well-known value
168 art.component_ids = [33]
169 self.assertEqual([1], accessor(art))
170 neg_accessor = MakeDescending(accessor)
171 self.assertEqual([-1], neg_accessor(art))
172
173 # Case 3: Multiple well-known and odd-ball values
174 art.component_ids = [33, 11, 99]
175 self.assertEqual([0, 1, sorting.MAX_STRING], accessor(art))
176 neg_accessor = MakeDescending(accessor)
177 self.assertEqual([sorting.MAX_STRING, -1, 0],
178 neg_accessor(art))
179
180 def testIndexListAccessor_NoWellKnownValues(self):
181 """When there are no well-known values, all values sort last."""
182 well_known_values = [] # Nothing pre-defined, so everything is oddball
183 art = fake.MakeTestIssue(789, 1, 'sum 1', 'New', 111)
184 base_accessor = lambda issue: issue.component_ids
185 accessor = sorting._IndexListAccessor(well_known_values, base_accessor)
186
187 # Case 1: accessor generates no values.
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100188 self.assertEqual([sorting.MAX_STRING], accessor(art))
Copybara854996b2021-09-07 19:36:02 +0000189 neg_accessor = MakeDescending(accessor)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100190 self.assertEqual([sorting.MAX_STRING], neg_accessor(art))
Copybara854996b2021-09-07 19:36:02 +0000191
192 # Case 2: A single oddball value
193 art.component_ids = [33]
194 self.assertEqual([sorting.MAX_STRING], accessor(art))
195 neg_accessor = MakeDescending(accessor)
196 self.assertEqual([sorting.MAX_STRING], neg_accessor(art))
197
198 # Case 3: Multiple odd-ball values
199 art.component_ids = [33, 11, 99]
200 self.assertEqual(
201 [sorting.MAX_STRING, sorting.MAX_STRING, sorting.MAX_STRING],
202 accessor(art))
203 neg_accessor = MakeDescending(accessor)
204 self.assertEqual(
205 [sorting.MAX_STRING, sorting.MAX_STRING, sorting.MAX_STRING],
206 neg_accessor(art))
207
208 def testIndexOrLexicalList(self):
209 well_known_values = ['Pri-High', 'Pri-Med', 'Pri-Low']
210 art = fake.MakeTestIssue(789, 1, 'sum 1', 'New', 111, merged_into=200001)
211
212 # Case 1: accessor generates no values.
213 accessor = sorting._IndexOrLexicalList(well_known_values, [], 'pri', {})
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100214 self.assertEqual([sorting.MAX_STRING], accessor(art))
Copybara854996b2021-09-07 19:36:02 +0000215 neg_accessor = MakeDescending(accessor)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100216 self.assertEqual([sorting.MAX_STRING], neg_accessor(art))
Copybara854996b2021-09-07 19:36:02 +0000217
218 # Case 2: A single well-known value
219 art.labels = ['Pri-Med']
220 accessor = sorting._IndexOrLexicalList(well_known_values, [], 'pri', {})
221 self.assertEqual([1], accessor(art))
222 neg_accessor = MakeDescending(accessor)
223 self.assertEqual([-1], neg_accessor(art))
224
225 # Case 3: Multiple well-known and odd-ball values
226 art.labels = ['Pri-zzz', 'Pri-Med', 'yyy', 'Pri-High']
227 accessor = sorting._IndexOrLexicalList(well_known_values, [], 'pri', {})
228 self.assertEqual([0, 1, 'zzz'], accessor(art))
229 neg_accessor = MakeDescending(accessor)
230 self.assertEqual([sorting.DescendingValue('zzz'), -1, 0],
231 neg_accessor(art))
232
233 # Case 4: Multi-part prefix.
234 well_known_values.extend(['X-Y-Header', 'X-Y-Footer'])
235 art.labels = ['X-Y-Footer', 'X-Y-Zone', 'X-Y-Header', 'X-Y-Area']
236 accessor = sorting._IndexOrLexicalList(well_known_values, [], 'x-y', {})
237 self.assertEqual([3, 4, 'area', 'zone'], accessor(art))
238 neg_accessor = MakeDescending(accessor)
239 self.assertEqual([sorting.DescendingValue('zone'),
240 sorting.DescendingValue('area'), -4, -3],
241 neg_accessor(art))
242
243 def testIndexOrLexicalList_CustomFields(self):
244 art = fake.MakeTestIssue(789, 1, 'sum 2', 'New', 111)
245 art.labels = ['samename-value1']
246 art.field_values = [tracker_bizobj.MakeFieldValue(
247 3, 6078, None, None, None, None, False)]
248
249 all_field_defs = [
250 tracker_bizobj.MakeFieldDef(
251 3, 789, 'samename', tracker_pb2.FieldTypes.INT_TYPE,
252 None, None, False, False, False, None, None, None, False, None,
253 None, None, None, 'cow spots', False),
254 tracker_bizobj.MakeFieldDef(
255 4, 788, 'samename', tracker_pb2.FieldTypes.APPROVAL_TYPE,
256 None, None, False, False, False, None, None, None, False, None,
257 None, None, None, 'cow spots', False),
258 tracker_bizobj.MakeFieldDef(
259 4, 788, 'notsamename', tracker_pb2.FieldTypes.APPROVAL_TYPE,
260 None, None, False, False, False, None, None, None, False, None,
261 None, None, None, 'should get filtered out', False)
262 ]
263
264 accessor = sorting._IndexOrLexicalList([], all_field_defs, 'samename', {})
265 self.assertEqual([6078, 'value1'], accessor(art))
266 neg_accessor = MakeDescending(accessor)
267 self.assertEqual(
268 [sorting.DescendingValue('value1'), -6078], neg_accessor(art))
269
270 def testIndexOrLexicalList_PhaseCustomFields(self):
271 art = fake.MakeTestIssue(789, 1, 'sum 2', 'New', 111)
272 art.labels = ['summer.goats-value1']
273 art.field_values = [
274 tracker_bizobj.MakeFieldValue(
275 3, 33, None, None, None, None, False, phase_id=77),
276 tracker_bizobj.MakeFieldValue(
277 3, 34, None, None, None, None, False, phase_id=77),
278 tracker_bizobj.MakeFieldValue(
279 3, 1000, None, None, None, None, False, phase_id=78)]
280 art.phases = [tracker_pb2.Phase(phase_id=77, name='summer'),
281 tracker_pb2.Phase(phase_id=78, name='winter')]
282
283 all_field_defs = [
284 tracker_bizobj.MakeFieldDef(
285 3, 789, 'goats', tracker_pb2.FieldTypes.INT_TYPE,
286 None, None, False, False, True, None, None, None, False, None,
287 None, None, None, 'goats love mineral', False, is_phase_field=True),
288 tracker_bizobj.MakeFieldDef(
289 4, 788, 'boo', tracker_pb2.FieldTypes.APPROVAL_TYPE,
290 None, None, False, False, False, None, None, None, False, None,
291 None, None, None, 'ahh', False),
292 ]
293
294 accessor = sorting._IndexOrLexicalList(
295 [], all_field_defs, 'summer.goats', {})
296 self.assertEqual([33, 34, 'value1'], accessor(art))
297 neg_accessor = MakeDescending(accessor)
298 self.assertEqual(
299 [sorting.DescendingValue('value1'), -34, -33], neg_accessor(art))
300
301 def testIndexOrLexicalList_ApprovalStatus(self):
302 art = fake.MakeTestIssue(789, 1, 'sum 2', 'New', 111)
303 art.labels = ['samename-value1']
304 art.approval_values = [tracker_pb2.ApprovalValue(approval_id=4)]
305
306 all_field_defs = [
307 tracker_bizobj.MakeFieldDef(
308 3, 789, 'samename', tracker_pb2.FieldTypes.INT_TYPE,
309 None, None, False, False, False, None, None, None, False, None,
310 None, None, None, 'cow spots', False),
311 tracker_bizobj.MakeFieldDef(
312 4, 788, 'samename', tracker_pb2.FieldTypes.APPROVAL_TYPE,
313 None, None, False, False, False, None, None, None, False, None,
314 None, None, None, 'cow spots', False)
315 ]
316
317 accessor = sorting._IndexOrLexicalList([], all_field_defs, 'samename', {})
318 self.assertEqual([0, 'value1'], accessor(art))
319 neg_accessor = MakeDescending(accessor)
320 self.assertEqual([sorting.DescendingValue('value1'),
321 sorting.DescendingValue(0)],
322 neg_accessor(art))
323
324 def testIndexOrLexicalList_ApprovalApprover(self):
325 art = art = fake.MakeTestIssue(789, 1, 'sum 2', 'New', 111)
326 art.labels = ['samename-approver-value1']
327 art.approval_values = [
328 tracker_pb2.ApprovalValue(approval_id=4, approver_ids=[333])]
329
330 all_field_defs = [
331 tracker_bizobj.MakeFieldDef(
332 4, 788, 'samename', tracker_pb2.FieldTypes.APPROVAL_TYPE,
333 None, None, False, False, False, None, None, None, False, None,
334 None, None, None, 'cow spots', False)
335 ]
336 users_by_id = {333: framework_views.StuffUserView(333, 'a@test.com', True)}
337
338 accessor = sorting._IndexOrLexicalList(
339 [], all_field_defs, 'samename-approver', users_by_id)
340 self.assertEqual(['a@test.com', 'value1'], accessor(art))
341 neg_accessor = MakeDescending(accessor)
342 self.assertEqual([sorting.DescendingValue('value1'),
343 sorting.DescendingValue('a@test.com')],
344 neg_accessor(art))
345
346 def testComputeSortDirectives(self):
347 config = tracker_pb2.ProjectIssueConfig()
348 self.assertEqual(
349 ['project', 'id'], sorting.ComputeSortDirectives(config, '', ''))
350
351 self.assertEqual(
352 ['a', 'b', 'c', 'project', 'id'],
353 sorting.ComputeSortDirectives(config, '', 'a b C'))
354
355 config.default_sort_spec = 'id -reporter Owner'
356 self.assertEqual(
357 ['id', '-reporter', 'owner', 'project'],
358 sorting.ComputeSortDirectives(config, '', ''))
359
360 self.assertEqual(
361 ['x', '-b', 'a', 'c', '-owner', 'id', '-reporter', 'project'],
362 sorting.ComputeSortDirectives(config, 'x -b', 'A -b c -owner'))