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

Side by Side 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 unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
31 import uuid
30 import webbrowser 32 import webbrowser
31 import zlib 33 import zlib
32 34
33 try: 35 try:
34 import readline # pylint: disable=F0401,W0611 36 import readline # pylint: disable=F0401,W0611
35 except ImportError: 37 except ImportError:
36 pass 38 pass
37 39
38 from third_party import colorama 40 from third_party import colorama
39 from third_party import httplib2 41 from third_party import httplib2
(...skipping 192 matching lines...) Expand 10 before | Expand all | Expand 10 after
232 name, while the developers always use shortened master name 234 name, while the developers always use shortened master name
233 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This 235 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
234 function does the conversion for buildbucket migration. 236 function does the conversion for buildbucket migration.
235 """ 237 """
236 prefix = 'master.' 238 prefix = 'master.'
237 if master.startswith(prefix): 239 if master.startswith(prefix):
238 return master 240 return master
239 return '%s%s' % (prefix, master) 241 return '%s%s' % (prefix, master)
240 242
241 243
244 def _buildbucket_retry(operation_name, http, *args, **kwargs):
245 """Retries requests to buildbucket service and returns parsed json content."""
246 try_count = 0
247 while True:
248 response, content = http.request(*args, **kwargs)
249 try:
250 content_json = json.loads(content)
251 except ValueError:
252 content_json = None
253
254 # Buildbucket could return an error even if status==200.
255 if content_json and content_json.get('error'):
256 msg = 'Error in response. Reason: %s. Message: %s.' % (
257 content_json['error'].get('reason', ''),
258 content_json['error'].get('message', ''))
259 raise BuildbucketResponseException(msg)
260
261 if response.status == 200:
262 if not content_json:
263 raise BuildbucketResponseException(
264 'Buildbucket returns invalid json content: %s.\n'
265 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
266 content)
267 return content_json
268 if response.status < 500 or try_count >= 2:
269 raise httplib2.HttpLib2Error(content)
270
271 # status >= 500 means transient failures.
272 logging.debug('Transient errors when %s. Will retry.', operation_name)
273 time.sleep(0.5 + 1.5*try_count)
274 try_count += 1
275 assert False, 'unreachable'
276
277
242 def trigger_luci_job(changelist, masters, options): 278 def trigger_luci_job(changelist, masters, options):
243 """Send a job to run on LUCI.""" 279 """Send a job to run on LUCI."""
244 issue_props = changelist.GetIssueProperties() 280 issue_props = changelist.GetIssueProperties()
245 issue = changelist.GetIssue() 281 issue = changelist.GetIssue()
246 patchset = changelist.GetMostRecentPatchset() 282 patchset = changelist.GetMostRecentPatchset()
247 for builders_and_tests in sorted(masters.itervalues()): 283 for builders_and_tests in sorted(masters.itervalues()):
248 # TODO(hinoka et al): add support for other properties. 284 # TODO(hinoka et al): add support for other properties.
249 # Currently, this completely ignores testfilter and other properties. 285 # Currently, this completely ignores testfilter and other properties.
250 for builder in sorted(builders_and_tests): 286 for builder in sorted(builders_and_tests):
251 luci_trigger.trigger( 287 luci_trigger.trigger(
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
299 if tests: 335 if tests:
300 parameters['properties']['testfilter'] = tests 336 parameters['properties']['testfilter'] = tests
301 if properties: 337 if properties:
302 parameters['properties'].update(properties) 338 parameters['properties'].update(properties)
303 if options.clobber: 339 if options.clobber:
304 parameters['properties']['clobber'] = True 340 parameters['properties']['clobber'] = True
305 batch_req_body['builds'].append( 341 batch_req_body['builds'].append(
306 { 342 {
307 'bucket': bucket, 343 'bucket': bucket,
308 'parameters_json': json.dumps(parameters), 344 'parameters_json': json.dumps(parameters),
345 'client_operation_id': str(uuid.uuid4()),
309 'tags': ['builder:%s' % builder, 346 'tags': ['builder:%s' % builder,
310 'buildset:%s' % buildset, 347 'buildset:%s' % buildset,
311 'master:%s' % master, 348 'master:%s' % master,
312 'user_agent:git_cl_try'] 349 'user_agent:git_cl_try']
313 } 350 }
314 ) 351 )
315 352
316 for try_count in xrange(3): 353 _buildbucket_retry(
317 response, content = http.request( 354 'triggering tryjobs',
318 buildbucket_put_url, 355 http,
319 'PUT', 356 buildbucket_put_url,
320 body=json.dumps(batch_req_body), 357 'PUT',
321 headers={'Content-Type': 'application/json'}, 358 body=json.dumps(batch_req_body),
322 ) 359 headers={'Content-Type': 'application/json'}
323 content_json = None 360 )
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) 361 print '\n'.join(print_text)
352 362
353 363
364 def fetch_try_jobs(auth_config, changelist, options):
365 """Fetches tryjobs from buildbucket.
366
367 Returns a map from build id to build info as json dictionary.
368 """
369 rietveld_url = settings.GetDefaultServerUrl()
370 rietveld_host = urlparse.urlparse(rietveld_url).hostname
371 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
372 if authenticator.has_cached_credentials():
373 http = authenticator.authorize(httplib2.Http())
374 else:
375 print ('Warning: Some results might be missing because %s' %
376 # Get the message on how to login.
377 auth.LoginRequiredError(rietveld_host).message)
378 http = httplib2.Http()
379
380 http.force_exception_to_status_code = True
381
382 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
383 hostname=rietveld_host,
384 issue=changelist.GetIssue(),
385 patch=options.patchset)
386 params = {'tag': 'buildset:%s' % buildset}
387
388 builds = {}
389 while True:
390 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
391 hostname=options.buildbucket_host,
392 params=urllib.urlencode(params))
393 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
394 for build in content.get('builds', []):
395 builds[build['id']] = build
396 if 'next_cursor' in content:
397 params['start_cursor'] = content['next_cursor']
398 else:
399 break
400 return builds
401
402
403 def print_tryjobs(options, builds):
404 """Prints nicely result of fetch_try_jobs."""
405 if not builds:
406 print 'No tryjobs scheduled'
407 return
408
409 # Make a copy, because we'll be modifying builds dictionary.
410 builds = builds.copy()
411 builder_names_cache = {}
412
413 def get_builder(b):
414 try:
415 return builder_names_cache[b['id']]
416 except KeyError:
417 try:
418 parameters = json.loads(b['parameters_json'])
419 name = parameters['builder_name']
420 except (ValueError, KeyError) as error:
421 print 'WARNING: failed to get builder name for build %s: %s' % (
422 b['id'], error)
423 name = None
424 builder_names_cache[b['id']] = name
425 return name
426
427 def get_bucket(b):
428 bucket = b['bucket']
429 if bucket.startswith('master.'):
430 return bucket[len('master.'):]
431 return bucket
432
433 if options.print_master:
434 name_fmt = '%%-%ds %%-%ds' % (
435 max(len(str(get_bucket(b))) for b in builds.itervalues()),
436 max(len(str(get_builder(b))) for b in builds.itervalues()))
437 def get_name(b):
438 return name_fmt % (get_bucket(b), get_builder(b))
439 else:
440 name_fmt = '%%-%ds' % (
441 max(len(str(get_builder(b))) for b in builds.itervalues()))
442 def get_name(b):
443 return name_fmt % get_builder(b)
444
445 def sort_key(b):
446 return b['status'], b.get('result'), get_name(b), b.get('url')
447
448 def pop(title, f, color=None, **kwargs):
449 """Pop matching builds from `builds` dict and print them."""
450
451 if not sys.stdout.isatty() or color is None:
452 colorize = str
453 else:
454 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
455
456 result = []
457 for b in builds.values():
458 if all(b.get(k) == v for k, v in kwargs.iteritems()):
459 builds.pop(b['id'])
460 result.append(b)
461 if result:
462 print colorize(title)
463 for b in sorted(result, key=sort_key):
464 print ' ', colorize('\t'.join(map(str, f(b))))
465
466 total = len(builds)
467 pop(status='COMPLETED', result='SUCCESS',
468 title='Successes:', color=Fore.GREEN,
469 f=lambda b: (get_name(b), b.get('url')))
470 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
471 title='Infra Failures:', color=Fore.MAGENTA,
472 f=lambda b: (get_name(b), b.get('url')))
473 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
474 title='Failures:', color=Fore.RED,
475 f=lambda b: (get_name(b), b.get('url')))
476 pop(status='COMPLETED', result='CANCELED',
477 title='Canceled:', color=Fore.MAGENTA,
478 f=lambda b: (get_name(b),))
479 pop(status='COMPLETED', result='FAILURE',
480 failure_reason='INVALID_BUILD_DEFINITION',
481 title='Wrong master/builder name:', color=Fore.MAGENTA,
482 f=lambda b: (get_name(b),))
483 pop(status='COMPLETED', result='FAILURE',
484 title='Other failures:',
485 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
486 pop(status='COMPLETED',
487 title='Other finished:',
488 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
489 pop(status='STARTED',
490 title='Started:', color=Fore.YELLOW,
491 f=lambda b: (get_name(b), b.get('url')))
492 pop(status='SCHEDULED',
493 title='Scheduled:',
494 f=lambda b: (get_name(b), 'id=%s' % b['id']))
495 # The last section is just in case buildbucket API changes OR there is a bug.
496 pop(title='Other:',
497 f=lambda b: (get_name(b), 'id=%s' % b['id']))
498 assert len(builds) == 0
499 print 'Total: %d tryjobs' % total
500
501
354 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): 502 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
355 """Return the corresponding git ref if |base_url| together with |glob_spec| 503 """Return the corresponding git ref if |base_url| together with |glob_spec|
356 matches the full |url|. 504 matches the full |url|.
357 505
358 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). 506 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
359 """ 507 """
360 fetch_suburl, as_ref = glob_spec.split(':') 508 fetch_suburl, as_ref = glob_spec.split(':')
361 if allow_wildcards: 509 if allow_wildcards:
362 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl) 510 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
363 if glob_match: 511 if glob_match:
(...skipping 2994 matching lines...) Expand 10 before | Expand all | Expand 10 after
3358 3506
3359 for (master, builders) in sorted(masters.iteritems()): 3507 for (master, builders) in sorted(masters.iteritems()):
3360 if master: 3508 if master:
3361 print 'Master: %s' % master 3509 print 'Master: %s' % master
3362 length = max(len(builder) for builder in builders) 3510 length = max(len(builder) for builder in builders)
3363 for builder in sorted(builders): 3511 for builder in sorted(builders):
3364 print ' %*s: %s' % (length, builder, ','.join(builders[builder])) 3512 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
3365 return 0 3513 return 0
3366 3514
3367 3515
3516 def CMDtry_results(parser, args):
3517 group = optparse.OptionGroup(parser, "Try job results options")
3518 group.add_option(
3519 "-p", "--patchset", type=int, help="patchset number if not current.")
3520 group.add_option(
3521 "--print-master", action='store_true', help="print master name as well")
3522 group.add_option(
3523 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3524 help="Host of buildbucket. The default host is %default.")
3525 parser.add_option_group(group)
3526 auth.add_auth_options(parser)
3527 options, args = parser.parse_args(args)
3528 if args:
3529 parser.error('Unrecognized args: %s' % ' '.join(args))
3530
3531 auth_config = auth.extract_auth_config_from_options(options)
3532 cl = Changelist(auth_config=auth_config)
3533 if not cl.GetIssue():
3534 parser.error('Need to upload first')
3535
3536 if not options.patchset:
3537 options.patchset = cl.GetMostRecentPatchset()
3538 if options.patchset and options.patchset != cl.GetPatchset():
3539 print(
3540 '\nWARNING Mismatch between local config and server. Did a previous '
3541 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3542 'Continuing using\npatchset %s.\n' % options.patchset)
3543 try:
3544 jobs = fetch_try_jobs(auth_config, cl, options)
3545 except BuildbucketResponseException as ex:
3546 print 'Buildbucket error: %s' % ex
3547 return 1
3548 except Exception as e:
3549 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3550 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3551 e, stacktrace)
3552 return 1
3553 print_tryjobs(options, jobs)
3554 return 0
3555
3556
3368 @subcommand.usage('[new upstream branch]') 3557 @subcommand.usage('[new upstream branch]')
3369 def CMDupstream(parser, args): 3558 def CMDupstream(parser, args):
3370 """Prints or sets the name of the upstream branch, if any.""" 3559 """Prints or sets the name of the upstream branch, if any."""
3371 _, args = parser.parse_args(args) 3560 _, args = parser.parse_args(args)
3372 if len(args) > 1: 3561 if len(args) > 1:
3373 parser.error('Unrecognized args: %s' % ' '.join(args)) 3562 parser.error('Unrecognized args: %s' % ' '.join(args))
3374 3563
3375 cl = Changelist() 3564 cl = Changelist()
3376 if args: 3565 if args:
3377 # One arg means set upstream branch. 3566 # One arg means set upstream branch.
(...skipping 382 matching lines...) Expand 10 before | Expand all | Expand 10 after
3760 if __name__ == '__main__': 3949 if __name__ == '__main__':
3761 # These affect sys.stdout so do it outside of main() to simplify mocks in 3950 # These affect sys.stdout so do it outside of main() to simplify mocks in
3762 # unit testing. 3951 # unit testing.
3763 fix_encoding.fix_encoding() 3952 fix_encoding.fix_encoding()
3764 colorama.init() 3953 colorama.init()
3765 try: 3954 try:
3766 sys.exit(main(sys.argv[1:])) 3955 sys.exit(main(sys.argv[1:]))
3767 except KeyboardInterrupt: 3956 except KeyboardInterrupt:
3768 sys.stderr.write('interrupted\n') 3957 sys.stderr.write('interrupted\n')
3769 sys.exit(1) 3958 sys.exit(1)
OLDNEW
« 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