2011-01-28 Adam Barth <abarth@webkit.org>
[WebKit.git] / Tools / Scripts / webkitpy / tool / mocktool.py
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.system.deprecated_logging import log
37 from webkitpy.common.system.filesystem_mock import MockFileSystem
38 from webkitpy.thirdparty.mock import Mock
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     "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
210 class 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
257 class 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
373 class 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
388 class 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
408 class 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
457 class 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):
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
468     def changed_files(self, git_commit=None):
469         return ["MockFile1"]
470
471     def create_patch(self, git_commit, changed_files=None):
472         return "Patch1"
473
474     def commit_ids_from_commitish_arguments(self, args):
475         return ["Commitish1", "Commitish2"]
476
477     def commit_message_for_local_commit(self, commit_id):
478         if commit_id == "Commitish1":
479             return CommitMessage("CommitMessage1\n" \
480                 "https://bugs.example.org/show_bug.cgi?id=42\n")
481         if commit_id == "Commitish2":
482             return CommitMessage("CommitMessage2\n" \
483                 "https://bugs.example.org/show_bug.cgi?id=75\n")
484         raise Exception("Bogus commit_id in commit_message_for_local_commit.")
485
486     def diff_for_revision(self, revision):
487         return "DiffForRevision%s\n" \
488                "http://bugs.webkit.org/show_bug.cgi?id=12345" % revision
489
490     def svn_revision_from_commit_text(self, commit_text):
491         return "49824"
492
493     def add(self, destination_path, return_exit_code=False):
494         if return_exit_code:
495             return 0
496
497
498 class MockDEPS(object):
499     def read_variable(self, name):
500         return 6564
501
502     def write_variable(self, name, value):
503         log("MOCK: MockDEPS.write_variable(%s, %s)" % (name, value))
504
505
506 class MockCheckout(object):
507
508     _committer_list = CommitterList()
509
510     def commit_info_for_revision(self, svn_revision):
511         # The real Checkout would probably throw an exception, but this is the only way tests have to get None back at the moment.
512         if not svn_revision:
513             return None
514         return CommitInfo(svn_revision, "eric@webkit.org", {
515             "bug_id": 42,
516             "author_name": "Adam Barth",
517             "author_email": "abarth@webkit.org",
518             "author": self._committer_list.committer_by_email("abarth@webkit.org"),
519             "reviewer_text": "Darin Adler",
520             "reviewer": self._committer_list.committer_by_name("Darin Adler"),
521         })
522
523     def bug_id_for_revision(self, svn_revision):
524         return 12345
525
526     def recent_commit_infos_for_files(self, paths):
527         return [self.commit_info_for_revision(32)]
528
529     def modified_changelogs(self, git_commit, changed_files=None):
530         # Ideally we'd return something more interesting here.  The problem is
531         # that LandDiff will try to actually read the patch from disk!
532         return []
533
534     def commit_message_for_this_commit(self, git_commit, changed_files=None):
535         commit_message = Mock()
536         commit_message.message = lambda:"This is a fake commit message that is at least 50 characters."
537         return commit_message
538
539     def chromium_deps(self):
540         return MockDEPS()
541
542     def apply_patch(self, patch, force=False):
543         pass
544
545     def apply_reverse_diffs(self, revision):
546         pass
547
548     def suggested_reviewers(self, git_commit, changed_files=None):
549         return [_mock_reviewer]
550
551
552 class MockUser(object):
553
554     @classmethod
555     def prompt(cls, message, repeat=1, raw_input=raw_input):
556         return "Mock user response"
557
558     @classmethod
559     def prompt_with_list(cls, list_title, list_items, can_choose_multiple=False, raw_input=raw_input):
560         pass
561
562     def __init__(self):
563         self.opened_urls = []
564
565     def edit(self, files):
566         pass
567
568     def edit_changelog(self, files):
569         pass
570
571     def page(self, message):
572         pass
573
574     def confirm(self, message=None, default='y'):
575         log(message)
576         return default == 'y'
577
578     def can_open_url(self):
579         return True
580
581     def open_url(self, url):
582         self.opened_urls.append(url)
583         if url.startswith("file://"):
584             log("MOCK: user.open_url: file://...")
585             return
586         log("MOCK: user.open_url: %s" % url)
587
588
589 class MockIRC(object):
590
591     def post(self, message):
592         log("MOCK: irc.post: %s" % message)
593
594     def disconnect(self):
595         log("MOCK: irc.disconnect")
596
597
598 class MockStatusServer(object):
599
600     def __init__(self, bot_id=None, work_items=None):
601         self.host = "example.com"
602         self.bot_id = bot_id
603         self._work_items = work_items or []
604
605     def patch_status(self, queue_name, patch_id):
606         return None
607
608     def svn_revision(self, svn_revision):
609         return None
610
611     def next_work_item(self, queue_name):
612         if not self._work_items:
613             return None
614         return self._work_items.pop(0)
615
616     def release_work_item(self, queue_name, patch):
617         log("MOCK: release_work_item: %s %s" % (queue_name, patch.id()))
618
619     def update_work_items(self, queue_name, work_items):
620         self._work_items = work_items
621         log("MOCK: update_work_items: %s %s" % (queue_name, work_items))
622
623     def submit_to_ews(self, patch_id):
624         log("MOCK: submit_to_ews: %s" % (patch_id))
625
626     def update_status(self, queue_name, status, patch=None, results_file=None):
627         log("MOCK: update_status: %s %s" % (queue_name, status))
628         return 187
629
630     def update_svn_revision(self, svn_revision, broken_bot):
631         return 191
632
633     def results_url_for_status(self, status_id):
634         return "http://dummy_url"
635
636
637 # FIXME: This should not inherit from Mock
638 # FIXME: Unify with common.system.executive_mock.MockExecutive.
639 class MockExecutive(Mock):
640     def __init__(self, should_log):
641         self.should_log = should_log
642
643     def run_and_throw_if_fail(self, args, quiet=False):
644         if self.should_log:
645             log("MOCK run_and_throw_if_fail: %s" % args)
646         return "MOCK output of child process"
647
648     def run_command(self,
649                     args,
650                     cwd=None,
651                     input=None,
652                     error_handler=None,
653                     return_exit_code=False,
654                     return_stderr=True,
655                     decode_output=False):
656         if self.should_log:
657             log("MOCK run_command: %s" % args)
658         return "MOCK output of child process"
659
660
661 class MockOptions(object):
662     """Mock implementation of optparse.Values."""
663
664     def __init__(self, **kwargs):
665         # The caller can set option values using keyword arguments. We don't
666         # set any values by default because we don't know how this
667         # object will be used. Generally speaking unit tests should
668         # subclass this or provider wrapper functions that set a common
669         # set of options.
670         for key, value in kwargs.items():
671             self.__dict__[key] = value
672
673
674 class MockPort(Mock):
675     def name(self):
676         return "MockPort"
677
678     def layout_tests_results_path(self):
679         return "/mock/results.html"
680
681 class MockTestPort1(object):
682
683     def skips_layout_test(self, test_name):
684         return test_name in ["media/foo/bar.html", "foo"]
685
686
687 class MockTestPort2(object):
688
689     def skips_layout_test(self, test_name):
690         return test_name == "media/foo/bar.html"
691
692
693 class MockPortFactory(object):
694
695     def get_all(self, options=None):
696         return {"test_port1": MockTestPort1(), "test_port2": MockTestPort2()}
697
698
699 class MockPlatformInfo(object):
700     def display_name(self):
701         return "MockPlatform 1.0"
702
703
704 class MockWorkspace(object):
705     def find_unused_filename(self, directory, name, extension, search_limit=10):
706         return "%s/%s.%s" % (directory, name, extension)
707
708     def create_zip(self, zip_path, source_path):
709         pass
710
711
712 class MockTool(object):
713
714     def __init__(self, log_executive=False):
715         self.wakeup_event = threading.Event()
716         self.bugs = MockBugzilla()
717         self.buildbot = MockBuildBot()
718         self.executive = MockExecutive(should_log=log_executive)
719         self.filesystem = MockFileSystem()
720         self.workspace = MockWorkspace()
721         self._irc = None
722         self.user = MockUser()
723         self._scm = MockSCM()
724         self._checkout = MockCheckout()
725         self.status_server = MockStatusServer()
726         self.irc_password = "MOCK irc password"
727         self.port_factory = MockPortFactory()
728         self.platform = MockPlatformInfo()
729
730     def scm(self):
731         return self._scm
732
733     def checkout(self):
734         return self._checkout
735
736     def ensure_irc_connected(self, delegate):
737         if not self._irc:
738             self._irc = MockIRC()
739
740     def irc(self):
741         return self._irc
742
743     def path(self):
744         return "echo"
745
746     def port(self):
747         return MockPort()
748
749
750 class MockBrowser(object):
751     params = {}
752
753     def open(self, url):
754         pass
755
756     def select_form(self, name):
757         pass
758
759     def __setitem__(self, key, value):
760         self.params[key] = value
761
762     def submit(self):
763         return Mock(file)