Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/services/test/issue_svc_test.py b/services/test/issue_svc_test.py
index fe41aa4..f6b6c29 100644
--- a/services/test/issue_svc_test.py
+++ b/services/test/issue_svc_test.py
@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
-# 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
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Unit tests for issue_svc module."""
 
@@ -11,6 +10,7 @@
 from __future__ import absolute_import
 
 import logging
+import six
 import time
 import unittest
 from mock import patch, Mock, ANY
@@ -27,7 +27,7 @@
 from framework import exceptions
 from framework import framework_constants
 from framework import sql
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from services import caches
 from services import chart_svc
 from services import issue_svc
@@ -63,12 +63,12 @@
   return issue_service
 
 
-class TestableIssueTwoLevelCache(issue_svc.IssueTwoLevelCache):
+class _TestableIssueTwoLevelCache(issue_svc.IssueTwoLevelCache):
 
   def __init__(self, issue_list):
     cache_manager = fake.CacheManager()
-    super(TestableIssueTwoLevelCache, self).__init__(
-        cache_manager, None, None, None)
+    super(_TestableIssueTwoLevelCache,
+          self).__init__(cache_manager, None, None, None)
     self.cache = caches.RamCache(cache_manager, 'issue')
     self.memcache_prefix = 'issue:'
     self.pb_class = tracker_pb2.Issue
@@ -134,8 +134,8 @@
     issue_dict = self.issue_id_2lc.FetchItems(
         self.cnxn, project_local_ids_list)
     self.mox.VerifyAll()
-    self.assertItemsEqual(project_local_ids_list, list(issue_dict.keys()))
-    self.assertItemsEqual(issue_ids, list(issue_dict.values()))
+    six.assertCountEqual(self, project_local_ids_list, list(issue_dict.keys()))
+    six.assertCountEqual(self, issue_ids, list(issue_dict.values()))
 
   def testKeyToStr(self):
     self.assertEqual('789,1', self.issue_id_2lc._KeyToStr((789, 1)))
@@ -161,9 +161,10 @@
     now = int(time.time())
     self.project_service.TestAddProject('proj', project_id=789)
     self.issue_rows = [
-        (78901, 789, 1, 1, 111, 222,
-         now, now, now, now, now, now,
-         0, 0, 0, 1, 0, False)]
+        (
+            78901, 789, 1, 1, 111, 222, now, now, now, now, now, now, now, 0, 0,
+            0, 1, 0, False)
+    ]
     self.summary_rows = [(78901, 'sum')]
     self.label_rows = [(78901, 1, 0)]
     self.component_rows = []
@@ -224,14 +225,14 @@
         self.component_rows, self.cc_rows, self.notify_rows,
         self.fieldvalue_rows, self.relation_rows, self.dangling_relation_rows,
         self.phase_rows, self.approvalvalue_rows, self.av_approver_rows)
-    self.assertItemsEqual([78901], list(issue_dict.keys()))
+    six.assertCountEqual(self, [78901], list(issue_dict.keys()))
     issue = issue_dict[78901]
     self.assertEqual(len(issue.phases), 2)
     self.assertIsNotNone(tracker_bizobj.FindPhaseByID(1, issue.phases))
     av_21 = tracker_bizobj.FindApprovalValueByID(
         21, issue.approval_values)
     self.assertEqual(av_21.phase_id, 1)
-    self.assertItemsEqual(av_21.approver_ids, [111, 222, 333])
+    six.assertCountEqual(self, av_21.approver_ids, [111, 222, 333])
     self.assertIsNotNone(tracker_bizobj.FindPhaseByID(2, issue.phases))
     self.assertEqual(issue.phases,
                      [tracker_pb2.Phase(rank=1, phase_id=1, name='Canary'),
@@ -356,7 +357,7 @@
     self.mox.ReplayAll()
     issue_dict = self.issue_2lc.FetchItems(self.cnxn, issue_ids)
     self.mox.VerifyAll()
-    self.assertItemsEqual(issue_ids, list(issue_dict.keys()))
+    six.assertCountEqual(self, issue_ids, list(issue_dict.keys()))
     self.assertEqual(2, len(issue_dict[78901].phases))
 
   def testFetchItemsNoApprovalValues(self):
@@ -365,7 +366,7 @@
     self.mox.ReplayAll()
     issue_dict = self.issue_2lc.FetchItems(self.cnxn, issue_ids)
     self.mox.VerifyAll()
-    self.assertItemsEqual(issue_ids, list(issue_dict.keys()))
+    six.assertCountEqual(self, issue_ids, list(issue_dict.keys()))
     self.assertEqual([], issue_dict[78901].phases)
 
 
@@ -750,7 +751,7 @@
   def testGetIssuesDict(self):
     issue_ids = [78901, 78902, 78903]
     issue_1, issue_2 = self.SetUpGetIssues()
-    self.services.issue.issue_2lc = TestableIssueTwoLevelCache(
+    self.services.issue.issue_2lc = _TestableIssueTwoLevelCache(
         [issue_1, issue_2])
     issues_dict, missed_iids = self.services.issue.GetIssuesDict(
         self.cnxn, issue_ids)
@@ -827,10 +828,9 @@
   def SetUpInsertIssue(
       self, label_rows=None, av_rows=None, approver_rows=None,
       dangling_relation_rows=None):
-    row = (789, 1, 1, 111, 111,
-           self.now, 0, self.now, self.now, self.now, self.now,
-           None, 0,
-           False, 0, 0, False)
+    row = (
+        789, 1, 1, 111, 111, self.now, 0, self.now, self.now, self.now,
+        self.now, self.now, None, 0, False, 0, 0, False)
     self.services.issue.issue_tbl.InsertRows(
         self.cnxn, issue_svc.ISSUE_COLS[1:], [row],
         commit=False, return_generated_ids=True).AndReturn([78901])
@@ -852,9 +852,9 @@
         commit=False)
 
   def SetUpInsertSpamIssue(self):
-    row = (789, 1, 1, 111, 111,
-           self.now, 0, self.now, self.now, self.now, self.now,
-           None, 0, False, 0, 0, True)
+    row = (
+        789, 1, 1, 111, 111, self.now, 0, self.now, self.now, self.now,
+        self.now, self.now, None, 0, False, 0, 0, True)
     self.services.issue.issue_tbl.InsertRows(
         self.cnxn, issue_svc.ISSUE_COLS[1:], [row],
         commit=False, return_generated_ids=True).AndReturn([78901])
@@ -972,13 +972,14 @@
         'owner_modified': 123456789,
         'status_modified': 123456789,
         'component_modified': 123456789,
+        'migration_modified': 123456789,
         'derived_owner_id': None,
         'derived_status_id': None,
         'deleted': False,
         'star_count': 12,
         'attachment_count': 0,
         'is_spam': False,
-        }
+    }
     self.services.issue.issue_tbl.Update(
         self.cnxn, delta, id=78901, commit=False)
     if not given_delta:
@@ -1006,9 +1007,15 @@
 
   def testUpdateIssues_Normal(self):
     issue = fake.MakeTestIssue(
-        project_id=789, local_id=1, owner_id=111, summary='sum',
-        status='Live', labels=['Type-Defect'], issue_id=78901,
-        opened_timestamp=123456789, modified_timestamp=123456789,
+        project_id=789,
+        local_id=1,
+        owner_id=111,
+        summary='sum',
+        status='Live',
+        labels=['Type-Defect'],
+        issue_id=78901,
+        opened_timestamp=123456789,
+        modified_timestamp=123456789,
         star_count=12)
     issue.assume_stale = False
     self.SetUpUpdateIssues()
@@ -1018,9 +1025,15 @@
 
   def testUpdateIssue_Normal(self):
     issue = fake.MakeTestIssue(
-        project_id=789, local_id=1, owner_id=111, summary='sum',
-        status='Live', labels=['Type-Defect'], issue_id=78901,
-        opened_timestamp=123456789, modified_timestamp=123456789,
+        project_id=789,
+        local_id=1,
+        owner_id=111,
+        summary='sum',
+        status='Live',
+        labels=['Type-Defect'],
+        issue_id=78901,
+        opened_timestamp=123456789,
+        modified_timestamp=123456789,
         star_count=12)
     issue.assume_stale = False
     self.SetUpUpdateIssues()
@@ -1030,9 +1043,15 @@
 
   def testUpdateIssue_Stale(self):
     issue = fake.MakeTestIssue(
-        project_id=789, local_id=1, owner_id=111, summary='sum',
-        status='Live', labels=['Type-Defect'], issue_id=78901,
-        opened_timestamp=123456789, modified_timestamp=123456789,
+        project_id=789,
+        local_id=1,
+        owner_id=111,
+        summary='sum',
+        status='Live',
+        labels=['Type-Defect'],
+        issue_id=78901,
+        opened_timestamp=123456789,
+        modified_timestamp=123456789,
         star_count=12)
     # Do not set issue.assume_stale = False
     # Do not call self.SetUpUpdateIssues() because nothing should be updated.
@@ -1270,7 +1289,8 @@
         7890101, is_description=True, approval_id=7,
         content=config.approval_defs[2].survey, commit=False)
     amendment_row = (
-        78901, 7890101, 'custom', None, '-Llama Roo', None, None, 'Approvals')
+        78901, 7890101, 'custom', None, '-Llama Roo', None, None, 'Approvals',
+        None, None)
     self.SetUpInsertComment(
         7890101, content=comment_content, amendment_rows=[amendment_row],
         commit=False)
@@ -1473,8 +1493,10 @@
 
     # Calls in ApplyIssueDelta
     # Call to find added blocking issues.
-    issue_refs = {blocking_issue: (
-        blocking_issue.project_name, blocking_issue.local_id)}
+    issue_refs = {
+        blocking_issue.issue_id:
+            (blocking_issue.project_name, blocking_issue.local_id)
+    }
     self.services.issue.LookupIssueRefs(
         self.cnxn, [blocking_issue.issue_id]).AndReturn(issue_refs)
     # Call to find removed blocking issues.
@@ -1636,10 +1658,10 @@
   def testSoftDeleteIssue(self):
     project = fake.Project(project_id=789)
     issue_1, issue_2 = self.SetUpGetIssues()
-    self.services.issue.issue_2lc = TestableIssueTwoLevelCache(
+    self.services.issue.issue_2lc = _TestableIssueTwoLevelCache(
         [issue_1, issue_2])
     self.services.issue.issue_id_2lc.CacheItem((789, 1), 78901)
-    delta = {'deleted': True}
+    delta = {'deleted': True, 'migration_modified': self.now}
     self.services.issue.issue_tbl.Update(
         self.cnxn, delta, id=78901, commit=False)
 
@@ -1842,7 +1864,10 @@
     commentcontent_rows = [(7890101, 'content', 'msg'),
                            (7890102, 'content2', 'msg')]
     amendment_rows = [
-        (1, 78901, 7890101, 'cc', 'old', 'new val', 222, None, None)]
+        (
+            1, 78901, 7890101, 'cc', 'old', 'new val', 222, None, None, None,
+            None)
+    ]
     attachment_rows = []
     approval_rows = [(23, 7890102)]
     importer_rows = []
@@ -1869,6 +1894,24 @@
     self.assertEqual(2, len(comments))
     self.assertEqual(222, comments[0].importer_id)
 
+  def testUpackAmendment(self):
+    amendment_row = (
+        1, 78901, 7890101, 'cc', 'old', 'new val', 222, None, None, None, None)
+    amendment, comment_id = self.services.issue._UnpackAmendment(amendment_row)
+    self.assertEqual(comment_id, 7890101)
+    self.assertEqual(amendment.field, tracker_pb2.FieldID('CC'))
+    self.assertEqual(amendment.newvalue, 'new val')
+    self.assertEqual(amendment.oldvalue, 'old')
+    self.assertEqual(amendment.added_user_ids, [222])
+
+  def testUpackAmendment_With_Unicode(self):
+    amendment_row = (
+        1, 78901, 7890102, 'custom', None, None, None, None, None, u'123', None)
+    amendment, comment_id = self.services.issue._UnpackAmendment(amendment_row)
+    self.assertEqual(comment_id, 7890102)
+    self.assertEqual(amendment.field, tracker_pb2.FieldID('CUSTOM'))
+    self.assertEqual(amendment.added_component_ids, [123])
+
   def MockTheRestOfGetCommentsByID(self, comment_ids):
     self.services.issue.commentcontent_tbl.Select = Mock(
         return_value=[
@@ -2117,6 +2160,32 @@
     self.mox.VerifyAll()
     self.assertEqual(7890101, comment.id)
 
+  def testInsertComment_WithIssueUpdate(self):
+    amendment = tracker_bizobj.MakeAmendment(
+        tracker_pb2.FieldID.COMPONENTS, 'aaa', [], [], added_component_ids=[1])
+    amendment_rows = [
+        (
+            78901, 7890101, 'components', None, 'aaa', None, None, None, None,
+            None),
+        (78901, 7890101, 'components', None, None, None, None, None, 1, None)
+    ]
+    comment = tracker_pb2.IssueComment(
+        issue_id=78901,
+        timestamp=self.now,
+        project_id=789,
+        user_id=111,
+        content='content',
+        amendments=[amendment])
+    self.services.issue.commentcontent_tbl.InsertRow = Mock(
+        return_value=78901010)
+    self.services.issue.comment_tbl.InsertRow = Mock(return_value=7890101)
+    self.services.issue.issueupdate_tbl.InsertRows = Mock()
+
+    self.services.issue.InsertComment(self.cnxn, comment, commit=True)
+
+    self.services.issue.issueupdate_tbl.InsertRows.assert_called_once_with(
+        self.cnxn, issue_svc.ISSUEUPDATE_COLS[1:], amendment_rows, commit=False)
+
   def SetUpUpdateComment(self, comment_id, delta=None):
     delta = delta or {
         'commenter_id': 111,
@@ -2189,7 +2258,7 @@
   def testSoftDeleteComment(self):
     """Deleting a comment with an attachment marks it and updates count."""
     issue_1, issue_2 = self.SetUpGetIssues()
-    self.services.issue.issue_2lc = TestableIssueTwoLevelCache(
+    self.services.issue.issue_2lc = _TestableIssueTwoLevelCache(
         [issue_1, issue_2])
     issue_1.attachment_count = 1
     issue_1.assume_stale = False
@@ -2198,7 +2267,11 @@
     self.services.issue.issue_id_2lc.CacheItem((789, 1), 78901)
     self.SetUpUpdateComment(
         comment.id, delta={'deleted_by': 222, 'is_spam': False})
-    self.SetUpUpdateIssues(given_delta={'attachment_count': 0})
+    self.SetUpUpdateIssues(
+        given_delta={
+            'attachment_count': 0,
+            'migration_modified': self.now
+        })
     self.SetUpEnqueueIssuesForIndexing([78901])
     self.mox.ReplayAll()
     self.services.issue.SoftDeleteComment(
@@ -2418,7 +2491,11 @@
     comment.attachments.append(attachment)
 
     self.SetUpUpdateAttachment(179901, 1234, {'deleted': True})
-    self.SetUpUpdateIssues(given_delta={'attachment_count': 0})
+    self.SetUpUpdateIssues(
+        given_delta={
+            'attachment_count': 0,
+            'migration_modified': self.now
+        })
     self.SetUpEnqueueIssuesForIndexing([78901])
 
     self.mox.ReplayAll()
@@ -2626,6 +2703,9 @@
     self.services.issue.issueapproval2approver_tbl.Delete = Mock()
     self.services.issue.issue2approvalvalue_tbl.Update = Mock()
 
+    issue_update_id_rows = [(78914,), (78915,)]
+    self.services.issue.issueupdate_tbl.Select = Mock(
+        return_value=issue_update_id_rows)
     self.services.issue.issueupdate_tbl.Update = Mock()
 
     self.services.issue.issue2notify_tbl.Delete = Mock()
@@ -2652,18 +2732,19 @@
     commit = False
     limit = 50
 
-    affected_user_ids = self.services.issue.ExpungeUsersInIssues(
+    affected_issue_ids = self.services.issue.ExpungeUsersInIssues(
         self.cnxn, user_ids_by_email, limit=limit)
-    self.assertItemsEqual(
-        affected_user_ids,
-        [78901, 78902, 78903, 78904, 78905, 78906, 78907, 78908, 78909,
-         78910, 78911, 78912, 78913])
+    six.assertCountEqual(
+        self, affected_issue_ids, [
+            78901, 78902, 78903, 78904, 78905, 78906, 78907, 78908, 78909,
+            78910, 78911, 78912, 78913, 78914, 78915
+        ])
 
     self.services.issue.comment_tbl.Select.assert_called_once()
     _cnxn, kwargs = self.services.issue.comment_tbl.Select.call_args
     self.assertEqual(
         kwargs['cols'], ['Comment.id', 'Comment.issue_id', 'commentcontent_id'])
-    self.assertItemsEqual(kwargs['commenter_id'], user_ids)
+    six.assertCountEqual(self, kwargs['commenter_id'], user_ids)
     self.assertEqual(kwargs['limit'], limit)
 
     # since user_ids are passed to ExpungeUsersInIssues via a dictionary,
@@ -2723,9 +2804,6 @@
         self.cnxn, {'reporter_id': framework_constants.DELETED_USER_ID},
         id=[row[0] for row in reporter_issue_id_rows], commit=commit)
 
-    self.assertEqual(
-        3, len(self.services.issue.issue_tbl.Update.call_args_list))
-
     # issue updates
     self.services.issue.issueupdate_tbl.Update.assert_any_call(
         self.cnxn, {'added_user_id': framework_constants.DELETED_USER_ID},
@@ -2736,11 +2814,19 @@
     self.assertEqual(
         2, len(self.services.issue.issueupdate_tbl.Update.call_args_list))
 
+    # check updates across all issues
+    self.services.issue.issue_tbl.Update.assert_any_call(
+        self.cnxn, {'migration_modified': self.now},
+        id=affected_issue_ids,
+        commit=commit)
+    self.assertEqual(
+        4, len(self.services.issue.issue_tbl.Update.call_args_list))
+
     # issue notify
     call_args_list = self.services.issue.issue2notify_tbl.Delete.call_args_list
     self.assertEqual(1, len(call_args_list))
     _cnxn, kwargs = call_args_list[0]
-    self.assertItemsEqual(kwargs['email'], emails)
+    six.assertCountEqual(self, kwargs['email'], emails)
     self.assertEqual(kwargs['commit'], commit)
 
     # issue snapshots