Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/testing/api_clients.cfg b/testing/api_clients.cfg
index c9588df..9168a4b 100644
--- a/testing/api_clients.cfg
+++ b/testing/api_clients.cfg
@@ -1,7 +1,6 @@
-# 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.
 
 # Defines fake monorail api clients for testing
 
diff --git a/testing/fake.py b/testing/fake.py
index f484b12..c503fa0 100644
--- a/testing/fake.py
+++ b/testing/fake.py
@@ -1,7 +1,6 @@
-# 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.
 
 """Fake object classes that are useful for unit tests."""
 from __future__ import print_function
@@ -27,11 +26,11 @@
 from framework import permissions
 from framework import profiler
 from framework import validate
-from proto import features_pb2
-from proto import project_pb2
-from proto import tracker_pb2
-from proto import user_pb2
-from proto import usergroup_pb2
+from mrproto import features_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
+from mrproto import user_pb2
+from mrproto import usergroup_pb2
 from services import caches
 from services import config_svc
 from services import features_svc
@@ -145,13 +144,35 @@
 
 
 def MakeTestIssue(
-    project_id, local_id, summary, status, owner_id, labels=None,
-    derived_labels=None, derived_status=None, merged_into=0, star_count=0,
-    derived_owner_id=0, issue_id=None, reporter_id=None, opened_timestamp=None,
-    closed_timestamp=None, modified_timestamp=None, is_spam=False,
-    component_ids=None, project_name=None, field_values=None, cc_ids=None,
-    derived_cc_ids=None, assume_stale=True, phases=None, approval_values=None,
-    merged_into_external=None, attachment_count=0, derived_component_ids=None):
+    project_id,
+    local_id,
+    summary,
+    status,
+    owner_id,
+    labels=None,
+    derived_labels=None,
+    derived_status=None,
+    merged_into=0,
+    star_count=0,
+    derived_owner_id=0,
+    issue_id=None,
+    reporter_id=None,
+    opened_timestamp=None,
+    closed_timestamp=None,
+    modified_timestamp=None,
+    migration_modified_timestamp=None,
+    is_spam=False,
+    component_ids=None,
+    project_name=None,
+    field_values=None,
+    cc_ids=None,
+    derived_cc_ids=None,
+    assume_stale=True,
+    phases=None,
+    approval_values=None,
+    merged_into_external=None,
+    attachment_count=0,
+    derived_component_ids=None):
   """Easily make an Issue for testing."""
   issue = tracker_pb2.Issue()
   issue.project_id = project_id
@@ -180,6 +201,11 @@
     issue.component_modified_timestamp = opened_timestamp
   if modified_timestamp:
     issue.modified_timestamp = modified_timestamp
+    # By default, make migration_modified_timestamp the same as
+    # modified_timestamp
+    issue.migration_modified_timestamp = modified_timestamp
+  if migration_modified_timestamp:
+    issue.migration_modified_timestamp = migration_modified_timestamp
   if closed_timestamp:
     issue.closed_timestamp = closed_timestamp
   if labels is not None:
@@ -1041,9 +1067,9 @@
     return [project_dict[pid] for pid in project_id_list
             if pid in project_dict]
 
-  def GetVisibleLiveProjects(
+  def GetVisibleProjects(
       self, _cnxn, logged_in_user, effective_ids, domain=None, use_cache=True):
-    project_ids = list(self.projects_by_id.keys())
+    project_ids = sorted(self.projects_by_id.keys())
     visible_project_ids = []
     for pid in project_ids:
       can_view = permissions.UserCanViewProject(
@@ -1245,17 +1271,23 @@
       return None
     return 'label_%d_%d' % (project_id, label_id)
 
-  def LookupLabelID(self, cnxn, project_id, label, autocreate=True):
+  def LookupLabelID(
+      self, cnxn, project_id, label, autocreate=True, case_sensitive=False):
     if label in self.label_to_id:
       return self.label_to_id[label]
+    # TODO: The condition here is specifically added to return 'None' and
+    # allow testing for label freezing. This can be removed after refactoring
+    # other dependent tests to not fail for returning 'None' instead of '1'
+    # when label is not found in 'label_to_id' dict.
+    if label == 'freeze_new_label':
+      return None
     return 1
 
   def LookupLabelIDs(self, cnxn, project_id, labels, autocreate=False):
     ids = []
     next_label_id = 0
     if self.id_to_label.keys():
-      existing_ids = self.id_to_label.keys()
-      existing_ids.sort()
+      existing_ids = sorted(self.id_to_label.keys())
       next_label_id = existing_ids[-1] + 1
     for label in labels:
       if self.label_to_id.get(label) is not None:
@@ -1550,6 +1582,17 @@
     # The next id to return if it is > 0.
     self.next_id = -1
 
+  def UpdateIssue(
+      self,
+      cnxn,
+      issue,
+      update_cols=None,
+      just_derived=False,
+      commit=True,
+      invalidate=True):
+    self.UpdateIssues(
+        cnxn, [issue], update_cols, just_derived, commit, invalidate)
+
   def UpdateIssues(
       self, cnxn, issues, update_cols=None, just_derived=False,
       commit=True, invalidate=True):
@@ -1971,6 +2014,7 @@
       timestamp = int(time.time())
       new_issue.opened_timestamp = timestamp
       new_issue.modified_timestamp = timestamp
+      new_issue.migration_modified_timestamp = timestamp
 
       target_comments = self.GetCommentsForIssue(cnxn, target_issue.issue_id)
       initial_summary_comment = target_comments[0]
@@ -2659,8 +2703,9 @@
         hotlist = self.test_hotlists.get((name, owner_id))
         if hotlist:
           if not hotlist.owner_ids:  # Should never happen.
-            logging.warn('Unowned Hotlist: id:%r, name:%r',
-                         hotlist.hotlist_id, hotlist.name)
+            logging.warning(
+                'Unowned Hotlist: id:%r, name:%r', hotlist.hotlist_id,
+                hotlist.name)
             continue
           id_dict[(name.lower(), owner_id)] = hotlist.hotlist_id
     return id_dict
@@ -2790,7 +2835,7 @@
     emails = user_ids_by_email.keys()
     user_ids = user_ids_by_email.values()
     project_rules_dict = collections.defaultdict(list)
-    for project_id, rules in self.test_rules.iteritems():
+    for project_id, rules in self.test_rules.items():
       for rule in rules:
         if rule.default_owner_id in user_ids:
           project_rules_dict[project_id].append(rule)
diff --git a/testing/test/fake_test.py b/testing/test/fake_test.py
index c098236..a710328 100644
--- a/testing/test/fake_test.py
+++ b/testing/test/fake_test.py
@@ -1,7 +1,6 @@
-# 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.
 
 """Tests for the fake module."""
 from __future__ import print_function
@@ -9,6 +8,7 @@
 from __future__ import absolute_import
 
 import inspect
+import six
 import unittest
 
 from services import cachemanager_svc
@@ -44,9 +44,12 @@
       to_test = [x for x in both_attrs if '__' not in x]
       for name in to_test:
         real_attr = getattr(real_cls, name)
-        assert inspect.ismethod(real_attr)
-        real_spec = inspect.getargspec(real_attr)
-        fake_spec = inspect.getargspec(getattr(fake_cls, name))
+        if six.PY2:
+          assert inspect.ismethod(real_attr)
+        else:
+          assert inspect.isfunction(real_attr)
+        real_spec = inspect.getfullargspec(real_attr)
+        fake_spec = inspect.getfullargspec(getattr(fake_cls, name))
         # check same number of args and kwargs
         real_kw_len = len(real_spec[3]) if real_spec[3] else 0
         fake_kw_len = len(fake_spec[3]) if fake_spec[3] else 0
diff --git a/testing/test/testing_helpers_test.py b/testing/test/testing_helpers_test.py
index 7493b04..6e62f4e 100644
--- a/testing/test/testing_helpers_test.py
+++ b/testing/test/testing_helpers_test.py
@@ -1,7 +1,6 @@
-# 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.
 
 """Tests for the testing_helpers module."""
 from __future__ import print_function
diff --git a/testing/testing_helpers.py b/testing/testing_helpers.py
index 097275a..70c40fd 100644
--- a/testing/testing_helpers.py
+++ b/testing/testing_helpers.py
@@ -1,7 +1,6 @@
-# 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.
 
 """Helpers for testing."""
 from __future__ import print_function
@@ -9,15 +8,15 @@
 from __future__ import absolute_import
 
 import email
+from six.moves import urllib
 
 from framework import emailfmt
 from framework import framework_bizobj
-from proto import user_pb2
+from mrproto import user_pb2
 from services import service_manager
 from services import template_svc
 from testing import fake
 from tracker import tracker_constants
-import webapp2
 
 DEFAULT_HOST = '127.0.0.1'
 
@@ -71,7 +70,7 @@
 
 
 def GetRequestObjects(
-    headers=None, path='/', params=None, payload=None, user_info=None,
+    headers=None, path='/', params=None, user_info=None,
     project=None, method='GET', perms=None, services=None, hotlist=None):
   """Make fake request and MonorailRequest objects for testing.
 
@@ -98,9 +97,7 @@
 
   headers.setdefault('Host', DEFAULT_HOST)
   post_items=None
-  if method == 'POST' and payload:
-    post_items = payload
-  elif method == 'POST' and params:
+  if method == 'POST' and params:
     post_items = params
 
   if not services:
@@ -112,7 +109,7 @@
     services.project.TestAddProject('proj')
     services.features.TestAddHotlist('hotlist')
 
-  request = webapp2.Request.blank(path, headers=headers, POST=post_items)
+  request = RequestStub(path, headers=headers, values=post_items)
   mr = fake.MonorailRequest(
       services, user_info=user_info, project=project, perms=perms,
       params=params, hotlist=hotlist)
@@ -122,6 +119,33 @@
   return request, mr
 
 
+class RequestStub(object):
+  """flask.Request stub object.
+
+  This stub is a drop-in replacement for flask.Request that implements all
+  fields used in MonorailRequest.ParseRequest(). Its constructor API is
+  designed to mimic webapp2.Request.blank() for backwards compatibility with
+  existing unit tests previously written for webapp2.
+  """
+
+  def __init__(self, path, headers=None, values=None):
+    self.scheme = 'http'
+    self.path = path
+    self.headers = headers or {}
+    # webapp2.Request.blank() overrides the host from the request headers.
+    self.host = self.headers.get('Host', 'localhost:80')
+    self.host_url = self.scheme + '://' + self.host + '/'
+    self.url = self.scheme + '://' + self.host + path
+
+    parsed_url = urllib.parse.urlsplit(self.url)
+    self.base_url = self.host_url + parsed_url.path  # No query string.
+
+    self.values = values or {}
+    # webapp2.Request.blank() parses the query string from the path.
+    query = urllib.parse.parse_qs(parsed_url.query, True)
+    self.values.update({key: value[0] for key, value in query.items()})
+
+
 class Blank(object):
   """Simple class that assigns all named args to attributes.