blob: fc1ff4a0070af6c49b794636ae3dee02cdb1ed24 [file] [log] [blame]
import copy
import json
import logging
import re
import common
from autotest_lib.client.common_lib import autotemp
from autotest_lib.client.common_lib import global_config
# Try importing the essential bug reporting libraries. Chromite and gdata_lib
# are useless unless they can import gdata too.
try:
__import__('chromite')
__import__('gdata')
except ImportError, e:
fundamental_libs = False
logging.debug('Will not be able to generate link '
'to the buildbot page when filing bugs. %s', e)
else:
from chromite.lib import cros_build_lib, gs
fundamental_libs = True
# Number of times to retry if a gs command fails. Defaults to 10,
# which is far too long given that we already wait on these files
# before starting HWTests.
_GS_RETRIES = 1
_HTTP_ERROR_THRESHOLD = 400
BUG_CONFIG_SECTION = 'BUG_REPORTING'
# 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='')
# Template for the url used to generate the link to the job
_job_view = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'job_view', default='')
# gs prefix to perform file like operations (gs://)
_gs_file_prefix = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'gs_file_prefix', default='')
# global configurations needed for buildbot stages link
_buildbot_builders = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'buildbot_builders', default='')
_build_prefix = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'build_prefix', default='')
_CRBUG_URL = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'crbug_url')
WMATRIX_RETRY_URL = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'wmatrix_retry_url')
WMATRIX_TEST_HISTORY_URL = global_config.global_config.get_config_value(
BUG_CONFIG_SECTION, 'wmatrix_test_history_url')
class InvalidBugTemplateException(Exception):
"""Exception raised when a bug template is not valid, e.g., missing value
for essential attributes.
"""
pass
class BugTemplate(object):
"""Wrapper class to merge a suite and test bug templates, and do validation.
"""
# Names of expected attributes.
EXPECTED_BUG_TEMPLATE_ATTRIBUTES = ['owner', 'labels', 'status', 'title',
'cc', 'summary', 'components']
LIST_ATTRIBUTES = ['cc', 'labels']
EMAIL_ATTRIBUTES = ['owner', 'cc']
EMAIL_REGEX = re.compile(r'[^@]+@[^@]+\.[^@]+')
def __init__(self, bug_template):
"""Initialize a BugTemplate object.
@param bug_template: initial bug template, e.g., bug template from suite
control file.
"""
self.bug_template = self.cleanup_bug_template(bug_template)
@classmethod
def validate_bug_template(cls, bug_template):
"""Verify if a bug template has value for all essential attributes.
@param bug_template: bug template to be verified.
@raise InvalidBugTemplateException: raised when a bug template
is invalid, e.g., has missing essential attribute, or any given
template is not a dictionary.
"""
if not type(bug_template) is dict:
raise InvalidBugTemplateException('Bug template must be a '
'dictionary.')
unexpected_keys = []
for key, value in bug_template.iteritems():
if not key in cls.EXPECTED_BUG_TEMPLATE_ATTRIBUTES:
raise InvalidBugTemplateException('Key %s is not expected in '
'bug template.' % key)
if (key in cls.LIST_ATTRIBUTES and
not isinstance(value, list)):
raise InvalidBugTemplateException('Value for %s must be a list.'
% key)
if key in cls.EMAIL_ATTRIBUTES:
emails = value if isinstance(value, list) else [value]
for email in emails:
if not email or not cls.EMAIL_REGEX.match(email):
raise InvalidBugTemplateException(
'Invalid email address: %s.' % email)
@classmethod
def cleanup_bug_template(cls, bug_template):
"""Remove empty entries in given bug template.
@param bug_template: bug template to be verified.
@return: A cleaned up bug template.
@raise InvalidBugTemplateException: raised when a bug template
is not a dictionary.
"""
if not type(bug_template) is dict:
raise InvalidBugTemplateException('Bug template must be a '
'dictionary.')
template = copy.deepcopy(bug_template)
# If owner or cc is set but the value is empty or None, remove it from
# the template.
for email_attribute in cls.EMAIL_ATTRIBUTES:
if email_attribute in template:
value = template[email_attribute]
if isinstance(value, list):
template[email_attribute] = [email for email in value
if email]
if not template[email_attribute]:
del(template[email_attribute])
return template
def finalize_bug_template(self, test_template):
"""Merge test and suite bug templates.
@param test_template: Bug template from test control file.
@return: Merged bug template.
@raise InvalidBugTemplateException: raised when the merged template is
invalid, e.g., has missing essential attribute, or any given
template is not a dictionary.
"""
test_template = self.cleanup_bug_template(test_template)
self.validate_bug_template(self.bug_template)
self.validate_bug_template(test_template)
merged_template = test_template
merged_template.update((k, v) for k, v in self.bug_template.iteritems()
if k not in merged_template)
# test_template wins for common keys, unless values are list that can be
# merged.
for key in set(merged_template.keys()).intersection(
self.bug_template.keys()):
if (type(merged_template[key]) is list and
type(self.bug_template[key]) is list):
merged_template[key] = (merged_template[key] +
self.bug_template[key])
elif not merged_template[key]:
merged_template[key] = self.bug_template[key]
self.validate_bug_template(merged_template)
return merged_template
def link_build_artifacts(build):
"""Returns a url to build artifacts on google storage.
@param build: A string, e.g. stout32-release/R30-4433.0.0
@returns: A url to build artifacts on google storage.
"""
return (_gs_domain + _arg_prefix +
_chromeos_image_archive + build)
def link_job(job_id, instance_server=None):
"""Returns an url to the job on cautotest.
@param job_id: A string, representing the job id.
@param instance_server: The instance server.
Eg: cautotest, cautotest-cq, localhost.
@returns: An url to the job on cautotest.
"""
if not job_id:
return 'Job did not run, or was aborted prematurely'
if not instance_server:
instance_server = global_config.global_config.get_config_value(
'SERVER', 'hostname', default='localhost')
if 'cautotest' in instance_server:
instance_server += '.corp.google.com'
return _job_view % (instance_server, job_id)
def _base_results_log(job_id, result_owner, hostname):
"""Returns the base url of the job's results.
@param job_id: A string, representing the job id.
@param result_owner: A string, representing the onwer of the job.
@param hostname: A string, representing the host on which
the job has run.
@returns: The base url of the job's results.
"""
if job_id and result_owner and hostname:
path_to_object = '%s-%s/%s' % (job_id, result_owner,
hostname)
return (_retrieve_logs_cgi + _generic_results_bin +
path_to_object)
def link_result_logs(job_id, result_owner, hostname):
"""Returns a url to test logs on google storage.
@param job_id: A string, representing the job id.
@param result_owner: A string, representing the owner of the job.
@param hostname: A string, representing the host on which the
jot has run.
@returns: A url to test logs on google storage.
"""
base_results = _base_results_log(job_id, result_owner, hostname)
if base_results:
return '%s/%s' % (base_results, _debug_dir)
return ('Could not generate results log: the job with id %s, '
'scheduled by: %s on host: %s did not run' %
(job_id, result_owner, hostname))
def link_status_log(job_id, result_owner, hostname):
"""Returns an url to status log of the job.
@param job_id: A string, representing the job id.
@param result_owner: A string, representing the owner of the job.
@param hostname: A string, representing the host on which the
jot has run.
@returns: A url to status log of the job.
"""
base_results = _base_results_log(job_id, result_owner, hostname)
if base_results:
return '%s/%s' % (base_results, 'status.log')
return 'NA'
def _get_metadata_dict(build):
"""
Get a dictionary of metadata related to this failure.
Metadata.json is created in the HWTest Archiving stage, if this file
isn't found the call to Cat will timeout after the number of retries
specified in the GSContext object. If metadata.json exists we parse
a json string of it's contents into a dictionary, which we return.
@param build: A string, e.g. stout32-release/R30-4433.0.0
@returns: A dictionary with the contents of metadata.json.
"""
if not fundamental_libs:
return
try:
tempdir = autotemp.tempdir()
gs_context = gs.GSContext(retries=_GS_RETRIES,
cache_dir=tempdir.name)
gs_cmd = '%s%s%s/metadata.json' % (_gs_file_prefix,
_chromeos_image_archive,
build)
return json.loads(gs_context.Cat(gs_cmd))
except (cros_build_lib.RunCommandError, gs.GSContextException) as e:
logging.debug(e)
finally:
tempdir.clean()
def link_buildbot_stages(build):
"""
Link to the buildbot page associated with this run of HWTests.
@param build: A string, e.g. stout32-release/R30-4433.0.0
@return: A link to the buildbot stages page, or 'NA' if we cannot glean
enough information from metadata.json (or it doesn't exist).
"""
metadata = _get_metadata_dict(build)
if (metadata and
metadata.get('builder-name') and
metadata.get('build-number')):
return ('%s%s/builds/%s' %
(_buildbot_builders,
metadata.get('builder-name'),
metadata.get('build-number'))).replace(' ', '%20')
return 'NA'
def link_retry_url(test_name):
"""Link to the wmatrix retry stats page for this test.
@param test_name: Test we want to search the retry stats page for.
@return: A link to the wmatrix retry stats dashboard for this test.
"""
return WMATRIX_RETRY_URL % test_name
def link_test_history(test_name):
"""Link to the wmatrix test history page for this test.
@param test_name: Test we want to search the test history for.
@return: A link to the wmatrix test history page for this test.
"""
return WMATRIX_TEST_HISTORY_URL % test_name
def link_crbug(bug_id):
"""Generate a bug link for the given bug_id.
@param bug_id: The id of the bug.
@return: A link, eg: https://crbug.com/<bug_id>.
"""
return _CRBUG_URL % (bug_id,)