Home | History | Annotate | Download | only in dynamic_suite
      1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import collections
      6 import logging
      7 
      8 import common
      9 
     10 from autotest_lib.client.common_lib import global_config
     11 from autotest_lib.server import site_utils
     12 from autotest_lib.server.cros.dynamic_suite import job_status
     13 from autotest_lib.server.cros.dynamic_suite import reporting_utils
     14 from autotest_lib.server.cros.dynamic_suite import tools
     15 from autotest_lib.site_utils  import gmail_lib
     16 
     17 try:
     18     from chromite.lib import metrics
     19 except ImportError:
     20     metrics = site_utils.metrics_mock
     21 
     22 
     23 EMAIL_CREDS_FILE = global_config.global_config.get_config_value(
     24         'NOTIFICATIONS', 'gmail_api_credentials_test_failure', default=None)
     25 
     26 
     27 class TestBug(object):
     28     """
     29     Wrap up all information needed to make an intelligent report about an
     30     issue. Each TestBug has a search marker associated with it that can be
     31     used to find similar reports.
     32     """
     33 
     34     def __init__(self, build, chrome_version, suite, result):
     35         """
     36         @param build: The build type, of the form <board>/<milestone>-<release>.
     37                       eg: x86-mario-release/R25-4321.0.0
     38         @param chrome_version: The chrome version associated with the build.
     39                                eg: 28.0.1498.1
     40         @param suite: The name of the suite that this test run is a part of.
     41         @param result: The status of the job associated with this issue.
     42                        This contains the status, job id, test name, hostname
     43                        and reason for issue.
     44         """
     45         self.build = build
     46         self.chrome_version = chrome_version
     47         self.suite = suite
     48         self.name = tools.get_test_name(build, suite, result.test_name)
     49         self.reason = result.reason
     50         # The result_owner is used to find results and logs.
     51         self.result_owner = result.owner
     52         self.hostname = result.hostname
     53         self.job_id = result.id
     54 
     55         # Aborts, server/client job failures or a test failure without a
     56         # reason field need lab attention. Lab bugs for the aborted case
     57         # are disabled till crbug.com/188217 is resolved.
     58         self.lab_error = job_status.is_for_infrastructure_fail(result)
     59 
     60         # The owner is who the bug is assigned to.
     61         self.owner = ''
     62         self.cc = []
     63         self.components = []
     64 
     65         if result.is_warn():
     66             self.labels = ['Test-Warning']
     67             self.status = 'Warning'
     68         else:
     69             self.labels = []
     70             self.status = 'Failure'
     71 
     72 
     73     def title(self):
     74         """Combines information about this bug into a title string."""
     75         return '[%s] %s %s on %s' % (self.suite, self.name,
     76                                      self.status, self.build)
     77 
     78 
     79     def summary(self):
     80         """Combines information about this bug into a summary string."""
     81 
     82         links = self._get_links_for_failure()
     83         template = ('This report is automatically generated to track the '
     84                     'following %(status)s:\n'
     85                     'Test: %(test)s.\n'
     86                     'Suite: %(suite)s.\n'
     87                     'Chrome Version: %(chrome_version)s.\n'
     88                     'Build: %(build)s.\n\nReason:\n%(reason)s.\n'
     89                     'build artifacts: %(build_artifacts)s.\n'
     90                     'results log: %(results_log)s.\n'
     91                     'status log: %(status_log)s.\n'
     92                     'job link: %(job)s.\n\n'
     93                     'You may want to check the test history: '
     94                     '%(test_history_url)s\n'
     95                     'You may also want to check the test retry dashboard in '
     96                     'case this is a flakey test: %(retry_url)s\n')
     97 
     98         specifics = {
     99             'status': self.status,
    100             'test': self.name,
    101             'suite': self.suite,
    102             'build': self.build,
    103             'chrome_version': self.chrome_version,
    104             'reason': self.reason,
    105             'build_artifacts': links.artifacts,
    106             'results_log': links.results,
    107             'status_log': links.status_log,
    108             'job': links.job,
    109             'test_history_url': links.test_history_url,
    110             'retry_url': links.retry_url,
    111         }
    112 
    113         return template % specifics
    114 
    115 
    116     # TO-DO(shuqianz) Fix the dedupe failing issue because reason contains
    117     # special characters after
    118     # https://bugs.chromium.org/p/monorail/issues/detail?id=806 being fixed.
    119     def search_marker(self):
    120         """Return an Anchor that we can use to dedupe this exact bug."""
    121         board = ''
    122         try:
    123             board = site_utils.ParseBuildName(self.build)[0]
    124         except site_utils.ParseBuildNameException as e:
    125             logging.error(str(e))
    126 
    127         # Substitute the board name for a placeholder. We try both build and
    128         # release board name variants.
    129         reason = self.reason
    130         if board:
    131             for b in (board, board.replace('_', '-')):
    132                 reason = reason.replace(b, 'BOARD_PLACEHOLDER')
    133 
    134         return "%s{%s,%s,%s}" % ('Test%s' % self.status, self.suite,
    135                                  self.name, reason)
    136 
    137 
    138     def _get_links_for_failure(self):
    139         """Returns a named tuple of links related to this failure."""
    140         links = collections.namedtuple('links', ('results,'
    141                                                  'status_log,'
    142                                                  'artifacts,'
    143                                                  'job,'
    144                                                  'test_history_url,'
    145                                                  'retry_url'))
    146         return links(reporting_utils.link_result_logs(
    147                          self.job_id, self.result_owner, self.hostname),
    148                      reporting_utils.link_status_log(
    149                          self.job_id, self.result_owner, self.hostname),
    150                      reporting_utils.link_build_artifacts(self.build),
    151                      reporting_utils.link_job(self.job_id),
    152                      reporting_utils.link_test_history(self.name),
    153                      reporting_utils.link_retry_url(self.name))
    154 
    155 
    156 ReportResult = collections.namedtuple('ReportResult', ['bug_id', 'update_count'])
    157 
    158 
    159 def send_email(bug, bug_template):
    160     """Send email to the owner and cc's to notify the TestBug.
    161 
    162     @param bug: TestBug instance.
    163     @param bug_template: A template dictionary specifying the default bug
    164                          filing options for failures in this suite.
    165     """
    166     to_set = set(bug.cc) if bug.cc else set()
    167     if bug.owner:
    168         to_set.add(bug.owner)
    169     if bug_template.get('cc'):
    170         to_set = to_set.union(bug_template.get('cc'))
    171     if bug_template.get('owner'):
    172         to_set.add(bug_template.get('owner'))
    173     recipients = ', '.join(to_set)
    174     if not recipients:
    175         logging.warning('No owner/cc found. Will skip sending a mail.')
    176         return
    177     success = False
    178     try:
    179         gmail_lib.send_email(
    180             recipients, bug.title(), bug.summary(), retry=False,
    181             creds_path=site_utils.get_creds_abspath(EMAIL_CREDS_FILE))
    182         success = True
    183     finally:
    184         (metrics.Counter('chromeos/autotest/errors/send_bug_email')
    185          .increment(fields={'success': success}))
    186