Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 # Copyright (C) 2008 Evan Martin <martine@danga.com> | 6 # Copyright (C) 2008 Evan Martin <martine@danga.com> |
| 7 | 7 |
| 8 """A git-command for integrating reviews on Rietveld.""" | 8 """A git-command for integrating reviews on Rietveld.""" |
| 9 | 9 |
| 10 from distutils.version import LooseVersion | 10 from distutils.version import LooseVersion |
| 11 from multiprocessing.pool import ThreadPool | 11 from multiprocessing.pool import ThreadPool |
| 12 import base64 | 12 import base64 |
| 13 import collections | 13 import collections |
| 14 import glob | 14 import glob |
| 15 import httplib | 15 import httplib |
| 16 import json | 16 import json |
| 17 import logging | 17 import logging |
| 18 import optparse | 18 import optparse |
| 19 import os | 19 import os |
| 20 import Queue | 20 import Queue |
| 21 import re | 21 import re |
| 22 import stat | 22 import stat |
| 23 import sys | 23 import sys |
| 24 import tempfile | 24 import tempfile |
| 25 import textwrap | 25 import textwrap |
| 26 import time | 26 import time |
| 27 import traceback | 27 import traceback |
| 28 import urllib | |
| 28 import urllib2 | 29 import urllib2 |
| 29 import urlparse | 30 import urlparse |
| 30 import webbrowser | 31 import webbrowser |
| 31 import zlib | 32 import zlib |
| 32 | 33 |
| 33 try: | 34 try: |
| 34 import readline # pylint: disable=F0401,W0611 | 35 import readline # pylint: disable=F0401,W0611 |
| 35 except ImportError: | 36 except ImportError: |
| 36 pass | 37 pass |
| 37 | 38 |
| (...skipping 194 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 232 name, while the developers always use shortened master name | 233 name, while the developers always use shortened master name |
| 233 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This | 234 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This |
| 234 function does the conversion for buildbucket migration. | 235 function does the conversion for buildbucket migration. |
| 235 """ | 236 """ |
| 236 prefix = 'master.' | 237 prefix = 'master.' |
| 237 if master.startswith(prefix): | 238 if master.startswith(prefix): |
| 238 return master | 239 return master |
| 239 return '%s%s' % (prefix, master) | 240 return '%s%s' % (prefix, master) |
| 240 | 241 |
| 241 | 242 |
| 243 def _buildbucket_retry(operation_name, http, *args, **kwargs): | |
| 244 """Retries requests to buildbucket service and returns parsed json content.""" | |
| 245 for try_count in xrange(3): | |
|
tandrii(chromium)
2016/02/25 17:59:33
this is a copy of code from before.
| |
| 246 response, content = http.request(*args, **kwargs) | |
| 247 try: | |
| 248 content_json = json.loads(content) | |
| 249 except ValueError: | |
| 250 content_json = None | |
| 251 | |
| 252 # Buildbucket could return an error even if status==200. | |
| 253 if content_json and content_json.get('error'): | |
| 254 msg = 'Error in response. Code: %s. Reason: %s. Message: %s.' % ( | |
| 255 content_json['error'].get('code', ''), | |
|
nodir
2016/02/25 17:14:52
there is no code, only reason and message
https://
tandrii(chromium)
2016/02/25 17:59:33
ok, ok.
| |
| 256 content_json['error'].get('reason', ''), | |
| 257 content_json['error'].get('message', '')) | |
| 258 raise BuildbucketResponseException(msg) | |
| 259 | |
| 260 if response.status == 200: | |
| 261 if not content_json: | |
| 262 raise BuildbucketResponseException( | |
| 263 'Buildbucket returns invalid json content: %s.\n' | |
| 264 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' % | |
| 265 content) | |
| 266 return content_json | |
| 267 if response.status < 500 or try_count >= 2: | |
|
nodir
2016/02/25 17:14:52
you have two checks for try_count value, here and
tandrii(chromium)
2016/02/25 17:59:33
DOne, but see https://codereview.chromium.org/1063
| |
| 268 raise httplib2.HttpLib2Error(content) | |
| 269 | |
| 270 # status >= 500 means transient failures. | |
| 271 logging.debug('Transient errors when %s. Will retry.', operation_name) | |
| 272 time.sleep(0.5 + 1.5*try_count) | |
| 273 assert False, 'unreachable' | |
| 274 | |
| 275 | |
| 242 def trigger_luci_job(changelist, masters, options): | 276 def trigger_luci_job(changelist, masters, options): |
| 243 """Send a job to run on LUCI.""" | 277 """Send a job to run on LUCI.""" |
| 244 issue_props = changelist.GetIssueProperties() | 278 issue_props = changelist.GetIssueProperties() |
| 245 issue = changelist.GetIssue() | 279 issue = changelist.GetIssue() |
| 246 patchset = changelist.GetMostRecentPatchset() | 280 patchset = changelist.GetMostRecentPatchset() |
| 247 for builders_and_tests in sorted(masters.itervalues()): | 281 for builders_and_tests in sorted(masters.itervalues()): |
| 248 # TODO(hinoka et al): add support for other properties. | 282 # TODO(hinoka et al): add support for other properties. |
| 249 # Currently, this completely ignores testfilter and other properties. | 283 # Currently, this completely ignores testfilter and other properties. |
| 250 for builder in sorted(builders_and_tests): | 284 for builder in sorted(builders_and_tests): |
| 251 luci_trigger.trigger( | 285 luci_trigger.trigger( |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 298 } | 332 } |
| 299 if tests: | 333 if tests: |
| 300 parameters['properties']['testfilter'] = tests | 334 parameters['properties']['testfilter'] = tests |
| 301 if properties: | 335 if properties: |
| 302 parameters['properties'].update(properties) | 336 parameters['properties'].update(properties) |
| 303 if options.clobber: | 337 if options.clobber: |
| 304 parameters['properties']['clobber'] = True | 338 parameters['properties']['clobber'] = True |
| 305 batch_req_body['builds'].append( | 339 batch_req_body['builds'].append( |
| 306 { | 340 { |
| 307 'bucket': bucket, | 341 'bucket': bucket, |
| 308 'parameters_json': json.dumps(parameters), | 342 'parameters_json': json.dumps(parameters), |
|
nodir
2016/02/25 17:14:53
Please set client_operation_id to a random string
tandrii(chromium)
2016/02/25 17:59:33
outside of CL scope - this is a copy of old code.
nodir
2016/02/25 18:31:06
it is the same in the retry loop, so if a build wa
tandrii(chromium)
2016/02/25 19:02:15
but of course, you are perfectly right! Done.
| |
| 309 'tags': ['builder:%s' % builder, | 343 'tags': ['builder:%s' % builder, |
| 310 'buildset:%s' % buildset, | 344 'buildset:%s' % buildset, |
| 311 'master:%s' % master, | 345 'master:%s' % master, |
| 312 'user_agent:git_cl_try'] | 346 'user_agent:git_cl_try'] |
| 313 } | 347 } |
| 314 ) | 348 ) |
| 315 | 349 |
| 316 for try_count in xrange(3): | 350 _buildbucket_retry( |
|
tandrii(chromium)
2016/02/25 17:59:33
this is original - i didn't change this, except fo
| |
| 317 response, content = http.request( | 351 'triggering tryjobs', |
| 318 buildbucket_put_url, | 352 http, |
| 319 'PUT', | 353 buildbucket_put_url, |
| 320 body=json.dumps(batch_req_body), | 354 'PUT', |
| 321 headers={'Content-Type': 'application/json'}, | 355 body=json.dumps(batch_req_body), |
| 322 ) | 356 headers={'Content-Type': 'application/json'} |
| 323 content_json = None | 357 ) |
| 324 try: | |
| 325 content_json = json.loads(content) | |
| 326 except ValueError: | |
| 327 pass | |
| 328 | |
| 329 # Buildbucket could return an error even if status==200. | |
| 330 if content_json and content_json.get('error'): | |
| 331 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % ( | |
| 332 content_json['error'].get('code', ''), | |
| 333 content_json['error'].get('reason', ''), | |
| 334 content_json['error'].get('message', '')) | |
| 335 raise BuildbucketResponseException(msg) | |
| 336 | |
| 337 if response.status == 200: | |
| 338 if not content_json: | |
| 339 raise BuildbucketResponseException( | |
| 340 'Buildbucket returns invalid json content: %s.\n' | |
| 341 'Please file bugs at crbug.com, label "Infra-BuildBucket".' % | |
| 342 content) | |
| 343 break | |
| 344 if response.status < 500 or try_count >= 2: | |
| 345 raise httplib2.HttpLib2Error(content) | |
| 346 | |
| 347 # status >= 500 means transient failures. | |
| 348 logging.debug('Transient errors when triggering tryjobs. Will retry.') | |
| 349 time.sleep(0.5 + 1.5*try_count) | |
| 350 | |
| 351 print '\n'.join(print_text) | 358 print '\n'.join(print_text) |
| 352 | 359 |
| 353 | 360 |
| 361 def fetch_try_jobs(auth_config, changelist, options): | |
| 362 """Fetches tryjobs from buildbucket. | |
| 363 | |
| 364 Returns a map from build id to build info as json dictionary. | |
| 365 """ | |
| 366 rietveld_url = settings.GetDefaultServerUrl() | |
| 367 rietveld_host = urlparse.urlparse(rietveld_url).hostname | |
| 368 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config) | |
| 369 http = authenticator.authorize(httplib2.Http()) | |
| 370 http.force_exception_to_status_code = True | |
| 371 | |
| 372 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format( | |
| 373 hostname=rietveld_host, | |
| 374 issue=changelist.GetIssue(), | |
| 375 patch=options.patchset) | |
| 376 params = {'tag': 'buildset:%s' % buildset} | |
| 377 | |
| 378 login_required_error = None | |
| 379 builds = {} | |
| 380 while True: | |
| 381 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format( | |
| 382 hostname=options.buildbucket_host, | |
| 383 params=urllib.urlencode(params)) | |
| 384 try: | |
| 385 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET') | |
| 386 except auth.LoginRequiredError as e: | |
| 387 if login_required_error: | |
| 388 # Avoid looping forever if login is required on this buildbucket host. | |
| 389 raise | |
| 390 print 'Warning: Some results might be missing because %s' % e | |
| 391 login_required_error = e | |
| 392 http = httplib2.Http() | |
| 393 http.force_exception_to_status_code = True | |
|
nodir
2016/02/25 17:14:52
From what I understand reading this code, you firs
tandrii(chromium)
2016/02/25 17:59:33
Good idea! Done.
| |
| 394 continue | |
| 395 for build in content.get('builds', []): | |
| 396 builds[build['id']] = build | |
| 397 if 'next_cursor' in content: | |
| 398 params['start_cursor'] = content['next_cursor'] | |
| 399 else: | |
| 400 break | |
| 401 return builds | |
| 402 | |
| 403 | |
| 404 def print_tryjobs(options, builds): | |
| 405 """Prints nicely result of fetch_try_jobs.""" | |
| 406 if not builds: | |
| 407 print 'No tryjobs scheduled' | |
| 408 return | |
| 409 | |
| 410 # Make a copy, because we'll be modifying builds dictionary. | |
| 411 builds = builds.copy() | |
| 412 def get_builder(b): | |
|
nodir
2016/02/25 17:14:52
nit: blank line before func def
tandrii(chromium)
2016/02/25 17:59:33
Done.
| |
| 413 for tag in b.get('tags', []): | |
|
nodir
2016/02/25 17:14:52
Beware that someone may forget to set tags. The so
tandrii(chromium)
2016/02/25 17:59:33
Indeed, machenbach@ showed me exactly that case, a
| |
| 414 name_value = tag.split(':', 1) | |
| 415 if len(name_value) == 2 and name_value[0] == 'builder': | |
| 416 return name_value[1] | |
| 417 return None | |
| 418 | |
| 419 def get_bucket(b): | |
| 420 bucket = b['bucket'] | |
| 421 if bucket.startswith('master.'): | |
| 422 return bucket[len('master.'):] | |
| 423 return bucket | |
| 424 | |
| 425 if options.print_master: | |
| 426 name_fmt = '%%-%ds %%-%ds' % ( | |
| 427 max(len(str(get_bucket(b))) for b in builds.itervalues()), | |
| 428 max(len(str(get_builder(b))) for b in builds.itervalues())) | |
| 429 def get_name(b): | |
| 430 return name_fmt % (get_bucket(b), get_builder(b)) | |
| 431 else: | |
| 432 name_fmt = '%%-%ds' % ( | |
| 433 max(len(str(get_builder(b))) for b in builds.itervalues())) | |
| 434 def get_name(b): | |
| 435 return name_fmt % get_builder(b) | |
| 436 | |
| 437 def sort_key(b): | |
| 438 return b['status'], b.get('result'), get_name(b), b.get('url') | |
| 439 | |
| 440 def pop(title, f, color=None, **kwargs): | |
| 441 """Pop matching builds from `builds` dict and print them.""" | |
| 442 | |
| 443 if not sys.stdout.isatty() or color is None: | |
| 444 colorize = str | |
| 445 else: | |
| 446 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET) | |
| 447 | |
| 448 result = [] | |
| 449 for b in builds.values(): | |
| 450 selected = True | |
|
nodir
2016/02/25 17:14:53
possibly, using `any` may simplify this code
tandrii(chromium)
2016/02/25 17:59:33
s/any/all and done, good idea.
| |
| 451 for k, v in kwargs.iteritems(): | |
| 452 if b.get(k) != v: | |
| 453 selected = False | |
| 454 break | |
| 455 if selected: | |
| 456 builds.pop(b['id']) | |
| 457 result.append(b) | |
| 458 if result: | |
| 459 print colorize(title) | |
| 460 for b in sorted(result, key=sort_key): | |
| 461 print ' ', colorize('\t'.join(map(str, f(b)))) | |
| 462 | |
| 463 total = len(builds) | |
| 464 pop(status='COMPLETED', result='SUCCESS', | |
| 465 title='Successes:', color=Fore.GREEN, | |
| 466 f=lambda b: (get_name(b), b.get('url'))) | |
| 467 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE', | |
| 468 title='Infra Failures:', color=Fore.MAGENTA, | |
| 469 f=lambda b: (get_name(b), b.get('url'))) | |
| 470 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE', | |
| 471 title='Failures:', color=Fore.RED, | |
| 472 f=lambda b: (get_name(b), b.get('url'))) | |
| 473 pop(status='COMPLETED', result='FAILURE', | |
| 474 failure_reason='INVALID_BUILD_DEFINITION', | |
| 475 title='Wrong master/builder name:', color=Fore.MAGENTA, | |
| 476 f=lambda b: (get_name(b),)) | |
| 477 pop(status='COMPLETED', result='FAILURE', | |
| 478 title='Other failures:', | |
| 479 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url'))) | |
| 480 pop(status='COMPLETED', | |
| 481 title='Other finished:', | |
| 482 f=lambda b: (get_name(b), b.get('result'), b.get('url'))) | |
|
nodir
2016/02/25 17:14:53
All cancelled builds will fall into this category.
tandrii(chromium)
2016/02/25 17:59:33
Done.
| |
| 483 pop(status='STARTED', | |
| 484 title='Started:', color=Fore.YELLOW, | |
| 485 f=lambda b: (get_name(b), b.get('url'))) | |
| 486 pop(status='SCHEDULED', | |
| 487 title='Scheduled:', | |
| 488 f=lambda b: (get_name(b), 'id=%s' % b['id'])) | |
| 489 # The last section is just in case buildbucket API changes OR there is a bug. | |
| 490 pop(title='Other:', | |
| 491 f=lambda b: (get_name(b), 'id=%s' % b['id'])) | |
|
Michael Achenbach
2016/02/25 15:20:29
Maybe
assert len(builds) = 0
?
tandrii(chromium)
2016/02/25 17:59:33
Done.
| |
| 492 print 'Total: %d tryjobs' % total | |
| 493 | |
| 494 | |
| 354 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): | 495 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): |
| 355 """Return the corresponding git ref if |base_url| together with |glob_spec| | 496 """Return the corresponding git ref if |base_url| together with |glob_spec| |
| 356 matches the full |url|. | 497 matches the full |url|. |
| 357 | 498 |
| 358 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). | 499 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). |
| 359 """ | 500 """ |
| 360 fetch_suburl, as_ref = glob_spec.split(':') | 501 fetch_suburl, as_ref = glob_spec.split(':') |
| 361 if allow_wildcards: | 502 if allow_wildcards: |
| 362 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) | 503 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) |
| 363 if glob_match: | 504 if glob_match: |
| (...skipping 2994 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 3358 | 3499 |
| 3359 for (master, builders) in sorted(masters.iteritems()): | 3500 for (master, builders) in sorted(masters.iteritems()): |
| 3360 if master: | 3501 if master: |
| 3361 print 'Master: %s' % master | 3502 print 'Master: %s' % master |
| 3362 length = max(len(builder) for builder in builders) | 3503 length = max(len(builder) for builder in builders) |
| 3363 for builder in sorted(builders): | 3504 for builder in sorted(builders): |
| 3364 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) | 3505 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) |
| 3365 return 0 | 3506 return 0 |
| 3366 | 3507 |
| 3367 | 3508 |
| 3509 def CMDtry_results(parser, args): | |
| 3510 group = optparse.OptionGroup(parser, "Try job results options") | |
| 3511 group.add_option( | |
| 3512 "-p", "--patchset", type=int, help="patchset number if not current.") | |
| 3513 group.add_option( | |
| 3514 "--print-master", action='store_true', help="print master name as well") | |
| 3515 group.add_option( | |
| 3516 "--buildbucket-host", default='cr-buildbucket.appspot.com', | |
| 3517 help="Host of buildbucket. The default host is %default.") | |
| 3518 parser.add_option_group(group) | |
| 3519 auth.add_auth_options(parser) | |
| 3520 options, args = parser.parse_args(args) | |
| 3521 if args: | |
| 3522 parser.error('Unrecognized args: %s' % ' '.join(args)) | |
| 3523 | |
| 3524 auth_config = auth.extract_auth_config_from_options(options) | |
| 3525 cl = Changelist(auth_config=auth_config) | |
| 3526 if not cl.GetIssue(): | |
| 3527 parser.error('Need to upload first') | |
| 3528 | |
| 3529 if not options.patchset: | |
| 3530 options.patchset = cl.GetMostRecentPatchset() | |
| 3531 if options.patchset and options.patchset != cl.GetPatchset(): | |
| 3532 print( | |
| 3533 '\nWARNING Mismatch between local config and server. Did a previous ' | |
| 3534 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' | |
| 3535 'Continuing using\npatchset %s.\n' % options.patchset) | |
| 3536 try: | |
| 3537 jobs = fetch_try_jobs(auth_config, cl, options) | |
| 3538 except BuildbucketResponseException as ex: | |
| 3539 print 'ERROR: %s' % ex | |
|
nodir
2016/02/25 17:14:53
nit: 'Buildbucket error: %s'
so users direct their
tandrii(chromium)
2016/02/25 17:59:33
Done.
| |
| 3540 return 1 | |
| 3541 except Exception as e: | |
| 3542 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc()) | |
| 3543 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % ( | |
|
Michael Achenbach
2016/02/25 15:20:29
nit: trigger? Maybe adapt error message?
tandrii(chromium)
2016/02/25 17:59:33
Done.
| |
| 3544 e, stacktrace) | |
| 3545 return 1 | |
| 3546 print_tryjobs(options, jobs) | |
| 3547 return 0 | |
| 3548 | |
| 3549 | |
| 3368 @subcommand.usage('[new upstream branch]') | 3550 @subcommand.usage('[new upstream branch]') |
| 3369 def CMDupstream(parser, args): | 3551 def CMDupstream(parser, args): |
| 3370 """Prints or sets the name of the upstream branch, if any.""" | 3552 """Prints or sets the name of the upstream branch, if any.""" |
| 3371 _, args = parser.parse_args(args) | 3553 _, args = parser.parse_args(args) |
| 3372 if len(args) > 1: | 3554 if len(args) > 1: |
| 3373 parser.error('Unrecognized args: %s' % ' '.join(args)) | 3555 parser.error('Unrecognized args: %s' % ' '.join(args)) |
| 3374 | 3556 |
| 3375 cl = Changelist() | 3557 cl = Changelist() |
| 3376 if args: | 3558 if args: |
| 3377 # One arg means set upstream branch. | 3559 # One arg means set upstream branch. |
| (...skipping 382 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 3760 if __name__ == '__main__': | 3942 if __name__ == '__main__': |
| 3761 # These affect sys.stdout so do it outside of main() to simplify mocks in | 3943 # These affect sys.stdout so do it outside of main() to simplify mocks in |
| 3762 # unit testing. | 3944 # unit testing. |
| 3763 fix_encoding.fix_encoding() | 3945 fix_encoding.fix_encoding() |
| 3764 colorama.init() | 3946 colorama.init() |
| 3765 try: | 3947 try: |
| 3766 sys.exit(main(sys.argv[1:])) | 3948 sys.exit(main(sys.argv[1:])) |
| 3767 except KeyboardInterrupt: | 3949 except KeyboardInterrupt: |
| 3768 sys.stderr.write('interrupted\n') | 3950 sys.stderr.write('interrupted\n') |
| 3769 sys.exit(1) | 3951 sys.exit(1) |
| OLD | NEW |