OLD | NEW |
| (Empty) |
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 os.path | |
32 | |
33 from webkitpy.common.net.layouttestresults import path_for_layout_test, LayoutTe
stResults | |
34 from webkitpy.common.config import urls | |
35 from webkitpy.tool.bot.botinfo import BotInfo | |
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 # FIXME: Use the real port object | |
46 self._bot_info = BotInfo(tool, tool.deprecated_port().name()) | |
47 | |
48 def _author_emails_for_test(self, flaky_test): | |
49 test_path = path_for_layout_test(flaky_test) | |
50 commit_infos = self._tool.checkout().recent_commit_infos_for_files([test
_path]) | |
51 # This ignores authors which are not committers because we don't have th
eir bugzilla_email. | |
52 return set([commit_info.author().bugzilla_email() for commit_info in com
mit_infos if commit_info.author()]) | |
53 | |
54 def _bugzilla_email(self): | |
55 # FIXME: This is kinda a funny way to get the bugzilla email, | |
56 # we could also just create a Credentials object directly | |
57 # but some of the Credentials logic is in bugzilla.py too... | |
58 self._tool.bugs.authenticate() | |
59 return self._tool.bugs.username | |
60 | |
61 # FIXME: This should move into common.config | |
62 _bot_emails = set([ | |
63 "commit-queue@webkit.org", # commit-queue | |
64 "eseidel@chromium.org", # old commit-queue | |
65 "webkit.review.bot@gmail.com", # style-queue, sheriff-bot, CrLx/Gtk EWS | |
66 "buildbot@hotmail.com", # Win EWS | |
67 # Mac EWS currently uses eric@webkit.org, but that's not normally a bot | |
68 ]) | |
69 | |
70 def _lookup_bug_for_flaky_test(self, flaky_test): | |
71 bugs = self._tool.bugs.queries.fetch_bugs_matching_search(search_string=
flaky_test) | |
72 if not bugs: | |
73 return None | |
74 # Match any bugs which are from known bots or the email this bot is usin
g. | |
75 allowed_emails = self._bot_emails | set([self._bugzilla_email]) | |
76 bugs = filter(lambda bug: bug.reporter_email() in allowed_emails, bugs) | |
77 if not bugs: | |
78 return None | |
79 if len(bugs) > 1: | |
80 # FIXME: There are probably heuristics we could use for finding | |
81 # the right bug instead of the first, like open vs. closed. | |
82 _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)) | |
83 return bugs[0] | |
84 | |
85 def _view_source_url_for_test(self, test_path): | |
86 return urls.view_source_url("LayoutTests/%s" % test_path) | |
87 | |
88 def _create_bug_for_flaky_test(self, flaky_test, author_emails, latest_flake
_message): | |
89 format_values = { | |
90 'test': flaky_test, | |
91 'authors': join_with_separators(sorted(author_emails)), | |
92 'flake_message': latest_flake_message, | |
93 'test_url': self._view_source_url_for_test(flaky_test), | |
94 'bot_name': self._bot_name, | |
95 } | |
96 title = "Flaky Test: %(test)s" % format_values | |
97 description = """This is an automatically generated bug from the %(bot_n
ame)s. | |
98 %(test)s has been flaky on the %(bot_name)s. | |
99 | |
100 %(test)s was authored by %(authors)s. | |
101 %(test_url)s | |
102 | |
103 %(flake_message)s | |
104 | |
105 The bots will update this with information from each new failure. | |
106 | |
107 If you believe this bug to be fixed or invalid, feel free to close. The bots wi
ll re-open if the flake re-occurs. | |
108 | |
109 If you would like to track this test fix with another bug, please close this bug
as a duplicate. The bots will follow the duplicate chain when making future co
mments. | |
110 """ % format_values | |
111 | |
112 master_flake_bug = 50856 # MASTER: Flaky tests found by the commit-queu
e | |
113 return self._tool.bugs.create_bug(title, description, | |
114 component="Tools / Tests", | |
115 cc=",".join(author_emails), | |
116 blocked="50856") | |
117 | |
118 # This is over-engineered, but it makes for pretty bug messages. | |
119 def _optional_author_string(self, author_emails): | |
120 if not author_emails: | |
121 return "" | |
122 heading_string = plural('author') if len(author_emails) > 1 else 'author
' | |
123 authors_string = join_with_separators(sorted(author_emails)) | |
124 return " (%s: %s)" % (heading_string, authors_string) | |
125 | |
126 def _latest_flake_message(self, flaky_result, patch): | |
127 failure_messages = [failure.message() for failure in flaky_result.failur
es] | |
128 flake_message = "The %s just saw %s flake (%s) while processing attachme
nt %s on bug %s." % (self._bot_name, flaky_result.test_name, ", ".join(failure_m
essages), patch.id(), patch.bug_id()) | |
129 return "%s\n%s" % (flake_message, self._bot_info.summary_text()) | |
130 | |
131 def _results_diff_path_for_test(self, test_path): | |
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 (test_path_root, _) = os.path.splitext(test_path) | |
136 return "%s-diffs.txt" % test_path_root | |
137 | |
138 def _follow_duplicate_chain(self, bug): | |
139 while bug.is_closed() and bug.duplicate_of(): | |
140 bug = self._tool.bugs.fetch_bug(bug.duplicate_of()) | |
141 return bug | |
142 | |
143 # Maybe this logic should move into Bugzilla? a reopen=True arg to post_comm
ent? | |
144 def _update_bug_for_flaky_test(self, bug, latest_flake_message): | |
145 if bug.is_closed(): | |
146 self._tool.bugs.reopen_bug(bug.id(), latest_flake_message) | |
147 else: | |
148 self._tool.bugs.post_comment_to_bug(bug.id(), latest_flake_message) | |
149 | |
150 # This method is needed because our archive paths include a leading tmp/layo
ut-test-results | |
151 def _find_in_archive(self, path, archive): | |
152 for archived_path in archive.namelist(): | |
153 # Archives are currently created with full paths. | |
154 if archived_path.endswith(path): | |
155 return archived_path | |
156 return None | |
157 | |
158 def _attach_failure_diff(self, flake_bug_id, flaky_test, results_archive_zip
): | |
159 results_diff_path = self._results_diff_path_for_test(flaky_test) | |
160 # Check to make sure that the path makes sense. | |
161 # Since we're not actually getting this path from the results.html | |
162 # there is a chance it's wrong. | |
163 bot_id = self._tool.status_server.bot_id or "bot" | |
164 archive_path = self._find_in_archive(results_diff_path, results_archive_
zip) | |
165 if archive_path: | |
166 results_diff = results_archive_zip.read(archive_path) | |
167 description = "Failure diff from %s" % bot_id | |
168 self._tool.bugs.add_attachment_to_bug(flake_bug_id, results_diff, de
scription, filename="failure.diff") | |
169 else: | |
170 _log.warn("%s does not exist in results archive, uploading entire ar
chive." % results_diff_path) | |
171 description = "Archive of layout-test-results from %s" % bot_id | |
172 # results_archive is a ZipFile object, grab the File object (.fp) to
pass to Mechanize for uploading. | |
173 results_archive_file = results_archive_zip.fp | |
174 # Rewind the file object to start (since Mechanize won't do that aut
omatically) | |
175 # See https://bugs.webkit.org/show_bug.cgi?id=54593 | |
176 results_archive_file.seek(0) | |
177 self._tool.bugs.add_attachment_to_bug(flake_bug_id, results_archive_
file, description, filename="layout-test-results.zip") | |
178 | |
179 def report_flaky_tests(self, patch, flaky_test_results, results_archive): | |
180 message = "The %s encountered the following flaky tests while processing
attachment %s:\n\n" % (self._bot_name, patch.id()) | |
181 for flaky_result in flaky_test_results: | |
182 flaky_test = flaky_result.test_name | |
183 bug = self._lookup_bug_for_flaky_test(flaky_test) | |
184 latest_flake_message = self._latest_flake_message(flaky_result, patc
h) | |
185 author_emails = self._author_emails_for_test(flaky_test) | |
186 if not bug: | |
187 _log.info("Bug does not already exist for %s, creating." % flaky
_test) | |
188 flake_bug_id = self._create_bug_for_flaky_test(flaky_test, autho
r_emails, latest_flake_message) | |
189 else: | |
190 bug = self._follow_duplicate_chain(bug) | |
191 # FIXME: Ideally we'd only make one comment per flake, not two.
But that's not possible | |
192 # in all cases (e.g. when reopening), so for now file attachment
and comment are separate. | |
193 self._update_bug_for_flaky_test(bug, latest_flake_message) | |
194 flake_bug_id = bug.id() | |
195 | |
196 self._attach_failure_diff(flake_bug_id, flaky_test, results_archive) | |
197 message += "%s bug %s%s\n" % (flaky_test, flake_bug_id, self._option
al_author_string(author_emails)) | |
198 | |
199 message += "The %s is continuing to process your patch." % self._bot_nam
e | |
200 self._tool.bugs.post_comment_to_bug(patch.bug_id(), message) | |
OLD | NEW |