Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 1 | # 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. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Unit tests for sorting.py functions.""" |
| 6 | from __future__ import print_function |
| 7 | from __future__ import division |
| 8 | from __future__ import absolute_import |
| 9 | |
| 10 | import unittest |
| 11 | # For convenient debugging |
| 12 | import logging |
| 13 | |
Adrià Vilanova Martínez | 9f9ade5 | 2022-10-10 23:20:11 +0200 | [diff] [blame] | 14 | try: |
| 15 | from mox3 import mox |
| 16 | except ImportError: |
| 17 | import mox |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 18 | |
| 19 | from framework import sorting |
| 20 | from framework import framework_views |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 21 | from mrproto import tracker_pb2 |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 22 | from testing import fake |
| 23 | from testing import testing_helpers |
| 24 | from tracker import tracker_bizobj |
| 25 | |
| 26 | |
| 27 | def MakeDescending(accessor): |
| 28 | return sorting._MaybeMakeDescending(accessor, True) |
| 29 | |
| 30 | |
| 31 | class 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ínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 47 | self.assertGreater(anti_a, anti_b) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 48 | |
| 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ínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 53 | self.assertEqual(asc_value, 'a') |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 54 | |
| 55 | desc_accessor = sorting._MaybeMakeDescending(lambda issue: 'a', True) |
| 56 | print(desc_accessor) |
| 57 | desc_value = desc_accessor('fake issue') |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 58 | self.assertIsInstance(desc_value, sorting.DescendingValue) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 59 | |
| 60 | |
| 61 | class 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ínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 163 | self.assertEqual([sorting.MAX_STRING], accessor(art)) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 164 | neg_accessor = MakeDescending(accessor) |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 165 | self.assertEqual([sorting.MAX_STRING], neg_accessor(art)) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 166 | |
| 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ínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 188 | self.assertEqual([sorting.MAX_STRING], accessor(art)) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 189 | neg_accessor = MakeDescending(accessor) |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 190 | self.assertEqual([sorting.MAX_STRING], neg_accessor(art)) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 191 | |
| 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ínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 214 | self.assertEqual([sorting.MAX_STRING], accessor(art)) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 215 | neg_accessor = MakeDescending(accessor) |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 216 | self.assertEqual([sorting.MAX_STRING], neg_accessor(art)) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 217 | |
| 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')) |