# 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 import common from autotest_lib.client.common_lib import global_config from autotest_lib.server import site_utils from autotest_lib.server.cros.dynamic_suite import job_status from autotest_lib.server.cros.dynamic_suite import reporting_utils from autotest_lib.server.cros.dynamic_suite import tools from autotest_lib.site_utils import gmail_lib try: from chromite.lib import metrics except ImportError: metrics = site_utils.metrics_mock EMAIL_CREDS_FILE = global_config.global_config.get_config_value( 'NOTIFICATIONS', 'gmail_api_credentials_test_failure', default=None) class TestBug(object): """ Wrap up all information needed to make an intelligent report about an issue. Each TestBug has a search marker associated with it that can be used to find similar reports. """ def __init__(self, build, chrome_version, suite, result): """ @param build: The build type, of the form /-. eg: x86-mario-release/R25-4321.0.0 @param chrome_version: The chrome version associated with the build. eg: 28.0.1498.1 @param suite: The name of the suite that this test run is a part of. @param result: The status of the job associated with this issue. This contains the status, job id, test name, hostname and reason for issue. """ self.build = build self.chrome_version = chrome_version self.suite = suite self.name = tools.get_test_name(build, suite, result.test_name) self.reason = result.reason # The result_owner is used to find results and logs. self.result_owner = result.owner self.hostname = result.hostname self.job_id = result.id # Aborts, server/client job failures or a test failure without a # reason field need lab attention. Lab bugs for the aborted case # are disabled till crbug.com/188217 is resolved. self.lab_error = job_status.is_for_infrastructure_fail(result) # The owner is who the bug is assigned to. self.owner = '' self.cc = [] self.components = [] if result.is_warn(): self.labels = ['Test-Warning'] self.status = 'Warning' else: self.labels = [] self.status = 'Failure' def title(self): """Combines information about this bug into a title string.""" return '[%s] %s %s on %s' % (self.suite, self.name, self.status, self.build) def summary(self): """Combines information about this bug into a summary string.""" links = self._get_links_for_failure() template = ('This report is automatically generated to track the ' 'following %(status)s:\n' 'Test: %(test)s.\n' 'Suite: %(suite)s.\n' 'Chrome Version: %(chrome_version)s.\n' 'Build: %(build)s.\n\nReason:\n%(reason)s.\n' 'build artifacts: %(build_artifacts)s.\n' 'results log: %(results_log)s.\n' 'status log: %(status_log)s.\n' 'job link: %(job)s.\n\n' 'You may want to check the test history: ' '%(test_history_url)s\n' 'You may also want to check the test retry dashboard in ' 'case this is a flakey test: %(retry_url)s\n') specifics = { 'status': self.status, 'test': self.name, 'suite': self.suite, 'build': self.build, 'chrome_version': self.chrome_version, 'reason': self.reason, 'build_artifacts': links.artifacts, 'results_log': links.results, 'status_log': links.status_log, 'job': links.job, 'test_history_url': links.test_history_url, 'retry_url': links.retry_url, } return template % specifics # TO-DO(shuqianz) Fix the dedupe failing issue because reason contains # special characters after # https://bugs.chromium.org/p/monorail/issues/detail?id=806 being fixed. def search_marker(self): """Return an Anchor that we can use to dedupe this exact bug.""" board = '' try: board = site_utils.ParseBuildName(self.build)[0] except site_utils.ParseBuildNameException as e: logging.error(str(e)) # Substitute the board name for a placeholder. We try both build and # release board name variants. reason = self.reason if board: for b in (board, board.replace('_', '-')): reason = reason.replace(b, 'BOARD_PLACEHOLDER') return "%s{%s,%s,%s}" % ('Test%s' % self.status, self.suite, self.name, reason) def _get_links_for_failure(self): """Returns a named tuple of links related to this failure.""" links = collections.namedtuple('links', ('results,' 'status_log,' 'artifacts,' 'job,' 'test_history_url,' 'retry_url')) return links(reporting_utils.link_result_logs( self.job_id, self.result_owner, self.hostname), reporting_utils.link_status_log( self.job_id, self.result_owner, self.hostname), reporting_utils.link_build_artifacts(self.build), reporting_utils.link_job(self.job_id), reporting_utils.link_test_history(self.name), reporting_utils.link_retry_url(self.name)) ReportResult = collections.namedtuple('ReportResult', ['bug_id', 'update_count']) def send_email(bug, bug_template): """Send email to the owner and cc's to notify the TestBug. @param bug: TestBug instance. @param bug_template: A template dictionary specifying the default bug filing options for failures in this suite. """ to_set = set(bug.cc) if bug.cc else set() if bug.owner: to_set.add(bug.owner) if bug_template.get('cc'): to_set = to_set.union(bug_template.get('cc')) if bug_template.get('owner'): to_set.add(bug_template.get('owner')) recipients = ', '.join(to_set) if not recipients: logging.warning('No owner/cc found. Will skip sending a mail.') return success = False try: gmail_lib.send_email( recipients, bug.title(), bug.summary(), retry=False, creds_path=site_utils.get_creds_abspath(EMAIL_CREDS_FILE)) success = True finally: (metrics.Counter('chromeos/autotest/errors/send_bug_email') .increment(fields={'success': success}))