Chromium Code Reviews| Index: git_cl.py |
| diff --git a/git_cl.py b/git_cl.py |
| index 3023213ddd903a9d696f48eea7c79f04964f079e..b5c0bde62dece186c962f3c31b8f235280ab0a9d 100755 |
| --- a/git_cl.py |
| +++ b/git_cl.py |
| @@ -25,6 +25,7 @@ import tempfile |
| import textwrap |
| import time |
| import traceback |
| +import urllib |
| import urllib2 |
| import urlparse |
| import webbrowser |
| @@ -239,6 +240,39 @@ def _prefix_master(master): |
| return '%s%s' % (prefix, master) |
| +def _buildbucket_retry(operation_name, http, *args, **kwargs): |
| + """Retries requests to buildbucket service and returns parsed json content.""" |
| + for try_count in xrange(3): |
|
tandrii(chromium)
2016/02/25 17:59:33
this is a copy of code from before.
|
| + response, content = http.request(*args, **kwargs) |
| + try: |
| + content_json = json.loads(content) |
| + except ValueError: |
| + content_json = None |
| + |
| + # Buildbucket could return an error even if status==200. |
| + if content_json and content_json.get('error'): |
| + msg = 'Error in response. Code: %s. Reason: %s. Message: %s.' % ( |
| + 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.
|
| + content_json['error'].get('reason', ''), |
| + content_json['error'].get('message', '')) |
| + raise BuildbucketResponseException(msg) |
| + |
| + if response.status == 200: |
| + if not content_json: |
| + raise BuildbucketResponseException( |
| + 'Buildbucket returns invalid json content: %s.\n' |
| + 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' % |
| + content) |
| + return content_json |
| + 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
|
| + raise httplib2.HttpLib2Error(content) |
| + |
| + # status >= 500 means transient failures. |
| + logging.debug('Transient errors when %s. Will retry.', operation_name) |
| + time.sleep(0.5 + 1.5*try_count) |
| + assert False, 'unreachable' |
| + |
| + |
| def trigger_luci_job(changelist, masters, options): |
| """Send a job to run on LUCI.""" |
| issue_props = changelist.GetIssueProperties() |
| @@ -313,42 +347,149 @@ def trigger_try_jobs(auth_config, changelist, options, masters, category): |
| } |
| ) |
| - for try_count in xrange(3): |
|
tandrii(chromium)
2016/02/25 17:59:33
this is original - i didn't change this, except fo
|
| - response, content = http.request( |
| - buildbucket_put_url, |
| - 'PUT', |
| - body=json.dumps(batch_req_body), |
| - headers={'Content-Type': 'application/json'}, |
| - ) |
| - content_json = None |
| - try: |
| - content_json = json.loads(content) |
| - except ValueError: |
| - pass |
| + _buildbucket_retry( |
| + 'triggering tryjobs', |
| + http, |
| + buildbucket_put_url, |
| + 'PUT', |
| + body=json.dumps(batch_req_body), |
| + headers={'Content-Type': 'application/json'} |
| + ) |
| + print '\n'.join(print_text) |
| - # Buildbucket could return an error even if status==200. |
| - if content_json and content_json.get('error'): |
| - msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % ( |
| - content_json['error'].get('code', ''), |
| - content_json['error'].get('reason', ''), |
| - content_json['error'].get('message', '')) |
| - raise BuildbucketResponseException(msg) |
| - if response.status == 200: |
| - if not content_json: |
| - raise BuildbucketResponseException( |
| - 'Buildbucket returns invalid json content: %s.\n' |
| - 'Please file bugs at crbug.com, label "Infra-BuildBucket".' % |
| - content) |
| +def fetch_try_jobs(auth_config, changelist, options): |
| + """Fetches tryjobs from buildbucket. |
| + |
| + Returns a map from build id to build info as json dictionary. |
| + """ |
| + rietveld_url = settings.GetDefaultServerUrl() |
| + rietveld_host = urlparse.urlparse(rietveld_url).hostname |
| + authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config) |
| + http = authenticator.authorize(httplib2.Http()) |
| + http.force_exception_to_status_code = True |
| + |
| + buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format( |
| + hostname=rietveld_host, |
| + issue=changelist.GetIssue(), |
| + patch=options.patchset) |
| + params = {'tag': 'buildset:%s' % buildset} |
| + |
| + login_required_error = None |
| + builds = {} |
| + while True: |
| + url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format( |
| + hostname=options.buildbucket_host, |
| + params=urllib.urlencode(params)) |
| + try: |
| + content = _buildbucket_retry('fetching tryjobs', http, url, 'GET') |
| + except auth.LoginRequiredError as e: |
| + if login_required_error: |
| + # Avoid looping forever if login is required on this buildbucket host. |
| + raise |
| + print 'Warning: Some results might be missing because %s' % e |
| + login_required_error = e |
| + http = httplib2.Http() |
| + 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.
|
| + continue |
| + for build in content.get('builds', []): |
| + builds[build['id']] = build |
| + if 'next_cursor' in content: |
| + params['start_cursor'] = content['next_cursor'] |
| + else: |
| break |
| - if response.status < 500 or try_count >= 2: |
| - raise httplib2.HttpLib2Error(content) |
| + return builds |
| - # status >= 500 means transient failures. |
| - logging.debug('Transient errors when triggering tryjobs. Will retry.') |
| - time.sleep(0.5 + 1.5*try_count) |
| - print '\n'.join(print_text) |
| +def print_tryjobs(options, builds): |
| + """Prints nicely result of fetch_try_jobs.""" |
| + if not builds: |
| + print 'No tryjobs scheduled' |
| + return |
| + |
| + # Make a copy, because we'll be modifying builds dictionary. |
| + builds = builds.copy() |
| + 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.
|
| + 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
|
| + name_value = tag.split(':', 1) |
| + if len(name_value) == 2 and name_value[0] == 'builder': |
| + return name_value[1] |
| + return None |
| + |
| + def get_bucket(b): |
| + bucket = b['bucket'] |
| + if bucket.startswith('master.'): |
| + return bucket[len('master.'):] |
| + return bucket |
| + |
| + if options.print_master: |
| + name_fmt = '%%-%ds %%-%ds' % ( |
| + max(len(str(get_bucket(b))) for b in builds.itervalues()), |
| + max(len(str(get_builder(b))) for b in builds.itervalues())) |
| + def get_name(b): |
| + return name_fmt % (get_bucket(b), get_builder(b)) |
| + else: |
| + name_fmt = '%%-%ds' % ( |
| + max(len(str(get_builder(b))) for b in builds.itervalues())) |
| + def get_name(b): |
| + return name_fmt % get_builder(b) |
| + |
| + def sort_key(b): |
| + return b['status'], b.get('result'), get_name(b), b.get('url') |
| + |
| + def pop(title, f, color=None, **kwargs): |
| + """Pop matching builds from `builds` dict and print them.""" |
| + |
| + if not sys.stdout.isatty() or color is None: |
| + colorize = str |
| + else: |
| + colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET) |
| + |
| + result = [] |
| + for b in builds.values(): |
| + 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.
|
| + for k, v in kwargs.iteritems(): |
| + if b.get(k) != v: |
| + selected = False |
| + break |
| + if selected: |
| + builds.pop(b['id']) |
| + result.append(b) |
| + if result: |
| + print colorize(title) |
| + for b in sorted(result, key=sort_key): |
| + print ' ', colorize('\t'.join(map(str, f(b)))) |
| + |
| + total = len(builds) |
| + pop(status='COMPLETED', result='SUCCESS', |
| + title='Successes:', color=Fore.GREEN, |
| + f=lambda b: (get_name(b), b.get('url'))) |
| + pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE', |
| + title='Infra Failures:', color=Fore.MAGENTA, |
| + f=lambda b: (get_name(b), b.get('url'))) |
| + pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE', |
| + title='Failures:', color=Fore.RED, |
| + f=lambda b: (get_name(b), b.get('url'))) |
| + pop(status='COMPLETED', result='FAILURE', |
| + failure_reason='INVALID_BUILD_DEFINITION', |
| + title='Wrong master/builder name:', color=Fore.MAGENTA, |
| + f=lambda b: (get_name(b),)) |
| + pop(status='COMPLETED', result='FAILURE', |
| + title='Other failures:', |
| + f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url'))) |
| + pop(status='COMPLETED', |
| + title='Other finished:', |
| + 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.
|
| + pop(status='STARTED', |
| + title='Started:', color=Fore.YELLOW, |
| + f=lambda b: (get_name(b), b.get('url'))) |
| + pop(status='SCHEDULED', |
| + title='Scheduled:', |
| + f=lambda b: (get_name(b), 'id=%s' % b['id'])) |
| + # The last section is just in case buildbucket API changes OR there is a bug. |
| + pop(title='Other:', |
| + 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.
|
| + print 'Total: %d tryjobs' % total |
| def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): |
| @@ -3365,6 +3506,47 @@ def CMDtry(parser, args): |
| return 0 |
| +def CMDtry_results(parser, args): |
| + group = optparse.OptionGroup(parser, "Try job results options") |
| + group.add_option( |
| + "-p", "--patchset", type=int, help="patchset number if not current.") |
| + group.add_option( |
| + "--print-master", action='store_true', help="print master name as well") |
| + group.add_option( |
| + "--buildbucket-host", default='cr-buildbucket.appspot.com', |
| + help="Host of buildbucket. The default host is %default.") |
| + parser.add_option_group(group) |
| + auth.add_auth_options(parser) |
| + options, args = parser.parse_args(args) |
| + if args: |
| + parser.error('Unrecognized args: %s' % ' '.join(args)) |
| + |
| + auth_config = auth.extract_auth_config_from_options(options) |
| + cl = Changelist(auth_config=auth_config) |
| + if not cl.GetIssue(): |
| + parser.error('Need to upload first') |
| + |
| + if not options.patchset: |
| + options.patchset = cl.GetMostRecentPatchset() |
| + if options.patchset and options.patchset != cl.GetPatchset(): |
| + print( |
| + '\nWARNING Mismatch between local config and server. Did a previous ' |
| + 'upload fail?\ngit-cl try always uses latest patchset from rietveld. ' |
| + 'Continuing using\npatchset %s.\n' % options.patchset) |
| + try: |
| + jobs = fetch_try_jobs(auth_config, cl, options) |
| + except BuildbucketResponseException as ex: |
| + 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.
|
| + return 1 |
| + except Exception as e: |
| + stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc()) |
| + 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.
|
| + e, stacktrace) |
| + return 1 |
| + print_tryjobs(options, jobs) |
| + return 0 |
| + |
| + |
| @subcommand.usage('[new upstream branch]') |
| def CMDupstream(parser, args): |
| """Prints or sets the name of the upstream branch, if any.""" |