OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered by a BSD-style |
| 3 # license that can be found in the LICENSE file or at |
| 4 # https://developers.google.com/open-source/licenses/bsd |
| 5 |
| 6 """Fake object classes that are useful for unit tests.""" |
| 7 |
| 8 import collections |
| 9 import logging |
| 10 import re |
| 11 |
| 12 import settings |
| 13 from framework import framework_helpers |
| 14 from framework import monorailrequest |
| 15 from framework import permissions |
| 16 from framework import validate |
| 17 from proto import project_pb2 |
| 18 from proto import tracker_pb2 |
| 19 from proto import user_pb2 |
| 20 from proto import usergroup_pb2 |
| 21 from services import caches |
| 22 from services import issue_svc |
| 23 from services import project_svc |
| 24 from services import user_svc |
| 25 from tracker import tracker_bizobj |
| 26 from tracker import tracker_constants |
| 27 |
| 28 # Many fakes return partial or constant values, regardless of their arguments. |
| 29 # pylint: disable=unused-argument |
| 30 |
| 31 BOUNDARY = '-----thisisaboundary' |
| 32 OWNER_ROLE = 'OWNER_ROLE' |
| 33 COMMITTER_ROLE = 'COMMITTER_ROLE' |
| 34 CONTRIBUTOR_ROLE = 'CONTRIBUTOR_ROLE' |
| 35 |
| 36 |
| 37 def Project( |
| 38 project_name='proj', project_id=None, state=project_pb2.ProjectState.LIVE, |
| 39 access=project_pb2.ProjectAccess.ANYONE, moved_to=None, |
| 40 cached_content_timestamp=None, |
| 41 owner_ids=None, committer_ids=None, contributor_ids=None): |
| 42 """Returns a project protocol buffer with the given attributes.""" |
| 43 project_id = project_id or hash(project_name) |
| 44 return project_pb2.MakeProject( |
| 45 project_name, project_id=project_id, state=state, access=access, |
| 46 moved_to=moved_to, cached_content_timestamp=cached_content_timestamp, |
| 47 owner_ids=owner_ids, committer_ids=committer_ids, |
| 48 contributor_ids=contributor_ids) |
| 49 |
| 50 |
| 51 def MakeTestIssue( |
| 52 project_id, local_id, summary, status, owner_id, labels=None, |
| 53 derived_labels=None, derived_status=None, merged_into=0, star_count=0, |
| 54 derived_owner_id=0, issue_id=None, reporter_id=None, opened_timestamp=None, |
| 55 closed_timestamp=None, modified_timestamp=None, is_spam=False, |
| 56 component_ids=None, project_name=None, field_values=None): |
| 57 """Easily make an Issue for testing.""" |
| 58 issue = tracker_pb2.Issue() |
| 59 issue.project_id = project_id |
| 60 issue.project_name = project_name |
| 61 issue.local_id = local_id |
| 62 issue.issue_id = issue_id if issue_id else 100000 + local_id |
| 63 issue.reporter_id = reporter_id if reporter_id else owner_id |
| 64 issue.summary = summary |
| 65 issue.status = status |
| 66 issue.owner_id = owner_id |
| 67 issue.derived_owner_id = derived_owner_id |
| 68 issue.star_count = star_count |
| 69 issue.merged_into = merged_into |
| 70 issue.is_spam = is_spam |
| 71 if opened_timestamp: |
| 72 issue.opened_timestamp = opened_timestamp |
| 73 if modified_timestamp: |
| 74 issue.modified_timestamp = modified_timestamp |
| 75 if closed_timestamp: |
| 76 issue.closed_timestamp = closed_timestamp |
| 77 if labels is not None: |
| 78 if isinstance(labels, basestring): |
| 79 labels = labels.split() |
| 80 issue.labels.extend(labels) |
| 81 if derived_labels is not None: |
| 82 if isinstance(derived_labels, basestring): |
| 83 derived_labels = derived_labels.split() |
| 84 issue.derived_labels.extend(derived_labels) |
| 85 if derived_status is not None: |
| 86 issue.derived_status = derived_status |
| 87 if component_ids is not None: |
| 88 issue.component_ids = component_ids |
| 89 if field_values is not None: |
| 90 issue.field_values = field_values |
| 91 return issue |
| 92 |
| 93 |
| 94 def MakeTestConfig(project_id, labels, statuses): |
| 95 """Convenient function to make a ProjectIssueConfig object.""" |
| 96 config = tracker_bizobj.MakeDefaultProjectIssueConfig(project_id) |
| 97 if isinstance(labels, basestring): |
| 98 labels = labels.split() |
| 99 if isinstance(statuses, basestring): |
| 100 statuses = statuses.split() |
| 101 config.well_known_labels = [ |
| 102 tracker_pb2.LabelDef(label=lab) for lab in labels] |
| 103 config.well_known_statuses = [ |
| 104 tracker_pb2.StatusDef(status=stat) for stat in statuses] |
| 105 return config |
| 106 |
| 107 |
| 108 class MonorailConnection(object): |
| 109 """Fake connection to databases for use in tests.""" |
| 110 |
| 111 def Commit(self): |
| 112 pass |
| 113 |
| 114 def Close(self): |
| 115 pass |
| 116 |
| 117 |
| 118 class MonorailRequest(monorailrequest.MonorailRequest): |
| 119 """Subclass of MonorailRequest suitable for testing.""" |
| 120 |
| 121 def __init__(self, user_info=None, project=None, perms=None, **kwargs): |
| 122 """Construct a test MonorailRequest. |
| 123 |
| 124 Typically, this is constructed via testing.helpers.GetRequestObjects, |
| 125 which also causes url parsing and optionally initializes the user, |
| 126 project, and permissions info. |
| 127 |
| 128 Args: |
| 129 user_info: a dict of user attributes to set on a MonorailRequest object. |
| 130 For example, "user_id: 5" causes self.auth.user_id=5. |
| 131 project: the Project pb for this request. |
| 132 perms: a PermissionSet for this request. |
| 133 """ |
| 134 super(MonorailRequest, self).__init__(**kwargs) |
| 135 |
| 136 if user_info is not None: |
| 137 for key in user_info: |
| 138 setattr(self.auth, key, user_info[key]) |
| 139 if 'user_id' in user_info: |
| 140 self.auth.effective_ids = {user_info['user_id']} |
| 141 |
| 142 self.perms = perms or permissions.ADMIN_PERMISSIONSET |
| 143 self.project = project |
| 144 |
| 145 |
| 146 class UserGroupService(object): |
| 147 """Fake UserGroupService class for testing other code.""" |
| 148 |
| 149 def __init__(self): |
| 150 self.group_settings = {} |
| 151 self.group_members = {} |
| 152 self.group_addrs = {} |
| 153 self.role_dict = {} |
| 154 |
| 155 def TestAddGroupSettings( |
| 156 self, group_id, email, who_can_view=None, anyone_can_join=False, |
| 157 who_can_add=None, external_group_type=None, |
| 158 last_sync_time=0, friend_projects=None): |
| 159 """Set up a fake group for testing. |
| 160 |
| 161 Args: |
| 162 group_id: int user ID of the new user group. |
| 163 email: string email address to identify the user group. |
| 164 who_can_view: string enum 'owners', 'members', or 'anyone'. |
| 165 anyone_can_join: optional boolean to allow any users to join the group. |
| 166 who_can_add: optional list of int user IDs of users who can add |
| 167 more members to the group. |
| 168 """ |
| 169 friend_projects = friend_projects or [] |
| 170 group_settings = usergroup_pb2.MakeSettings( |
| 171 who_can_view or 'members', |
| 172 external_group_type, last_sync_time, friend_projects) |
| 173 self.group_settings[group_id] = group_settings |
| 174 self.group_addrs[group_id] = email |
| 175 # TODO(jrobbins): store the other settings. |
| 176 |
| 177 def TestAddMembers(self, group_id, user_ids, role='member'): |
| 178 self.group_members.setdefault(group_id, []).extend(user_ids) |
| 179 for user_id in user_ids: |
| 180 self.role_dict.setdefault(group_id, {})[user_id] = role |
| 181 |
| 182 def LookupMemberships(self, _cnxn, user_id): |
| 183 memberships = { |
| 184 group_id for group_id, member_ids in self.group_members.iteritems() |
| 185 if user_id in member_ids} |
| 186 return memberships |
| 187 |
| 188 def DetermineWhichUserIDsAreGroups(self, _cnxn, user_ids): |
| 189 return [uid for uid in user_ids |
| 190 if uid in self.group_settings] |
| 191 |
| 192 def GetAllUserGroupsInfo(self, cnxn): |
| 193 infos = [] |
| 194 for group_id in self.group_settings: |
| 195 infos.append( |
| 196 (self.group_addrs[group_id], |
| 197 len(self.group_members.get(group_id, [])), |
| 198 self.group_settings[group_id], group_id)) |
| 199 |
| 200 return infos |
| 201 |
| 202 def GetAllGroupSettings(self, _cnxn, group_ids): |
| 203 return {gid: self.group_settings[gid] |
| 204 for gid in group_ids |
| 205 if gid in self.group_settings} |
| 206 |
| 207 def GetGroupSettings(self, cnxn, group_id): |
| 208 return self.GetAllGroupSettings(cnxn, [group_id]).get(group_id) |
| 209 |
| 210 def CreateGroup(self, cnxn, services, email, who_can_view_members, |
| 211 ext_group_type=None, friend_projects=None): |
| 212 friend_projects = friend_projects or [] |
| 213 group_id = services.user.LookupUserID( |
| 214 cnxn, email, autocreate=True, allowgroups=True) |
| 215 group_settings = usergroup_pb2.MakeSettings( |
| 216 who_can_view_members, ext_group_type, 0, friend_projects) |
| 217 self.UpdateSettings(cnxn, group_id, group_settings) |
| 218 return group_id |
| 219 |
| 220 def DeleteGroups(self, cnxn, group_ids): |
| 221 member_ids_dict, owner_ids_dict = self.LookupMembers(cnxn, group_ids) |
| 222 citizens_id_dict = collections.defaultdict(list) |
| 223 for g_id, user_ids in member_ids_dict.iteritems(): |
| 224 citizens_id_dict[g_id].extend(user_ids) |
| 225 for g_id, user_ids in owner_ids_dict.iteritems(): |
| 226 citizens_id_dict[g_id].extend(user_ids) |
| 227 for g_id, citizen_ids in citizens_id_dict.iteritems(): |
| 228 # Remove group members, friend projects and settings |
| 229 self.RemoveMembers(cnxn, g_id, citizen_ids) |
| 230 self.group_settings.pop(g_id, None) |
| 231 |
| 232 def LookupMembers(self, _cnxn, group_id_list): |
| 233 members_dict = {} |
| 234 owners_dict = {} |
| 235 for gid in group_id_list: |
| 236 members_dict[gid] = [] |
| 237 owners_dict[gid] = [] |
| 238 for mid in self.group_members.get(gid, []): |
| 239 if self.role_dict.get(gid, {}).get(mid) == 'owner': |
| 240 owners_dict[gid].append(mid) |
| 241 elif self.role_dict.get(gid, {}).get(mid) == 'member': |
| 242 members_dict[gid].append(mid) |
| 243 return members_dict, owners_dict |
| 244 |
| 245 def LookupAllMembers(self, _cnxn, group_id_list): |
| 246 direct_members, direct_owners = self.LookupMembers( |
| 247 _cnxn, group_id_list) |
| 248 members_dict = {} |
| 249 owners_dict = {} |
| 250 for gid in group_id_list: |
| 251 members = direct_members[gid] |
| 252 owners = direct_owners[gid] |
| 253 owners_dict[gid] = owners |
| 254 members_dict[gid] = members |
| 255 group_ids = set([uid for uid in members + owners |
| 256 if uid in self.group_settings]) |
| 257 while group_ids: |
| 258 indirect_members, indirect_owners = self.LookupMembers( |
| 259 _cnxn, group_ids) |
| 260 child_members = set() |
| 261 child_owners = set() |
| 262 for _, children in indirect_members.iteritems(): |
| 263 child_members.update(children) |
| 264 for _, children in indirect_owners.iteritems(): |
| 265 child_owners.update(children) |
| 266 members_dict[gid].extend(list(child_members)) |
| 267 owners_dict[gid].extend(list(child_owners)) |
| 268 group_ids = set(self.DetermineWhichUserIDsAreGroups( |
| 269 _cnxn, list(child_members) + list(child_owners))) |
| 270 members_dict[gid] = list(set(members_dict[gid])) |
| 271 return members_dict, owners_dict |
| 272 |
| 273 |
| 274 def RemoveMembers(self, _cnxn, group_id, old_member_ids): |
| 275 current_member_ids = self.group_members.get(group_id, []) |
| 276 revised_member_ids = [mid for mid in current_member_ids |
| 277 if mid not in old_member_ids] |
| 278 self.group_members[group_id] = revised_member_ids |
| 279 |
| 280 def UpdateMembers(self, _cnxn, group_id, member_ids, new_role): |
| 281 self.RemoveMembers(_cnxn, group_id, member_ids) |
| 282 self.TestAddMembers(group_id, member_ids, new_role) |
| 283 |
| 284 def UpdateSettings(self, _cnxn, group_id, group_settings): |
| 285 self.group_settings[group_id] = group_settings |
| 286 |
| 287 def ExpandAnyUserGroups(self, cnxn, user_ids): |
| 288 group_ids = set(self.DetermineWhichUserIDsAreGroups(cnxn, user_ids)) |
| 289 direct_ids = [uid for uid in user_ids if uid not in group_ids] |
| 290 member_ids_dict, owner_ids_dict = self.LookupAllMembers(cnxn, group_ids) |
| 291 |
| 292 indirect_ids = set() |
| 293 for gid in group_ids: |
| 294 indirect_ids.update(member_ids_dict[gid]) |
| 295 indirect_ids.update(owner_ids_dict[gid]) |
| 296 # It's possible that a user has both direct and indirect memberships of |
| 297 # one group. In this case, mark the user as direct member only. |
| 298 indirect_ids = [iid for iid in indirect_ids if iid not in direct_ids] |
| 299 |
| 300 return direct_ids, list(indirect_ids) |
| 301 |
| 302 def LookupVisibleMembers( |
| 303 self, cnxn, group_id_list, perms, effective_ids, services): |
| 304 settings_dict = self.GetAllGroupSettings(cnxn, group_id_list) |
| 305 group_ids = settings_dict.keys() |
| 306 |
| 307 direct_member_ids_dict, direct_owner_ids_dict = self.LookupMembers( |
| 308 cnxn, group_ids) |
| 309 all_member_ids_dict, all_owner_ids_dict = self.LookupAllMembers( |
| 310 cnxn, group_ids) |
| 311 visible_member_ids_dict = {} |
| 312 visible_owner_ids_dict = {} |
| 313 for gid in group_ids: |
| 314 member_ids = all_member_ids_dict[gid] |
| 315 owner_ids = all_owner_ids_dict[gid] |
| 316 if permissions.CanViewGroup(perms, effective_ids, settings_dict[gid], |
| 317 member_ids, owner_ids, []): |
| 318 visible_member_ids_dict[gid] = direct_member_ids_dict[gid] |
| 319 visible_owner_ids_dict[gid] = direct_owner_ids_dict[gid] |
| 320 |
| 321 return visible_member_ids_dict, visible_owner_ids_dict |
| 322 |
| 323 def ValidateFriendProjects(self, cnxn, services, friend_projects): |
| 324 project_names = filter(None, re.split('; |, | |;|,', friend_projects)) |
| 325 id_dict = services.project.LookupProjectIDs(cnxn, project_names) |
| 326 missed_projects = [] |
| 327 result = [] |
| 328 for p_name in project_names: |
| 329 if p_name in id_dict: |
| 330 result.append(id_dict[p_name]) |
| 331 else: |
| 332 missed_projects.append(p_name) |
| 333 error_msg = '' |
| 334 if missed_projects: |
| 335 error_msg = 'Project(s) %s do not exist' % ', '.join(missed_projects) |
| 336 return None, error_msg |
| 337 else: |
| 338 return result, None |
| 339 |
| 340 |
| 341 class CacheManager(object): |
| 342 |
| 343 def __init__(self, invalidate_tbl=None): |
| 344 self.last_call = None |
| 345 self.processed_invalidations_up_to = 0 |
| 346 |
| 347 def MakeCache(self, kind, max_size=None, use_value_centric_cache=False): |
| 348 """Make a new cache and register it for future invalidations.""" |
| 349 if use_value_centric_cache: |
| 350 cache = caches.ValueCentricRamCache(self, kind, max_size=max_size) |
| 351 else: |
| 352 cache = caches.RamCache(self, kind, max_size=max_size) |
| 353 return cache |
| 354 |
| 355 def DoDistributedInvalidation(self, cnxn): |
| 356 """Drop any cache entries that were invalidated by other jobs.""" |
| 357 self.last_call = 'DoDistributedInvalidation', cnxn |
| 358 |
| 359 def StoreInvalidateRows(self, cnxn, kind, keys): |
| 360 """Store database rows to let all frontends know to invalidate.""" |
| 361 self.last_call = 'StoreInvalidateRows', cnxn, kind, keys |
| 362 |
| 363 def StoreInvalidateAll(self, cnxn, kind): |
| 364 """Store a database row to let all frontends know to invalidate.""" |
| 365 self.last_call = 'StoreInvalidateAll', cnxn, kind |
| 366 |
| 367 |
| 368 |
| 369 class UserService(object): |
| 370 |
| 371 def __init__(self): |
| 372 """Creates a test-appropriate UserService object.""" |
| 373 self.users_by_email = {} |
| 374 self.users_by_id = {} |
| 375 self.test_users = {} |
| 376 |
| 377 def TestAddUser(self, email, user_id, add_user=True, banned=False): |
| 378 """Add a user to the fake UserService instance. |
| 379 |
| 380 Args: |
| 381 email: Email of the user. |
| 382 user_id: int user ID. |
| 383 add_user: Flag whether user pb should be created, i.e. whether a |
| 384 Monorail account should be created |
| 385 banned: Boolean to set the user as banned |
| 386 |
| 387 Returns: |
| 388 The User PB that was added, or None. |
| 389 """ |
| 390 self.users_by_email[email] = user_id |
| 391 self.users_by_id[user_id] = email |
| 392 |
| 393 user = None |
| 394 if add_user: |
| 395 user = user_pb2.MakeUser() |
| 396 user.is_site_admin = False |
| 397 user.email = email |
| 398 user.obscure_email = True |
| 399 if banned: |
| 400 user.banned = 'is banned' |
| 401 self.test_users[user_id] = user |
| 402 |
| 403 return user |
| 404 |
| 405 def GetUser(self, _cnxn, user_id): |
| 406 return self.test_users.get(user_id) |
| 407 |
| 408 def _CreateUser(self, _cnxn, email): |
| 409 if email in self.users_by_email: |
| 410 return |
| 411 user_id = framework_helpers.MurmurHash3_x86_32(email) |
| 412 self.users_by_id[user_id] = email |
| 413 self.users_by_email[email] = user_id |
| 414 |
| 415 def _CreateUsers(self, cnxn, emails): |
| 416 for email in emails: |
| 417 self._CreateUser(cnxn, email) |
| 418 |
| 419 def LookupUserID(self, cnxn, email, autocreate=False, allowgroups=False): |
| 420 user_id = self.users_by_email.get(email) |
| 421 if not user_id and validate.IsValidEmail(email): |
| 422 if autocreate: |
| 423 self._CreateUser(cnxn, email) |
| 424 user_id = self.users_by_email.get(email) |
| 425 else: |
| 426 raise user_svc.NoSuchUserException(email) |
| 427 |
| 428 return user_id |
| 429 |
| 430 def GetUsersByIDs(self, cnxn, user_ids, use_cache=True): |
| 431 user_dict = {} |
| 432 for user_id in user_ids: |
| 433 if user_id and self.test_users.get(user_id): |
| 434 user_dict[user_id] = self.test_users[user_id] |
| 435 return user_dict |
| 436 |
| 437 def LookupExistingUserIDs(self, cnxn, emails): |
| 438 email_dict = { |
| 439 email: self.users_by_email[email] |
| 440 for email in emails |
| 441 if email in self.users_by_email} |
| 442 return email_dict |
| 443 |
| 444 def LookupUserIDs(self, cnxn, emails, autocreate=False, |
| 445 allowgroups=False): |
| 446 email_dict = {} |
| 447 for email in emails: |
| 448 user_id = self.LookupUserID( |
| 449 cnxn, email, autocreate=autocreate, allowgroups=allowgroups) |
| 450 if user_id: |
| 451 email_dict[email] = user_id |
| 452 return email_dict |
| 453 |
| 454 def LookupUserEmail(self, _cnxn, user_id): |
| 455 email = self.users_by_id.get(user_id) |
| 456 return email |
| 457 |
| 458 def LookupUserEmails(self, cnxn, user_ids): |
| 459 user_dict = { |
| 460 user_id: self.LookupUserEmail(cnxn, user_id) |
| 461 for user_id in user_ids} |
| 462 return user_dict |
| 463 |
| 464 def UpdateUser(self, _cnxn, user_id, user): |
| 465 """Updates the user pb.""" |
| 466 self.test_users[user_id] = user |
| 467 |
| 468 def UpdateUserSettings( |
| 469 self, cnxn, user_id, user, notify=None, notify_starred=None, |
| 470 obscure_email=None, after_issue_update=None, |
| 471 is_site_admin=None, ignore_action_limits=None, |
| 472 is_banned=None, banned_reason=None, action_limit_updates=None, |
| 473 dismissed_cues=None, keep_people_perms_open=None, preview_on_hover=None): |
| 474 self.UpdateUser(cnxn, user_id, user) |
| 475 |
| 476 |
| 477 class AbstractStarService(object): |
| 478 """Fake StarService.""" |
| 479 |
| 480 def __init__(self): |
| 481 self.stars_by_item_id = {} |
| 482 self.stars_by_starrer_id = {} |
| 483 self.expunged_item_ids = [] |
| 484 |
| 485 def ExpungeStars(self, _cnxn, item_id): |
| 486 self.expunged_item_ids.append(item_id) |
| 487 old_starrer = self.stars_by_item_id.get(item_id) |
| 488 self.stars_by_item_id[item_id] = [] |
| 489 if self.stars_by_starrer_id.get(old_starrer): |
| 490 self.stars_by_starrer_id[old_starrer] = [ |
| 491 it for it in self.stars_by_starrer_id[old_starrer] |
| 492 if it != item_id] |
| 493 |
| 494 def LookupItemStarrers(self, _cnxn, item_id): |
| 495 return self.stars_by_item_id.get(item_id, []) |
| 496 |
| 497 def LookupStarredItemIDs(self, _cnxn, starrer_user_id): |
| 498 return self.stars_by_starrer_id.get(starrer_user_id, []) |
| 499 |
| 500 def IsItemStarredBy(self, cnxn, item_id, starrer_user_id): |
| 501 return item_id in self.LookupStarredItemIDs(cnxn, starrer_user_id) |
| 502 |
| 503 def CountItemStars(self, cnxn, item_id): |
| 504 return len(self.LookupItemStarrers(cnxn, item_id)) |
| 505 |
| 506 def CountItemsStars(self, cnxn, item_ids): |
| 507 return {item_id: self.CountItemStars(cnxn, item_id) |
| 508 for item_id in item_ids} |
| 509 |
| 510 def SetStar(self, cnxn, item_id, starrer_user_id, starred): |
| 511 if starred and not self.IsItemStarredBy(cnxn, item_id, starrer_user_id): |
| 512 self.stars_by_item_id.setdefault(item_id, []).append(starrer_user_id) |
| 513 self.stars_by_starrer_id.setdefault(starrer_user_id, []).append(item_id) |
| 514 |
| 515 elif not starred and self.IsItemStarredBy(cnxn, item_id, starrer_user_id): |
| 516 self.stars_by_item_id[item_id].remove(starrer_user_id) |
| 517 self.stars_by_starrer_id[starrer_user_id].remove(item_id) |
| 518 |
| 519 |
| 520 class UserStarService(AbstractStarService): |
| 521 pass |
| 522 |
| 523 |
| 524 class ProjectStarService(AbstractStarService): |
| 525 pass |
| 526 |
| 527 |
| 528 class IssueStarService(AbstractStarService): |
| 529 |
| 530 # pylint: disable=arguments-differ |
| 531 def SetStar( |
| 532 self, cnxn, _service, _config, issue_id, starrer_user_id, |
| 533 starred): |
| 534 super(IssueStarService, self).SetStar( |
| 535 cnxn, issue_id, starrer_user_id, starred) |
| 536 |
| 537 |
| 538 class ProjectService(object): |
| 539 """Fake ProjectService object. |
| 540 |
| 541 Provides methods for creating users and projects, which are accessible |
| 542 through parts of the real ProjectService interface. |
| 543 """ |
| 544 |
| 545 def __init__(self): |
| 546 self.test_projects = {} # project_name -> project_pb |
| 547 self.projects_by_id = {} |
| 548 self.test_star_manager = None |
| 549 self.indexed_projects = {} |
| 550 self.unindexed_projects = set() |
| 551 self.index_counter = 0 |
| 552 self.project_commitments = {} |
| 553 |
| 554 def TestAddProject( |
| 555 self, name, summary='', state=project_pb2.ProjectState.LIVE, |
| 556 owner_ids=None, committer_ids=None, contrib_ids=None, |
| 557 issue_notify_address=None, state_reason='', |
| 558 description=None, project_id=None, process_inbound_email=None, |
| 559 access=None): |
| 560 """Add a project to the fake ProjectService object. |
| 561 |
| 562 Args: |
| 563 name: The name of the project. Will replace any existing project under |
| 564 the same name. |
| 565 summary: The summary string of the project. |
| 566 state: Initial state for the project from project_pb2.ProjectState. |
| 567 owner_ids: List of user ids for project owners |
| 568 committer_ids: List of user ids for project committers |
| 569 contrib_ids: List of user ids for project contributors |
| 570 issue_notify_address: email address to send issue change notifications |
| 571 state_reason: string describing the reason the project is in its current |
| 572 state. |
| 573 description: The description string for this project |
| 574 project_id: A unique integer identifier for the created project. |
| 575 process_inbound_email: True to make this project accept inbound email. |
| 576 access: One of the values of enum project_pb2.ProjectAccess. |
| 577 |
| 578 Returns: |
| 579 A populated project PB. |
| 580 """ |
| 581 proj_pb = project_pb2.Project() |
| 582 proj_pb.project_id = project_id or hash(name) % 100000 |
| 583 proj_pb.project_name = name |
| 584 proj_pb.summary = summary |
| 585 proj_pb.state = state |
| 586 proj_pb.state_reason = state_reason |
| 587 if description is not None: |
| 588 proj_pb.description = description |
| 589 |
| 590 self.TestAddProjectMembers(owner_ids, proj_pb, OWNER_ROLE) |
| 591 self.TestAddProjectMembers(committer_ids, proj_pb, COMMITTER_ROLE) |
| 592 self.TestAddProjectMembers(contrib_ids, proj_pb, CONTRIBUTOR_ROLE) |
| 593 |
| 594 if issue_notify_address is not None: |
| 595 proj_pb.issue_notify_address = issue_notify_address |
| 596 if process_inbound_email is not None: |
| 597 proj_pb.process_inbound_email = process_inbound_email |
| 598 if access is not None: |
| 599 proj_pb.access = access |
| 600 |
| 601 self.test_projects[name] = proj_pb |
| 602 self.projects_by_id[proj_pb.project_id] = proj_pb |
| 603 return proj_pb |
| 604 |
| 605 def TestAddProjectMembers(self, user_id_list, proj_pb, role): |
| 606 if user_id_list is not None: |
| 607 for user_id in user_id_list: |
| 608 if role == OWNER_ROLE: |
| 609 proj_pb.owner_ids.append(user_id) |
| 610 elif role == COMMITTER_ROLE: |
| 611 proj_pb.committer_ids.append(user_id) |
| 612 elif role == CONTRIBUTOR_ROLE: |
| 613 proj_pb.contributor_ids.append(user_id) |
| 614 |
| 615 def LookupProjectIDs(self, cnxn, project_names): |
| 616 return { |
| 617 project_name: self.test_projects[project_name].project_id |
| 618 for project_name in project_names |
| 619 if project_name in self.test_projects} |
| 620 |
| 621 def LookupProjectNames(self, cnxn, project_ids): |
| 622 projects_dict = self.GetProjects(cnxn, project_ids) |
| 623 return {p.project_id: p.project_name |
| 624 for p in projects_dict.itervalues()} |
| 625 |
| 626 def CreateProject( |
| 627 self, _cnxn, project_name, owner_ids, committer_ids, |
| 628 contributor_ids, summary, description, |
| 629 state=project_pb2.ProjectState.LIVE, access=None, read_only=None, |
| 630 home_page=None, docs_url=None, logo_gcs_id=None, logo_file_name=None): |
| 631 """Create and store a Project with the given attributes.""" |
| 632 if project_name in self.test_projects: |
| 633 raise project_svc.ProjectAlreadyExists() |
| 634 self.TestAddProject( |
| 635 project_name, summary=summary, state=state, |
| 636 owner_ids=owner_ids, committer_ids=committer_ids, |
| 637 contrib_ids=contributor_ids, description=description, |
| 638 access=access) |
| 639 |
| 640 def ExpungeProject(self, _cnxn, project_id): |
| 641 project = self.projects_by_id.get(project_id) |
| 642 if project: |
| 643 self.test_projects.pop(project.project_name, None) |
| 644 |
| 645 def GetProjectsByName(self, _cnxn, project_name_list, use_cache=True): |
| 646 return { |
| 647 pn: self.test_projects[pn] for pn in project_name_list |
| 648 if pn in self.test_projects} |
| 649 |
| 650 def GetProjectByName(self, _cnxn, name, use_cache=True): |
| 651 return self.test_projects.get(name) |
| 652 |
| 653 def GetProjectList(self, cnxn, project_id_list, use_cache=True): |
| 654 project_dict = self.GetProjects(cnxn, project_id_list, use_cache=use_cache) |
| 655 return [project_dict[pid] for pid in project_id_list |
| 656 if pid in project_dict] |
| 657 |
| 658 def GetVisibleLiveProjects(self, _cnxn, logged_in_user, effective_ids, |
| 659 use_cache=True): |
| 660 return self.projects_by_id.keys() |
| 661 |
| 662 def GetProjects(self, _cnxn, project_ids, use_cache=True): |
| 663 result = {} |
| 664 for project_id in project_ids: |
| 665 project = self.projects_by_id.get(project_id) |
| 666 if project: |
| 667 result[project_id] = project |
| 668 return result |
| 669 |
| 670 def GetProject(self, cnxn, project_id, use_cache=True): |
| 671 """Load the specified project from the database.""" |
| 672 project_id_dict = self.GetProjects(cnxn, [project_id], use_cache=use_cache) |
| 673 return project_id_dict.get(project_id) |
| 674 |
| 675 @staticmethod |
| 676 def IsValidProjectName(string): |
| 677 """Return true if the given string is a valid project name.""" |
| 678 return project_svc.RE_PROJECT_NAME.match(string) |
| 679 |
| 680 def GetProjectCommitments(self, _cnxn, project_id): |
| 681 if project_id in self.project_commitments: |
| 682 return self.project_commitments[project_id] |
| 683 |
| 684 project_commitments = project_pb2.ProjectCommitments() |
| 685 project_commitments.project_id = project_id |
| 686 return project_commitments |
| 687 |
| 688 def TestStoreProjectCommitments(self, project_commitments): |
| 689 key = project_commitments.project_id |
| 690 self.project_commitments[key] = project_commitments |
| 691 |
| 692 def UpdateProject( |
| 693 self, _cnxn, project_id, summary=None, description=None, |
| 694 state=None, state_reason=None, access=None, |
| 695 issue_notify_address=None, attachment_bytes_used=None, |
| 696 attachment_quota=None, moved_to=None, process_inbound_email=None, |
| 697 only_owners_remove_restrictions=None, |
| 698 read_only_reason=None, cached_content_timestamp=None, |
| 699 only_owners_see_contributors=None, delete_time=None, |
| 700 recent_activity=None, revision_url_format=None, home_page=None, |
| 701 docs_url=None, logo_gcs_id=None, logo_file_name=None): |
| 702 project = self.projects_by_id.get(project_id) |
| 703 if not project: |
| 704 raise project_svc.NoSuchProjectException( |
| 705 'Project "%s" not found!' % project_id) |
| 706 |
| 707 # TODO(jrobbins): implement all passed arguments - probably as a utility |
| 708 # method shared with the real persistence implementation. |
| 709 if read_only_reason is not None: |
| 710 project.read_only_reason = read_only_reason |
| 711 |
| 712 def UpdateProjectRoles( |
| 713 self, _cnxn, project_id, owner_ids, committer_ids, |
| 714 contributor_ids, now=None): |
| 715 project = self.projects_by_id.get(project_id) |
| 716 if not project: |
| 717 raise project_svc.NoSuchProjectException( |
| 718 'Project "%s" not found!' % project_id) |
| 719 |
| 720 project.owner_ids = owner_ids |
| 721 project.committer_ids = committer_ids |
| 722 project.contributor_ids = contributor_ids |
| 723 |
| 724 def MarkProjectDeletable( |
| 725 self, _cnxn, project_id, _config_service): |
| 726 project = self.projects_by_id[project_id] |
| 727 project.project_name = 'DELETABLE_%d' % project_id |
| 728 project.state = project_pb2.ProjectState.DELETABLE |
| 729 |
| 730 def UpdateRecentActivity(self, _cnxn, _project_id, now=None): |
| 731 pass |
| 732 |
| 733 def GetUserRolesInAllProjects(self, _cnxn, effective_ids): |
| 734 owned_project_ids = set() |
| 735 membered_project_ids = set() |
| 736 contrib_project_ids = set() |
| 737 |
| 738 for project in self.projects_by_id.itervalues(): |
| 739 if not effective_ids.isdisjoint(project.owner_ids): |
| 740 owned_project_ids.add(project.project_id) |
| 741 elif not effective_ids.isdisjoint(project.committer_ids): |
| 742 membered_project_ids.add(project.project_id) |
| 743 elif not effective_ids.isdisjoint(project.contributor_ids): |
| 744 contrib_project_ids.add(project.project_id) |
| 745 |
| 746 return owned_project_ids, membered_project_ids, contrib_project_ids |
| 747 |
| 748 |
| 749 class ConfigService(object): |
| 750 """Fake version of ConfigService that just works in-RAM.""" |
| 751 |
| 752 def __init__(self, user_id=None): |
| 753 self.project_configs = {} |
| 754 self.next_field_id = 123 |
| 755 self.next_component_id = 345 |
| 756 self.expunged_configs = [] |
| 757 self.component_ids_to_templates = {} |
| 758 |
| 759 def TemplatesWithComponent(self, _cnxn, component_id, _config): |
| 760 return self.component_ids_to_templates.get(component_id, []) |
| 761 |
| 762 def ExpungeConfig(self, _cnxn, project_id): |
| 763 self.expunged_configs.append(project_id) |
| 764 |
| 765 def GetLabelDefRows(self, cnxn, project_id): |
| 766 """This always returns empty results. Mock it to test other cases.""" |
| 767 return [] |
| 768 |
| 769 def GetLabelDefRowsAnyProject(self, cnxn, where=None): |
| 770 """This always returns empty results. Mock it to test other cases.""" |
| 771 return [] |
| 772 |
| 773 def LookupLabel(self, cnxn, project_id, label_id): |
| 774 if label_id == 999: |
| 775 return None |
| 776 return 'label_%d_%d' % (project_id, label_id) |
| 777 |
| 778 def LookupLabelID(self, cnxn, project_id, label, autocreate=True): |
| 779 return 1 |
| 780 |
| 781 def LookupLabelIDs(self, cnxn, project_id, labels, autocreate=False): |
| 782 return [idx for idx, _label in enumerate(labels)] |
| 783 |
| 784 def LookupIDsOfLabelsMatching(self, cnxn, project_id, regex): |
| 785 return [1, 2, 3] |
| 786 |
| 787 def LookupStatus(self, cnxn, project_id, status_id): |
| 788 return 'status_%d_%d' % (project_id, status_id) |
| 789 |
| 790 def LookupStatusID(self, cnxn, project_id, status, autocreate=True): |
| 791 if status: |
| 792 return 1 |
| 793 else: |
| 794 return 0 |
| 795 |
| 796 def LookupStatusIDs(self, cnxn, project_id, statuses): |
| 797 return [idx for idx, _status in enumerate(statuses)] |
| 798 |
| 799 def LookupClosedStatusIDs(self, cnxn, project_id): |
| 800 return [7, 8, 9] |
| 801 |
| 802 def StoreConfig(self, _cnxn, config): |
| 803 self.project_configs[config.project_id] = config |
| 804 |
| 805 def GetProjectConfig(self, _cnxn, project_id, use_cache=True): |
| 806 if project_id in self.project_configs: |
| 807 return self.project_configs[project_id] |
| 808 else: |
| 809 return tracker_bizobj.MakeDefaultProjectIssueConfig(project_id) |
| 810 |
| 811 def GetProjectConfigs(self, _cnxn, project_ids, use_cache=True): |
| 812 config_dict = {} |
| 813 for project_id in project_ids: |
| 814 if project_id in self.project_configs: |
| 815 config_dict[project_id] = self.project_configs[project_id] |
| 816 else: |
| 817 config_dict[project_id] = tracker_bizobj.MakeDefaultProjectIssueConfig( |
| 818 project_id) |
| 819 |
| 820 return config_dict |
| 821 |
| 822 def UpdateConfig( |
| 823 self, cnxn, project, well_known_statuses=None, |
| 824 statuses_offer_merge=None, well_known_labels=None, |
| 825 excl_label_prefixes=None, templates=None, |
| 826 default_template_for_developers=None, default_template_for_users=None, |
| 827 list_prefs=None, restrict_to_known=None): |
| 828 project_id = project.project_id |
| 829 project_config = self.GetProjectConfig(cnxn, project_id, use_cache=False) |
| 830 |
| 831 if well_known_statuses is not None: |
| 832 tracker_bizobj.SetConfigStatuses(project_config, well_known_statuses) |
| 833 |
| 834 if statuses_offer_merge is not None: |
| 835 project_config.statuses_offer_merge = statuses_offer_merge |
| 836 |
| 837 if well_known_labels is not None: |
| 838 tracker_bizobj.SetConfigLabels(project_config, well_known_labels) |
| 839 |
| 840 if excl_label_prefixes is not None: |
| 841 project_config.exclusive_label_prefixes = excl_label_prefixes |
| 842 |
| 843 if templates is not None: |
| 844 project_config.templates = templates |
| 845 |
| 846 if default_template_for_developers is not None: |
| 847 project_config.default_template_for_developers = ( |
| 848 default_template_for_developers) |
| 849 if default_template_for_users is not None: |
| 850 project_config.default_template_for_users = default_template_for_users |
| 851 |
| 852 if list_prefs: |
| 853 default_col_spec, default_sort_spec, x_attr, y_attr = list_prefs |
| 854 project_config.default_col_spec = default_col_spec |
| 855 project_config.default_sort_spec = default_sort_spec |
| 856 project_config.default_x_attr = x_attr |
| 857 project_config.default_y_attr = y_attr |
| 858 |
| 859 if restrict_to_known is not None: |
| 860 project_config.restrict_to_known = restrict_to_known |
| 861 |
| 862 self.StoreConfig(cnxn, project_config) |
| 863 return project_config |
| 864 |
| 865 def CreateFieldDef( |
| 866 self, cnxn, project_id, field_name, field_type_str, applic_type, |
| 867 applic_pred, is_required, is_multivalued, |
| 868 min_value, max_value, regex, needs_member, needs_perm, |
| 869 grants_perm, notify_on, docstring, admin_ids): |
| 870 config = self.GetProjectConfig(cnxn, project_id) |
| 871 field_type = tracker_pb2.FieldTypes(field_type_str) |
| 872 field_id = self.next_field_id |
| 873 self.next_field_id += 1 |
| 874 fd = tracker_bizobj.MakeFieldDef( |
| 875 field_id, project_id, field_name, field_type, applic_type, applic_pred, |
| 876 is_required, is_multivalued, min_value, max_value, regex, |
| 877 needs_member, needs_perm, grants_perm, notify_on, docstring, False) |
| 878 config.field_defs.append(fd) |
| 879 self.StoreConfig(cnxn, config) |
| 880 |
| 881 def SoftDeleteFieldDef(self, cnxn, project_id, field_id): |
| 882 config = self.GetProjectConfig(cnxn, project_id) |
| 883 fd = tracker_bizobj.FindFieldDefByID(field_id, config) |
| 884 fd.is_deleted = True |
| 885 self.StoreConfig(cnxn, config) |
| 886 |
| 887 def UpdateFieldDef( |
| 888 self, cnxn, project_id, field_id, field_name=None, |
| 889 applicable_type=None, applicable_predicate=None, is_required=None, |
| 890 is_multivalued=None, min_value=None, max_value=None, regex=None, |
| 891 needs_member=None, needs_perm=None, grants_perm=None, notify_on=None, |
| 892 docstring=None, admin_ids=None): |
| 893 config = self.GetProjectConfig(cnxn, project_id) |
| 894 fd = tracker_bizobj.FindFieldDefByID(field_id, config) |
| 895 # pylint: disable=multiple-statements |
| 896 if field_name is not None: fd.field_name = field_name |
| 897 if applicable_type is not None: fd.applicable_type = applicable_type |
| 898 if applicable_predicate is not None: |
| 899 fd.applicable_predicate = applicable_predicate |
| 900 if is_required is not None: fd.is_required = is_required |
| 901 if is_multivalued is not None: fd.is_multivalued = is_multivalued |
| 902 if min_value is not None: fd.min_value = min_value |
| 903 if max_value is not None: fd.max_value = max_value |
| 904 if regex is not None: fd.regex = regex |
| 905 if docstring is not None: fd.docstring = docstring |
| 906 if admin_ids is not None: fd.admin_ids = admin_ids |
| 907 self.StoreConfig(cnxn, config) |
| 908 |
| 909 def CreateComponentDef( |
| 910 self, cnxn, project_id, path, docstring, deprecated, admin_ids, cc_ids, |
| 911 created, creator_id): |
| 912 config = self.GetProjectConfig(cnxn, project_id) |
| 913 cd = tracker_bizobj.MakeComponentDef( |
| 914 self.next_component_id, project_id, path, docstring, deprecated, |
| 915 admin_ids, cc_ids, created, creator_id) |
| 916 config.component_defs.append(cd) |
| 917 self.next_component_id += 1 |
| 918 self.StoreConfig(cnxn, config) |
| 919 return self.next_component_id - 1 |
| 920 |
| 921 def UpdateComponentDef( |
| 922 self, cnxn, project_id, component_id, path=None, docstring=None, |
| 923 deprecated=None, admin_ids=None, cc_ids=None, created=None, |
| 924 creator_id=None, modified=None, modifier_id=None): |
| 925 config = self.GetProjectConfig(cnxn, project_id) |
| 926 cd = tracker_bizobj.FindComponentDefByID(component_id, config) |
| 927 if path is not None: |
| 928 assert path |
| 929 cd.path = path |
| 930 # pylint: disable=multiple-statements |
| 931 if docstring is not None: cd.docstring = docstring |
| 932 if deprecated is not None: cd.deprecated = deprecated |
| 933 if admin_ids is not None: cd.admin_ids = admin_ids |
| 934 if cc_ids is not None: cd.cc_ids = cc_ids |
| 935 if created is not None: cd.created = created |
| 936 if creator_id is not None: cd.creator_id = creator_id |
| 937 if modified is not None: cd.modified = modified |
| 938 if modifier_id is not None: cd.modifier_id = modifier_id |
| 939 self.StoreConfig(cnxn, config) |
| 940 |
| 941 def DeleteComponentDef(self, cnxn, project_id, component_id): |
| 942 """Delete the specified component definition.""" |
| 943 config = self.GetProjectConfig(cnxn, project_id) |
| 944 config.component_defs = [ |
| 945 cd for cd in config.component_defs |
| 946 if cd.component_id != component_id] |
| 947 self.StoreConfig(cnxn, config) |
| 948 |
| 949 def InvalidateMemcache(self, issues, key_prefix=''): |
| 950 pass |
| 951 |
| 952 |
| 953 class IssueService(object): |
| 954 """Fake version of IssueService that just works in-RAM.""" |
| 955 # pylint: disable=unused-argument |
| 956 |
| 957 def __init__(self, user_id=None): |
| 958 self.user_id = user_id |
| 959 # Dictionary {project_id: issue_pb_dict} |
| 960 # where issue_pb_dict is a dictionary of the form |
| 961 # {local_id: issue_pb} |
| 962 self.issues_by_project = {} |
| 963 self.issues_by_iid = {} |
| 964 # Dictionary {project_id: comment_pb_dict} |
| 965 # where comment_pb_dict is a dictionary of the form |
| 966 # {local_id: comment_pb_list} |
| 967 self.comments_by_project = {} |
| 968 self.comments_by_iid = {} |
| 969 self.comments_by_cid = {} |
| 970 self.attachments_by_id = {} |
| 971 |
| 972 # Set of issue IDs for issues that have been indexed by calling |
| 973 # IndexIssues(). |
| 974 self.indexed_issue_iids = set() |
| 975 |
| 976 # Test-only indication that the indexer would have been called |
| 977 # by the real DITPersist. |
| 978 self.indexer_called = False |
| 979 |
| 980 # Test-only sequence of updated and enqueued. |
| 981 self.updated_issues = [] |
| 982 self.enqueued_issues = [] |
| 983 |
| 984 # Test-only sequence of expunged issues and projects. |
| 985 self.expunged_issues = [] |
| 986 self.expunged_former_locations = [] |
| 987 self.expunged_local_ids = [] |
| 988 |
| 989 # Test-only indicators that methods were called. |
| 990 self.get_all_issues_in_project_called = False |
| 991 self.update_issues_called = False |
| 992 self.enqueue_issues_called = False |
| 993 |
| 994 # The next id to return if it is > 0. |
| 995 self.next_id = -1 |
| 996 |
| 997 def UpdateIssues( |
| 998 self, cnxn, issues, update_cols=None, just_derived=False, |
| 999 commit=True, invalidate=True): |
| 1000 self.update_issues_called = True |
| 1001 self.updated_issues.extend(issues) |
| 1002 |
| 1003 def EnqueueIssuesForIndexing(self, _cnxn, issues): |
| 1004 self.enqueue_issues_called = True |
| 1005 self.enqueued_issues.extend(issues) |
| 1006 |
| 1007 def ExpungeIssues(self, _cnxn, issue_ids): |
| 1008 self.expunged_issues.extend(issue_ids) |
| 1009 |
| 1010 def ExpungeFormerLocations(self, _cnxn, project_id): |
| 1011 self.expunged_former_locations.append(project_id) |
| 1012 |
| 1013 def ExpungeLocalIDCounters(self, _cnxn, project_id): |
| 1014 self.expunged_local_ids.append(project_id) |
| 1015 |
| 1016 def TestAddIssue(self, issue): |
| 1017 project_id = issue.project_id |
| 1018 self.issues_by_project.setdefault(project_id, {}) |
| 1019 self.issues_by_project[project_id][issue.local_id] = issue |
| 1020 self.issues_by_iid[issue.issue_id] = issue |
| 1021 |
| 1022 # Adding a new issue should add the first comment to the issue |
| 1023 comment = tracker_pb2.IssueComment() |
| 1024 comment.project_id = issue.project_id |
| 1025 comment.issue_id = issue.issue_id |
| 1026 comment.content = issue.summary |
| 1027 comment.timestamp = issue.opened_timestamp |
| 1028 if issue.reporter_id: |
| 1029 comment.user_id = issue.reporter_id |
| 1030 comment.sequence = 0 |
| 1031 self.TestAddComment(comment, issue.local_id) |
| 1032 |
| 1033 def TestAddComment(self, comment, local_id): |
| 1034 pid = comment.project_id |
| 1035 if not comment.id: |
| 1036 comment.id = len(self.comments_by_cid) |
| 1037 |
| 1038 self.comments_by_project.setdefault(pid, {}) |
| 1039 self.comments_by_project[pid].setdefault(local_id, []).append(comment) |
| 1040 self.comments_by_iid.setdefault(comment.issue_id, []).append(comment) |
| 1041 self.comments_by_cid[comment.id] = comment |
| 1042 |
| 1043 def TestAddAttachment(self, attachment, comment_id, issue_id): |
| 1044 if not attachment.attachment_id: |
| 1045 attachment.attachment_id = len(self.attachments_by_id) |
| 1046 |
| 1047 aid = attachment.attachment_id |
| 1048 self.attachments_by_id[aid] = attachment, comment_id, issue_id |
| 1049 comment = self.comments_by_cid[comment_id] |
| 1050 if attachment not in comment.attachments: |
| 1051 comment.attachments.extend([attachment]) |
| 1052 |
| 1053 def GetAttachmentAndContext(self, _cnxn, attachment_id): |
| 1054 if attachment_id in self.attachments_by_id: |
| 1055 attach, comment_id, issue_id = self.attachments_by_id[attachment_id] |
| 1056 if not attach.deleted: |
| 1057 return attach, comment_id, issue_id |
| 1058 |
| 1059 raise issue_svc.NoSuchAttachmentException() |
| 1060 |
| 1061 def GetComments(self, _cnxn, where=None, order_by=None, **kwargs): |
| 1062 # This is a very limited subset of what the real GetComments() can do. |
| 1063 cid = kwargs.get('id') |
| 1064 |
| 1065 comment = self.comments_by_cid.get(cid) |
| 1066 if comment: |
| 1067 return [comment] |
| 1068 else: |
| 1069 return [] |
| 1070 |
| 1071 def GetComment(self, cnxn, comment_id): |
| 1072 """Get the requested comment, or raise an exception.""" |
| 1073 comments = self.GetComments(cnxn, id=comment_id) |
| 1074 if len(comments) == 1: |
| 1075 return comments[0] |
| 1076 |
| 1077 raise issue_svc.NoSuchCommentException() |
| 1078 |
| 1079 def ResolveIssueRefs(self, cnxn, ref_projects, default_project_name, refs): |
| 1080 result = [] |
| 1081 for project_name, local_id in refs: |
| 1082 project = ref_projects.get(project_name or default_project_name) |
| 1083 if not project or project.state == project_pb2.ProjectState.DELETABLE: |
| 1084 continue # ignore any refs to issues in deleted projects |
| 1085 try: |
| 1086 issue = self.GetIssueByLocalID(cnxn, project.project_id, local_id) |
| 1087 result.append(issue.issue_id) |
| 1088 except issue_svc.NoSuchIssueException: |
| 1089 pass # ignore any refs to issues that don't exist |
| 1090 |
| 1091 return result |
| 1092 |
| 1093 def GetAllIssuesInProject(self, _cnxn, project_id, min_local_id=None): |
| 1094 self.get_all_issues_in_project_called = True |
| 1095 if project_id in self.issues_by_project: |
| 1096 return self.issues_by_project[project_id].values() |
| 1097 else: |
| 1098 return [] |
| 1099 |
| 1100 def GetIssuesByLocalIDs( |
| 1101 self, _cnxn, project_id, local_id_list, shard_id=None): |
| 1102 results = [] |
| 1103 for local_id in local_id_list: |
| 1104 if (project_id in self.issues_by_project |
| 1105 and local_id in self.issues_by_project[project_id]): |
| 1106 results.append(self.issues_by_project[project_id][local_id]) |
| 1107 |
| 1108 return results |
| 1109 |
| 1110 def GetIssueByLocalID(self, _cnxn, project_id, local_id): |
| 1111 try: |
| 1112 return self.issues_by_project[project_id][local_id] |
| 1113 except KeyError: |
| 1114 raise issue_svc.NoSuchIssueException() |
| 1115 |
| 1116 def GetAnyOnHandIssue(self, issue_ids, start=None, end=None): |
| 1117 return None # Treat them all like misses. |
| 1118 |
| 1119 def GetIssue(self, _cnxn, issue_id): |
| 1120 if issue_id in self.issues_by_iid: |
| 1121 return self.issues_by_iid[issue_id] |
| 1122 else: |
| 1123 raise issue_svc.NoSuchIssueException() |
| 1124 |
| 1125 def LookupIssueID(self, _cnxn, project_id, local_id): |
| 1126 try: |
| 1127 issue = self.issues_by_project[project_id][local_id] |
| 1128 except KeyError: |
| 1129 raise issue_svc.NoSuchIssueException() |
| 1130 return issue.issue_id |
| 1131 |
| 1132 def GetCommentsForIssue(self, _cnxn, issue_id): |
| 1133 comments = self.comments_by_iid.get(issue_id, []) |
| 1134 for idx, c in enumerate(comments): |
| 1135 c.sequence = idx |
| 1136 |
| 1137 return comments |
| 1138 |
| 1139 def InsertIssue(self, cnxn, issue): |
| 1140 issue.issue_id = issue.project_id * 1000000 + issue.local_id |
| 1141 self.issues_by_project.setdefault(issue.project_id, {}) |
| 1142 self.issues_by_project[issue.project_id][issue.local_id] = issue |
| 1143 self.issues_by_iid[issue.issue_id] = issue |
| 1144 return issue.issue_id |
| 1145 |
| 1146 def CreateIssue( |
| 1147 self, cnxn, services, project_id, |
| 1148 summary, status, owner_id, cc_ids, labels, field_values, |
| 1149 component_ids, reporter_id, marked_description, blocked_on=None, |
| 1150 blocking=None, attachments=None, timestamp=None, index_now=True): |
| 1151 issue = tracker_pb2.Issue() |
| 1152 issue.project_id = project_id |
| 1153 issue.summary = summary |
| 1154 issue.status = status |
| 1155 if owner_id: |
| 1156 issue.owner_id = owner_id |
| 1157 issue.cc_ids.extend(cc_ids) |
| 1158 issue.labels.extend(labels) |
| 1159 issue.field_values.extend(field_values) |
| 1160 issue.reporter_id = reporter_id |
| 1161 if timestamp: |
| 1162 issue.opened_timestamp = timestamp |
| 1163 |
| 1164 if blocked_on: |
| 1165 issue.blocked_on_iids.extend(blocked_on) |
| 1166 if blocking: |
| 1167 issue.blocking.extend(blocking) |
| 1168 |
| 1169 if blocking: |
| 1170 issue.blocking_iids.extend(blocking) |
| 1171 |
| 1172 issue.local_id = self.AllocateNextLocalID(cnxn, project_id) |
| 1173 issue.issue_id = project_id * 1000000 + issue.local_id |
| 1174 |
| 1175 self.TestAddIssue(issue) |
| 1176 self.comments_by_iid[issue.issue_id][0].content = marked_description |
| 1177 return issue.local_id |
| 1178 |
| 1179 def SetUsedLocalID(self, cnxn, project_id): |
| 1180 self.next_id = self.GetHighestLocalID(cnxn, project_id) + 1 |
| 1181 |
| 1182 def AllocateNextLocalID(self, cnxn, project_id): |
| 1183 return self.GetHighestLocalID(cnxn, project_id) + 1 |
| 1184 |
| 1185 def GetHighestLocalID(self, _cnxn, project_id): |
| 1186 if self.next_id > 0: |
| 1187 return self.next_id - 1 |
| 1188 else: |
| 1189 issue_dict = self.issues_by_project.get(project_id, {}) |
| 1190 highest = max([0] + [issue.local_id for issue in issue_dict.itervalues()]) |
| 1191 return highest |
| 1192 |
| 1193 def ApplyIssueComment( |
| 1194 self, cnxn, services, reporter_id, project_id, |
| 1195 local_id, summary, status, owner_id, cc_ids, labels, field_values, |
| 1196 component_ids, blocked_on, blocking, dangling_blocked_on_refs, |
| 1197 dangling_blocking_refs, merged_into, index_now=True, |
| 1198 page_gen_ts=None, comment=None, inbound_message=None, attachments=None, |
| 1199 timestamp=None): |
| 1200 """Feel free to implement a spec-compliant return value.""" |
| 1201 issue = self.issues_by_project[project_id][local_id] |
| 1202 amendments = [] |
| 1203 |
| 1204 if summary and summary != issue.summary: |
| 1205 issue.summary = summary |
| 1206 amendments.append(tracker_bizobj.MakeSummaryAmendment( |
| 1207 summary, issue.summary)) |
| 1208 |
| 1209 if status and status != issue.status: |
| 1210 issue.status = status |
| 1211 amendments.append(tracker_bizobj.MakeStatusAmendment( |
| 1212 status, issue.status)) |
| 1213 |
| 1214 issue.owner_id = owner_id |
| 1215 issue.cc_ids = cc_ids |
| 1216 issue.labels = labels |
| 1217 issue.field_values = field_values |
| 1218 issue.component_ids = component_ids |
| 1219 |
| 1220 issue.blocked_on_iids.extend(blocked_on) |
| 1221 issue.blocking_iids.extend(blocking) |
| 1222 issue.dangling_blocked_on_refs.extend(dangling_blocked_on_refs) |
| 1223 issue.dangling_blocking_refs.extend(dangling_blocking_refs) |
| 1224 |
| 1225 if merged_into is not None: |
| 1226 issue.merged_into = merged_into |
| 1227 |
| 1228 if amendments or (comment and comment.strip()) or attachments: |
| 1229 comment_pb = self.CreateIssueComment( |
| 1230 cnxn, project_id, local_id, reporter_id, comment, |
| 1231 amendments=amendments, inbound_message=inbound_message) |
| 1232 else: |
| 1233 comment_pb = None |
| 1234 |
| 1235 return amendments, comment_pb |
| 1236 |
| 1237 def GetCommentsForIssues(self, _cnxn, issue_ids): |
| 1238 comments_dict = {} |
| 1239 for issue_id in issue_ids: |
| 1240 comments_dict[issue_id] = self.comments_by_iid[issue_id] |
| 1241 |
| 1242 return comments_dict |
| 1243 |
| 1244 def InsertComment(self, cnxn, comment, commit=True): |
| 1245 issue = self.GetIssue(cnxn, comment.issue_id) |
| 1246 self.TestAddComment(comment, issue.local_id) |
| 1247 |
| 1248 # pylint: disable=unused-argument |
| 1249 def DeltaUpdateIssue( |
| 1250 self, cnxn, services, reporter_id, project_id, |
| 1251 config, issue, status, owner_id, cc_add, cc_remove, comp_ids_add, |
| 1252 comp_ids_remove, labels_add, labels_remove, field_vals_add, |
| 1253 field_vals_remove, fields_clear, blocked_on_add=None, |
| 1254 blocked_on_remove=None, blocking_add=None, blocking_remove=None, |
| 1255 merged_into=None, index_now=False, comment=None, summary=None, |
| 1256 iids_to_invalidate=None, rules=None, predicate_asts=None, |
| 1257 timestamp=None): |
| 1258 # Return a bogus amendments list if any of the fields changed |
| 1259 amendments = [] |
| 1260 comment_pb = tracker_pb2.IssueComment() |
| 1261 if (status or owner_id or cc_add or cc_remove or labels_add or |
| 1262 labels_remove or field_vals_add or field_vals_remove or fields_clear or |
| 1263 blocked_on_add or blocked_on_remove or blocking_add or |
| 1264 blocking_remove or merged_into or summary): |
| 1265 amendments.append(tracker_bizobj.MakeStatusAmendment( |
| 1266 'Updated', issue.status)) |
| 1267 |
| 1268 if not amendments and (not comment or not comment.strip()): |
| 1269 return [], None |
| 1270 |
| 1271 comment_pb = self.CreateIssueComment( |
| 1272 cnxn, project_id, issue.local_id, reporter_id, comment, |
| 1273 amendments=amendments) |
| 1274 |
| 1275 self.indexer_called = index_now |
| 1276 return amendments, comment_pb |
| 1277 |
| 1278 def InvalidateIIDs(self, cnxn, iids_to_invalidate): |
| 1279 pass |
| 1280 |
| 1281 # pylint: disable=unused-argument |
| 1282 def CreateIssueComment( |
| 1283 self, _cnxn, project_id, local_id, user_id, content, |
| 1284 inbound_message=None, amendments=None, attachments=None, timestamp=None, |
| 1285 is_spam=False, commit=True): |
| 1286 # Add a comment to an issue |
| 1287 issue = self.issues_by_project[project_id][local_id] |
| 1288 |
| 1289 comment = tracker_pb2.IssueComment() |
| 1290 comment.id = len(self.comments_by_cid) |
| 1291 comment.project_id = project_id |
| 1292 comment.issue_id = issue.issue_id |
| 1293 comment.content = content |
| 1294 comment.user_id = user_id |
| 1295 if timestamp is not None: |
| 1296 comment.timestamp = timestamp |
| 1297 else: |
| 1298 comment.timestamp = 1234567890 |
| 1299 if amendments: |
| 1300 comment.amendments.extend(amendments) |
| 1301 if inbound_message: |
| 1302 comment.inbound_message = inbound_message |
| 1303 |
| 1304 pid = project_id |
| 1305 self.comments_by_project.setdefault(pid, {}) |
| 1306 self.comments_by_project[pid].setdefault(local_id, []).append(comment) |
| 1307 self.comments_by_iid.setdefault(issue.issue_id, []).append(comment) |
| 1308 self.comments_by_cid[comment.id] = comment |
| 1309 |
| 1310 if attachments: |
| 1311 for filename, filecontent, mimetype in attachments: |
| 1312 aid = len(self.attachments_by_id) |
| 1313 attach = comment.attachments_add( |
| 1314 attachment_id=aid, |
| 1315 filename=filename, |
| 1316 filesize=len(filecontent), |
| 1317 mimetype=mimetype, |
| 1318 blobkey='blob(%s)' % filename) |
| 1319 self.attachments_by_id[aid] = attach, pid, comment.id |
| 1320 |
| 1321 return comment |
| 1322 |
| 1323 def GetOpenAndClosedIssues(self, _cnxn, issue_ids): |
| 1324 open_issues = [] |
| 1325 closed_issues = [] |
| 1326 for issue_id in issue_ids: |
| 1327 try: |
| 1328 issue = self.issues_by_iid[issue_id] |
| 1329 if issue.status == 'Fixed': |
| 1330 closed_issues.append(issue) |
| 1331 else: |
| 1332 open_issues.append(issue) |
| 1333 except KeyError: |
| 1334 continue |
| 1335 |
| 1336 return open_issues, closed_issues |
| 1337 |
| 1338 def GetIssuesDict( |
| 1339 self, _cnxn, issue_ids, use_cache=True, shard_id=None): |
| 1340 return {iid: self.issues_by_iid[iid] for iid in issue_ids} |
| 1341 |
| 1342 def GetIssues(self, _cnxn, issue_ids, use_cache=True, shard_id=None): |
| 1343 results = [self.issues_by_iid[issue_id] for issue_id in issue_ids |
| 1344 if issue_id in self.issues_by_iid] |
| 1345 |
| 1346 return results |
| 1347 |
| 1348 def SoftDeleteIssue( |
| 1349 self, _cnxn, project_id, local_id, deleted, user_service): |
| 1350 issue = self.issues_by_project[project_id][local_id] |
| 1351 issue.deleted = deleted |
| 1352 |
| 1353 def SoftDeleteComment( |
| 1354 self, cnxn, project_id, local_id, sequence_num, |
| 1355 deleted_by_user_id, user_service, delete=True, reindex=True, |
| 1356 is_spam=False): |
| 1357 issue = self.GetIssueByLocalID(cnxn, project_id, local_id) |
| 1358 comments = self.GetCommentsForIssue(cnxn, issue.issue_id) |
| 1359 if not comments: |
| 1360 raise Exception( |
| 1361 'No comments for issue, project, seq (%s, %s, %s), cannot delete' |
| 1362 % (local_id, project_id, sequence_num)) |
| 1363 if len(comments) < sequence_num: |
| 1364 raise Exception( |
| 1365 'Attempting to delete comment %s only %s comments created' % |
| 1366 (sequence_num, len(comments))) |
| 1367 comments[sequence_num].is_spam = is_spam |
| 1368 if delete: |
| 1369 comments[sequence_num].deleted_by = deleted_by_user_id |
| 1370 else: |
| 1371 comments[sequence_num].reset('deleted_by') |
| 1372 |
| 1373 def DeleteComponentReferences(self, _cnxn, component_id): |
| 1374 for _, issue in self.issues_by_iid.iteritems(): |
| 1375 issue.component_ids = [ |
| 1376 cid for cid in issue.component_ids if cid != component_id] |
| 1377 |
| 1378 def RunIssueQuery( |
| 1379 self, cnxn, left_joins, where, order_by, shard_id=None, limit=None): |
| 1380 """This always returns empty results. Mock it to test other cases.""" |
| 1381 return [], False |
| 1382 |
| 1383 def GetIIDsByLabelIDs(self, cnxn, label_ids, project_id, shard_id): |
| 1384 """This always returns empty results. Mock it to test other cases.""" |
| 1385 return [] |
| 1386 |
| 1387 def GetIIDsByParticipant(self, cnxn, user_ids, project_ids, shard_id): |
| 1388 """This always returns empty results. Mock it to test other cases.""" |
| 1389 return [] |
| 1390 |
| 1391 def MoveIssues(self, cnxn, dest_project, issues, user_service): |
| 1392 move_to = dest_project.project_id |
| 1393 self.issues_by_project.setdefault(move_to, {}) |
| 1394 for issue in issues: |
| 1395 project_id = issue.project_id |
| 1396 self.issues_by_project[project_id].pop(issue.local_id) |
| 1397 issue.local_id = self.AllocateNextLocalID(cnxn, move_to) |
| 1398 self.issues_by_project[move_to][issue.local_id] = issue |
| 1399 issue.project_id = move_to |
| 1400 return [] |
| 1401 |
| 1402 |
| 1403 class SpamService(object): |
| 1404 """Fake version of SpamService that just works in-RAM.""" |
| 1405 |
| 1406 def __init__(self, user_id=None): |
| 1407 self.user_id = user_id |
| 1408 self.reports_by_issue_id = collections.defaultdict(list) |
| 1409 self.comment_reports_by_issue_id = collections.defaultdict(dict) |
| 1410 self.manual_verdicts_by_issue_id = collections.defaultdict(dict) |
| 1411 self.manual_verdicts_by_comment_id = collections.defaultdict(dict) |
| 1412 |
| 1413 def FlagIssues(self, cnxn, issue_service, issues, user_id, flagged_spam): |
| 1414 for issue in issues: |
| 1415 if flagged_spam: |
| 1416 self.reports_by_issue_id[issue.issue_id].append(user_id) |
| 1417 else: |
| 1418 self.reports_by_issue_id[issue.issue_id].remove(user_id) |
| 1419 |
| 1420 def FlagComment(self, cnxn, issue_id, comment_id, reported_user_id, user_id, |
| 1421 flagged_spam): |
| 1422 if not comment_id in self.comment_reports_by_issue_id[issue_id]: |
| 1423 self.comment_reports_by_issue_id[issue_id][comment_id] = [] |
| 1424 if flagged_spam: |
| 1425 self.comment_reports_by_issue_id[issue_id][comment_id].append(user_id) |
| 1426 else: |
| 1427 self.comment_reports_by_issue_id[issue_id][comment_id].remove(user_id) |
| 1428 |
| 1429 def RecordManualIssueVerdicts( |
| 1430 self, cnxn, issue_service, issues, user_id, is_spam): |
| 1431 for issue in issues: |
| 1432 self.manual_verdicts_by_issue_id[issue.issue_id][user_id] = is_spam |
| 1433 |
| 1434 def RecordManualCommentVerdict( |
| 1435 self, cnxn, issue_service, user_service, comment_id, |
| 1436 sequnce_num, user_id, is_spam): |
| 1437 self.manual_verdicts_by_comment_id[comment_id][user_id] = is_spam |
| 1438 |
| 1439 def RecordClassifierIssueVerdict(self, cnxn, issue, is_spam, confidence): |
| 1440 return |
| 1441 |
| 1442 def RecordClassifierCommentVerdict(self, cnxn, issue, is_spam, confidence): |
| 1443 return |
| 1444 |
| 1445 def ClassifyComment(self, comment): |
| 1446 return {'outputLabel': 'ham', |
| 1447 'outputMulti': [{'label': 'ham', 'score': '1.0'}]} |
| 1448 |
| 1449 def ClassifyIssue(self, issue, firstComment): |
| 1450 return {'outputLabel': 'ham', |
| 1451 'outputMulti': [{'label': 'ham', 'score': '1.0'}]} |
| 1452 |
| 1453 |
| 1454 class FeaturesService(object): |
| 1455 """A fake implementation of FeaturesService.""" |
| 1456 def __init__(self): |
| 1457 # Test-only sequence of expunged projects. |
| 1458 self.expunged_saved_queries = [] |
| 1459 self.expunged_filter_rules = [] |
| 1460 self.expunged_quick_edit = [] |
| 1461 |
| 1462 def ExpungeSavedQueriesExecuteInProject(self, _cnxn, project_id): |
| 1463 self.expunged_saved_queries.append(project_id) |
| 1464 |
| 1465 def ExpungeFilterRules(self, _cnxn, project_id): |
| 1466 self.expunged_filter_rules.append(project_id) |
| 1467 |
| 1468 def ExpungeQuickEditHistory(self, _cnxn, project_id): |
| 1469 self.expunged_quick_edit.append(project_id) |
| 1470 |
| 1471 def GetFilterRules(self, cnxn, project_id): |
| 1472 return [] |
| 1473 |
| 1474 def GetCannedQueriesByProjectID(self, cnxn, project_id): |
| 1475 return [] |
| 1476 |
| 1477 def UpdateCannedQueries(self, cnxn, project_id, canned_queries): |
| 1478 pass |
| 1479 |
| 1480 def GetSubscriptionsInProjects(self, cnxn, project_ids): |
| 1481 return {} |
| 1482 |
| 1483 def GetSavedQuery(self, cnxn, query_id): |
| 1484 return tracker_pb2.SavedQuery() |
| 1485 |
| 1486 |
| 1487 class PostData(object): |
| 1488 """A dictionary-like object that also implements getall().""" |
| 1489 |
| 1490 def __init__(self, *args, **kwargs): |
| 1491 self.dictionary = dict(*args, **kwargs) |
| 1492 |
| 1493 def getall(self, key): |
| 1494 """Return all values, assume that the value at key is already a list.""" |
| 1495 return self.dictionary.get(key, []) |
| 1496 |
| 1497 def get(self, key, default=None): |
| 1498 """Return first value, assume that the value at key is already a list.""" |
| 1499 return self.dictionary.get(key, [default])[0] |
| 1500 |
| 1501 def __getitem__(self, key): |
| 1502 """Return first value, assume that the value at key is already a list.""" |
| 1503 return self.dictionary[key][0] |
| 1504 |
| 1505 def __contains__(self, key): |
| 1506 return key in self.dictionary |
| 1507 |
| 1508 def keys(self): |
| 1509 """Return the keys in the POST data.""" |
| 1510 return self.dictionary.keys() |
| 1511 |
| 1512 |
| 1513 class FakeFile: |
| 1514 def __init__(self, data=None): |
| 1515 self.data = data |
| 1516 |
| 1517 def read(self): |
| 1518 return self.data |
| 1519 |
| 1520 def write(self, content): |
| 1521 return |
| 1522 |
| 1523 def __enter__(self): |
| 1524 return self |
| 1525 |
| 1526 def __exit__(self, __1, __2, __3): |
| 1527 return None |
| 1528 |
| 1529 |
| 1530 def gcs_open(filename, mode): |
| 1531 return FakeFile(filename) |
OLD | NEW |