 Chromium Code Reviews
 Chromium Code Reviews Issue 1725053002:
  git cl try-results: show buildbucket tryjobs.  (Closed) 
  Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
    
  
    Issue 1725053002:
  git cl try-results: show buildbucket tryjobs.  (Closed) 
  Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master| 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 try_count = 0 | |
| 246 while True: | |
| 247 response, content = http.request(*args, **kwargs) | |
| 248 try: | |
| 249 content_json = json.loads(content) | |
| 250 except ValueError: | |
| 251 content_json = None | |
| 252 | |
| 253 # Buildbucket could return an error even if status==200. | |
| 254 if content_json and content_json.get('error'): | |
| 255 msg = 'Error in response. Reason: %s. Message: %s.' % ( | |
| 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: | |
| 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 try_count += 1 | |
| 274 assert False, 'unreachable' | |
| 275 | |
| 276 | |
| 242 def trigger_luci_job(changelist, masters, options): | 277 def trigger_luci_job(changelist, masters, options): | 
| 243 """Send a job to run on LUCI.""" | 278 """Send a job to run on LUCI.""" | 
| 244 issue_props = changelist.GetIssueProperties() | 279 issue_props = changelist.GetIssueProperties() | 
| 245 issue = changelist.GetIssue() | 280 issue = changelist.GetIssue() | 
| 246 patchset = changelist.GetMostRecentPatchset() | 281 patchset = changelist.GetMostRecentPatchset() | 
| 247 for builders_and_tests in sorted(masters.itervalues()): | 282 for builders_and_tests in sorted(masters.itervalues()): | 
| 248 # TODO(hinoka et al): add support for other properties. | 283 # TODO(hinoka et al): add support for other properties. | 
| 249 # Currently, this completely ignores testfilter and other properties. | 284 # Currently, this completely ignores testfilter and other properties. | 
| 250 for builder in sorted(builders_and_tests): | 285 for builder in sorted(builders_and_tests): | 
| 251 luci_trigger.trigger( | 286 luci_trigger.trigger( | 
| (...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 306 { | 341 { | 
| 307 'bucket': bucket, | 342 'bucket': bucket, | 
| 308 'parameters_json': json.dumps(parameters), | 343 'parameters_json': json.dumps(parameters), | 
| 309 'tags': ['builder:%s' % builder, | 344 'tags': ['builder:%s' % builder, | 
| 310 'buildset:%s' % buildset, | 345 'buildset:%s' % buildset, | 
| 311 'master:%s' % master, | 346 'master:%s' % master, | 
| 312 'user_agent:git_cl_try'] | 347 'user_agent:git_cl_try'] | 
| 313 } | 348 } | 
| 314 ) | 349 ) | 
| 315 | 350 | 
| 316 for try_count in xrange(3): | 351 _buildbucket_retry( | 
| 317 response, content = http.request( | 352 'triggering tryjobs', | 
| 318 buildbucket_put_url, | 353 http, | 
| 319 'PUT', | 354 buildbucket_put_url, | 
| 320 body=json.dumps(batch_req_body), | 355 'PUT', | 
| 321 headers={'Content-Type': 'application/json'}, | 356 body=json.dumps(batch_req_body), | 
| 322 ) | 357 headers={'Content-Type': 'application/json'} | 
| 323 content_json = None | 358 ) | 
| 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) | 359 print '\n'.join(print_text) | 
| 352 | 360 | 
| 353 | 361 | 
| 362 def fetch_try_jobs(auth_config, changelist, options): | |
| 363 """Fetches tryjobs from buildbucket. | |
| 364 | |
| 365 Returns a map from build id to build info as json dictionary. | |
| 366 """ | |
| 367 rietveld_url = settings.GetDefaultServerUrl() | |
| 368 rietveld_host = urlparse.urlparse(rietveld_url).hostname | |
| 369 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config) | |
| 370 if authenticator.has_cached_credentials(): | |
| 371 http = authenticator.authorize(httplib2.Http()) | |
| 372 else: | |
| 373 print ('Warning: Some results might be missing because %s' % | |
| 374 # Get the message on how to login. | |
| 375 auth.LoginRequiredError(rietveld_host).message) | |
| 376 http = httplib2.Http() | |
| 377 | |
| 378 http.force_exception_to_status_code = True | |
| 379 | |
| 380 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format( | |
| 381 hostname=rietveld_host, | |
| 382 issue=changelist.GetIssue(), | |
| 383 patch=options.patchset) | |
| 384 params = {'tag': 'buildset:%s' % buildset} | |
| 385 | |
| 386 builds = {} | |
| 387 while True: | |
| 388 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format( | |
| 389 hostname=options.buildbucket_host, | |
| 390 params=urllib.urlencode(params)) | |
| 391 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET') | |
| 392 for build in content.get('builds', []): | |
| 393 builds[build['id']] = build | |
| 394 if 'next_cursor' in content: | |
| 395 params['start_cursor'] = content['next_cursor'] | |
| 396 else: | |
| 397 break | |
| 398 return builds | |
| 399 | |
| 400 | |
| 401 def print_tryjobs(options, builds): | |
| 402 """Prints nicely result of fetch_try_jobs.""" | |
| 403 if not builds: | |
| 404 print 'No tryjobs scheduled' | |
| 405 return | |
| 406 | |
| 407 # Make a copy, because we'll be modifying builds dictionary. | |
| 408 builds = builds.copy() | |
| 409 builder_names_cache = {} | |
| 410 | |
| 411 def get_builder(b): | |
| 412 try: | |
| 413 return builder_names_cache[b['id']] | |
| 414 except KeyError: | |
| 415 name = None | |
| 416 try: | |
| 417 parameters = json.loads(b['parameters_json']) | |
| 418 name = parameters['builder_name'] | |
| 419 except (ValueError, KeyError) as error: | |
| 420 print 'WARNING: failed to get builder name for build %s: %s' % ( | |
| 421 b['id'], error) | |
| 422 # Try to fetch from tags. | |
| 423 for tag in b.get('tags', []): | |
| 
nodir
2016/02/25 18:31:07
I'd omit this. The user will probably believe git-
 
tandrii(chromium)
2016/02/25 19:02:15
Yeah, i agree. I guess was too attached to my pars
 | |
| 424 name_value = tag.split(':', 1) | |
| 425 if len(name_value) == 2 and name_value[0] == 'builder': | |
| 426 name = name_value[1] | |
| 427 break | |
| 428 builder_names_cache[b['id']] = name | |
| 429 return name | |
| 430 | |
| 431 def get_bucket(b): | |
| 432 bucket = b['bucket'] | |
| 433 if bucket.startswith('master.'): | |
| 434 return bucket[len('master.'):] | |
| 435 return bucket | |
| 436 | |
| 437 if options.print_master: | |
| 438 name_fmt = '%%-%ds %%-%ds' % ( | |
| 439 max(len(str(get_bucket(b))) for b in builds.itervalues()), | |
| 440 max(len(str(get_builder(b))) for b in builds.itervalues())) | |
| 441 def get_name(b): | |
| 442 return name_fmt % (get_bucket(b), get_builder(b)) | |
| 443 else: | |
| 444 name_fmt = '%%-%ds' % ( | |
| 445 max(len(str(get_builder(b))) for b in builds.itervalues())) | |
| 446 def get_name(b): | |
| 447 return name_fmt % get_builder(b) | |
| 448 | |
| 449 def sort_key(b): | |
| 450 return b['status'], b.get('result'), get_name(b), b.get('url') | |
| 451 | |
| 452 def pop(title, f, color=None, **kwargs): | |
| 453 """Pop matching builds from `builds` dict and print them.""" | |
| 454 | |
| 455 if not sys.stdout.isatty() or color is None: | |
| 456 colorize = str | |
| 457 else: | |
| 458 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET) | |
| 459 | |
| 460 result = [] | |
| 461 for b in builds.values(): | |
| 462 if all(b.get(k) == v for k, v in kwargs.iteritems()): | |
| 463 builds.pop(b['id']) | |
| 464 result.append(b) | |
| 465 if result: | |
| 466 print colorize(title) | |
| 467 for b in sorted(result, key=sort_key): | |
| 468 print ' ', colorize('\t'.join(map(str, f(b)))) | |
| 469 | |
| 470 total = len(builds) | |
| 471 pop(status='COMPLETED', result='SUCCESS', | |
| 472 title='Successes:', color=Fore.GREEN, | |
| 473 f=lambda b: (get_name(b), b.get('url'))) | |
| 474 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE', | |
| 475 title='Infra Failures:', color=Fore.MAGENTA, | |
| 476 f=lambda b: (get_name(b), b.get('url'))) | |
| 477 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE', | |
| 478 title='Failures:', color=Fore.RED, | |
| 479 f=lambda b: (get_name(b), b.get('url'))) | |
| 480 pop(status='COMPLETED', result='CANCELED', | |
| 481 title='Canceled:', color=Fore.MAGENTA, | |
| 
nodir
2016/02/25 18:31:07
nit: Cancelled :)
don't repeat my typo :)
 
tandrii(chromium)
2016/02/25 19:02:15
it's not a typo, unless you have a pedantic Britis
 | |
| 482 f=lambda b: (get_name(b),)) | |
| 483 pop(status='COMPLETED', result='FAILURE', | |
| 484 failure_reason='INVALID_BUILD_DEFINITION', | |
| 485 title='Wrong master/builder name:', color=Fore.MAGENTA, | |
| 486 f=lambda b: (get_name(b),)) | |
| 487 pop(status='COMPLETED', result='FAILURE', | |
| 488 title='Other failures:', | |
| 489 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url'))) | |
| 490 pop(status='COMPLETED', | |
| 491 title='Other finished:', | |
| 492 f=lambda b: (get_name(b), b.get('result'), b.get('url'))) | |
| 493 pop(status='STARTED', | |
| 494 title='Started:', color=Fore.YELLOW, | |
| 495 f=lambda b: (get_name(b), b.get('url'))) | |
| 496 pop(status='SCHEDULED', | |
| 497 title='Scheduled:', | |
| 498 f=lambda b: (get_name(b), 'id=%s' % b['id'])) | |
| 499 # The last section is just in case buildbucket API changes OR there is a bug. | |
| 500 pop(title='Other:', | |
| 501 f=lambda b: (get_name(b), 'id=%s' % b['id'])) | |
| 502 assert len(builds) == 0 | |
| 503 print 'Total: %d tryjobs' % total | |
| 504 | |
| 505 | |
| 354 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): | 506 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): | 
| 355 """Return the corresponding git ref if |base_url| together with |glob_spec| | 507 """Return the corresponding git ref if |base_url| together with |glob_spec| | 
| 356 matches the full |url|. | 508 matches the full |url|. | 
| 357 | 509 | 
| 358 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). | 510 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). | 
| 359 """ | 511 """ | 
| 360 fetch_suburl, as_ref = glob_spec.split(':') | 512 fetch_suburl, as_ref = glob_spec.split(':') | 
| 361 if allow_wildcards: | 513 if allow_wildcards: | 
| 362 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) | 514 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) | 
| 363 if glob_match: | 515 if glob_match: | 
| (...skipping 2994 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 3358 | 3510 | 
| 3359 for (master, builders) in sorted(masters.iteritems()): | 3511 for (master, builders) in sorted(masters.iteritems()): | 
| 3360 if master: | 3512 if master: | 
| 3361 print 'Master: %s' % master | 3513 print 'Master: %s' % master | 
| 3362 length = max(len(builder) for builder in builders) | 3514 length = max(len(builder) for builder in builders) | 
| 3363 for builder in sorted(builders): | 3515 for builder in sorted(builders): | 
| 3364 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) | 3516 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) | 
| 3365 return 0 | 3517 return 0 | 
| 3366 | 3518 | 
| 3367 | 3519 | 
| 3520 def CMDtry_results(parser, args): | |
| 3521 group = optparse.OptionGroup(parser, "Try job results options") | |
| 3522 group.add_option( | |
| 3523 "-p", "--patchset", type=int, help="patchset number if not current.") | |
| 3524 group.add_option( | |
| 3525 "--print-master", action='store_true', help="print master name as well") | |
| 3526 group.add_option( | |
| 3527 "--buildbucket-host", default='cr-buildbucket.appspot.com', | |
| 3528 help="Host of buildbucket. The default host is %default.") | |
| 3529 parser.add_option_group(group) | |
| 3530 auth.add_auth_options(parser) | |
| 3531 options, args = parser.parse_args(args) | |
| 3532 if args: | |
| 3533 parser.error('Unrecognized args: %s' % ' '.join(args)) | |
| 3534 | |
| 3535 auth_config = auth.extract_auth_config_from_options(options) | |
| 3536 cl = Changelist(auth_config=auth_config) | |
| 3537 if not cl.GetIssue(): | |
| 3538 parser.error('Need to upload first') | |
| 3539 | |
| 3540 if not options.patchset: | |
| 3541 options.patchset = cl.GetMostRecentPatchset() | |
| 3542 if options.patchset and options.patchset != cl.GetPatchset(): | |
| 3543 print( | |
| 3544 '\nWARNING Mismatch between local config and server. Did a previous ' | |
| 3545 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' | |
| 3546 'Continuing using\npatchset %s.\n' % options.patchset) | |
| 3547 try: | |
| 3548 jobs = fetch_try_jobs(auth_config, cl, options) | |
| 3549 except BuildbucketResponseException as ex: | |
| 3550 print 'Buildbucket error: %s' % ex | |
| 3551 return 1 | |
| 3552 except Exception as e: | |
| 3553 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc()) | |
| 3554 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % ( | |
| 3555 e, stacktrace) | |
| 3556 return 1 | |
| 3557 print_tryjobs(options, jobs) | |
| 3558 return 0 | |
| 3559 | |
| 3560 | |
| 3368 @subcommand.usage('[new upstream branch]') | 3561 @subcommand.usage('[new upstream branch]') | 
| 3369 def CMDupstream(parser, args): | 3562 def CMDupstream(parser, args): | 
| 3370 """Prints or sets the name of the upstream branch, if any.""" | 3563 """Prints or sets the name of the upstream branch, if any.""" | 
| 3371 _, args = parser.parse_args(args) | 3564 _, args = parser.parse_args(args) | 
| 3372 if len(args) > 1: | 3565 if len(args) > 1: | 
| 3373 parser.error('Unrecognized args: %s' % ' '.join(args)) | 3566 parser.error('Unrecognized args: %s' % ' '.join(args)) | 
| 3374 | 3567 | 
| 3375 cl = Changelist() | 3568 cl = Changelist() | 
| 3376 if args: | 3569 if args: | 
| 3377 # One arg means set upstream branch. | 3570 # One arg means set upstream branch. | 
| (...skipping 382 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 3760 if __name__ == '__main__': | 3953 if __name__ == '__main__': | 
| 3761 # These affect sys.stdout so do it outside of main() to simplify mocks in | 3954 # These affect sys.stdout so do it outside of main() to simplify mocks in | 
| 3762 # unit testing. | 3955 # unit testing. | 
| 3763 fix_encoding.fix_encoding() | 3956 fix_encoding.fix_encoding() | 
| 3764 colorama.init() | 3957 colorama.init() | 
| 3765 try: | 3958 try: | 
| 3766 sys.exit(main(sys.argv[1:])) | 3959 sys.exit(main(sys.argv[1:])) | 
| 3767 except KeyboardInterrupt: | 3960 except KeyboardInterrupt: | 
| 3768 sys.stderr.write('interrupted\n') | 3961 sys.stderr.write('interrupted\n') | 
| 3769 sys.exit(1) | 3962 sys.exit(1) | 
| OLD | NEW |