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 29import os 30import threading 31 32from webkitpy.common.config.committers import CommitterList, Reviewer 33from webkitpy.common.checkout.commitinfo import CommitInfo 34from webkitpy.common.checkout.scm import CommitMessage 35from webkitpy.common.net.bugzilla import Bug, Attachment 36from webkitpy.common.system.deprecated_logging import log 37from webkitpy.common.system.filesystem_mock import MockFileSystem 38from webkitpy.thirdparty.mock import Mock 39 40 41def _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 "review": "?", 90 "attacher_email": "eric@webkit.org", 91} 92 93 94_patch4 = { 95 "id": 104, 96 "bug_id": 77, 97 "url": "http://example.com/103", 98 "name": "Patch3", 99 "is_obsolete": False, 100 "is_patch": True, 101 "review": "+", 102 "commit-queue": "?", 103 "reviewer_email": "foo@bar.com", 104 "attacher_email": "Contributer2", 105} 106 107 108_patch5 = { 109 "id": 105, 110 "bug_id": 77, 111 "url": "http://example.com/103", 112 "name": "Patch5", 113 "is_obsolete": False, 114 "is_patch": True, 115 "review": "+", 116 "reviewer_email": "foo@bar.com", 117 "attacher_email": "eric@webkit.org", 118} 119 120 121_patch6 = { # Valid committer, but no reviewer. 122 "id": 106, 123 "bug_id": 77, 124 "url": "http://example.com/103", 125 "name": "ROLLOUT of r3489", 126 "is_obsolete": False, 127 "is_patch": True, 128 "commit-queue": "+", 129 "committer_email": "foo@bar.com", 130 "attacher_email": "eric@webkit.org", 131} 132 133 134_patch7 = { # Valid review, patch is marked obsolete. 135 "id": 107, 136 "bug_id": 76, 137 "url": "http://example.com/103", 138 "name": "Patch7", 139 "is_obsolete": True, 140 "is_patch": True, 141 "review": "+", 142 "reviewer_email": "foo@bar.com", 143 "attacher_email": "eric@webkit.org", 144} 145 146 147# This matches one of Bug.unassigned_emails 148_unassigned_email = "webkit-unassigned@lists.webkit.org" 149# This is needed for the FlakyTestReporter to believe the bug 150# was filed by one of the webkitpy bots. 151_commit_queue_email = "commit-queue@webkit.org" 152 153 154# FIXME: The ids should be 1, 2, 3 instead of crazy numbers. 155 156 157_bug1 = { 158 "id": 42, 159 "title": "Bug with two r+'d and cq+'d patches, one of which has an " 160 "invalid commit-queue setter.", 161 "reporter_email": "foo@foo.com", 162 "assigned_to_email": _unassigned_email, 163 "attachments": [_patch1, _patch2], 164 "bug_status": "UNCONFIRMED", 165} 166 167 168_bug2 = { 169 "id": 75, 170 "title": "Bug with a patch needing review.", 171 "reporter_email": "foo@foo.com", 172 "assigned_to_email": "foo@foo.com", 173 "attachments": [_patch3], 174 "bug_status": "ASSIGNED", 175} 176 177 178_bug3 = { 179 "id": 76, 180 "title": "The third bug", 181 "reporter_email": "foo@foo.com", 182 "assigned_to_email": _unassigned_email, 183 "attachments": [_patch7], 184 "bug_status": "NEW", 185} 186 187 188_bug4 = { 189 "id": 77, 190 "title": "The fourth bug", 191 "reporter_email": "foo@foo.com", 192 "assigned_to_email": "foo@foo.com", 193 "attachments": [_patch4, _patch5, _patch6], 194 "bug_status": "REOPENED", 195} 196 197 198_bug5 = { 199 "id": 78, 200 "title": "The fifth bug", 201 "reporter_email": _commit_queue_email, 202 "assigned_to_email": "foo@foo.com", 203 "attachments": [], 204 "bug_status": "RESOLVED", 205 "dup_id": 76, 206} 207 208 209# FIXME: This should not inherit from Mock 210class MockBugzillaQueries(Mock): 211 212 def __init__(self, bugzilla): 213 Mock.__init__(self) 214 self._bugzilla = bugzilla 215 216 def _all_bugs(self): 217 return map(lambda bug_dictionary: Bug(bug_dictionary, self._bugzilla), 218 self._bugzilla.bug_cache.values()) 219 220 def fetch_bug_ids_from_commit_queue(self): 221 bugs_with_commit_queued_patches = filter( 222 lambda bug: bug.commit_queued_patches(), 223 self._all_bugs()) 224 return map(lambda bug: bug.id(), bugs_with_commit_queued_patches) 225 226 def fetch_attachment_ids_from_review_queue(self): 227 unreviewed_patches = sum([bug.unreviewed_patches() 228 for bug in self._all_bugs()], []) 229 return map(lambda patch: patch.id(), unreviewed_patches) 230 231 def fetch_patches_from_commit_queue(self): 232 return sum([bug.commit_queued_patches() 233 for bug in self._all_bugs()], []) 234 235 def fetch_bug_ids_from_pending_commit_list(self): 236 bugs_with_reviewed_patches = filter(lambda bug: bug.reviewed_patches(), 237 self._all_bugs()) 238 bug_ids = map(lambda bug: bug.id(), bugs_with_reviewed_patches) 239 # NOTE: This manual hack here is to allow testing logging in 240 # test_assign_to_committer the real pending-commit query on bugzilla 241 # will return bugs with patches which have r+, but are also obsolete. 242 return bug_ids + [76] 243 244 def fetch_patches_from_pending_commit_list(self): 245 return sum([bug.reviewed_patches() for bug in self._all_bugs()], []) 246 247 def fetch_bugs_matching_search(self, search_string, author_email=None): 248 return [self._bugzilla.fetch_bug(78), self._bugzilla.fetch_bug(77)] 249 250_mock_reviewer = Reviewer("Foo Bar", "foo@bar.com") 251 252 253# FIXME: Bugzilla is the wrong Mock-point. Once we have a BugzillaNetwork 254# class we should mock that instead. 255# Most of this class is just copy/paste from Bugzilla. 256# FIXME: This should not inherit from Mock 257class MockBugzilla(Mock): 258 259 bug_server_url = "http://example.com" 260 261 bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3, _bug4, _bug5) 262 263 attachment_cache = _id_to_object_dictionary(_patch1, 264 _patch2, 265 _patch3, 266 _patch4, 267 _patch5, 268 _patch6, 269 _patch7) 270 271 def __init__(self): 272 Mock.__init__(self) 273 self.queries = MockBugzillaQueries(self) 274 self.committers = CommitterList(reviewers=[_mock_reviewer]) 275 self._override_patch = None 276 277 def create_bug(self, 278 bug_title, 279 bug_description, 280 component=None, 281 diff=None, 282 patch_description=None, 283 cc=None, 284 blocked=None, 285 mark_for_review=False, 286 mark_for_commit_queue=False): 287 log("MOCK create_bug") 288 log("bug_title: %s" % bug_title) 289 log("bug_description: %s" % bug_description) 290 if component: 291 log("component: %s" % component) 292 if cc: 293 log("cc: %s" % cc) 294 if blocked: 295 log("blocked: %s" % blocked) 296 return 78 297 298 def quips(self): 299 return ["Good artists copy. Great artists steal. - Pablo Picasso"] 300 301 def fetch_bug(self, bug_id): 302 return Bug(self.bug_cache.get(bug_id), self) 303 304 def set_override_patch(self, patch): 305 self._override_patch = patch 306 307 def fetch_attachment(self, attachment_id): 308 if self._override_patch: 309 return self._override_patch 310 311 attachment_dictionary = self.attachment_cache.get(attachment_id) 312 if not attachment_dictionary: 313 print "MOCK: fetch_attachment: %s is not a known attachment id" % attachment_id 314 return None 315 bug = self.fetch_bug(attachment_dictionary["bug_id"]) 316 for attachment in bug.attachments(include_obsolete=True): 317 if attachment.id() == int(attachment_id): 318 return attachment 319 320 def bug_url_for_bug_id(self, bug_id): 321 return "%s/%s" % (self.bug_server_url, bug_id) 322 323 def fetch_bug_dictionary(self, bug_id): 324 return self.bug_cache.get(bug_id) 325 326 def attachment_url_for_id(self, attachment_id, action="view"): 327 action_param = "" 328 if action and action != "view": 329 action_param = "&action=%s" % action 330 return "%s/%s%s" % (self.bug_server_url, attachment_id, action_param) 331 332 def set_flag_on_attachment(self, 333 attachment_id, 334 flag_name, 335 flag_value, 336 comment_text=None, 337 additional_comment_text=None): 338 log("MOCK setting flag '%s' to '%s' on attachment '%s' with comment '%s' and additional comment '%s'" % ( 339 flag_name, flag_value, attachment_id, comment_text, additional_comment_text)) 340 341 def post_comment_to_bug(self, bug_id, comment_text, cc=None): 342 log("MOCK bug comment: bug_id=%s, cc=%s\n--- Begin comment ---\n%s\n--- End comment ---\n" % ( 343 bug_id, cc, comment_text)) 344 345 def add_attachment_to_bug(self, 346 bug_id, 347 file_or_string, 348 description, 349 filename=None, 350 comment_text=None): 351 log("MOCK add_attachment_to_bug: bug_id=%s, description=%s filename=%s" % (bug_id, description, filename)) 352 if comment_text: 353 log("-- Begin comment --") 354 log(comment_text) 355 log("-- End comment --") 356 357 def add_patch_to_bug(self, 358 bug_id, 359 diff, 360 description, 361 comment_text=None, 362 mark_for_review=False, 363 mark_for_commit_queue=False, 364 mark_for_landing=False): 365 log("MOCK add_patch_to_bug: bug_id=%s, description=%s, mark_for_review=%s, mark_for_commit_queue=%s, mark_for_landing=%s" % 366 (bug_id, description, mark_for_review, mark_for_commit_queue, mark_for_landing)) 367 if comment_text: 368 log("-- Begin comment --") 369 log(comment_text) 370 log("-- End comment --") 371 372 373class MockBuilder(object): 374 def __init__(self, name): 375 self._name = name 376 377 def name(self): 378 return self._name 379 380 def results_url(self): 381 return "http://example.com/builders/%s/results/" % self.name() 382 383 def force_build(self, username, comments): 384 log("MOCK: force_build: name=%s, username=%s, comments=%s" % ( 385 self._name, username, comments)) 386 387 388class MockFailureMap(object): 389 def __init__(self, buildbot): 390 self._buildbot = buildbot 391 392 def is_empty(self): 393 return False 394 395 def filter_out_old_failures(self, is_old_revision): 396 pass 397 398 def failing_revisions(self): 399 return [29837] 400 401 def builders_failing_for(self, revision): 402 return [self._buildbot.builder_with_name("Builder1")] 403 404 def tests_failing_for(self, revision): 405 return ["mock-test-1"] 406 407 408class MockBuildBot(object): 409 buildbot_host = "dummy_buildbot_host" 410 def __init__(self): 411 self._mock_builder1_status = { 412 "name": "Builder1", 413 "is_green": True, 414 "activity": "building", 415 } 416 self._mock_builder2_status = { 417 "name": "Builder2", 418 "is_green": True, 419 "activity": "idle", 420 } 421 422 def builder_with_name(self, name): 423 return MockBuilder(name) 424 425 def builder_statuses(self): 426 return [ 427 self._mock_builder1_status, 428 self._mock_builder2_status, 429 ] 430 431 def red_core_builders_names(self): 432 if not self._mock_builder2_status["is_green"]: 433 return [self._mock_builder2_status["name"]] 434 return [] 435 436 def red_core_builders(self): 437 if not self._mock_builder2_status["is_green"]: 438 return [self._mock_builder2_status] 439 return [] 440 441 def idle_red_core_builders(self): 442 if not self._mock_builder2_status["is_green"]: 443 return [self._mock_builder2_status] 444 return [] 445 446 def last_green_revision(self): 447 return 9479 448 449 def light_tree_on_fire(self): 450 self._mock_builder2_status["is_green"] = False 451 452 def failure_map(self): 453 return MockFailureMap(self) 454 455 456# FIXME: This should not inherit from Mock 457class MockSCM(Mock): 458 459 fake_checkout_root = os.path.realpath("/tmp") # realpath is needed to allow for Mac OS X's /private/tmp 460 461 def __init__(self, filesystem=None): 462 Mock.__init__(self) 463 # FIXME: We should probably use real checkout-root detection logic here. 464 # os.getcwd() can't work here because other parts of the code assume that "checkout_root" 465 # will actually be the root. Since getcwd() is wrong, use a globally fake root for now. 466 self.checkout_root = self.fake_checkout_root 467 self.added_paths = set() 468 self._filesystem = filesystem 469 470 def add(self, destination_path, return_exit_code=False): 471 self.added_paths.add(destination_path) 472 if return_exit_code: 473 return 0 474 475 def changed_files(self, git_commit=None): 476 return ["MockFile1"] 477 478 def create_patch(self, git_commit, changed_files=None): 479 return "Patch1" 480 481 def commit_ids_from_commitish_arguments(self, args): 482 return ["Commitish1", "Commitish2"] 483 484 def commit_message_for_local_commit(self, commit_id): 485 if commit_id == "Commitish1": 486 return CommitMessage("CommitMessage1\n" \ 487 "https://bugs.example.org/show_bug.cgi?id=42\n") 488 if commit_id == "Commitish2": 489 return CommitMessage("CommitMessage2\n" \ 490 "https://bugs.example.org/show_bug.cgi?id=75\n") 491 raise Exception("Bogus commit_id in commit_message_for_local_commit.") 492 493 def diff_for_file(self, path, log=None): 494 return path + '-diff' 495 496 def diff_for_revision(self, revision): 497 return "DiffForRevision%s\n" \ 498 "http://bugs.webkit.org/show_bug.cgi?id=12345" % revision 499 500 def show_head(self, path): 501 return path 502 503 def svn_revision_from_commit_text(self, commit_text): 504 return "49824" 505 506 def delete(self, path): 507 if not self._filesystem: 508 return 509 if self._filesystem.exists(path): 510 self._filesystem.remove(path) 511 512 513class MockDEPS(object): 514 def read_variable(self, name): 515 return 6564 516 517 def write_variable(self, name, value): 518 log("MOCK: MockDEPS.write_variable(%s, %s)" % (name, value)) 519 520 521class MockCheckout(object): 522 523 _committer_list = CommitterList() 524 525 def commit_info_for_revision(self, svn_revision): 526 # The real Checkout would probably throw an exception, but this is the only way tests have to get None back at the moment. 527 if not svn_revision: 528 return None 529 return CommitInfo(svn_revision, "eric@webkit.org", { 530 "bug_id": 42, 531 "author_name": "Adam Barth", 532 "author_email": "abarth@webkit.org", 533 "author": self._committer_list.committer_by_email("abarth@webkit.org"), 534 "reviewer_text": "Darin Adler", 535 "reviewer": self._committer_list.committer_by_name("Darin Adler"), 536 }) 537 538 def bug_id_for_revision(self, svn_revision): 539 return 12345 540 541 def recent_commit_infos_for_files(self, paths): 542 return [self.commit_info_for_revision(32)] 543 544 def modified_changelogs(self, git_commit, changed_files=None): 545 # Ideally we'd return something more interesting here. The problem is 546 # that LandDiff will try to actually read the patch from disk! 547 return [] 548 549 def commit_message_for_this_commit(self, git_commit, changed_files=None): 550 commit_message = Mock() 551 commit_message.message = lambda:"This is a fake commit message that is at least 50 characters." 552 return commit_message 553 554 def chromium_deps(self): 555 return MockDEPS() 556 557 def apply_patch(self, patch, force=False): 558 pass 559 560 def apply_reverse_diffs(self, revision): 561 pass 562 563 def suggested_reviewers(self, git_commit, changed_files=None): 564 return [_mock_reviewer] 565 566 567class MockUser(object): 568 569 @classmethod 570 def prompt(cls, message, repeat=1, raw_input=raw_input): 571 return "Mock user response" 572 573 @classmethod 574 def prompt_with_list(cls, list_title, list_items, can_choose_multiple=False, raw_input=raw_input): 575 pass 576 577 def __init__(self): 578 self.opened_urls = [] 579 580 def edit(self, files): 581 pass 582 583 def edit_changelog(self, files): 584 pass 585 586 def page(self, message): 587 pass 588 589 def confirm(self, message=None, default='y'): 590 log(message) 591 return default == 'y' 592 593 def can_open_url(self): 594 return True 595 596 def open_url(self, url): 597 self.opened_urls.append(url) 598 if url.startswith("file://"): 599 log("MOCK: user.open_url: file://...") 600 return 601 log("MOCK: user.open_url: %s" % url) 602 603 604class MockIRC(object): 605 606 def post(self, message): 607 log("MOCK: irc.post: %s" % message) 608 609 def disconnect(self): 610 log("MOCK: irc.disconnect") 611 612 613class MockStatusServer(object): 614 615 def __init__(self, bot_id=None, work_items=None): 616 self.host = "example.com" 617 self.bot_id = bot_id 618 self._work_items = work_items or [] 619 620 def patch_status(self, queue_name, patch_id): 621 return None 622 623 def svn_revision(self, svn_revision): 624 return None 625 626 def next_work_item(self, queue_name): 627 if not self._work_items: 628 return None 629 return self._work_items.pop(0) 630 631 def release_work_item(self, queue_name, patch): 632 log("MOCK: release_work_item: %s %s" % (queue_name, patch.id())) 633 634 def update_work_items(self, queue_name, work_items): 635 self._work_items = work_items 636 log("MOCK: update_work_items: %s %s" % (queue_name, work_items)) 637 638 def submit_to_ews(self, patch_id): 639 log("MOCK: submit_to_ews: %s" % (patch_id)) 640 641 def update_status(self, queue_name, status, patch=None, results_file=None): 642 log("MOCK: update_status: %s %s" % (queue_name, status)) 643 return 187 644 645 def update_svn_revision(self, svn_revision, broken_bot): 646 return 191 647 648 def results_url_for_status(self, status_id): 649 return "http://dummy_url" 650 651 652# FIXME: This should not inherit from Mock 653# FIXME: Unify with common.system.executive_mock.MockExecutive. 654class MockExecutive(Mock): 655 def __init__(self, should_log): 656 self.should_log = should_log 657 658 def run_and_throw_if_fail(self, args, quiet=False): 659 if self.should_log: 660 log("MOCK run_and_throw_if_fail: %s" % args) 661 return "MOCK output of child process" 662 663 def run_command(self, 664 args, 665 cwd=None, 666 input=None, 667 error_handler=None, 668 return_exit_code=False, 669 return_stderr=True, 670 decode_output=False): 671 if self.should_log: 672 log("MOCK run_command: %s" % args) 673 return "MOCK output of child process" 674 675 676class MockOptions(object): 677 """Mock implementation of optparse.Values.""" 678 679 def __init__(self, **kwargs): 680 # The caller can set option values using keyword arguments. We don't 681 # set any values by default because we don't know how this 682 # object will be used. Generally speaking unit tests should 683 # subclass this or provider wrapper functions that set a common 684 # set of options. 685 for key, value in kwargs.items(): 686 self.__dict__[key] = value 687 688 689class MockPort(Mock): 690 def name(self): 691 return "MockPort" 692 693 def layout_tests_results_path(self): 694 return "/mock/results.html" 695 696 def check_webkit_style_command(self): 697 return ["mock-check-webkit-style"] 698 699 def update_webkit_command(self): 700 return ["mock-update-webkit"] 701 702 703class MockTestPort1(object): 704 705 def skips_layout_test(self, test_name): 706 return test_name in ["media/foo/bar.html", "foo"] 707 708 709class MockTestPort2(object): 710 711 def skips_layout_test(self, test_name): 712 return test_name == "media/foo/bar.html" 713 714 715class MockPortFactory(object): 716 717 def get_all(self, options=None): 718 return {"test_port1": MockTestPort1(), "test_port2": MockTestPort2()} 719 720 721class MockPlatformInfo(object): 722 def display_name(self): 723 return "MockPlatform 1.0" 724 725 726class MockWorkspace(object): 727 def find_unused_filename(self, directory, name, extension, search_limit=10): 728 return "%s/%s.%s" % (directory, name, extension) 729 730 def create_zip(self, zip_path, source_path): 731 pass 732 733 734class MockTool(object): 735 736 def __init__(self, log_executive=False): 737 self.wakeup_event = threading.Event() 738 self.bugs = MockBugzilla() 739 self.buildbot = MockBuildBot() 740 self.executive = MockExecutive(should_log=log_executive) 741 self.filesystem = MockFileSystem() 742 self.workspace = MockWorkspace() 743 self._irc = None 744 self.user = MockUser() 745 self._scm = MockSCM() 746 self._checkout = MockCheckout() 747 self.status_server = MockStatusServer() 748 self.irc_password = "MOCK irc password" 749 self.port_factory = MockPortFactory() 750 self.platform = MockPlatformInfo() 751 752 def scm(self): 753 return self._scm 754 755 def checkout(self): 756 return self._checkout 757 758 def ensure_irc_connected(self, delegate): 759 if not self._irc: 760 self._irc = MockIRC() 761 762 def irc(self): 763 return self._irc 764 765 def path(self): 766 return "echo" 767 768 def port(self): 769 return MockPort() 770 771 772class MockBrowser(object): 773 params = {} 774 775 def open(self, url): 776 pass 777 778 def select_form(self, name): 779 pass 780 781 def __setitem__(self, key, value): 782 self.params[key] = value 783 784 def submit(self): 785 return Mock(file) 786