1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121

content / test / gpu / bad_machine_finder / buganizer.py [blame]

# 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 bad_machine_finder import detection

from blinkpy.w3c import buganizer

_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