| # Copyright 2015 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Provides an endpoint and web interface for associating alerts with bug.""" |
| |
| import re |
| |
| from google.appengine.api import users |
| from google.appengine.ext import ndb |
| |
| from dashboard import issue_tracker_service |
| from dashboard import oauth2_decorator |
| from dashboard import request_handler |
| from dashboard import utils |
| from dashboard.models import anomaly |
| from dashboard.models import stoppage_alert |
| |
| |
| class AssociateAlertsHandler(request_handler.RequestHandler): |
| """Associates alerts with a bug.""" |
| |
| def post(self): |
| """POST is the same as GET for this endpoint.""" |
| self.get() |
| |
| @oauth2_decorator.DECORATOR.oauth_required |
| def get(self): |
| """Response handler for the page used to group an alert with a bug. |
| |
| Request parameters: |
| bug_id: Bug ID number, as a string (when submitting the form). |
| keys: Comma-separated alert keys in urlsafe format. |
| confirm: If non-empty, associate alerts with a bug ID even if |
| it appears that the alerts already associated with that bug |
| have a non-overlapping revision range. |
| |
| Outputs: |
| HTML with result. |
| """ |
| if not utils.IsValidSheriffUser(): |
| user = users.get_current_user() |
| self.ReportError('User "%s" not authorized.' % user, status=403) |
| return |
| |
| urlsafe_keys = self.request.get('keys') |
| if not urlsafe_keys: |
| self.RenderHtml('bug_result.html', { |
| 'error': 'No alerts specified to add bugs to.'}) |
| return |
| |
| is_confirmed = bool(self.request.get('confirm')) |
| bug_id = self.request.get('bug_id') |
| if bug_id: |
| self._AssociateAlertsWithBug(bug_id, urlsafe_keys, is_confirmed) |
| else: |
| self._ShowCommentDialog(urlsafe_keys) |
| |
| def _ShowCommentDialog(self, urlsafe_keys): |
| """Sends a HTML page with a form for selecting a bug number. |
| |
| Args: |
| urlsafe_keys: Comma-separated Alert keys in urlsafe format. |
| """ |
| # Get information about Alert entities and related TestMetadata entities, |
| # so that they can be compared with recent bugs. |
| alert_keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys.split(',')] |
| alert_entities = ndb.get_multi(alert_keys) |
| ranges = [(a.start_revision, a.end_revision) for a in alert_entities] |
| |
| # Mark bugs that have overlapping revision ranges as potentially relevant. |
| # On the alerts page, alerts are only highlighted if the revision range |
| # overlaps with the revision ranges for all of the selected alerts; the |
| # same thing is done here. |
| bugs = self._FetchBugs() |
| for bug in bugs: |
| this_range = _RevisionRangeFromSummary(bug['summary']) |
| bug['relevant'] = all(_RangesOverlap(this_range, r) for r in ranges) |
| |
| self.RenderHtml('bug_result.html', { |
| 'bug_associate_form': True, |
| 'keys': urlsafe_keys, |
| 'bugs': bugs |
| }) |
| |
| def _FetchBugs(self): |
| http = oauth2_decorator.DECORATOR.http() |
| issue_tracker = issue_tracker_service.IssueTrackerService(http) |
| response = issue_tracker.List( |
| q='opened-after:today-5', label='Type-Bug-Regression,Performance', |
| sort='-id') |
| return response.get('items', []) if response else [] |
| |
| def _AssociateAlertsWithBug(self, bug_id, urlsafe_keys, is_confirmed): |
| """Sets the bug ID for a set of alerts. |
| |
| This is done after the user enters and submits a bug ID. |
| |
| Args: |
| bug_id: Bug ID number, as a string. |
| urlsafe_keys: Comma-separated Alert keys in urlsafe format. |
| is_confirmed: Whether the user has confirmed that they really want |
| to associate the alerts with a bug even if it appears that the |
| revision ranges don't overlap. |
| """ |
| # Validate bug ID. |
| try: |
| bug_id = int(bug_id) |
| except ValueError: |
| self.RenderHtml( |
| 'bug_result.html', |
| {'error': 'Invalid bug ID "%s".' % str(bug_id)}) |
| return |
| |
| # Get Anomaly entities and related TestMetadata entities. |
| alert_keys = [ndb.Key(urlsafe=k) for k in urlsafe_keys.split(',')] |
| alert_entities = ndb.get_multi(alert_keys) |
| |
| if not is_confirmed: |
| warning_msg = self._VerifyAnomaliesOverlap(alert_entities, bug_id) |
| if warning_msg: |
| self._ShowConfirmDialog('associate_alerts', warning_msg, { |
| 'bug_id': bug_id, |
| 'keys': urlsafe_keys, |
| }) |
| return |
| |
| for a in alert_entities: |
| a.bug_id = bug_id |
| ndb.put_multi(alert_entities) |
| |
| self.RenderHtml('bug_result.html', {'bug_id': bug_id}) |
| |
| def _VerifyAnomaliesOverlap(self, alerts, bug_id): |
| """Checks whether the alerts' revision ranges intersect. |
| |
| Args: |
| alerts: A list of Alert entities to verify. |
| bug_id: Bug ID number. |
| |
| Returns: |
| A string with warning message, or None if there's no warning. |
| """ |
| if not utils.MinimumAlertRange(alerts): |
| return 'Selected alerts do not have overlapping revision range.' |
| else: |
| anomalies_with_bug = anomaly.Anomaly.query( |
| anomaly.Anomaly.bug_id == bug_id).fetch() |
| stoppage_alerts_with_bug = stoppage_alert.StoppageAlert.query( |
| stoppage_alert.StoppageAlert.bug_id == bug_id).fetch() |
| alerts_with_bug = anomalies_with_bug + stoppage_alerts_with_bug |
| |
| if not alerts_with_bug: |
| return None |
| if not utils.MinimumAlertRange(alerts_with_bug): |
| return ('Alerts in bug %s do not have overlapping revision ' |
| 'range.' % bug_id) |
| elif not utils.MinimumAlertRange(alerts + alerts_with_bug): |
| return ('Selected alerts do not have overlapping revision ' |
| 'range with alerts in bug %s.' % bug_id) |
| return None |
| |
| def _ShowConfirmDialog(self, handler, message, parameters): |
| """Sends a HTML page with a form to confirm an action. |
| |
| Args: |
| handler: Name of URL handler to submit confirm dialog. |
| message: Confirmation message. |
| parameters: Dictionary of request parameters to submit with confirm |
| dialog. |
| """ |
| self.RenderHtml('bug_result.html', { |
| 'confirmation_required': True, |
| 'handler': handler, |
| 'message': message, |
| 'parameters': parameters or {} |
| }) |
| |
| |
| def _RevisionRangeFromSummary(summary): |
| """Uses regex to extract revision range from bug a summary string. |
| |
| Note: Information such as test path and revision range for a bug could |
| also be gotten by querying the datastore for Anomaly entities for |
| each bug ID. However, these queries might be relatively costly. Also, |
| it is acceptable if the information extracted isn't 100% accurate, |
| because it is only used to make a list of bugs for convenience. |
| |
| Note: The format of the summary is determined by the triage-dialog element. |
| |
| Args: |
| summary: The bug summary string. |
| |
| Returns: |
| A pair of revision numbers (start, end), or None. |
| """ |
| match = re.match(r'.* (\d+):(\d+)$', summary) |
| if match: |
| start, end = match.groups() |
| # Since start and end matched '\d+', we know they can be parsed as ints. |
| return (int(start), int(end)) |
| return None |
| |
| |
| def _RangesOverlap(range1, range2): |
| """Checks whether two revision ranges overlap. |
| |
| Note, sharing an endpoint is considered overlap for this function. |
| |
| Args: |
| range1: A pair of integers (start, end). |
| range2: Another pair of integers. |
| |
| Returns: |
| True if there is any overlap, False otherwise. |
| """ |
| if not range1 or not range2: |
| return False |
| return range1[0] <= range2[1] and range1[1] >= range2[0] |