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