Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/framework/test/sorting_test.py b/framework/test/sorting_test.py
new file mode 100644
index 0000000..4b1feb3
--- /dev/null
+++ b/framework/test/sorting_test.py
@@ -0,0 +1,360 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for sorting.py functions."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+# For convenient debugging
+import logging
+
+import mox
+
+from framework import sorting
+from framework import framework_views
+from proto import tracker_pb2
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_bizobj
+
+
+def MakeDescending(accessor):
+  return sorting._MaybeMakeDescending(accessor, True)
+
+
+class DescendingValueTest(unittest.TestCase):
+
+  def testMinString(self):
+    """When sorting desc, a min string will sort last instead of first."""
+    actual = sorting.DescendingValue.MakeDescendingValue(sorting.MIN_STRING)
+    self.assertEqual(sorting.MAX_STRING, actual)
+
+  def testMaxString(self):
+    """When sorting desc, a max string will sort first instead of last."""
+    actual = sorting.DescendingValue.MakeDescendingValue(sorting.MAX_STRING)
+    self.assertEqual(sorting.MIN_STRING, actual)
+
+  def testDescValues(self):
+    """The point of DescendingValue is to reverse the sort order."""
+    anti_a = sorting.DescendingValue.MakeDescendingValue('a')
+    anti_b = sorting.DescendingValue.MakeDescendingValue('b')
+    self.assertTrue(anti_a > anti_b)
+
+  def testMaybeMakeDescending(self):
+    """It returns an accessor that makes DescendingValue iff arg is True."""
+    asc_accessor = sorting._MaybeMakeDescending(lambda issue: 'a', False)
+    asc_value = asc_accessor('fake issue')
+    self.assertTrue(asc_value is 'a')
+
+    desc_accessor = sorting._MaybeMakeDescending(lambda issue: 'a', True)
+    print(desc_accessor)
+    desc_value = desc_accessor('fake issue')
+    self.assertTrue(isinstance(desc_value, sorting.DescendingValue))
+
+
+class SortingTest(unittest.TestCase):
+
+  def setUp(self):
+    self.mox = mox.Mox()
+    self.default_cols = 'a b c'
+    self.builtin_cols = 'a b x y z'
+    self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+    self.config.component_defs.append(tracker_bizobj.MakeComponentDef(
+        11, 789, 'Database', 'doc', False, [], [], 0, 0))
+    self.config.component_defs.append(tracker_bizobj.MakeComponentDef(
+        22, 789, 'User Interface', 'doc', True, [], [], 0, 0))
+    self.config.component_defs.append(tracker_bizobj.MakeComponentDef(
+        33, 789, 'Installer', 'doc', False, [], [], 0, 0))
+
+  def tearDown(self):
+    self.mox.UnsetStubs()
+    self.mox.ResetAll()
+
+  def testMakeSingleSortKeyAccessor_Status(self):
+    """Sorting by status should create an accessor for that column."""
+    self.mox.StubOutWithMock(sorting, '_IndexOrLexical')
+    status_names = [wks.status for wks in self.config.well_known_statuses]
+    sorting._IndexOrLexical(status_names, 'status accessor')
+    self.mox.ReplayAll()
+
+    sorting._MakeSingleSortKeyAccessor(
+      'status', self.config, {'status': 'status accessor'}, [], {}, [])
+    self.mox.VerifyAll()
+
+  def testMakeSingleSortKeyAccessor_Component(self):
+    """Sorting by component should create an accessor for that column."""
+    self.mox.StubOutWithMock(sorting, '_IndexListAccessor')
+    component_ids = [11, 33, 22]
+    sorting._IndexListAccessor(component_ids, 'component accessor')
+    self.mox.ReplayAll()
+
+    sorting._MakeSingleSortKeyAccessor(
+      'component', self.config, {'component': 'component accessor'}, [], {}, [])
+    self.mox.VerifyAll()
+
+  def testMakeSingleSortKeyAccessor_OtherBuiltInColunms(self):
+    """Sorting a built-in column should create an accessor for that column."""
+    accessor = sorting._MakeSingleSortKeyAccessor(
+      'buildincol', self.config, {'buildincol': 'accessor'}, [], {}, [])
+    self.assertEqual('accessor', accessor)
+
+  def testMakeSingleSortKeyAccessor_WithPostProcessor(self):
+    """Sorting a built-in user column should create a user accessor."""
+    self.mox.StubOutWithMock(sorting, '_MakeAccessorWithPostProcessor')
+    users_by_id = {111: 'fake user'}
+    sorting._MakeAccessorWithPostProcessor(
+        users_by_id, 'mock owner accessor', 'mock postprocessor')
+    self.mox.ReplayAll()
+
+    sorting._MakeSingleSortKeyAccessor(
+      'owner', self.config, {'owner': 'mock owner accessor'},
+      {'owner': 'mock postprocessor'}, users_by_id, [])
+    self.mox.VerifyAll()
+
+  def testIndexOrLexical(self):
+    well_known_values = ['x-a', 'x-b', 'x-c', 'x-d']
+    art = 'this is a fake artifact'
+
+    # Case 1: accessor generates no values.
+    base_accessor = lambda art: None
+    accessor = sorting._IndexOrLexical(well_known_values, base_accessor)
+    self.assertEqual(sorting.MAX_STRING, accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(sorting.DescendingValue(sorting.MAX_STRING),
+                     neg_accessor(art))
+
+    # Case 2: accessor generates a value, but it is an empty value.
+    base_accessor = lambda art: ''
+    accessor = sorting._IndexOrLexical(well_known_values, base_accessor)
+    self.assertEqual(sorting.MAX_STRING, accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(sorting.DescendingValue(sorting.MAX_STRING),
+                     neg_accessor(art))
+
+    # Case 3: A single well-known value
+    base_accessor = lambda art: 'x-c'
+    accessor = sorting._IndexOrLexical(well_known_values, base_accessor)
+    self.assertEqual(2, accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(-2, neg_accessor(art))
+
+    # Case 4: A single odd-ball value
+    base_accessor = lambda art: 'x-zzz'
+    accessor = sorting._IndexOrLexical(well_known_values, base_accessor)
+    self.assertEqual('x-zzz', accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(
+        sorting.DescendingValue('x-zzz'), neg_accessor(art))
+
+  def testIndexListAccessor_SomeWellKnownValues(self):
+    """Values sort according to their position in the well-known list."""
+    well_known_values = [11, 33, 22]  # These represent component IDs.
+    art = fake.MakeTestIssue(789, 1, 'sum 1', 'New', 111)
+    base_accessor = lambda issue: issue.component_ids
+    accessor = sorting._IndexListAccessor(well_known_values, base_accessor)
+
+    # Case 1: accessor generates no values.
+    self.assertEqual(sorting.MAX_STRING, accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(sorting.MAX_STRING, neg_accessor(art))
+
+    # Case 2: A single well-known value
+    art.component_ids = [33]
+    self.assertEqual([1], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual([-1], neg_accessor(art))
+
+    # Case 3: Multiple well-known and odd-ball values
+    art.component_ids = [33, 11, 99]
+    self.assertEqual([0, 1, sorting.MAX_STRING], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual([sorting.MAX_STRING, -1, 0],
+                     neg_accessor(art))
+
+  def testIndexListAccessor_NoWellKnownValues(self):
+    """When there are no well-known values, all values sort last."""
+    well_known_values = []  # Nothing pre-defined, so everything is oddball
+    art = fake.MakeTestIssue(789, 1, 'sum 1', 'New', 111)
+    base_accessor = lambda issue: issue.component_ids
+    accessor = sorting._IndexListAccessor(well_known_values, base_accessor)
+
+    # Case 1: accessor generates no values.
+    self.assertEqual(sorting.MAX_STRING, accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(sorting.MAX_STRING, neg_accessor(art))
+
+    # Case 2: A single oddball value
+    art.component_ids = [33]
+    self.assertEqual([sorting.MAX_STRING], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual([sorting.MAX_STRING], neg_accessor(art))
+
+    # Case 3: Multiple odd-ball values
+    art.component_ids = [33, 11, 99]
+    self.assertEqual(
+      [sorting.MAX_STRING, sorting.MAX_STRING, sorting.MAX_STRING],
+      accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(
+      [sorting.MAX_STRING, sorting.MAX_STRING, sorting.MAX_STRING],
+      neg_accessor(art))
+
+  def testIndexOrLexicalList(self):
+    well_known_values = ['Pri-High', 'Pri-Med', 'Pri-Low']
+    art = fake.MakeTestIssue(789, 1, 'sum 1', 'New', 111, merged_into=200001)
+
+    # Case 1: accessor generates no values.
+    accessor = sorting._IndexOrLexicalList(well_known_values, [], 'pri', {})
+    self.assertEqual(sorting.MAX_STRING, accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(sorting.MAX_STRING, neg_accessor(art))
+
+    # Case 2: A single well-known value
+    art.labels = ['Pri-Med']
+    accessor = sorting._IndexOrLexicalList(well_known_values, [], 'pri', {})
+    self.assertEqual([1], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual([-1], neg_accessor(art))
+
+    # Case 3: Multiple well-known and odd-ball values
+    art.labels = ['Pri-zzz', 'Pri-Med', 'yyy', 'Pri-High']
+    accessor = sorting._IndexOrLexicalList(well_known_values, [], 'pri', {})
+    self.assertEqual([0, 1, 'zzz'], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual([sorting.DescendingValue('zzz'), -1, 0],
+                     neg_accessor(art))
+
+    # Case 4: Multi-part prefix.
+    well_known_values.extend(['X-Y-Header', 'X-Y-Footer'])
+    art.labels = ['X-Y-Footer', 'X-Y-Zone', 'X-Y-Header', 'X-Y-Area']
+    accessor = sorting._IndexOrLexicalList(well_known_values, [], 'x-y', {})
+    self.assertEqual([3, 4, 'area', 'zone'], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual([sorting.DescendingValue('zone'),
+                      sorting.DescendingValue('area'), -4, -3],
+                     neg_accessor(art))
+
+  def testIndexOrLexicalList_CustomFields(self):
+    art = fake.MakeTestIssue(789, 1, 'sum 2', 'New', 111)
+    art.labels = ['samename-value1']
+    art.field_values = [tracker_bizobj.MakeFieldValue(
+        3, 6078, None, None, None, None, False)]
+
+    all_field_defs = [
+        tracker_bizobj.MakeFieldDef(
+            3, 789, 'samename', tracker_pb2.FieldTypes.INT_TYPE,
+            None, None, False, False, False, None, None, None, False, None,
+            None, None, None, 'cow spots', False),
+        tracker_bizobj.MakeFieldDef(
+            4, 788, 'samename', tracker_pb2.FieldTypes.APPROVAL_TYPE,
+            None, None, False, False, False, None, None, None, False, None,
+            None, None, None, 'cow spots', False),
+        tracker_bizobj.MakeFieldDef(
+            4, 788, 'notsamename', tracker_pb2.FieldTypes.APPROVAL_TYPE,
+            None, None, False, False, False, None, None, None, False, None,
+            None, None, None, 'should get filtered out', False)
+    ]
+
+    accessor = sorting._IndexOrLexicalList([], all_field_defs, 'samename', {})
+    self.assertEqual([6078, 'value1'], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(
+        [sorting.DescendingValue('value1'), -6078], neg_accessor(art))
+
+  def testIndexOrLexicalList_PhaseCustomFields(self):
+    art = fake.MakeTestIssue(789, 1, 'sum 2', 'New', 111)
+    art.labels = ['summer.goats-value1']
+    art.field_values = [
+        tracker_bizobj.MakeFieldValue(
+            3, 33, None, None, None, None, False, phase_id=77),
+        tracker_bizobj.MakeFieldValue(
+            3, 34, None, None, None, None, False, phase_id=77),
+        tracker_bizobj.MakeFieldValue(
+            3, 1000, None, None, None, None, False, phase_id=78)]
+    art.phases = [tracker_pb2.Phase(phase_id=77, name='summer'),
+                  tracker_pb2.Phase(phase_id=78, name='winter')]
+
+    all_field_defs = [
+        tracker_bizobj.MakeFieldDef(
+            3, 789, 'goats', tracker_pb2.FieldTypes.INT_TYPE,
+            None, None, False, False, True, None, None, None, False, None,
+            None, None, None, 'goats love mineral', False, is_phase_field=True),
+        tracker_bizobj.MakeFieldDef(
+            4, 788, 'boo', tracker_pb2.FieldTypes.APPROVAL_TYPE,
+            None, None, False, False, False, None, None, None, False, None,
+            None, None, None, 'ahh', False),
+        ]
+
+    accessor = sorting._IndexOrLexicalList(
+        [], all_field_defs, 'summer.goats', {})
+    self.assertEqual([33, 34, 'value1'], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual(
+        [sorting.DescendingValue('value1'), -34, -33], neg_accessor(art))
+
+  def testIndexOrLexicalList_ApprovalStatus(self):
+    art = fake.MakeTestIssue(789, 1, 'sum 2', 'New', 111)
+    art.labels = ['samename-value1']
+    art.approval_values = [tracker_pb2.ApprovalValue(approval_id=4)]
+
+    all_field_defs = [
+        tracker_bizobj.MakeFieldDef(
+            3, 789, 'samename', tracker_pb2.FieldTypes.INT_TYPE,
+            None, None, False, False, False, None, None, None, False, None,
+            None, None, None, 'cow spots', False),
+        tracker_bizobj.MakeFieldDef(
+            4, 788, 'samename', tracker_pb2.FieldTypes.APPROVAL_TYPE,
+            None, None, False, False, False, None, None, None, False, None,
+            None, None, None, 'cow spots', False)
+    ]
+
+    accessor = sorting._IndexOrLexicalList([], all_field_defs, 'samename', {})
+    self.assertEqual([0, 'value1'], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual([sorting.DescendingValue('value1'),
+                      sorting.DescendingValue(0)],
+                     neg_accessor(art))
+
+  def testIndexOrLexicalList_ApprovalApprover(self):
+    art = art = fake.MakeTestIssue(789, 1, 'sum 2', 'New', 111)
+    art.labels = ['samename-approver-value1']
+    art.approval_values = [
+        tracker_pb2.ApprovalValue(approval_id=4, approver_ids=[333])]
+
+    all_field_defs = [
+        tracker_bizobj.MakeFieldDef(
+            4, 788, 'samename', tracker_pb2.FieldTypes.APPROVAL_TYPE,
+            None, None, False, False, False, None, None, None, False, None,
+            None, None, None, 'cow spots', False)
+    ]
+    users_by_id = {333: framework_views.StuffUserView(333, 'a@test.com', True)}
+
+    accessor = sorting._IndexOrLexicalList(
+        [], all_field_defs, 'samename-approver', users_by_id)
+    self.assertEqual(['a@test.com', 'value1'], accessor(art))
+    neg_accessor = MakeDescending(accessor)
+    self.assertEqual([sorting.DescendingValue('value1'),
+                      sorting.DescendingValue('a@test.com')],
+                     neg_accessor(art))
+
+  def testComputeSortDirectives(self):
+    config = tracker_pb2.ProjectIssueConfig()
+    self.assertEqual(
+        ['project', 'id'], sorting.ComputeSortDirectives(config, '', ''))
+
+    self.assertEqual(
+        ['a', 'b', 'c', 'project', 'id'],
+        sorting.ComputeSortDirectives(config, '', 'a b C'))
+
+    config.default_sort_spec = 'id -reporter Owner'
+    self.assertEqual(
+        ['id', '-reporter', 'owner', 'project'],
+        sorting.ComputeSortDirectives(config, '', ''))
+
+    self.assertEqual(
+        ['x', '-b', 'a', 'c', '-owner', 'id', '-reporter', 'project'],
+        sorting.ComputeSortDirectives(config, 'x -b', 'A -b c -owner'))