• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2015 The Chromium 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"""Provides an endpoint and web interface for associating alerts with bug."""
6
7import re
8
9from google.appengine.api import users
10from google.appengine.ext import ndb
11
12from dashboard import issue_tracker_service
13from dashboard import oauth2_decorator
14from dashboard import request_handler
15from dashboard import utils
16from dashboard.models import anomaly
17from dashboard.models import stoppage_alert
18
19
20class AssociateAlertsHandler(request_handler.RequestHandler):
21  """Associates alerts with a bug."""
22
23  def post(self):
24    """POST is the same as GET for this endpoint."""
25    self.get()
26
27  @oauth2_decorator.DECORATOR.oauth_required
28  def get(self):
29    """Response handler for the page used to group an alert with a bug.
30
31    Request parameters:
32      bug_id: Bug ID number, as a string (when submitting the form).
33      keys: Comma-separated alert keys in urlsafe format.
34      confirm: If non-empty, associate alerts with a bug ID even if
35          it appears that the alerts already associated with that bug
36          have a non-overlapping revision range.
37
38    Outputs:
39      HTML with result.
40    """
41    if not utils.IsValidSheriffUser():
42      user = users.get_current_user()
43      self.ReportError('User "%s" not authorized.' % user, status=403)
44      return
45
46    urlsafe_keys = self.request.get('keys')
47    if not urlsafe_keys:
48      self.RenderHtml('bug_result.html', {
49          'error': 'No alerts specified to add bugs to.'})
50      return
51
52    is_confirmed = bool(self.request.get('confirm'))
53    bug_id = self.request.get('bug_id')
54    if bug_id:
55      self._AssociateAlertsWithBug(bug_id, urlsafe_keys, is_confirmed)
56    else:
57      self._ShowCommentDialog(urlsafe_keys)
58
59  def _ShowCommentDialog(self, urlsafe_keys):
60    """Sends a HTML page with a form for selecting a bug number.
61
62    Args:
63      urlsafe_keys: Comma-separated Alert keys in urlsafe format.
64    """
65    # Get information about Alert entities and related TestMetadata entities,
66    # so that they can be compared with recent bugs.
67    alert_keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys.split(',')]
68    alert_entities = ndb.get_multi(alert_keys)
69    ranges = [(a.start_revision, a.end_revision) for a in alert_entities]
70
71    # Mark bugs that have overlapping revision ranges as potentially relevant.
72    # On the alerts page, alerts are only highlighted if the revision range
73    # overlaps with the revision ranges for all of the selected alerts; the
74    # same thing is done here.
75    bugs = self._FetchBugs()
76    for bug in bugs:
77      this_range = _RevisionRangeFromSummary(bug['summary'])
78      bug['relevant'] = all(_RangesOverlap(this_range, r) for r in ranges)
79
80    self.RenderHtml('bug_result.html', {
81        'bug_associate_form': True,
82        'keys': urlsafe_keys,
83        'bugs': bugs
84    })
85
86  def _FetchBugs(self):
87    http = oauth2_decorator.DECORATOR.http()
88    issue_tracker = issue_tracker_service.IssueTrackerService(http)
89    response = issue_tracker.List(
90        q='opened-after:today-5', label='Type-Bug-Regression,Performance',
91        sort='-id')
92    return response.get('items', []) if response else []
93
94  def _AssociateAlertsWithBug(self, bug_id, urlsafe_keys, is_confirmed):
95    """Sets the bug ID for a set of alerts.
96
97    This is done after the user enters and submits a bug ID.
98
99    Args:
100      bug_id: Bug ID number, as a string.
101      urlsafe_keys: Comma-separated Alert keys in urlsafe format.
102      is_confirmed: Whether the user has confirmed that they really want
103          to associate the alerts with a bug even if it appears that the
104          revision ranges don't overlap.
105    """
106    # Validate bug ID.
107    try:
108      bug_id = int(bug_id)
109    except ValueError:
110      self.RenderHtml(
111          'bug_result.html',
112          {'error': 'Invalid bug ID "%s".' % str(bug_id)})
113      return
114
115    # Get Anomaly entities and related TestMetadata entities.
116    alert_keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys.split(',')]
117    alert_entities = ndb.get_multi(alert_keys)
118
119    if not is_confirmed:
120      warning_msg = self._VerifyAnomaliesOverlap(alert_entities, bug_id)
121      if warning_msg:
122        self._ShowConfirmDialog('associate_alerts', warning_msg, {
123            'bug_id': bug_id,
124            'keys': urlsafe_keys,
125        })
126        return
127
128    for a in alert_entities:
129      a.bug_id = bug_id
130    ndb.put_multi(alert_entities)
131
132    self.RenderHtml('bug_result.html', {'bug_id': bug_id})
133
134  def _VerifyAnomaliesOverlap(self, alerts, bug_id):
135    """Checks whether the alerts' revision ranges intersect.
136
137    Args:
138      alerts: A list of Alert entities to verify.
139      bug_id: Bug ID number.
140
141    Returns:
142      A string with warning message, or None if there's no warning.
143    """
144    if not utils.MinimumAlertRange(alerts):
145      return 'Selected alerts do not have overlapping revision range.'
146    else:
147      anomalies_with_bug = anomaly.Anomaly.query(
148          anomaly.Anomaly.bug_id == bug_id).fetch()
149      stoppage_alerts_with_bug = stoppage_alert.StoppageAlert.query(
150          stoppage_alert.StoppageAlert.bug_id == bug_id).fetch()
151      alerts_with_bug = anomalies_with_bug + stoppage_alerts_with_bug
152
153      if not alerts_with_bug:
154        return None
155      if not utils.MinimumAlertRange(alerts_with_bug):
156        return ('Alerts in bug %s do not have overlapping revision '
157                'range.' % bug_id)
158      elif not utils.MinimumAlertRange(alerts + alerts_with_bug):
159        return ('Selected alerts do not have overlapping revision '
160                'range with alerts in bug %s.' % bug_id)
161    return None
162
163  def _ShowConfirmDialog(self, handler, message, parameters):
164    """Sends a HTML page with a form to confirm an action.
165
166    Args:
167      handler: Name of URL handler to submit confirm dialog.
168      message: Confirmation message.
169      parameters: Dictionary of request parameters to submit with confirm
170                  dialog.
171    """
172    self.RenderHtml('bug_result.html', {
173        'confirmation_required': True,
174        'handler': handler,
175        'message': message,
176        'parameters': parameters or {}
177    })
178
179
180def _RevisionRangeFromSummary(summary):
181  """Uses regex to extract revision range from bug a summary string.
182
183  Note: Information such as test path and revision range for a bug could
184  also be gotten by querying the datastore for Anomaly entities for
185  each bug ID. However, these queries might be relatively costly. Also,
186  it is acceptable if the information extracted isn't 100% accurate,
187  because it is only used to make a list of bugs for convenience.
188
189  Note: The format of the summary is determined by the triage-dialog element.
190
191  Args:
192    summary: The bug summary string.
193
194  Returns:
195    A pair of revision numbers (start, end), or None.
196  """
197  match = re.match(r'.* (\d+):(\d+)$', summary)
198  if match:
199    start, end = match.groups()
200    # Since start and end matched '\d+', we know they can be parsed as ints.
201    return (int(start), int(end))
202  return None
203
204
205def _RangesOverlap(range1, range2):
206  """Checks whether two revision ranges overlap.
207
208  Note, sharing an endpoint is considered overlap for this function.
209
210  Args:
211    range1: A pair of integers (start, end).
212    range2: Another pair of integers.
213
214  Returns:
215    True if there is any overlap, False otherwise.
216  """
217  if not range1 or not range2:
218    return False
219  return range1[0] <= range2[1] and range1[1] >= range2[0]
220