|
1 # Copyright (C) 2009 Google Inc. All rights reserved. |
|
2 # |
|
3 # Redistribution and use in source and binary forms, with or without |
|
4 # modification, are permitted provided that the following conditions are |
|
5 # met: |
|
6 # |
|
7 # * Redistributions of source code must retain the above copyright |
|
8 # notice, this list of conditions and the following disclaimer. |
|
9 # * Redistributions in binary form must reproduce the above |
|
10 # copyright notice, this list of conditions and the following disclaimer |
|
11 # in the documentation and/or other materials provided with the |
|
12 # distribution. |
|
13 # * Neither the name of Google Inc. nor the names of its |
|
14 # contributors may be used to endorse or promote products derived from |
|
15 # this software without specific prior written permission. |
|
16 # |
|
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|
21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
28 |
|
29 import os |
|
30 import threading |
|
31 |
|
32 from webkitpy.common.config.committers import CommitterList, Reviewer |
|
33 from webkitpy.common.checkout.commitinfo import CommitInfo |
|
34 from webkitpy.common.checkout.scm import CommitMessage |
|
35 from webkitpy.common.net.bugzilla import Bug, Attachment |
|
36 from webkitpy.common.net.rietveld import Rietveld |
|
37 from webkitpy.thirdparty.mock import Mock |
|
38 from webkitpy.common.system.deprecated_logging import log |
|
39 |
|
40 |
|
41 def _id_to_object_dictionary(*objects): |
|
42 dictionary = {} |
|
43 for thing in objects: |
|
44 dictionary[thing["id"]] = thing |
|
45 return dictionary |
|
46 |
|
47 # Testing |
|
48 |
|
49 # FIXME: The ids should be 1, 2, 3 instead of crazy numbers. |
|
50 |
|
51 |
|
52 _patch1 = { |
|
53 "id": 197, |
|
54 "bug_id": 42, |
|
55 "url": "http://example.com/197", |
|
56 "name": "Patch1", |
|
57 "is_obsolete": False, |
|
58 "is_patch": True, |
|
59 "review": "+", |
|
60 "reviewer_email": "foo@bar.com", |
|
61 "commit-queue": "+", |
|
62 "committer_email": "foo@bar.com", |
|
63 "attacher_email": "Contributer1", |
|
64 } |
|
65 |
|
66 |
|
67 _patch2 = { |
|
68 "id": 128, |
|
69 "bug_id": 42, |
|
70 "url": "http://example.com/128", |
|
71 "name": "Patch2", |
|
72 "is_obsolete": False, |
|
73 "is_patch": True, |
|
74 "review": "+", |
|
75 "reviewer_email": "foo@bar.com", |
|
76 "commit-queue": "+", |
|
77 "committer_email": "non-committer@example.com", |
|
78 "attacher_email": "eric@webkit.org", |
|
79 } |
|
80 |
|
81 |
|
82 _patch3 = { |
|
83 "id": 103, |
|
84 "bug_id": 75, |
|
85 "url": "http://example.com/103", |
|
86 "name": "Patch3", |
|
87 "is_obsolete": False, |
|
88 "is_patch": True, |
|
89 "in-rietveld": "?", |
|
90 "review": "?", |
|
91 "attacher_email": "eric@webkit.org", |
|
92 } |
|
93 |
|
94 |
|
95 _patch4 = { |
|
96 "id": 104, |
|
97 "bug_id": 77, |
|
98 "url": "http://example.com/103", |
|
99 "name": "Patch3", |
|
100 "is_obsolete": False, |
|
101 "is_patch": True, |
|
102 "review": "+", |
|
103 "commit-queue": "?", |
|
104 "reviewer_email": "foo@bar.com", |
|
105 "attacher_email": "Contributer2", |
|
106 } |
|
107 |
|
108 |
|
109 _patch5 = { |
|
110 "id": 105, |
|
111 "bug_id": 77, |
|
112 "url": "http://example.com/103", |
|
113 "name": "Patch5", |
|
114 "is_obsolete": False, |
|
115 "is_patch": True, |
|
116 "in-rietveld": "?", |
|
117 "review": "+", |
|
118 "reviewer_email": "foo@bar.com", |
|
119 "attacher_email": "eric@webkit.org", |
|
120 } |
|
121 |
|
122 |
|
123 _patch6 = { # Valid committer, but no reviewer. |
|
124 "id": 106, |
|
125 "bug_id": 77, |
|
126 "url": "http://example.com/103", |
|
127 "name": "ROLLOUT of r3489", |
|
128 "is_obsolete": False, |
|
129 "is_patch": True, |
|
130 "in-rietveld": "-", |
|
131 "commit-queue": "+", |
|
132 "committer_email": "foo@bar.com", |
|
133 "attacher_email": "eric@webkit.org", |
|
134 } |
|
135 |
|
136 |
|
137 _patch7 = { # Valid review, patch is marked obsolete. |
|
138 "id": 107, |
|
139 "bug_id": 76, |
|
140 "url": "http://example.com/103", |
|
141 "name": "Patch7", |
|
142 "is_obsolete": True, |
|
143 "is_patch": True, |
|
144 "in-rietveld": "+", |
|
145 "review": "+", |
|
146 "reviewer_email": "foo@bar.com", |
|
147 "attacher_email": "eric@webkit.org", |
|
148 } |
|
149 |
|
150 |
|
151 # This matches one of Bug.unassigned_emails |
|
152 _unassigned_email = "webkit-unassigned@lists.webkit.org" |
|
153 |
|
154 |
|
155 # FIXME: The ids should be 1, 2, 3 instead of crazy numbers. |
|
156 |
|
157 |
|
158 _bug1 = { |
|
159 "id": 42, |
|
160 "title": "Bug with two r+'d and cq+'d patches, one of which has an " |
|
161 "invalid commit-queue setter.", |
|
162 "assigned_to_email": _unassigned_email, |
|
163 "attachments": [_patch1, _patch2], |
|
164 } |
|
165 |
|
166 |
|
167 _bug2 = { |
|
168 "id": 75, |
|
169 "title": "Bug with a patch needing review.", |
|
170 "assigned_to_email": "foo@foo.com", |
|
171 "attachments": [_patch3], |
|
172 } |
|
173 |
|
174 |
|
175 _bug3 = { |
|
176 "id": 76, |
|
177 "title": "The third bug", |
|
178 "assigned_to_email": _unassigned_email, |
|
179 "attachments": [_patch7], |
|
180 } |
|
181 |
|
182 |
|
183 _bug4 = { |
|
184 "id": 77, |
|
185 "title": "The fourth bug", |
|
186 "assigned_to_email": "foo@foo.com", |
|
187 "attachments": [_patch4, _patch5, _patch6], |
|
188 } |
|
189 |
|
190 |
|
191 class MockBugzillaQueries(Mock): |
|
192 |
|
193 def __init__(self, bugzilla): |
|
194 Mock.__init__(self) |
|
195 self._bugzilla = bugzilla |
|
196 |
|
197 def _all_bugs(self): |
|
198 return map(lambda bug_dictionary: Bug(bug_dictionary, self._bugzilla), |
|
199 self._bugzilla.bug_cache.values()) |
|
200 |
|
201 def fetch_bug_ids_from_commit_queue(self): |
|
202 bugs_with_commit_queued_patches = filter( |
|
203 lambda bug: bug.commit_queued_patches(), |
|
204 self._all_bugs()) |
|
205 return map(lambda bug: bug.id(), bugs_with_commit_queued_patches) |
|
206 |
|
207 def fetch_attachment_ids_from_review_queue(self): |
|
208 unreviewed_patches = sum([bug.unreviewed_patches() |
|
209 for bug in self._all_bugs()], []) |
|
210 return map(lambda patch: patch.id(), unreviewed_patches) |
|
211 |
|
212 def fetch_patches_from_commit_queue(self): |
|
213 return sum([bug.commit_queued_patches() |
|
214 for bug in self._all_bugs()], []) |
|
215 |
|
216 def fetch_bug_ids_from_pending_commit_list(self): |
|
217 bugs_with_reviewed_patches = filter(lambda bug: bug.reviewed_patches(), |
|
218 self._all_bugs()) |
|
219 bug_ids = map(lambda bug: bug.id(), bugs_with_reviewed_patches) |
|
220 # NOTE: This manual hack here is to allow testing logging in |
|
221 # test_assign_to_committer the real pending-commit query on bugzilla |
|
222 # will return bugs with patches which have r+, but are also obsolete. |
|
223 return bug_ids + [76] |
|
224 |
|
225 def fetch_patches_from_pending_commit_list(self): |
|
226 return sum([bug.reviewed_patches() for bug in self._all_bugs()], []) |
|
227 |
|
228 def fetch_first_patch_from_rietveld_queue(self): |
|
229 for bug in self._all_bugs(): |
|
230 patches = bug.in_rietveld_queue_patches() |
|
231 if len(patches): |
|
232 return patches[0] |
|
233 raise Exception('No patches in the rietveld queue') |
|
234 |
|
235 # FIXME: Bugzilla is the wrong Mock-point. Once we have a BugzillaNetwork |
|
236 # class we should mock that instead. |
|
237 # Most of this class is just copy/paste from Bugzilla. |
|
238 |
|
239 |
|
240 class MockBugzilla(Mock): |
|
241 |
|
242 bug_server_url = "http://example.com" |
|
243 |
|
244 bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3, _bug4) |
|
245 |
|
246 attachment_cache = _id_to_object_dictionary(_patch1, |
|
247 _patch2, |
|
248 _patch3, |
|
249 _patch4, |
|
250 _patch5, |
|
251 _patch6, |
|
252 _patch7) |
|
253 |
|
254 def __init__(self): |
|
255 Mock.__init__(self) |
|
256 self.queries = MockBugzillaQueries(self) |
|
257 self.committers = CommitterList(reviewers=[Reviewer("Foo Bar", |
|
258 "foo@bar.com")]) |
|
259 |
|
260 def create_bug(self, |
|
261 bug_title, |
|
262 bug_description, |
|
263 component=None, |
|
264 diff=None, |
|
265 patch_description=None, |
|
266 cc=None, |
|
267 blocked=None, |
|
268 mark_for_review=False, |
|
269 mark_for_commit_queue=False): |
|
270 log("MOCK create_bug") |
|
271 log("bug_title: %s" % bug_title) |
|
272 log("bug_description: %s" % bug_description) |
|
273 |
|
274 def quips(self): |
|
275 return ["Good artists copy. Great artists steal. - Pablo Picasso"] |
|
276 |
|
277 def fetch_bug(self, bug_id): |
|
278 return Bug(self.bug_cache.get(bug_id), self) |
|
279 |
|
280 def fetch_attachment(self, attachment_id): |
|
281 # This could be changed to .get() if we wish to allow failed lookups. |
|
282 attachment_dictionary = self.attachment_cache[attachment_id] |
|
283 bug = self.fetch_bug(attachment_dictionary["bug_id"]) |
|
284 for attachment in bug.attachments(include_obsolete=True): |
|
285 if attachment.id() == int(attachment_id): |
|
286 return attachment |
|
287 |
|
288 def bug_url_for_bug_id(self, bug_id): |
|
289 return "%s/%s" % (self.bug_server_url, bug_id) |
|
290 |
|
291 def fetch_bug_dictionary(self, bug_id): |
|
292 return self.bug_cache.get(bug_id) |
|
293 |
|
294 def attachment_url_for_id(self, attachment_id, action="view"): |
|
295 action_param = "" |
|
296 if action and action != "view": |
|
297 action_param = "&action=%s" % action |
|
298 return "%s/%s%s" % (self.bug_server_url, attachment_id, action_param) |
|
299 |
|
300 def set_flag_on_attachment(self, |
|
301 attachment_id, |
|
302 flag_name, |
|
303 flag_value, |
|
304 comment_text=None, |
|
305 additional_comment_text=None): |
|
306 log("MOCK setting flag '%s' to '%s' on attachment '%s' with comment '%s' and additional comment '%s'" % ( |
|
307 flag_name, flag_value, attachment_id, comment_text, additional_comment_text)) |
|
308 |
|
309 def post_comment_to_bug(self, bug_id, comment_text, cc=None): |
|
310 log("MOCK bug comment: bug_id=%s, cc=%s\n--- Begin comment ---\%s\n--- End comment ---\n" % ( |
|
311 bug_id, cc, comment_text)) |
|
312 |
|
313 def add_patch_to_bug(self, |
|
314 bug_id, |
|
315 diff, |
|
316 description, |
|
317 comment_text=None, |
|
318 mark_for_review=False, |
|
319 mark_for_commit_queue=False, |
|
320 mark_for_landing=False): |
|
321 log("MOCK add_patch_to_bug: bug_id=%s, description=%s, mark_for_review=%s, mark_for_commit_queue=%s, mark_for_landing=%s" % |
|
322 (bug_id, description, mark_for_review, mark_for_commit_queue, mark_for_landing)) |
|
323 log("-- Begin comment --") |
|
324 log(comment_text) |
|
325 log("-- End comment --") |
|
326 |
|
327 |
|
328 class MockBuilder(object): |
|
329 def __init__(self, name): |
|
330 self._name = name |
|
331 |
|
332 def name(self): |
|
333 return self._name |
|
334 |
|
335 def results_url(self): |
|
336 return "http://example.com/builders/%s/results/" % self.name() |
|
337 |
|
338 def force_build(self, username, comments): |
|
339 log("MOCK: force_build: name=%s, username=%s, comments=%s" % ( |
|
340 self._name, username, comments)) |
|
341 |
|
342 |
|
343 class MockBuildBot(object): |
|
344 def __init__(self): |
|
345 self._mock_builder1_status = { |
|
346 "name": "Builder1", |
|
347 "is_green": True, |
|
348 "activity": "building", |
|
349 } |
|
350 self._mock_builder2_status = { |
|
351 "name": "Builder2", |
|
352 "is_green": True, |
|
353 "activity": "idle", |
|
354 } |
|
355 |
|
356 def builder_with_name(self, name): |
|
357 return MockBuilder(name) |
|
358 |
|
359 def builder_statuses(self): |
|
360 return [ |
|
361 self._mock_builder1_status, |
|
362 self._mock_builder2_status, |
|
363 ] |
|
364 |
|
365 def red_core_builders_names(self): |
|
366 if not self._mock_builder2_status["is_green"]: |
|
367 return [self._mock_builder2_status["name"]] |
|
368 return [] |
|
369 |
|
370 def red_core_builders(self): |
|
371 if not self._mock_builder2_status["is_green"]: |
|
372 return [self._mock_builder2_status] |
|
373 return [] |
|
374 |
|
375 def idle_red_core_builders(self): |
|
376 if not self._mock_builder2_status["is_green"]: |
|
377 return [self._mock_builder2_status] |
|
378 return [] |
|
379 |
|
380 def last_green_revision(self): |
|
381 return 9479 |
|
382 |
|
383 def light_tree_on_fire(self): |
|
384 self._mock_builder2_status["is_green"] = False |
|
385 |
|
386 def revisions_causing_failures(self): |
|
387 return { |
|
388 "29837": [self.builder_with_name("Builder1")], |
|
389 } |
|
390 |
|
391 |
|
392 class MockSCM(Mock): |
|
393 |
|
394 fake_checkout_root = os.path.realpath("/tmp") # realpath is needed to allow for Mac OS X's /private/tmp |
|
395 |
|
396 def __init__(self): |
|
397 Mock.__init__(self) |
|
398 # FIXME: We should probably use real checkout-root detection logic here. |
|
399 # os.getcwd() can't work here because other parts of the code assume that "checkout_root" |
|
400 # will actually be the root. Since getcwd() is wrong, use a globally fake root for now. |
|
401 self.checkout_root = self.fake_checkout_root |
|
402 |
|
403 def create_patch(self, git_commit): |
|
404 return "Patch1" |
|
405 |
|
406 def commit_ids_from_commitish_arguments(self, args): |
|
407 return ["Commitish1", "Commitish2"] |
|
408 |
|
409 def commit_message_for_local_commit(self, commit_id): |
|
410 if commit_id == "Commitish1": |
|
411 return CommitMessage("CommitMessage1\n" \ |
|
412 "https://bugs.example.org/show_bug.cgi?id=42\n") |
|
413 if commit_id == "Commitish2": |
|
414 return CommitMessage("CommitMessage2\n" \ |
|
415 "https://bugs.example.org/show_bug.cgi?id=75\n") |
|
416 raise Exception("Bogus commit_id in commit_message_for_local_commit.") |
|
417 |
|
418 def diff_for_revision(self, revision): |
|
419 return "DiffForRevision%s\n" \ |
|
420 "http://bugs.webkit.org/show_bug.cgi?id=12345" % revision |
|
421 |
|
422 def svn_revision_from_commit_text(self, commit_text): |
|
423 return "49824" |
|
424 |
|
425 |
|
426 class MockCheckout(object): |
|
427 |
|
428 _committer_list = CommitterList() |
|
429 |
|
430 def commit_info_for_revision(self, svn_revision): |
|
431 return CommitInfo(svn_revision, "eric@webkit.org", { |
|
432 "bug_id": 42, |
|
433 "author_name": "Adam Barth", |
|
434 "author_email": "abarth@webkit.org", |
|
435 "author": self._committer_list.committer_by_email("abarth@webkit.org"), |
|
436 "reviewer_text": "Darin Adler", |
|
437 "reviewer": self._committer_list.committer_by_name("Darin Adler"), |
|
438 }) |
|
439 |
|
440 def bug_id_for_revision(self, svn_revision): |
|
441 return 12345 |
|
442 |
|
443 def modified_changelogs(self, git_commit): |
|
444 # Ideally we'd return something more interesting here. The problem is |
|
445 # that LandDiff will try to actually read the patch from disk! |
|
446 return [] |
|
447 |
|
448 def commit_message_for_this_commit(self, git_commit): |
|
449 commit_message = Mock() |
|
450 commit_message.message = lambda:"This is a fake commit message that is at least 50 characters." |
|
451 return commit_message |
|
452 |
|
453 def apply_patch(self, patch, force=False): |
|
454 pass |
|
455 |
|
456 def apply_reverse_diff(self, revision): |
|
457 pass |
|
458 |
|
459 |
|
460 class MockUser(object): |
|
461 |
|
462 @staticmethod |
|
463 def prompt(message, repeat=1, raw_input=raw_input): |
|
464 return "Mock user response" |
|
465 |
|
466 def edit(self, files): |
|
467 pass |
|
468 |
|
469 def edit_changelog(self, files): |
|
470 pass |
|
471 |
|
472 def page(self, message): |
|
473 pass |
|
474 |
|
475 def confirm(self, message=None): |
|
476 return True |
|
477 |
|
478 def can_open_url(self): |
|
479 return True |
|
480 |
|
481 def open_url(self, url): |
|
482 if url.startswith("file://"): |
|
483 log("MOCK: user.open_url: file://...") |
|
484 return |
|
485 log("MOCK: user.open_url: %s" % url) |
|
486 |
|
487 |
|
488 class MockIRC(object): |
|
489 |
|
490 def post(self, message): |
|
491 log("MOCK: irc.post: %s" % message) |
|
492 |
|
493 def disconnect(self): |
|
494 log("MOCK: irc.disconnect") |
|
495 |
|
496 |
|
497 class MockStatusServer(object): |
|
498 |
|
499 def __init__(self): |
|
500 self.host = "example.com" |
|
501 |
|
502 def patch_status(self, queue_name, patch_id): |
|
503 return None |
|
504 |
|
505 def svn_revision(self, svn_revision): |
|
506 return None |
|
507 |
|
508 def update_work_items(self, queue_name, work_items): |
|
509 log("MOCK: update_work_items: %s %s" % (queue_name, work_items)) |
|
510 |
|
511 def update_status(self, queue_name, status, patch=None, results_file=None): |
|
512 log("MOCK: update_status: %s %s" % (queue_name, status)) |
|
513 return 187 |
|
514 |
|
515 def update_svn_revision(self, svn_revision, broken_bot): |
|
516 return 191 |
|
517 |
|
518 def results_url_for_status(self, status_id): |
|
519 return "http://dummy_url" |
|
520 |
|
521 |
|
522 class MockExecute(Mock): |
|
523 def __init__(self, should_log): |
|
524 self._should_log = should_log |
|
525 |
|
526 def run_and_throw_if_fail(self, args, quiet=False): |
|
527 if self._should_log: |
|
528 log("MOCK run_and_throw_if_fail: %s" % args) |
|
529 return "MOCK output of child process" |
|
530 |
|
531 def run_command(self, |
|
532 args, |
|
533 cwd=None, |
|
534 input=None, |
|
535 error_handler=None, |
|
536 return_exit_code=False, |
|
537 return_stderr=True, |
|
538 decode_output=False): |
|
539 if self._should_log: |
|
540 log("MOCK run_command: %s" % args) |
|
541 return "MOCK output of child process" |
|
542 |
|
543 |
|
544 class MockOptions(Mock): |
|
545 no_squash = False |
|
546 squash = False |
|
547 |
|
548 |
|
549 class MockRietveld(): |
|
550 |
|
551 def __init__(self, executive, dryrun=False): |
|
552 pass |
|
553 |
|
554 def post(self, diff, message=None, codereview_issue=None, cc=None): |
|
555 log("MOCK: Uploading patch to rietveld") |
|
556 |
|
557 |
|
558 class MockTool(): |
|
559 |
|
560 def __init__(self, log_executive=False): |
|
561 self.wakeup_event = threading.Event() |
|
562 self.bugs = MockBugzilla() |
|
563 self.buildbot = MockBuildBot() |
|
564 self.executive = MockExecute(should_log=log_executive) |
|
565 self._irc = None |
|
566 self.user = MockUser() |
|
567 self._scm = MockSCM() |
|
568 self._checkout = MockCheckout() |
|
569 self.status_server = MockStatusServer() |
|
570 self.irc_password = "MOCK irc password" |
|
571 self.codereview = MockRietveld(self.executive) |
|
572 |
|
573 def scm(self): |
|
574 return self._scm |
|
575 |
|
576 def checkout(self): |
|
577 return self._checkout |
|
578 |
|
579 def ensure_irc_connected(self, delegate): |
|
580 if not self._irc: |
|
581 self._irc = MockIRC() |
|
582 |
|
583 def irc(self): |
|
584 return self._irc |
|
585 |
|
586 def path(self): |
|
587 return "echo" |