Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(301)

Side by Side Diff: appengine/monorail/testing/fake.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « appengine/monorail/testing/api_clients.cfg ('k') | appengine/monorail/testing/test/__init__.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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)
OLDNEW
« no previous file with comments | « appengine/monorail/testing/api_clients.cfg ('k') | appengine/monorail/testing/test/__init__.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698