| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 | |
| 7 import datetime | |
| 8 import unittest | |
| 9 | |
| 10 import auto_roll | |
| 11 | |
| 12 # auto_roll.py imports find_depot_tools. | |
| 13 from testing_support.super_mox import SuperMoxTestBase | |
| 14 | |
| 15 | |
| 16 # pylint: disable=W0212 | |
| 17 | |
| 18 | |
| 19 def _do_fetches(): | |
| 20 auto_roll.subprocess2.check_call( | |
| 21 ['git', '--git-dir', './.git', 'fetch']) | |
| 22 auto_roll.subprocess2.check_call( | |
| 23 ['git', '--git-dir', './third_party/test_project/.git', 'fetch']) | |
| 24 | |
| 25 | |
| 26 class SheriffCalendarTest(SuperMoxTestBase): | |
| 27 | |
| 28 def test_complete_email(self): | |
| 29 expected_emails = ['foo@chromium.org', 'bar@google.com', 'baz@chromium.org'] | |
| 30 names = ['foo', 'bar@google.com', 'baz'] | |
| 31 self.assertEqual(map(auto_roll._complete_email, names), expected_emails) | |
| 32 | |
| 33 def test_emails(self): | |
| 34 expected_emails = ['foo@bar.com', 'baz@baz.com'] | |
| 35 auto_roll._emails_from_url = lambda urls: expected_emails | |
| 36 self.assertEqual(auto_roll._current_gardener_emails(), expected_emails) | |
| 37 self.assertEqual(auto_roll._current_sheriff_emails(), expected_emails) | |
| 38 | |
| 39 def _assert_parse(self, js_string, expected_emails): | |
| 40 self.assertEqual( | |
| 41 auto_roll._names_from_sheriff_js(js_string), expected_emails) | |
| 42 | |
| 43 def test_names_from_sheriff_js(self): | |
| 44 self._assert_parse('document.write(\'none (channel is sheriff)\')', []) | |
| 45 self._assert_parse('document.write(\'foo, bar\')', ['foo', 'bar']) | |
| 46 | |
| 47 def test_email_regexp(self): | |
| 48 self.assertTrue(auto_roll._email_is_valid('somebody@example.com')) | |
| 49 self.assertTrue(auto_roll._email_is_valid('somebody@example.domain.com')) | |
| 50 self.assertTrue(auto_roll._email_is_valid('somebody@example-domain.com')) | |
| 51 self.assertTrue(auto_roll._email_is_valid('some.body@example.com')) | |
| 52 self.assertTrue(auto_roll._email_is_valid('some_body@example.com')) | |
| 53 self.assertTrue(auto_roll._email_is_valid('some+body@example.com')) | |
| 54 self.assertTrue(auto_roll._email_is_valid('some+body@com')) | |
| 55 self.assertTrue(auto_roll._email_is_valid('some/body@example.com')) | |
| 56 # These are valid according to the standard, but not supported here. | |
| 57 self.assertFalse(auto_roll._email_is_valid('some~body@example.com')) | |
| 58 self.assertFalse(auto_roll._email_is_valid('some!body@example.com')) | |
| 59 self.assertFalse(auto_roll._email_is_valid('some?body@example.com')) | |
| 60 self.assertFalse(auto_roll._email_is_valid('some" "body@example.com')) | |
| 61 self.assertFalse(auto_roll._email_is_valid('"{somebody}"@example.com')) | |
| 62 # Bogus. | |
| 63 self.assertFalse(auto_roll._email_is_valid('rm -rf /#@example.com')) | |
| 64 self.assertFalse(auto_roll._email_is_valid('some body@example.com')) | |
| 65 self.assertFalse(auto_roll._email_is_valid('[some body]@example.com')) | |
| 66 | |
| 67 def test_filter_emails(self): | |
| 68 input_emails = ['foo@bar.com', 'baz@baz.com', 'bogus email @ !!!'] | |
| 69 expected_emails = ['foo@bar.com', 'baz@baz.com'] | |
| 70 self.assertEquals(auto_roll._filter_emails(input_emails), expected_emails) | |
| 71 self.checkstdout('WARNING: Not including bogus email @ !!! ' | |
| 72 '(invalid email address)\n') | |
| 73 | |
| 74 class AutoRollTest(SuperMoxTestBase): | |
| 75 | |
| 76 TEST_PROJECT = 'test_project' | |
| 77 TEST_AUTHOR = 'test_author@chromium.org' | |
| 78 PATH_TO_CHROME = '.' | |
| 79 | |
| 80 DATETIME_FORMAT = '%d-%d-%d %d:%d:%d.%d' | |
| 81 CURRENT_DATETIME = (2014, 4, 1, 14, 57, 21, 01) | |
| 82 RECENT_ISSUE_CREATED = (2014, 4, 1, 13, 57, 21, 01) | |
| 83 OLD_ISSUE_CREATED = (2014, 2, 1, 13, 57, 21, 01) | |
| 84 CURRENT_DATETIME_STR = DATETIME_FORMAT % CURRENT_DATETIME | |
| 85 RECENT_ISSUE_CREATED_STR = DATETIME_FORMAT % RECENT_ISSUE_CREATED | |
| 86 OLD_ISSUE_CREATED_STR = DATETIME_FORMAT % OLD_ISSUE_CREATED | |
| 87 | |
| 88 OLDER_REV = 'cfcf604fbdcf6e2d9b982a2fab3fc9f1e3f8cd65' | |
| 89 OLD_REV = 'b9af6489f6f2004ad11b82c6057f7007e3c35372' | |
| 90 NEW_REV = '79539998e04afab3ee9c3016881755ca52f60a73' | |
| 91 | |
| 92 _GIT_LOG = ''' | |
| 93 commit %s | |
| 94 Author: Test Author <test_author@example.com> | |
| 95 Date: Wed Apr 2 14:00:14 2014 -0400 | |
| 96 | |
| 97 Make some changes. | |
| 98 ''' | |
| 99 GIT_LOG_UPDATED = _GIT_LOG % NEW_REV | |
| 100 GIT_LOG_TOO_OLD = _GIT_LOG % OLDER_REV | |
| 101 | |
| 102 _commit_timestamps = { | |
| 103 OLDER_REV: '1399573100', | |
| 104 OLD_REV: '1399573342', | |
| 105 NEW_REV: '1399598876', | |
| 106 } | |
| 107 | |
| 108 class MockHttpRpcServer(object): | |
| 109 def __init__(self, *args, **kwargs): | |
| 110 pass | |
| 111 | |
| 112 class MockDateTime(datetime.datetime): | |
| 113 @classmethod | |
| 114 def utcnow(cls): | |
| 115 return AutoRollTest.MockDateTime(*AutoRollTest.CURRENT_DATETIME) | |
| 116 | |
| 117 class MockFile(object): | |
| 118 def __init__(self, contents): | |
| 119 self._contents = contents | |
| 120 | |
| 121 def read(self): | |
| 122 return self._contents | |
| 123 | |
| 124 def setUp(self): | |
| 125 SuperMoxTestBase.setUp(self) | |
| 126 self.mox.StubOutWithMock(auto_roll.rietveld.Rietveld, 'add_comment') | |
| 127 self.mox.StubOutWithMock(auto_roll.rietveld.Rietveld, 'close_issue') | |
| 128 self.mox.StubOutWithMock(auto_roll.rietveld.Rietveld, | |
| 129 'get_issue_properties') | |
| 130 self.mox.StubOutWithMock(auto_roll.rietveld.Rietveld, 'search') | |
| 131 self.mox.StubOutWithMock(auto_roll.scm.GIT, 'Capture') | |
| 132 self.mox.StubOutWithMock(auto_roll.subprocess2, 'call') | |
| 133 self.mox.StubOutWithMock(auto_roll.subprocess2, 'check_call') | |
| 134 self.mox.StubOutWithMock(auto_roll.subprocess2, 'check_output') | |
| 135 self.mox.StubOutWithMock(auto_roll.urllib2, 'urlopen') | |
| 136 auto_roll.datetime.datetime = self.MockDateTime | |
| 137 auto_roll.rietveld.upload.HttpRpcServer = self.MockHttpRpcServer | |
| 138 self._arb = auto_roll.AutoRoller(self.TEST_PROJECT, | |
| 139 self.TEST_AUTHOR, | |
| 140 self.PATH_TO_CHROME) | |
| 141 | |
| 142 def _make_issue(self, old_rev, new_rev, created_datetime=None, | |
| 143 svn_range_str=''): | |
| 144 description = auto_roll.ROLL_DESCRIPTION_STR % { | |
| 145 'dep_path': 'src/third_party/test_project', | |
| 146 'before_rev': self._display_rev(old_rev), | |
| 147 'after_rev': self._display_rev(new_rev), | |
| 148 'svn_range': svn_range_str, | |
| 149 'revlog_url': '', | |
| 150 } | |
| 151 return { | |
| 152 'author': self.TEST_AUTHOR, | |
| 153 'commit': created_datetime or self.RECENT_ISSUE_CREATED_STR, | |
| 154 'created': created_datetime or self.RECENT_ISSUE_CREATED_STR, | |
| 155 'description': description, | |
| 156 'issue': 1234567, | |
| 157 'messages': [], | |
| 158 'modified': created_datetime or self.RECENT_ISSUE_CREATED_STR, | |
| 159 'subject': description.splitlines()[0], | |
| 160 } | |
| 161 | |
| 162 def _get_last_revision(self): | |
| 163 revinfo = ( | |
| 164 'src/third_party/test_project: ' | |
| 165 'https://chromium.googlesource.com/test_project.git@%s' % | |
| 166 str(self.OLD_REV)) | |
| 167 auto_roll.subprocess2.check_output(['gclient', 'revinfo'], | |
| 168 cwd='.').AndReturn(revinfo) | |
| 169 | |
| 170 def _get_current_revision(self): | |
| 171 self._parse_origin_master(returnval=self.NEW_REV) | |
| 172 | |
| 173 def _upload_issue(self, custom_message=None): | |
| 174 _do_fetches() | |
| 175 | |
| 176 self._get_last_revision() | |
| 177 self._get_current_revision() | |
| 178 | |
| 179 self._compare_revs(self.OLD_REV, self.NEW_REV) | |
| 180 | |
| 181 auto_roll.subprocess2.check_call(['git', 'clean', '-d', '-f'], cwd='.') | |
| 182 auto_roll.subprocess2.call(['git', 'rebase', '--abort'], cwd='.') | |
| 183 auto_roll.subprocess2.call(['git', 'branch', '-D', 'test_project_roll'], | |
| 184 cwd='.') | |
| 185 auto_roll.subprocess2.check_call(['git', 'checkout', 'origin/master', '-f'], | |
| 186 cwd='.') | |
| 187 auto_roll.subprocess2.check_call(['git', 'checkout', | |
| 188 '-b', 'test_project_roll', | |
| 189 '-t', 'origin/master', '-f'], cwd='.') | |
| 190 | |
| 191 issue = self._make_issue(self.OLD_REV, self.NEW_REV, | |
| 192 created_datetime=self.CURRENT_DATETIME_STR) | |
| 193 | |
| 194 if custom_message: | |
| 195 message = custom_message | |
| 196 else: | |
| 197 message = issue['description'] | |
| 198 | |
| 199 message += '\nTBR=' | |
| 200 cmd = [ | |
| 201 'roll-dep-svn', 'third_party/%s' % self.TEST_PROJECT, str(self.NEW_REV), | |
| 202 ] | |
| 203 auto_roll.subprocess2.check_call(cmd, cwd='.') | |
| 204 | |
| 205 auto_roll.subprocess2.check_call(['git', 'add', 'DEPS'], cwd='.') | |
| 206 auto_roll.subprocess2.check_call(['git', 'commit', '--no-edit'], cwd='.') | |
| 207 auto_roll.subprocess2.check_output( | |
| 208 ['git', 'log', '-n1', '--format=%B', 'HEAD'], | |
| 209 cwd='.').AndReturn(issue['description']) | |
| 210 auto_roll.subprocess2.check_call(['git', 'cl', 'upload', '--bypass-hooks', | |
| 211 '-f', '--use-commit-queue', | |
| 212 '-m', message], | |
| 213 cwd='.') | |
| 214 auto_roll.subprocess2.check_call(['git', 'checkout', 'origin/master', '-f'], | |
| 215 cwd='.') | |
| 216 auto_roll.subprocess2.check_call(['git', 'branch', '-D', | |
| 217 'test_project_roll'], cwd='.') | |
| 218 | |
| 219 self._arb._rietveld.search(owner=self.TEST_AUTHOR, | |
| 220 closed=2).AndReturn([issue]) | |
| 221 self._arb._rietveld.add_comment(issue['issue'], | |
| 222 self._arb.ROLL_BOT_INSTRUCTIONS) | |
| 223 | |
| 224 def test_should_stop(self): | |
| 225 _do_fetches() | |
| 226 issue = self._make_issue(self.OLD_REV, self.NEW_REV, | |
| 227 created_datetime=self.OLD_ISSUE_CREATED_STR) | |
| 228 issue['messages'].append({ | |
| 229 'text': 'STOP', | |
| 230 'sender': self.TEST_AUTHOR, | |
| 231 'date': '2014-3-31 13:57:21.01' | |
| 232 }) | |
| 233 search_results = [issue] | |
| 234 self._arb._rietveld.search(owner=self.TEST_AUTHOR, | |
| 235 closed=2).AndReturn(search_results) | |
| 236 self._arb._rietveld.get_issue_properties(issue['issue'], | |
| 237 messages=True).AndReturn(issue) | |
| 238 self._arb._rietveld.add_comment(issue['issue'], ''' | |
| 239 Rollbot was stopped by the presence of 'STOP' in an earlier comment. | |
| 240 The last update to this issue was over 12:00:00 hours ago. | |
| 241 Please close this issue as soon as possible to allow the bot to continue. | |
| 242 | |
| 243 Please email (dpranke@chromium.org) if the Rollbot is causing trouble. | |
| 244 ''') | |
| 245 | |
| 246 self.mox.ReplayAll() | |
| 247 self.assertEquals(self._arb.main(), 1) | |
| 248 self.checkstdout('https://codereview.chromium.org/%d/: Rollbot was ' | |
| 249 'stopped by test_author@chromium.org on at 2014-3-31 ' | |
| 250 '13:57:21.01, waiting.\n' % issue['issue']) | |
| 251 | |
| 252 def test_already_rolling(self): | |
| 253 _do_fetches() | |
| 254 issue = self._make_issue(self.OLD_REV, self.NEW_REV) | |
| 255 search_results = [issue] | |
| 256 self._arb._rietveld.search(owner=self.TEST_AUTHOR, | |
| 257 closed=2).AndReturn(search_results) | |
| 258 self._arb._rietveld.get_issue_properties(issue['issue'], | |
| 259 messages=True).AndReturn(issue) | |
| 260 self._get_last_revision() | |
| 261 self._short_rev(self.OLD_REV) | |
| 262 self.mox.ReplayAll() | |
| 263 self.assertEquals(self._arb.main(), 0) | |
| 264 self.checkstdout('https://codereview.chromium.org/%d/ started %s ago\n' | |
| 265 'https://codereview.chromium.org/%d/ is still active, ' | |
| 266 'nothing to do.\n' | |
| 267 % (issue['issue'], '0:59:59.900001', issue['issue'])) | |
| 268 | |
| 269 def test_old_issue(self): | |
| 270 issue = self._make_issue(self.OLD_REV, self.NEW_REV, | |
| 271 created_datetime=self.OLD_ISSUE_CREATED_STR) | |
| 272 search_results = [issue] | |
| 273 self._arb._rietveld.search(owner=self.TEST_AUTHOR, | |
| 274 closed=2).AndReturn(search_results) | |
| 275 self._arb._rietveld.get_issue_properties(issue['issue'], | |
| 276 messages=True).AndReturn(issue) | |
| 277 comment_str = ('Giving up on this roll after 1 day, 0:00:00. Closing, will ' | |
| 278 'open a new roll.') | |
| 279 self._arb._rietveld.add_comment(issue['issue'], comment_str) | |
| 280 self._arb._rietveld.close_issue(issue['issue']) | |
| 281 self._upload_issue() | |
| 282 self.mox.ReplayAll() | |
| 283 self.assertEquals(self._arb.main(), 0) | |
| 284 self.checkstdout('https://codereview.chromium.org/%d/ started %s ago\n' | |
| 285 'Closing https://codereview.chromium.org/%d/ with message:' | |
| 286 ' \'%s\'\n' | |
| 287 % (issue['issue'], '59 days, 0:59:59.900001', | |
| 288 issue['issue'], comment_str)) | |
| 289 | |
| 290 def test_failed_cq(self): | |
| 291 issue = self._make_issue(self.OLD_REV, self.NEW_REV) | |
| 292 issue['commit'] = False | |
| 293 search_results = [issue] | |
| 294 self._arb._rietveld.search(owner=self.TEST_AUTHOR, | |
| 295 closed=2).AndReturn(search_results) | |
| 296 self._arb._rietveld.get_issue_properties(issue['issue'], | |
| 297 messages=True).AndReturn(issue) | |
| 298 comment_str = 'No longer marked for the CQ. Closing, will open a new roll.' | |
| 299 self._arb._rietveld.add_comment(issue['issue'], comment_str) | |
| 300 self._arb._rietveld.close_issue(issue['issue']) | |
| 301 self._upload_issue() | |
| 302 self.mox.ReplayAll() | |
| 303 self.assertEquals(self._arb.main(), 0) | |
| 304 self.checkstdout('Closing https://codereview.chromium.org/%d/ with message:' | |
| 305 ' \'%s\'\n' % (issue['issue'], comment_str)) | |
| 306 | |
| 307 def test_no_roll_backwards(self): | |
| 308 _do_fetches() | |
| 309 self._arb._rietveld.search(owner=self.TEST_AUTHOR, closed=2).AndReturn([]) | |
| 310 self._get_last_revision() | |
| 311 auto_roll.subprocess2.check_output( | |
| 312 ['git', '--git-dir', './third_party/test_project/.git', 'rev-parse', | |
| 313 'origin/master']).AndReturn(self.OLDER_REV) | |
| 314 self._compare_revs(self.OLD_REV, self.OLDER_REV) | |
| 315 | |
| 316 self.mox.ReplayAll() | |
| 317 self.assertEquals(self._arb.main(), 0) | |
| 318 self.checkstdout('Already at %s refusing to roll backwards to %s.\n' % ( | |
| 319 self.OLD_REV, self.OLDER_REV)) | |
| 320 | |
| 321 def test_upload_issue(self): | |
| 322 self._arb._rietveld.search(owner=self.TEST_AUTHOR, closed=2).AndReturn([]) | |
| 323 self._upload_issue() | |
| 324 self.mox.ReplayAll() | |
| 325 self.assertEquals(self._arb.main(), 0) | |
| 326 | |
| 327 def test_notry(self): | |
| 328 class MockOptions: | |
| 329 def __init__(self): | |
| 330 self.notry = True | |
| 331 | |
| 332 self._arb = auto_roll.AutoRoller(self.TEST_PROJECT, | |
| 333 self.TEST_AUTHOR, | |
| 334 self.PATH_TO_CHROME, | |
| 335 options=MockOptions()) | |
| 336 | |
| 337 self._arb._rietveld.search(owner=self.TEST_AUTHOR, closed=2).AndReturn([]) | |
| 338 svn_range_str = '' | |
| 339 commit_msg = self._make_issue(self.OLD_REV, self.NEW_REV, | |
| 340 svn_range_str=svn_range_str)['description'] | |
| 341 commit_msg += ('\nSheriffs: In case of breakage, do NOT revert this roll, ' | |
| 342 'revert the\noffending commit in the test_project ' | |
| 343 'repository instead.\n\nNOTRY=true') | |
| 344 self._upload_issue(custom_message=commit_msg) | |
| 345 self.mox.ReplayAll() | |
| 346 self.assertEquals(self._arb.main(), 0) | |
| 347 | |
| 348 def test_last_revision(self): | |
| 349 # Verify that AutoRoll._last_roll_revision() returns a string. | |
| 350 self._get_last_revision() | |
| 351 self.mox.ReplayAll() | |
| 352 self.assertEquals(type(self._arb._last_roll_revision()), str) | |
| 353 | |
| 354 def test_current_revision(self): | |
| 355 # Verify that AutoRoll._current_revision() returns a string. | |
| 356 self._get_current_revision() | |
| 357 self.mox.ReplayAll() | |
| 358 self.assertEquals(type(self._arb._current_revision()), str) | |
| 359 | |
| 360 def test_extra_trybots(self): | |
| 361 self._arb._cq_extra_trybots = ['sometrybot'] | |
| 362 self._arb._rietveld.search(owner=self.TEST_AUTHOR, closed=2).AndReturn([]) | |
| 363 svn_range_str = '' | |
| 364 commit_msg = self._make_issue(self.OLD_REV, self.NEW_REV, | |
| 365 svn_range_str=svn_range_str)['description'] | |
| 366 commit_msg += '\nCQ_INCLUDE_TRYBOTS=sometrybot' | |
| 367 self._upload_issue(custom_message=commit_msg) | |
| 368 self.mox.ReplayAll() | |
| 369 self.assertEquals(self._arb.main(), 0) | |
| 370 | |
| 371 # pylint: disable=R0201 | |
| 372 def _display_rev(self, rev): | |
| 373 return rev[:7] | |
| 374 | |
| 375 # pylint: disable=R0201 | |
| 376 def _short_rev(self, rev): | |
| 377 auto_roll.subprocess2.check_output(['git', '--git-dir', | |
| 378 './third_party/test_project/.git', | |
| 379 'rev-parse', '--short', rev] | |
| 380 ).AndReturn(self._display_rev(rev)) | |
| 381 | |
| 382 def _compare_revs(self, old_rev, new_rev): | |
| 383 merge_base_cmd = ['git', '--git-dir', './third_party/test_project/.git', | |
| 384 'merge-base', '--is-ancestor', new_rev, old_rev] | |
| 385 if self._commit_timestamps[old_rev] < self._commit_timestamps[new_rev]: | |
| 386 err = auto_roll.subprocess2.CalledProcessError(1, '', '', '', '') | |
| 387 auto_roll.subprocess2.check_call(merge_base_cmd).AndRaise(err) | |
| 388 else: | |
| 389 auto_roll.subprocess2.check_call(merge_base_cmd) | |
| 390 return | |
| 391 | |
| 392 # pylint: disable=R0201 | |
| 393 def _parse_origin_master(self, returnval): | |
| 394 auto_roll.subprocess2.check_output( | |
| 395 ['git', '--git-dir', './third_party/test_project/.git', 'rev-parse', | |
| 396 'origin/master']).AndReturn(returnval) | |
| 397 | |
| 398 | |
| 399 if __name__ == '__main__': | |
| 400 unittest.main() | |
| OLD | NEW |