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 """Client-side script to send a try job to the try server. It communicates to | 6 """Client-side script to send a try job to the try server. It communicates to |
7 the try server by either writting to a svn repository or by directly connecting | 7 the try server by either writting to a svn repository or by directly connecting |
8 to the server by HTTP. | 8 to the server by HTTP. |
9 """ | 9 """ |
10 | 10 |
11 import datetime | 11 import datetime |
12 import errno | 12 import errno |
13 import getpass | 13 import getpass |
| 14 import itertools |
14 import json | 15 import json |
15 import logging | 16 import logging |
16 import optparse | 17 import optparse |
17 import os | 18 import os |
18 import posixpath | 19 import posixpath |
19 import re | 20 import re |
20 import shutil | 21 import shutil |
21 import sys | 22 import sys |
22 import tempfile | 23 import tempfile |
23 import urllib | 24 import urllib |
(...skipping 283 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
307 if RunGit(['diff-index', 'HEAD']): | 308 if RunGit(['diff-index', 'HEAD']): |
308 print 'Cannot try with a dirty tree. You must commit locally first.' | 309 print 'Cannot try with a dirty tree. You must commit locally first.' |
309 return None | 310 return None |
310 return scm.GIT.GenerateDiff( | 311 return scm.GIT.GenerateDiff( |
311 self.checkout_root, | 312 self.checkout_root, |
312 files=self.files, | 313 files=self.files, |
313 full_move=True, | 314 full_move=True, |
314 branch=self.diff_against) | 315 branch=self.diff_against) |
315 | 316 |
316 | 317 |
317 def _ParseSendChangeOptions(options): | 318 def _ParseBotList(botlist, testfilter): |
| 319 """Parses bot configurations from a list of strings.""" |
| 320 bots = [] |
| 321 if testfilter: |
| 322 for bot in itertools.chain.from_iterable(botspec.split(',') |
| 323 for botspec in botlist): |
| 324 tests = set() |
| 325 if ':' in bot: |
| 326 if bot.endswith(':compile'): |
| 327 tests |= set(['compile']) |
| 328 else: |
| 329 raise ValueError( |
| 330 'Can\'t use both --testfilter and --bot builder:test formats ' |
| 331 'at the same time') |
| 332 |
| 333 bots.append((bot, tests)) |
| 334 else: |
| 335 for botspec in botlist: |
| 336 botname = botspec.split(':')[0] |
| 337 tests = set() |
| 338 if ':' in botspec: |
| 339 tests |= set(filter(None, botspec.split(':')[1].split(','))) |
| 340 bots.append((botname, tests)) |
| 341 return bots |
| 342 |
| 343 |
| 344 def _ApplyTestFilter(testfilter, bot_spec): |
| 345 """Applies testfilter from CLI. |
| 346 |
| 347 Specifying a testfilter strips off any builder-specified tests (except for |
| 348 compile). |
| 349 """ |
| 350 if testfilter: |
| 351 return [(botname, set(testfilter) | (tests & set(['compile']))) |
| 352 for botname, tests in bot_spec] |
| 353 else: |
| 354 return bot_spec |
| 355 |
| 356 |
| 357 def _GenTSBotSpec(checkouts, change, changed_files, options): |
| 358 bot_spec = [] |
| 359 # Get try slaves from PRESUBMIT.py files if not specified. |
| 360 # Even if the diff comes from options.url, use the local checkout for bot |
| 361 # selection. |
| 362 try: |
| 363 import presubmit_support |
| 364 root_presubmit = checkouts[0].ReadRootFile('PRESUBMIT.py') |
| 365 if not change: |
| 366 if not changed_files: |
| 367 changed_files = checkouts[0].file_tuples |
| 368 change = presubmit_support.Change(options.name, |
| 369 '', |
| 370 checkouts[0].checkout_root, |
| 371 changed_files, |
| 372 options.issue, |
| 373 options.patchset, |
| 374 options.email) |
| 375 trybots = presubmit_support.DoGetTrySlaves( |
| 376 change, |
| 377 checkouts[0].GetFileNames(), |
| 378 checkouts[0].checkout_root, |
| 379 root_presubmit, |
| 380 options.project, |
| 381 options.verbose, |
| 382 sys.stdout) |
| 383 if trybots: |
| 384 if isinstance(trybots[0], basestring): |
| 385 # PRESUBMIT.py sent us an old-style string list of bots. |
| 386 # _ParseBotList's testfilter is set to None otherwise it will complain. |
| 387 bot_spec = _ApplyTestFilter(options.testfilter, |
| 388 _ParseBotList(trybots, None)) |
| 389 else: |
| 390 # PRESUBMIT.py sent us a new-style (bot, set()) specification. |
| 391 bot_spec = _ApplyTestFilter(options.testfilter, trybots) |
| 392 |
| 393 except ImportError: |
| 394 pass |
| 395 |
| 396 return bot_spec |
| 397 |
| 398 |
| 399 def _ParseSendChangeOptions(bot_spec, options): |
318 """Parse common options passed to _SendChangeHTTP and _SendChangeSVN.""" | 400 """Parse common options passed to _SendChangeHTTP and _SendChangeSVN.""" |
319 values = [ | 401 values = [ |
320 ('user', options.user), | 402 ('user', options.user), |
321 ('name', options.name), | 403 ('name', options.name), |
322 ] | 404 ] |
323 if options.email: | 405 if options.email: |
324 values.append(('email', options.email)) | 406 values.append(('email', options.email)) |
325 if options.revision: | 407 if options.revision: |
326 values.append(('revision', options.revision)) | 408 values.append(('revision', options.revision)) |
327 if options.clobber: | 409 if options.clobber: |
328 values.append(('clobber', 'true')) | 410 values.append(('clobber', 'true')) |
329 if options.root: | 411 if options.root: |
330 values.append(('root', options.root)) | 412 values.append(('root', options.root)) |
331 if options.patchlevel: | 413 if options.patchlevel: |
332 values.append(('patchlevel', options.patchlevel)) | 414 values.append(('patchlevel', options.patchlevel)) |
333 if options.issue: | 415 if options.issue: |
334 values.append(('issue', options.issue)) | 416 values.append(('issue', options.issue)) |
335 if options.patchset: | 417 if options.patchset: |
336 values.append(('patchset', options.patchset)) | 418 values.append(('patchset', options.patchset)) |
337 if options.target: | 419 if options.target: |
338 values.append(('target', options.target)) | 420 values.append(('target', options.target)) |
339 if options.project: | 421 if options.project: |
340 values.append(('project', options.project)) | 422 values.append(('project', options.project)) |
341 | 423 |
342 filters = ','.join(options.testfilter) | 424 for bot, tests in bot_spec: |
343 if filters: | 425 values.append(('bot', ('%s:%s' % (bot, ','.join(tests))))) |
344 for botlist in options.bot: | 426 |
345 for bot in botlist.split(','): | |
346 if ':' in bot: | |
347 raise ValueError( | |
348 'Can\'t use both --testfilter and --bot builder:test formats ' | |
349 'at the same time') | |
350 else: | |
351 values.append(('bot', '%s:%s' % (bot, filters))) | |
352 else: | |
353 for bot in options.bot: | |
354 values.append(('bot', bot)) | |
355 return values | 427 return values |
356 | 428 |
357 | 429 |
358 def _SendChangeHTTP(options): | 430 def _SendChangeHTTP(bot_spec, options): |
359 """Send a change to the try server using the HTTP protocol.""" | 431 """Send a change to the try server using the HTTP protocol.""" |
360 if not options.host: | 432 if not options.host: |
361 raise NoTryServerAccess('Please use the --host option to specify the try ' | 433 raise NoTryServerAccess('Please use the --host option to specify the try ' |
362 'server host to connect to.') | 434 'server host to connect to.') |
363 if not options.port: | 435 if not options.port: |
364 raise NoTryServerAccess('Please use the --port option to specify the try ' | 436 raise NoTryServerAccess('Please use the --port option to specify the try ' |
365 'server port to connect to.') | 437 'server port to connect to.') |
366 | 438 |
367 values = _ParseSendChangeOptions(options) | 439 values = _ParseSendChangeOptions(bot_spec, options) |
368 values.append(('patch', options.diff)) | 440 values.append(('patch', options.diff)) |
369 | 441 |
370 url = 'http://%s:%s/send_try_patch' % (options.host, options.port) | 442 url = 'http://%s:%s/send_try_patch' % (options.host, options.port) |
371 proxies = None | 443 proxies = None |
372 if options.proxy: | 444 if options.proxy: |
373 if options.proxy.lower() == 'none': | 445 if options.proxy.lower() == 'none': |
374 # Effectively disable HTTP_PROXY or Internet settings proxy setup. | 446 # Effectively disable HTTP_PROXY or Internet settings proxy setup. |
375 proxies = {} | 447 proxies = {} |
376 else: | 448 else: |
377 proxies = {'http': options.proxy, 'https': options.proxy} | 449 proxies = {'http': options.proxy, 'https': options.proxy} |
378 | 450 |
379 logging.info('Sending by HTTP') | 451 logging.info('Sending by HTTP') |
380 logging.info(''.join("%s=%s\n" % (k, v) for k, v in values)) | 452 logging.info(''.join("%s=%s\n" % (k, v) for k, v in values)) |
381 logging.info(url) | 453 logging.info(url) |
382 logging.info(options.diff) | 454 logging.info(options.diff) |
383 if options.dry_run: | 455 if options.dry_run: |
384 return | 456 return |
385 | 457 |
386 try: | 458 try: |
387 logging.info('Opening connection...') | 459 logging.info('Opening connection...') |
388 connection = urllib.urlopen(url, urllib.urlencode(values), proxies=proxies) | 460 connection = urllib.urlopen(url, urllib.urlencode(values), proxies=proxies) |
389 logging.info('Done') | 461 logging.info('Done') |
390 except IOError, e: | 462 except IOError, e: |
391 logging.info(str(e)) | 463 logging.info(str(e)) |
392 if options.bot and len(e.args) > 2 and e.args[2] == 'got a bad status line': | 464 if bot_spec and len(e.args) > 2 and e.args[2] == 'got a bad status line': |
393 raise NoTryServerAccess('%s is unaccessible. Bad --bot argument?' % url) | 465 raise NoTryServerAccess('%s is unaccessible. Bad --bot argument?' % url) |
394 else: | 466 else: |
395 raise NoTryServerAccess('%s is unaccessible. Reason: %s' % (url, | 467 raise NoTryServerAccess('%s is unaccessible. Reason: %s' % (url, |
396 str(e.args))) | 468 str(e.args))) |
397 if not connection: | 469 if not connection: |
398 raise NoTryServerAccess('%s is unaccessible.' % url) | 470 raise NoTryServerAccess('%s is unaccessible.' % url) |
399 logging.info('Reading response...') | 471 logging.info('Reading response...') |
400 response = connection.read() | 472 response = connection.read() |
401 logging.info('Done') | 473 logging.info('Done') |
402 if response != 'OK': | 474 if response != 'OK': |
403 raise NoTryServerAccess('%s is unaccessible. Got:\n%s' % (url, response)) | 475 raise NoTryServerAccess('%s is unaccessible. Got:\n%s' % (url, response)) |
404 | 476 |
405 | 477 |
406 def _SendChangeSVN(options): | 478 def _SendChangeSVN(bot_spec, options): |
407 """Send a change to the try server by committing a diff file on a subversion | 479 """Send a change to the try server by committing a diff file on a subversion |
408 server.""" | 480 server.""" |
409 if not options.svn_repo: | 481 if not options.svn_repo: |
410 raise NoTryServerAccess('Please use the --svn_repo option to specify the' | 482 raise NoTryServerAccess('Please use the --svn_repo option to specify the' |
411 ' try server svn repository to connect to.') | 483 ' try server svn repository to connect to.') |
412 | 484 |
413 values = _ParseSendChangeOptions(options) | 485 values = _ParseSendChangeOptions(bot_spec, options) |
414 description = ''.join("%s=%s\n" % (k, v) for k, v in values) | 486 description = ''.join("%s=%s\n" % (k, v) for k, v in values) |
415 logging.info('Sending by SVN') | 487 logging.info('Sending by SVN') |
416 logging.info(description) | 488 logging.info(description) |
417 logging.info(options.svn_repo) | 489 logging.info(options.svn_repo) |
418 logging.info(options.diff) | 490 logging.info(options.diff) |
419 if options.dry_run: | 491 if options.dry_run: |
420 return | 492 return |
421 | 493 |
422 # Create a temporary directory, put a uniquely named file in it with the diff | 494 # Create a temporary directory, put a uniquely named file in it with the diff |
423 # content and svn import that. | 495 # content and svn import that. |
(...skipping 29 matching lines...) Expand all Loading... |
453 command.append('--no-ignore') | 525 command.append('--no-ignore') |
454 | 526 |
455 subprocess2.check_call(command) | 527 subprocess2.check_call(command) |
456 except subprocess2.CalledProcessError, e: | 528 except subprocess2.CalledProcessError, e: |
457 raise NoTryServerAccess(str(e)) | 529 raise NoTryServerAccess(str(e)) |
458 finally: | 530 finally: |
459 temp_file.close() | 531 temp_file.close() |
460 shutil.rmtree(temp_dir, True) | 532 shutil.rmtree(temp_dir, True) |
461 | 533 |
462 | 534 |
463 def PrintSuccess(options): | 535 def PrintSuccess(bot_spec, options): |
464 if not options.dry_run: | 536 if not options.dry_run: |
465 text = 'Patch \'%s\' sent to try server' % options.name | 537 text = 'Patch \'%s\' sent to try server' % options.name |
466 if options.bot: | 538 if bot_spec: |
467 text += ': %s' % ', '.join(options.bot) | 539 text += ': %s' % ', '.join( |
| 540 '%s:%s' % (b[0], ','.join(b[1])) for b in bot_spec) |
468 print(text) | 541 print(text) |
469 | 542 |
470 | 543 |
471 def GuessVCS(options, path, file_list): | 544 def GuessVCS(options, path, file_list): |
472 """Helper to guess the version control system. | 545 """Helper to guess the version control system. |
473 | 546 |
474 NOTE: Very similar to upload.GuessVCS. Doesn't look for hg since we don't | 547 NOTE: Very similar to upload.GuessVCS. Doesn't look for hg since we don't |
475 support it yet. | 548 support it yet. |
476 | 549 |
477 This examines the path directory, guesses which SCM we're using, and | 550 This examines the path directory, guesses which SCM we're using, and |
(...skipping 326 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
804 options.name = 'Issue %s' % options.issue | 877 options.name = 'Issue %s' % options.issue |
805 else: | 878 else: |
806 options.name = 'Unnamed' | 879 options.name = 'Unnamed' |
807 print('Note: use --name NAME to change the try job name.') | 880 print('Note: use --name NAME to change the try job name.') |
808 | 881 |
809 if not options.email: | 882 if not options.email: |
810 parser.error('Using an anonymous checkout. Please use --email or set ' | 883 parser.error('Using an anonymous checkout. Please use --email or set ' |
811 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment variable.') | 884 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment variable.') |
812 print('Results will be emailed to: ' + options.email) | 885 print('Results will be emailed to: ' + options.email) |
813 | 886 |
814 if not options.bot: | 887 if options.bot: |
815 # Get try slaves from PRESUBMIT.py files if not specified. | 888 bot_spec = _ApplyTestFilter( |
816 # Even if the diff comes from options.url, use the local checkout for bot | 889 options.testfilter, _ParseBotList(options.bot, options.testfilter)) |
817 # selection. | 890 else: |
818 try: | 891 bot_spec = _GenTSBotSpec(checkouts, change, changed_files, options) |
819 import presubmit_support | |
820 root_presubmit = checkouts[0].ReadRootFile('PRESUBMIT.py') | |
821 if not change: | |
822 if not changed_files: | |
823 changed_files = checkouts[0].file_tuples | |
824 change = presubmit_support.Change(options.name, | |
825 '', | |
826 checkouts[0].checkout_root, | |
827 changed_files, | |
828 options.issue, | |
829 options.patchset, | |
830 options.email) | |
831 options.bot = presubmit_support.DoGetTrySlaves( | |
832 change, | |
833 checkouts[0].GetFileNames(), | |
834 checkouts[0].checkout_root, | |
835 root_presubmit, | |
836 options.project, | |
837 options.verbose, | |
838 sys.stdout) | |
839 except ImportError: | |
840 pass | |
841 if options.testfilter: | |
842 bots = set() | |
843 for bot in options.bot: | |
844 assert ',' not in bot | |
845 if bot.endswith(':compile'): | |
846 # Skip over compile-only builders for now. | |
847 continue | |
848 bots.add(bot.split(':', 1)[0]) | |
849 options.bot = list(bots) | |
850 | 892 |
851 # If no bot is specified, either the default pool will be selected or the | 893 if options.testfilter: |
852 # try server will refuse the job. Either case we don't need to interfere. | 894 bot_spec = _ApplyTestFilter(options.testfilter, bot_spec) |
853 | 895 |
854 if any('triggered' in b.split(':', 1)[0] for b in options.bot): | 896 if any('triggered' in b[0] for b in bot_spec): |
855 print >> sys.stderr, ( | 897 print >> sys.stderr, ( |
856 'ERROR You are trying to send a job to a triggered bot. This type of' | 898 'ERROR You are trying to send a job to a triggered bot. This type of' |
857 ' bot requires an\ninitial job from a parent (usually a builder). ' | 899 ' bot requires an\ninitial job from a parent (usually a builder). ' |
858 'Instead send your job to the parent.\nBot list: %s' % options.bot) | 900 'Instead send your job to the parent.\nBot list: %s' % bot_spec) |
859 return 1 | 901 return 1 |
860 | 902 |
861 if options.print_bots: | 903 if options.print_bots: |
862 print 'Bots which would be used:' | 904 print 'Bots which would be used:' |
863 for bot in options.bot: | 905 for bot in bot_spec: |
864 print ' %s' % bot | 906 if bot[1]: |
| 907 print ' %s:%s' % (bot[0], ','.join(bot[1])) |
| 908 else: |
| 909 print ' %s' % (bot[0]) |
865 return 0 | 910 return 0 |
866 | 911 |
867 # Send the patch. | 912 # Send the patch. |
868 if options.send_patch: | 913 if options.send_patch: |
869 # If forced. | 914 # If forced. |
870 options.send_patch(options) | 915 options.send_patch(bot_spec, options) |
871 PrintSuccess(options) | 916 PrintSuccess(bot_spec, options) |
872 return 0 | 917 return 0 |
873 try: | 918 try: |
874 if can_http: | 919 if can_http: |
875 _SendChangeHTTP(options) | 920 _SendChangeHTTP(bot_spec, options) |
876 PrintSuccess(options) | 921 PrintSuccess(bot_spec, options) |
877 return 0 | 922 return 0 |
878 except NoTryServerAccess: | 923 except NoTryServerAccess: |
879 if not can_svn: | 924 if not can_svn: |
880 raise | 925 raise |
881 _SendChangeSVN(options) | 926 _SendChangeSVN(bot_spec, options) |
882 PrintSuccess(options) | 927 PrintSuccess(bot_spec, options) |
883 return 0 | 928 return 0 |
884 except (InvalidScript, NoTryServerAccess), e: | 929 except (InvalidScript, NoTryServerAccess), e: |
885 if swallow_exception: | 930 if swallow_exception: |
886 return 1 | 931 return 1 |
887 print >> sys.stderr, e | 932 print >> sys.stderr, e |
888 return 1 | 933 return 1 |
889 except (gclient_utils.Error, subprocess2.CalledProcessError), e: | 934 except (gclient_utils.Error, subprocess2.CalledProcessError), e: |
890 print >> sys.stderr, e | 935 print >> sys.stderr, e |
891 return 1 | 936 return 1 |
892 return 0 | 937 return 0 |
893 | 938 |
894 | 939 |
895 if __name__ == "__main__": | 940 if __name__ == "__main__": |
896 fix_encoding.fix_encoding() | 941 fix_encoding.fix_encoding() |
897 sys.exit(TryChange(None, None, False)) | 942 sys.exit(TryChange(None, None, False)) |
OLD | NEW |