Clean up ChunkedUpdateDrawingAreaProxy
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / tool / bot / flakytestreporter.py
1 # Copyright (c) 2010 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 codecs
30 import logging
31 import platform
32 import os.path
33
34 from webkitpy.common.net.layouttestresults import path_for_layout_test, LayoutTestResults
35 from webkitpy.common.config import urls
36 from webkitpy.tool.grammar import plural, pluralize, join_with_separators
37
38 _log = logging.getLogger(__name__)
39
40
41 class FlakyTestReporter(object):
42     def __init__(self, tool, bot_name):
43         self._tool = tool
44         self._bot_name = bot_name
45
46     def _author_emails_for_test(self, flaky_test):
47         test_path = path_for_layout_test(flaky_test)
48         commit_infos = self._tool.checkout().recent_commit_infos_for_files([test_path])
49         # This ignores authors which are not committers because we don't have their bugzilla_email.
50         return set([commit_info.author().bugzilla_email() for commit_info in commit_infos if commit_info.author()])
51
52     def _bugzilla_email(self):
53         # FIXME: This is kinda a funny way to get the bugzilla email,
54         # we could also just create a Credentials object directly
55         # but some of the Credentials logic is in bugzilla.py too...
56         self._tool.bugs.authenticate()
57         return self._tool.bugs.username
58
59     # FIXME: This should move into common.config
60     _bot_emails = set([
61         "commit-queue@webkit.org",  # commit-queue
62         "eseidel@chromium.org",  # old commit-queue
63         "webkit.review.bot@gmail.com",  # style-queue, sheriff-bot, CrLx/Gtk EWS
64         "buildbot@hotmail.com",  # Win EWS
65         # Mac EWS currently uses eric@webkit.org, but that's not normally a bot
66     ])
67
68     def _lookup_bug_for_flaky_test(self, flaky_test):
69         bugs = self._tool.bugs.queries.fetch_bugs_matching_search(search_string=flaky_test)
70         if not bugs:
71             return None
72         # Match any bugs which are from known bots or the email this bot is using.
73         allowed_emails = self._bot_emails | set([self._bugzilla_email])
74         bugs = filter(lambda bug: bug.reporter_email() in allowed_emails, bugs)
75         if not bugs:
76             return None
77         if len(bugs) > 1:
78             # FIXME: There are probably heuristics we could use for finding
79             # the right bug instead of the first, like open vs. closed.
80             _log.warn("Found %s %s matching '%s' filed by a bot, using the first." % (pluralize('bug', len(bugs)), [bug.id() for bug in bugs], flaky_test))
81         return bugs[0]
82
83     def _view_source_url_for_test(self, test_path):
84         return urls.view_source_url("LayoutTests/%s" % test_path)
85
86     def _create_bug_for_flaky_test(self, flaky_test, author_emails, latest_flake_message):
87         format_values = {
88             'test': flaky_test,
89             'authors': join_with_separators(sorted(author_emails)),
90             'flake_message': latest_flake_message,
91             'test_url': self._view_source_url_for_test(flaky_test),
92             'bot_name': self._bot_name,
93         }
94         title = "Flaky Test: %(test)s" % format_values
95         description = """This is an automatically generated bug from the %(bot_name)s.
96 %(test)s has been flaky on the %(bot_name)s.
97
98 %(test)s was authored by %(authors)s.
99 %(test_url)s
100
101 %(flake_message)s
102
103 The bots will update this with information from each new failure.
104
105 If you would like to track this test fix with another bug, please close this bug as a duplicate.
106 """ % format_values
107
108         master_flake_bug = 50856  # MASTER: Flaky tests found by the commit-queue
109         return self._tool.bugs.create_bug(title, description,
110             component="Tools / Tests",
111             cc=",".join(author_emails),
112             blocked="50856")
113
114     # This is over-engineered, but it makes for pretty bug messages.
115     def _optional_author_string(self, author_emails):
116         if not author_emails:
117             return ""
118         heading_string = plural('author') if len(author_emails) > 1 else 'author'
119         authors_string = join_with_separators(sorted(author_emails))
120         return " (%s: %s)" % (heading_string, authors_string)
121
122     def _bot_information(self):
123         bot_id = self._tool.status_server.bot_id
124         bot_id_string = "Bot: %s  " % (bot_id) if bot_id else ""
125         return "%sPort: %s  Platform: %s" % (bot_id_string, self._tool.port().name(), self._tool.platform.display_name())
126
127     def _latest_flake_message(self, flaky_test, patch):
128         flake_message = "The %s just saw %s flake while processing attachment %s on bug %s." % (self._bot_name, flaky_test, patch.id(), patch.bug_id())
129         return "%s\n%s" % (flake_message, self._bot_information())
130
131     def _results_diff_path_for_test(self, flaky_test):
132         # FIXME: This is a big hack.  We should get this path from results.json
133         # except that old-run-webkit-tests doesn't produce a results.json
134         # so we just guess at the file path.
135         results_path = self._tool.port().layout_tests_results_path()
136         results_directory = os.path.dirname(results_path)
137         test_path = os.path.join(results_directory, flaky_test)
138         (test_path_root, _) = os.path.splitext(test_path)
139         return "%s-diffs.txt" % test_path_root
140
141     def _follow_duplicate_chain(self, bug):
142         while bug.is_closed() and bug.duplicate_of():
143             bug = self._tool.bugs.fetch_bug(bug.duplicate_of())
144         return bug
145
146     # Maybe this logic should move into Bugzilla? a reopen=True arg to post_comment?
147     def _update_bug_for_flaky_test(self, bug, latest_flake_message):
148         if bug.is_closed():
149             self._tool.bugs.reopen_bug(bug.id(), latest_flake_message)
150         else:
151             self._tool.bugs.post_comment_to_bug(bug.id(), latest_flake_message)
152
153     def report_flaky_tests(self, flaky_tests, patch):
154         message = "The %s encountered the following flaky tests while processing attachment %s:\n\n" % (self._bot_name, patch.id())
155         for flaky_test in flaky_tests:
156             bug = self._lookup_bug_for_flaky_test(flaky_test)
157             latest_flake_message = self._latest_flake_message(flaky_test, patch)
158             author_emails = self._author_emails_for_test(flaky_test)
159             if not bug:
160                 _log.info("Bug does not already exist for %s, creating." % flaky_test)
161                 flake_bug_id = self._create_bug_for_flaky_test(flaky_test, author_emails, latest_flake_message)
162             else:
163                 bug = self._follow_duplicate_chain(bug)
164                 self._update_bug_for_flaky_test(bug, latest_flake_message)
165                 flake_bug_id = bug.id()
166             # FIXME: Ideally we'd only make one comment per flake, not two.  But that's not possible
167             # in all cases (e.g. when reopening), so for now we do the attachment in a second step.
168             results_diff_path = self._results_diff_path_for_test(flaky_test)
169             # Check to make sure that the path makes sense.
170             # Since we're not actually getting this path from the results.html
171             # there is a high probaility it's totally wrong.
172             if self._tool.filesystem.exists(results_diff_path):
173                 results_diff = self._tool.filesystem.read_binary_file(results_diff_path)
174                 self._tool.bugs.add_attachment_to_bug(flake_bug_id, results_diff, "Failure diff from bot", filename="failure.diff")
175             else:
176                 _log.error("%s does not exist as expected, not uploading." % results_diff_path)
177             message += "%s bug %s%s\n" % (flaky_test, flake_bug_id, self._optional_author_string(author_emails))
178
179         message += "The %s is continuing to process your patch." % self._bot_name
180         self._tool.bugs.post_comment_to_bug(patch.bug_id(), message)