Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(182)

Unified Diff: git_cl.py

Issue 1725053002: git cl try-results: show buildbucket tryjobs. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: review Created 4 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: git_cl.py
diff --git a/git_cl.py b/git_cl.py
index 3023213ddd903a9d696f48eea7c79f04964f079e..30956f54a0ea3ee261df71a66303fef94607d917 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -25,8 +25,10 @@ import tempfile
import textwrap
import time
import traceback
+import urllib
import urllib2
import urlparse
+import uuid
import webbrowser
import zlib
@@ -239,6 +241,40 @@ 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."""
+ try_count = 0
+ while True:
+ 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. Reason: %s. Message: %s.' % (
+ 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:
+ 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)
+ try_count += 1
+ assert False, 'unreachable'
+
+
def trigger_luci_job(changelist, masters, options):
"""Send a job to run on LUCI."""
issue_props = changelist.GetIssueProperties()
@@ -306,6 +342,7 @@ def trigger_try_jobs(auth_config, changelist, options, masters, category):
{
'bucket': bucket,
'parameters_json': json.dumps(parameters),
+ 'client_operation_id': str(uuid.uuid4()),
'tags': ['builder:%s' % builder,
'buildset:%s' % buildset,
'master:%s' % master,
@@ -313,42 +350,153 @@ def trigger_try_jobs(auth_config, changelist, options, masters, category):
}
)
- for try_count in xrange(3):
- 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)
+ if authenticator.has_cached_credentials():
+ http = authenticator.authorize(httplib2.Http())
+ else:
+ print ('Warning: Some results might be missing because %s' %
+ # Get the message on how to login.
+ auth.LoginRequiredError(rietveld_host).message)
+ http = 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}
+
+ builds = {}
+ while True:
+ url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
+ hostname=options.buildbucket_host,
+ params=urllib.urlencode(params))
+ content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
+ 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()
+ builder_names_cache = {}
+
+ def get_builder(b):
+ try:
+ return builder_names_cache[b['id']]
+ except KeyError:
+ try:
+ parameters = json.loads(b['parameters_json'])
+ name = parameters['builder_name']
+ except (ValueError, KeyError) as error:
+ print 'WARNING: failed to get builder name for build %s: %s' % (
+ b['id'], error)
+ name = None
+ builder_names_cache[b['id']] = name
+ return name
+
+ 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():
+ if all(b.get(k) == v for k, v in kwargs.iteritems()):
+ 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='CANCELED',
+ title='Canceled:', color=Fore.MAGENTA,
+ f=lambda b: (get_name(b),))
+ 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')))
+ 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']))
+ assert len(builds) == 0
+ print 'Total: %d tryjobs' % total
def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
@@ -3365,6 +3513,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 'Buildbucket error: %s' % ex
+ return 1
+ except Exception as e:
+ stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
+ print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
+ 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."""
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698