| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Code for interacting with Buganizer.""" |
| |
| import datetime |
| from typing import Iterable, Set |
| |
| from blinkpy.w3c import buganizer |
| |
| from bad_machine_finder import detection |
| |
| _AUTOMATED_COMMENT_START = 'Automated Report Of Bad Machines' |
| |
| |
| class BuganizerException(Exception): |
| """A general exception for Buganizer-related errors.""" |
| |
| |
| class ClientNotAvailableException(BuganizerException): |
| """Indicates that a Buganzier client could not be created.""" |
| |
| |
| class BugNotAccessibleException(BuganizerException): |
| """Indicates that the specified bug could not be accessed.""" |
| |
| |
| def UpdateBug(bug_id: int, |
| mixin_grouped_bad_machines: detection.MixinGroupedBadMachines, |
| grace_period: int) -> None: |
| """Updates the given |bug_id| with bad machine results. |
| |
| Will automatically omit bad machines that have previously been reported in the |
| past |grace_period| days. |
| |
| Args: |
| bug_id: The Buganizer bug ID to update. |
| mixin_groupd_bad_machines: A MixinGroupedBadMachines object containing all |
| of the bad machine data to use when updating the bug. |
| grace_period: The minimum number of days between when a bad machine was |
| reported and when it can be reported again. |
| """ |
| client = _GetBuganizerClient() |
| |
| bad_machine_names = mixin_grouped_bad_machines.GetAllBadMachineNames() |
| recently_reported_bots = _GetRecentlyReportedBots(bug_id, client, |
| bad_machine_names, |
| grace_period) |
| |
| markdown_components = [ |
| _AUTOMATED_COMMENT_START, |
| ] |
| mixin_report_markdown = mixin_grouped_bad_machines.GenerateMarkdown( |
| bots_to_skip=recently_reported_bots) |
| if mixin_report_markdown: |
| markdown_components.append(mixin_report_markdown) |
| else: |
| markdown_components.append('No new bad machines detected') |
| markdown_comment = '\n\n'.join(markdown_components) |
| |
| client.NewComment(bug_id, markdown_comment, use_markdown=True) |
| |
| |
| def _GetRecentlyReportedBots(bug_id: int, client: buganizer.BuganizerClient, |
| bad_machine_names: Iterable[str], |
| grace_period: int) -> Set[str]: |
| """Retrieves the subset of |bad_machine_names| which were reported recently. |
| |
| Args: |
| bug_id: The Buganizer bug ID to check. |
| client: The BuganizerClient to use for interacting with Buganizer. |
| bad_machine_names: All machine names that should be looked for within |
| recent comments on the bug. |
| grace_period: The minimum number of days since the last mention of a machine |
| on the bug for it not to be considered recently reported. |
| |
| Returns: |
| A set of machine names which were reported on |bug_id| within the past |
| |grace_period| days. Guaranteed to be a subset of |bad_machine_names|. |
| """ |
| comment_list = client.GetIssueComments(bug_id) |
| # GetIssueComments currently returns a dict if something goes wrong instead of |
| # raising an exception. |
| # TODO(crbug.com/361602059): Switch to catching exceptions once those are |
| # raised. |
| if isinstance(comment_list, dict): |
| raise BugNotAccessibleException( |
| f'Failed to get comments from {bug_id}: ' |
| f'{comment_list.get("error", "error not provided")}') |
| |
| recent_comment_bodies = [] |
| for c in comment_list: |
| comment_iso_timestamp = c['timestamp'] |
| # Z indicates the UTC timezone, but is not supported until Python 3.11. |
| if comment_iso_timestamp.endswith(('z', 'Z')): |
| comment_iso_timestamp = comment_iso_timestamp[:-1] |
| comment_date = datetime.datetime.fromisoformat(comment_iso_timestamp) |
| n_days_ago = (datetime.datetime.now(comment_date.tzinfo) - |
| datetime.timedelta(days=grace_period)) |
| if comment_date < n_days_ago: |
| continue |
| if _AUTOMATED_COMMENT_START not in c['comment']: |
| continue |
| recent_comment_bodies.append(c['comment']) |
| |
| recently_reported_bots = set() |
| for bot_id in bad_machine_names: |
| for cb in recent_comment_bodies: |
| if bot_id in cb: |
| recently_reported_bots.add(bot_id) |
| break |
| |
| return recently_reported_bots |
| |
| |
| def _GetBuganizerClient() -> buganizer.BuganizerClient: |
| try: |
| return buganizer.BuganizerClient() |
| except Exception as e: # pylint: disable=broad-except |
| raise ClientNotAvailableException( |
| 'Failed to create Buganizer client') from e |