blob: 1469f65c070ba96a974b8d9f21fcdce0a0255f14 [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import logging
# We need to import common to be able to import chromite and requests.
import common
from autotest_lib.client.common_lib import global_config
try:
from chromite.lib import gdata_lib
except ImportError as e:
gdata_lib = None
logging.info("Bug filing disabled. %s", e)
BUG_CONFIG_SECTION = 'BUG_REPORTING'
class TestFailure(object):
"""Wrap up all information needed to make an intelligent report about a
test failure.
Each TestFailure has a search marker associated with it that can be used to
find reports of the same error."""
# global configurations needed for build artifacts
_gs_domain = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'gs_domain', default='')
_chromeos_image_archive = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'chromeos_image_archive', default='')
_arg_prefix = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'arg_prefix', default='')
# global configurations needed for results log
_retrieve_logs_cgi = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'retrieve_logs_cgi', default='')
_generic_results_bin = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'generic_results_bin', default='')
_debug_dir = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'debug_dir', default='')
_HTTP_ERROR_THRESHOLD = 400
def __init__(self, build, suite, test, reason, owner, hostname, job_id):
"""
@param build The build type, of the form <board>/<milestone>-<release>.
ie. x86-mario-release/R25-4321.0.0
@param suite The name of the suite that this test run was a part of.
@param test The name of the test that this failure is about.
@param reason The reason that this test failed.
@param owner The owner of the test that failed.
@param hostname The host this test failure occured on.
@param job_id The id of the test that failed.
"""
self.build = build
self.suite = suite
self.test = test
self.reason = reason
self.owner = owner
self.hostname = hostname
self.job_id = job_id
def bug_title(self):
"""Converts information about a failure into a string appropriate to
be the title of a bug."""
return '[%s] %s failed on %s' % (self.suite, self.test, self.build)
def bug_summary(self):
"""
Converts information about this failure into a string appropriate
to be the summary of this bug. Includes the reason field and links
to the build artifacts and results.
"""
links = self._get_links_for_failure()
summary = """
This bug has been automatically filed to track
the failure of %(test)s in the %(suite)s suite on %(build)s.
It failed with a reason of:%(reason)s.
build artifacts: %(build_artifacts)s
results log: %(results_log)s.
"""
specifics = {
'test': self.test,
'suite': self.suite,
'build': self.build,
'reason': self.reason,
'build_artifacts': links.artifacts,
'results_log': links.results,
}
return summary % specifics
def search_marker(self):
"""When filing a report about this failure, include the returned line in
the report to provide a way to search for this exact failure."""
return "%s(%s,%s,%s)" % ('TestFailure', self.suite,
self.test, self.reason)
def _link_build_artifacts(self):
"""
Link to the build artifacts.
@return: url to build artifacts on google storage.
"""
return (self._gs_domain + self._arg_prefix +
self._chromeos_image_archive + self.build)
def _link_result_logs(self):
"""
Link to test failure logs.
@return: url to test logs on google storage.
"""
if self.job_id and self.owner and self.hostname:
path_to_object = '%s-%s/%s/%s' % (self.job_id, self.owner,
self.hostname, self._debug_dir)
return (self._retrieve_logs_cgi + self._generic_results_bin +
path_to_object)
return 'NA'
def _get_links_for_failure(self):
"""
Get links related to this test failure.
@return: Returns a named tuple of links.
"""
links = collections.namedtuple('links', 'results, artifacts')
return links(self._link_result_logs(), self._link_build_artifacts())
class Reporter(object):
"""Files external reports about bug failures that happened inside of
autotest."""
_project_name = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'project_name', default='')
_username = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'username', default='')
_password = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'password', default='')
_SEARCH_MARKER = 'ANCHOR '
def _get_tracker(self, project, user, password):
""" Gets an initialized tracker object. """
if project and user and password:
creds = gdata_lib.Creds()
creds.SetCreds(user, password)
tracker = gdata_lib.TrackerComm()
tracker.Connect(creds, project)
return tracker
logging.error('Tracker auth not set up in shadow_config.ini, '
'cannot file bugs.')
return None
def __init__(self):
if gdata_lib is None:
logging.warning("Bug filing disabled due to missing imports.")
return
self._tracker = self._get_tracker(self._project_name,
self._username, self._password)
def _check_tracker(self):
"""
Checks if we have a tracker object to use for filing bugs.
@return: True if we have a tracker object.
"""
return gdata_lib and self._tracker
def report(self, failure):
"""
Report about a failure on the bug tracker. If this failure has already
happened, post a comment on the existing bug about it occurring again.
If this is a new failure, create a new bug about it.
@param failure A TestFailure instance about the failure.
"""
if not self._check_tracker():
logging.error("Can't file %s", failure.bug_title())
return
issue = self._find_issue_by_marker(failure.search_marker())
summary = '%s\n\n%s%s\n' % (failure.bug_summary(),
self._SEARCH_MARKER,
failure.search_marker())
self._add_issue_to_tracker(issue, summary, failure.bug_title())
def _add_issue_to_tracker(self, issue, summary, title):
"""
Adds an issue to the tracker.
Either file a new issue or append a comment to an existing issue.
@param issue: The new issue.
@param summary: A summary of the failure.
@param title: Title of the bug. If a bug already exists the summary gets
prefixed with the title.
@return: None
"""
if issue:
summary = '%s\n\n%s' % (title, summary)
self._tracker.AppendTrackerIssueById(issue.id, summary)
logging.info("Filed comment %s on %s", summary, issue.id)
else:
issue = gdata_lib.Issue(title=title, summary=summary,
labels=['Test-Support', 'autofiled'],
status='Untriaged', owner='')
bugid = self._tracker.CreateTrackerIssue(issue)
logging.info("Filing new bug %s, with summary %s", bugid,
summary)
def _find_issue_by_marker(self, marker):
"""
Queries the tracker to find if there is a bug filed for this issue.
@param marker The marker string to search for to find a duplicate of
this issue.
@return A gdata_lib.Issue instance of the issue that was found, or
None if no issue was found.
"""
# This will return at most 25 matches, as that's how the
# code.google.com API limits this query.
issues = self._tracker.GetTrackerIssuesByText(
self._SEARCH_MARKER + marker)
# TODO(milleral) The tracker doesn't support exact text searching, even
# with quotes around the search term. Therefore, to hack around this, we
# need to filter through the results we get back and search for the
# string ourselves.
# We could have gotten no results...
if not issues:
return None
# We could have gotten some results, but we need to wade through them
# to find if there's an actually correct one.
for issue in issues:
if marker in issue.summary:
return issue
for comment in issue.comments:
# Sometimes, comment.text is None...
if comment.text and marker in comment.text:
return issue
# Or, if we make it this far, we have only gotten similar, but not
# actually matching results.
return None