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