Home | History | Annotate | Download | only in dashboard
      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 import unittest
      6 
      7 import mock
      8 import webapp2
      9 import webtest
     10 
     11 # pylint: disable=unused-import
     12 from dashboard import mock_oauth2_decorator
     13 # pylint: enable=unused-import
     14 
     15 from dashboard import associate_alerts
     16 from dashboard import issue_tracker_service
     17 from dashboard import testing_common
     18 from dashboard import utils
     19 from dashboard.models import anomaly
     20 from dashboard.models import sheriff
     21 from dashboard.models import stoppage_alert
     22 
     23 
     24 class AssociateAlertsTest(testing_common.TestCase):
     25 
     26   def setUp(self):
     27     super(AssociateAlertsTest, self).setUp()
     28     app = webapp2.WSGIApplication([(
     29         '/associate_alerts', associate_alerts.AssociateAlertsHandler)])
     30     self.testapp = webtest.TestApp(app)
     31     testing_common.SetSheriffDomains(['chromium.org'])
     32     self.SetCurrentUser('foo (at] chromium.org', is_admin=True)
     33 
     34   def _AddSheriff(self):
     35     """Adds a Sheriff and returns its key."""
     36     return sheriff.Sheriff(
     37         id='Chromium Perf Sheriff', email='sullivan (at] google.com').put()
     38 
     39   def _AddTests(self):
     40     """Adds sample Tests and returns a list of their keys."""
     41     testing_common.AddTests(['ChromiumGPU'], ['linux-release'], {
     42         'scrolling-benchmark': {
     43             'first_paint': {},
     44             'mean_frame_time': {},
     45         }
     46     })
     47     return map(utils.TestKey, [
     48         'ChromiumGPU/linux-release/scrolling-benchmark/first_paint',
     49         'ChromiumGPU/linux-release/scrolling-benchmark/mean_frame_time',
     50     ])
     51 
     52   def _AddAnomalies(self):
     53     """Adds sample Anomaly data and returns a dict of revision to key."""
     54     sheriff_key = self._AddSheriff()
     55     test_keys = self._AddTests()
     56     key_map = {}
     57 
     58     # Add anomalies to the two tests alternately.
     59     for end_rev in range(10000, 10120, 10):
     60       test_key = test_keys[0] if end_rev % 20 == 0 else test_keys[1]
     61       anomaly_key = anomaly.Anomaly(
     62           start_revision=(end_rev - 5), end_revision=end_rev, test=test_key,
     63           median_before_anomaly=100, median_after_anomaly=200,
     64           sheriff=sheriff_key).put()
     65       key_map[end_rev] = anomaly_key.urlsafe()
     66 
     67     # Add an anomaly that overlaps.
     68     anomaly_key = anomaly.Anomaly(
     69         start_revision=9990, end_revision=9996, test=test_keys[0],
     70         median_before_anomaly=100, median_after_anomaly=200,
     71         sheriff=sheriff_key).put()
     72     key_map[9996] = anomaly_key.urlsafe()
     73 
     74     # Add an anomaly that overlaps and has bug ID.
     75     anomaly_key = anomaly.Anomaly(
     76         start_revision=9990, end_revision=9997, test=test_keys[0],
     77         median_before_anomaly=100, median_after_anomaly=200,
     78         sheriff=sheriff_key, bug_id=12345).put()
     79     key_map[9997] = anomaly_key.urlsafe()
     80     return key_map
     81 
     82   def testGet_NoKeys_ShowsError(self):
     83     response = self.testapp.get('/associate_alerts')
     84     self.assertIn('<div class="error">', response.body)
     85 
     86   def testGet_SameAsPost(self):
     87     get_response = self.testapp.get('/associate_alerts')
     88     post_response = self.testapp.post('/associate_alerts')
     89     self.assertEqual(get_response.body, post_response.body)
     90 
     91   def testGet_InvalidBugId_ShowsError(self):
     92     key_map = self._AddAnomalies()
     93     response = self.testapp.get(
     94         '/associate_alerts?keys=%s&bug_id=foo' % key_map[9996])
     95     self.assertIn('<div class="error">', response.body)
     96     self.assertIn('Invalid bug ID', response.body)
     97 
     98   # Mocks fetching bugs from issue tracker.
     99   @mock.patch('issue_tracker_service.discovery.build', mock.MagicMock())
    100   @mock.patch.object(
    101       issue_tracker_service.IssueTrackerService, 'List',
    102       mock.MagicMock(return_value={
    103           'items': [
    104               {
    105                   'id': 12345,
    106                   'summary': '5% regression in bot/suite/x at 10000:20000',
    107                   'state': 'open',
    108                   'status': 'New',
    109                   'author': {'name': 'exam... (at] google.com'},
    110               },
    111               {
    112                   'id': 13579,
    113                   'summary': '1% regression in bot/suite/y at 10000:20000',
    114                   'state': 'closed',
    115                   'status': 'WontFix',
    116                   'author': {'name': 'exam... (at] google.com'},
    117               },
    118           ]}))
    119   def testGet_NoBugId_ShowsDialog(self):
    120     # When a GET request is made with some anomaly keys but no bug ID,
    121     # A HTML form is shown for the user to input a bug number.
    122     key_map = self._AddAnomalies()
    123     response = self.testapp.get('/associate_alerts?keys=%s' % key_map[10000])
    124     # The response contains a table of recent bugs and a form.
    125     self.assertIn('12345', response.body)
    126     self.assertIn('13579', response.body)
    127     self.assertIn('<form', response.body)
    128 
    129   def testGet_WithBugId_AlertIsAssociatedWithBugId(self):
    130     # When the bug ID is given and the alerts overlap, then the Anomaly
    131     # entities are updated and there is a response indicating success.
    132     key_map = self._AddAnomalies()
    133     response = self.testapp.get(
    134         '/associate_alerts?keys=%s,%s&bug_id=12345' % (
    135             key_map[9996], key_map[10000]))
    136     # The response page should have a bug number.
    137     self.assertIn('12345', response.body)
    138     # The Anomaly entities should be updated.
    139     for anomaly_entity in anomaly.Anomaly.query().fetch():
    140       if anomaly_entity.end_revision in (10000, 9996):
    141         self.assertEqual(12345, anomaly_entity.bug_id)
    142       elif anomaly_entity.end_revision != 9997:
    143         self.assertIsNone(anomaly_entity.bug_id)
    144 
    145   def testGet_WithStoppageAlert_ChangesAlertBugId(self):
    146     test_keys = self._AddTests()
    147     rows = testing_common.AddRows(utils.TestPath(test_keys[0]), {10, 20})
    148     alert_key = stoppage_alert.CreateStoppageAlert(
    149         test_keys[0].get(), rows[0]).put()
    150     self.testapp.get(
    151         '/associate_alerts?bug_id=123&keys=%s' % alert_key.urlsafe())
    152     self.assertEqual(123, alert_key.get().bug_id)
    153 
    154   def testGet_TargetBugHasNoAlerts_DoesNotAskForConfirmation(self):
    155     # Associating alert with bug ID that has no alerts is always OK.
    156     key_map = self._AddAnomalies()
    157     response = self.testapp.get(
    158         '/associate_alerts?keys=%s,%s&bug_id=578' % (
    159             key_map[9996], key_map[10000]))
    160     # The response page should have a bug number.
    161     self.assertIn('578', response.body)
    162     # The Anomaly entities should be updated.
    163     self.assertEqual(
    164         578, anomaly.Anomaly.query(
    165             anomaly.Anomaly.end_revision == 9996).get().bug_id)
    166     self.assertEqual(
    167         578, anomaly.Anomaly.query(
    168             anomaly.Anomaly.end_revision == 10000).get().bug_id)
    169 
    170   def testGet_NonOverlappingAlerts_AsksForConfirmation(self):
    171     # Associating alert with bug ID that contains non-overlapping revision
    172     # ranges should show a confirmation page.
    173     key_map = self._AddAnomalies()
    174     response = self.testapp.get(
    175         '/associate_alerts?keys=%s,%s&bug_id=12345' % (
    176             key_map[10000], key_map[10010]))
    177     # The response page should show confirmation page.
    178     self.assertIn('Do you want to continue?', response.body)
    179     # The Anomaly entities should not be updated.
    180     for anomaly_entity in anomaly.Anomaly.query().fetch():
    181       if anomaly_entity.end_revision != 9997:
    182         self.assertIsNone(anomaly_entity.bug_id)
    183 
    184   def testGet_WithConfirm_AssociatesWithNewBugId(self):
    185     # Associating alert with bug ID and with confirmed non-overlapping revision
    186     # range should update alert with bug ID.
    187     key_map = self._AddAnomalies()
    188     response = self.testapp.get(
    189         '/associate_alerts?confirm=true&keys=%s,%s&bug_id=12345' % (
    190             key_map[10000], key_map[10010]))
    191     # The response page should have the bug number.
    192     self.assertIn('12345', response.body)
    193     # The Anomaly entities should be updated.
    194     for anomaly_entity in anomaly.Anomaly.query().fetch():
    195       if anomaly_entity.end_revision in (10000, 10010):
    196         self.assertEqual(12345, anomaly_entity.bug_id)
    197       elif anomaly_entity.end_revision != 9997:
    198         self.assertIsNone(anomaly_entity.bug_id)
    199 
    200   def testRevisionRangeFromSummary(self):
    201     # If the summary is in the expected format, a pair is returned.
    202     self.assertEqual(
    203         (10000, 10500),
    204         associate_alerts._RevisionRangeFromSummary(
    205             '1% regression in bot/my_suite/test at 10000:10500'))
    206     # Otherwise None is returned.
    207     self.assertIsNone(
    208         associate_alerts._RevisionRangeFromSummary(
    209             'Regression in rev ranges 12345 to 20000'))
    210 
    211   def testRangesOverlap_NonOverlapping_ReturnsFalse(self):
    212     self.assertFalse(associate_alerts._RangesOverlap((1, 5), (6, 9)))
    213     self.assertFalse(associate_alerts._RangesOverlap((6, 9), (1, 5)))
    214 
    215   def testRangesOverlap_NoneGiven_ReturnsFalse(self):
    216     self.assertFalse(associate_alerts._RangesOverlap((1, 5), None))
    217     self.assertFalse(associate_alerts._RangesOverlap(None, (1, 5)))
    218     self.assertFalse(associate_alerts._RangesOverlap(None, None))
    219 
    220   def testRangesOverlap_OneIncludesOther_ReturnsTrue(self):
    221     # True if one range envelopes the other.
    222     self.assertTrue(associate_alerts._RangesOverlap((1, 9), (2, 5)))
    223     self.assertTrue(associate_alerts._RangesOverlap((2, 5), (1, 9)))
    224 
    225   def testRangesOverlap_PartlyOverlap_ReturnsTrue(self):
    226     self.assertTrue(associate_alerts._RangesOverlap((1, 6), (5, 9)))
    227     self.assertTrue(associate_alerts._RangesOverlap((5, 9), (1, 6)))
    228 
    229   def testRangesOverlap_CommonBoundary_ReturnsTrue(self):
    230     self.assertTrue(associate_alerts._RangesOverlap((1, 6), (6, 9)))
    231     self.assertTrue(associate_alerts._RangesOverlap((6, 9), (1, 6)))
    232 
    233 
    234 if __name__ == '__main__':
    235   unittest.main()
    236