• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5import collections
6import logging
7
8import common
9
10from autotest_lib.client.common_lib import global_config
11from autotest_lib.server import site_utils
12from autotest_lib.server.cros.dynamic_suite import job_status
13from autotest_lib.server.cros.dynamic_suite import reporting_utils
14from autotest_lib.server.cros.dynamic_suite import tools
15from autotest_lib.site_utils import gmail_lib
16
17try:
18    from chromite.lib import metrics
19except ImportError:
20    metrics = site_utils.metrics_mock
21
22
23EMAIL_CREDS_FILE = global_config.global_config.get_config_value(
24        'NOTIFICATIONS', 'gmail_api_credentials_test_failure', default=None)
25
26
27class 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
156ReportResult = collections.namedtuple('ReportResult', ['bug_id', 'update_count'])
157
158
159def 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