summaryrefslogtreecommitdiffstats
path: root/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py')
-rw-r--r--Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py181
1 files changed, 181 insertions, 0 deletions
diff --git a/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py
new file mode 100644
index 0000000..01cbf39
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py
@@ -0,0 +1,181 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import codecs
+import logging
+import platform
+import os.path
+
+from webkitpy.common.net.layouttestresults import path_for_layout_test, LayoutTestResults
+from webkitpy.common.config import urls
+from webkitpy.tool.grammar import plural, pluralize, join_with_separators
+
+_log = logging.getLogger(__name__)
+
+
+class FlakyTestReporter(object):
+ def __init__(self, tool, bot_name):
+ self._tool = tool
+ self._bot_name = bot_name
+
+ def _author_emails_for_test(self, flaky_test):
+ test_path = path_for_layout_test(flaky_test)
+ commit_infos = self._tool.checkout().recent_commit_infos_for_files([test_path])
+ # This ignores authors which are not committers because we don't have their bugzilla_email.
+ return set([commit_info.author().bugzilla_email() for commit_info in commit_infos if commit_info.author()])
+
+ def _bugzilla_email(self):
+ # FIXME: This is kinda a funny way to get the bugzilla email,
+ # we could also just create a Credentials object directly
+ # but some of the Credentials logic is in bugzilla.py too...
+ self._tool.bugs.authenticate()
+ return self._tool.bugs.username
+
+ # FIXME: This should move into common.config
+ _bot_emails = set([
+ "commit-queue@webkit.org", # commit-queue
+ "eseidel@chromium.org", # old commit-queue
+ "webkit.review.bot@gmail.com", # style-queue, sheriff-bot, CrLx/Gtk EWS
+ "buildbot@hotmail.com", # Win EWS
+ # Mac EWS currently uses eric@webkit.org, but that's not normally a bot
+ ])
+
+ def _lookup_bug_for_flaky_test(self, flaky_test):
+ bugs = self._tool.bugs.queries.fetch_bugs_matching_search(search_string=flaky_test)
+ if not bugs:
+ return None
+ # Match any bugs which are from known bots or the email this bot is using.
+ allowed_emails = self._bot_emails | set([self._bugzilla_email])
+ bugs = filter(lambda bug: bug.reporter_email() in allowed_emails, bugs)
+ if not bugs:
+ return None
+ if len(bugs) > 1:
+ # FIXME: There are probably heuristics we could use for finding
+ # the right bug instead of the first, like open vs. closed.
+ _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))
+ return bugs[0]
+
+ def _view_source_url_for_test(self, test_path):
+ return urls.view_source_url("LayoutTests/%s" % test_path)
+
+ def _create_bug_for_flaky_test(self, flaky_test, author_emails, latest_flake_message):
+ format_values = {
+ 'test': flaky_test,
+ 'authors': join_with_separators(sorted(author_emails)),
+ 'flake_message': latest_flake_message,
+ 'test_url': self._view_source_url_for_test(flaky_test),
+ 'bot_name': self._bot_name,
+ }
+ title = "Flaky Test: %(test)s" % format_values
+ description = """This is an automatically generated bug from the %(bot_name)s.
+%(test)s has been flaky on the %(bot_name)s.
+
+%(test)s was authored by %(authors)s.
+%(test_url)s
+
+%(flake_message)s
+
+The bots will update this with information from each new failure.
+
+If you would like to track this test fix with another bug, please close this bug as a duplicate.
+""" % format_values
+
+ master_flake_bug = 50856 # MASTER: Flaky tests found by the commit-queue
+ return self._tool.bugs.create_bug(title, description,
+ component="Tools / Tests",
+ cc=",".join(author_emails),
+ blocked="50856")
+
+ # This is over-engineered, but it makes for pretty bug messages.
+ def _optional_author_string(self, author_emails):
+ if not author_emails:
+ return ""
+ heading_string = plural('author') if len(author_emails) > 1 else 'author'
+ authors_string = join_with_separators(sorted(author_emails))
+ return " (%s: %s)" % (heading_string, authors_string)
+
+ def _bot_information(self):
+ bot_id = self._tool.status_server.bot_id
+ bot_id_string = "Bot: %s " % (bot_id) if bot_id else ""
+ return "%sPort: %s Platform: %s" % (bot_id_string, self._tool.port().name(), self._tool.platform.display_name())
+
+ def _latest_flake_message(self, flaky_test, patch):
+ 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())
+ return "%s\n%s" % (flake_message, self._bot_information())
+
+ def _results_diff_path_for_test(self, flaky_test):
+ # FIXME: This is a big hack. We should get this path from results.json
+ # except that old-run-webkit-tests doesn't produce a results.json
+ # so we just guess at the file path.
+ results_path = self._tool.port().layout_tests_results_path()
+ results_directory = os.path.dirname(results_path)
+ test_path = os.path.join(results_directory, flaky_test)
+ (test_path_root, _) = os.path.splitext(test_path)
+ return "%s-diffs.txt" % test_path_root
+
+ def _follow_duplicate_chain(self, bug):
+ while bug.is_closed() and bug.duplicate_of():
+ bug = self._tool.bugs.fetch_bug(bug.duplicate_of())
+ return bug
+
+ # Maybe this logic should move into Bugzilla? a reopen=True arg to post_comment?
+ def _update_bug_for_flaky_test(self, bug, latest_flake_message):
+ if bug.is_closed():
+ self._tool.bugs.reopen_bug(bug.id(), latest_flake_message)
+ else:
+ self._tool.bugs.post_comment_to_bug(bug.id(), latest_flake_message)
+
+ def report_flaky_tests(self, flaky_tests, patch):
+ message = "The %s encountered the following flaky tests while processing attachment %s:\n\n" % (self._bot_name, patch.id())
+ for flaky_test in flaky_tests:
+ bug = self._lookup_bug_for_flaky_test(flaky_test)
+ latest_flake_message = self._latest_flake_message(flaky_test, patch)
+ author_emails = self._author_emails_for_test(flaky_test)
+ if not bug:
+ _log.info("Bug does not already exist for %s, creating." % flaky_test)
+ flake_bug_id = self._create_bug_for_flaky_test(flaky_test, author_emails, latest_flake_message)
+ else:
+ bug = self._follow_duplicate_chain(bug)
+ self._update_bug_for_flaky_test(bug, latest_flake_message)
+ flake_bug_id = bug.id()
+ # FIXME: Ideally we'd only make one comment per flake, not two. But that's not possible
+ # in all cases (e.g. when reopening), so for now we do the attachment in a second step.
+ results_diff_path = self._results_diff_path_for_test(flaky_test)
+ # Check to make sure that the path makes sense.
+ # Since we're not actually getting this path from the results.html
+ # there is a high probaility it's totally wrong.
+ if self._tool.filesystem.exists(results_diff_path):
+ results_diff = self._tool.filesystem.read_binary_file(results_diff_path)
+ bot_id = self._tool.status_server.bot_id or "bot"
+ self._tool.bugs.add_attachment_to_bug(flake_bug_id, results_diff, "Failure diff from %s" % bot_id, filename="failure.diff")
+ else:
+ _log.error("%s does not exist as expected, not uploading." % results_diff_path)
+ message += "%s bug %s%s\n" % (flaky_test, flake_bug_id, self._optional_author_string(author_emails))
+
+ message += "The %s is continuing to process your patch." % self._bot_name
+ self._tool.bugs.post_comment_to_bug(patch.bug_id(), message)