| OLD | NEW |
| 1 # Copyright (c) 2010 Google Inc. All rights reserved. | 1 # Copyright (c) 2010 Google Inc. All rights reserved. |
| 2 # | 2 # |
| 3 # Redistribution and use in source and binary forms, with or without | 3 # Redistribution and use in source and binary forms, with or without |
| 4 # modification, are permitted provided that the following conditions are | 4 # modification, are permitted provided that the following conditions are |
| 5 # met: | 5 # met: |
| 6 # | 6 # |
| 7 # * Redistributions of source code must retain the above copyright | 7 # * Redistributions of source code must retain the above copyright |
| 8 # notice, this list of conditions and the following disclaimer. | 8 # notice, this list of conditions and the following disclaimer. |
| 9 # * Redistributions in binary form must reproduce the above | 9 # * Redistributions in binary form must reproduce the above |
| 10 # copyright notice, this list of conditions and the following disclaimer | 10 # copyright notice, this list of conditions and the following disclaimer |
| (...skipping 12 matching lines...) Expand all Loading... |
| 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 26 # (INCLUDING NEGLIGENCE OR/ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 26 # (INCLUDING NEGLIGENCE OR/ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 28 | 28 |
| 29 from __future__ import print_function | 29 from __future__ import print_function |
| 30 import json | 30 import json |
| 31 import logging | 31 import logging |
| 32 import optparse | 32 import optparse |
| 33 import re | |
| 34 import sys | 33 import sys |
| 35 import time | |
| 36 import traceback | 34 import traceback |
| 37 import urllib2 | |
| 38 | 35 |
| 39 from webkitpy.common.memoized import memoized | 36 from webkitpy.common.memoized import memoized |
| 40 from webkitpy.common.net.buildbot import Build | 37 from webkitpy.common.net.buildbot import Build |
| 41 from webkitpy.common.system.executive import ScriptError | 38 from webkitpy.common.system.executive import ScriptError |
| 42 from webkitpy.layout_tests.models.test_expectations import TestExpectations, BAS
ELINE_SUFFIX_LIST, SKIP | 39 from webkitpy.layout_tests.models.test_expectations import TestExpectations, BAS
ELINE_SUFFIX_LIST, SKIP |
| 43 from webkitpy.layout_tests.port import factory | 40 from webkitpy.layout_tests.port import factory |
| 44 from webkitpy.tool.commands.command import Command | 41 from webkitpy.tool.commands.command import Command |
| 45 | 42 |
| 46 | 43 |
| 47 _log = logging.getLogger(__name__) | 44 _log = logging.getLogger(__name__) |
| (...skipping 587 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 635 for test in args: | 632 for test in args: |
| 636 if test not in test_prefix_list: | 633 if test not in test_prefix_list: |
| 637 test_prefix_list[test] = {} | 634 test_prefix_list[test] = {} |
| 638 build = Build(builder) | 635 build = Build(builder) |
| 639 test_prefix_list[test][build] = suffixes_to_update | 636 test_prefix_list[test][build] = suffixes_to_update |
| 640 | 637 |
| 641 if options.verbose: | 638 if options.verbose: |
| 642 _log.debug("rebaseline-json: " + str(test_prefix_list)) | 639 _log.debug("rebaseline-json: " + str(test_prefix_list)) |
| 643 | 640 |
| 644 self._rebaseline(options, test_prefix_list) | 641 self._rebaseline(options, test_prefix_list) |
| 645 | |
| 646 | |
| 647 class AutoRebaseline(AbstractParallelRebaselineCommand): | |
| 648 name = "auto-rebaseline" | |
| 649 help_text = "Rebaselines any NeedsRebaseline lines in TestExpectations that
have cycled through all the bots." | |
| 650 AUTO_REBASELINE_BRANCH_NAME = "auto-rebaseline-temporary-branch" | |
| 651 AUTO_REBASELINE_ALT_BRANCH_NAME = "auto-rebaseline-alt-temporary-branch" | |
| 652 | |
| 653 # Rietveld uploader stinks. Limit the number of rebaselines in a given patch
to keep upload from failing. | |
| 654 # FIXME: http://crbug.com/263676 Obviously we should fix the uploader here. | |
| 655 MAX_LINES_TO_REBASELINE = 200 | |
| 656 | |
| 657 SECONDS_BEFORE_GIVING_UP = 300 | |
| 658 | |
| 659 def __init__(self): | |
| 660 super(AutoRebaseline, self).__init__(options=[ | |
| 661 # FIXME: Remove this option. | |
| 662 self.no_optimize_option, | |
| 663 # FIXME: Remove this option. | |
| 664 self.results_directory_option, | |
| 665 optparse.make_option("--auth-refresh-token-json", help="Rietveld aut
h refresh JSON token."), | |
| 666 optparse.make_option("--dry-run", action='store_true', default=False
, | |
| 667 help='Run without creating a temporary branch,
committing locally, or uploading/landing ' | |
| 668 'changes to the remote repository.') | |
| 669 ]) | |
| 670 self._blame_regex = re.compile(r""" | |
| 671 ^(\S*) # Commit hash | |
| 672 [^(]* \( # Whitespace and open parenthesis | |
| 673 < # Email address is surrounded by <> | |
| 674 ( | |
| 675 [^@]+ # Username preceding @ | |
| 676 @ | |
| 677 [^@>]+ # Domain terminated by @ or >, some lines have an ad
ditional @ fragment after the email. | |
| 678 ) | |
| 679 .*?([^ ]*) # Test file name | |
| 680 \ \[ # Single space followed by opening [ for expectation
specifier | |
| 681 [^[]*$ # Prevents matching previous [ for version specifier
s instead of expectation specifiers | |
| 682 """, re.VERBOSE) | |
| 683 | |
| 684 def bot_revision_data(self, scm): | |
| 685 revisions = [] | |
| 686 for result in self.build_data().values(): | |
| 687 if result.run_was_interrupted(): | |
| 688 _log.error("Can't rebaseline because the latest run on %s exited
early.", result.builder_name()) | |
| 689 return [] | |
| 690 revisions.append({ | |
| 691 "builder": result.builder_name(), | |
| 692 "revision": result.chromium_revision(scm), | |
| 693 }) | |
| 694 return revisions | |
| 695 | |
| 696 def _strip_comments(self, line): | |
| 697 comment_index = line.find("#") | |
| 698 if comment_index == -1: | |
| 699 comment_index = len(line) | |
| 700 return re.sub(r"\s+", " ", line[:comment_index].strip()) | |
| 701 | |
| 702 def tests_to_rebaseline(self, tool, min_revision, print_revisions): | |
| 703 port = tool.port_factory.get() | |
| 704 expectations_file_path = port.path_to_generic_test_expectations_file() | |
| 705 | |
| 706 tests = set() | |
| 707 revision = None | |
| 708 commit = None | |
| 709 author = None | |
| 710 bugs = set() | |
| 711 has_any_needs_rebaseline_lines = False | |
| 712 | |
| 713 for line in tool.scm().blame(expectations_file_path).split("\n"): | |
| 714 line = self._strip_comments(line) | |
| 715 if "NeedsRebaseline" not in line: | |
| 716 continue | |
| 717 | |
| 718 has_any_needs_rebaseline_lines = True | |
| 719 | |
| 720 parsed_line = self._blame_regex.match(line) | |
| 721 if not parsed_line: | |
| 722 # Deal gracefully with inability to parse blame info for a line
in TestExpectations. | |
| 723 # Parsing could fail if for example during local debugging the d
eveloper modifies | |
| 724 # TestExpectations and does not commit. | |
| 725 _log.info("Couldn't find blame info for expectations line, skipp
ing [line=%s].", line) | |
| 726 continue | |
| 727 | |
| 728 commit_hash = parsed_line.group(1) | |
| 729 commit_position = tool.scm().commit_position_from_git_commit(commit_
hash) | |
| 730 | |
| 731 test = parsed_line.group(3) | |
| 732 if print_revisions: | |
| 733 _log.info("%s is waiting for r%s", test, commit_position) | |
| 734 | |
| 735 if not commit_position or commit_position > min_revision: | |
| 736 continue | |
| 737 | |
| 738 if revision and commit_position != revision: | |
| 739 continue | |
| 740 | |
| 741 if not revision: | |
| 742 revision = commit_position | |
| 743 commit = commit_hash | |
| 744 author = parsed_line.group(2) | |
| 745 | |
| 746 bugs.update(re.findall(r"crbug\.com\/(\d+)", line)) | |
| 747 tests.add(test) | |
| 748 | |
| 749 if len(tests) >= self.MAX_LINES_TO_REBASELINE: | |
| 750 _log.info("Too many tests to rebaseline in one patch. Doing the
first %d.", self.MAX_LINES_TO_REBASELINE) | |
| 751 break | |
| 752 | |
| 753 return tests, revision, commit, author, bugs, has_any_needs_rebaseline_l
ines | |
| 754 | |
| 755 def link_to_patch(self, commit): | |
| 756 return "https://chromium.googlesource.com/chromium/src/+/" + commit | |
| 757 | |
| 758 def commit_message(self, author, revision, commit, bugs): | |
| 759 bug_string = "" | |
| 760 if bugs: | |
| 761 bug_string = "BUG=%s\n" % ",".join(bugs) | |
| 762 | |
| 763 return """Auto-rebaseline for r%s | |
| 764 | |
| 765 %s | |
| 766 | |
| 767 %sTBR=%s | |
| 768 """ % (revision, self.link_to_patch(commit), bug_string, author) | |
| 769 | |
| 770 def get_test_prefix_list(self, tests): | |
| 771 test_prefix_list = {} | |
| 772 lines_to_remove = {} | |
| 773 | |
| 774 for builder_name in self._release_builders(): | |
| 775 port_name = self._tool.builders.port_name_for_builder_name(builder_n
ame) | |
| 776 port = self._tool.port_factory.get(port_name) | |
| 777 expectations = TestExpectations(port, include_overrides=True) | |
| 778 for test in expectations.get_needs_rebaseline_failures(): | |
| 779 if test not in tests: | |
| 780 continue | |
| 781 | |
| 782 if test not in test_prefix_list: | |
| 783 lines_to_remove[test] = [] | |
| 784 test_prefix_list[test] = {} | |
| 785 lines_to_remove[test].append(builder_name) | |
| 786 test_prefix_list[test][Build(builder_name)] = BASELINE_SUFFIX_LI
ST | |
| 787 | |
| 788 return test_prefix_list, lines_to_remove | |
| 789 | |
| 790 def _run_git_cl_command(self, options, command): | |
| 791 subprocess_command = ['git', 'cl'] + command | |
| 792 if options.verbose: | |
| 793 subprocess_command.append('--verbose') | |
| 794 if options.auth_refresh_token_json: | |
| 795 subprocess_command.append('--auth-refresh-token-json') | |
| 796 subprocess_command.append(options.auth_refresh_token_json) | |
| 797 | |
| 798 process = self._tool.executive.popen(subprocess_command, stdout=self._to
ol.executive.PIPE, | |
| 799 stderr=self._tool.executive.STDOUT) | |
| 800 last_output_time = time.time() | |
| 801 | |
| 802 # git cl sometimes completely hangs. Bail if we haven't gotten any outpu
t to stdout/stderr in a while. | |
| 803 while process.poll() is None and time.time() < last_output_time + self.S
ECONDS_BEFORE_GIVING_UP: | |
| 804 # FIXME: This doesn't make any sense. readline blocks, so all this c
ode to | |
| 805 # try and bail is useless. Instead, we should do the readline calls
on a | |
| 806 # subthread. Then the rest of this code would make sense. | |
| 807 out = process.stdout.readline().rstrip('\n') | |
| 808 if out: | |
| 809 last_output_time = time.time() | |
| 810 _log.info(out) | |
| 811 | |
| 812 if process.poll() is None: | |
| 813 _log.error('Command hung: %s', subprocess_command) | |
| 814 return False | |
| 815 return True | |
| 816 | |
| 817 # FIXME: Move this somewhere more general. | |
| 818 def tree_status(self): | |
| 819 blink_tree_status_url = "http://chromium-status.appspot.com/status" | |
| 820 status = urllib2.urlopen(blink_tree_status_url).read().lower() | |
| 821 if 'closed' in status or status == "0": | |
| 822 return 'closed' | |
| 823 elif 'open' in status or status == "1": | |
| 824 return 'open' | |
| 825 return 'unknown' | |
| 826 | |
| 827 def execute(self, options, args, tool): | |
| 828 if tool.scm().executable_name == "svn": | |
| 829 _log.error("Auto rebaseline only works with a git checkout.") | |
| 830 return | |
| 831 | |
| 832 if not options.dry_run and tool.scm().has_working_directory_changes(): | |
| 833 _log.error("Cannot proceed with working directory changes. Clean wor
king directory first.") | |
| 834 return | |
| 835 | |
| 836 revision_data = self.bot_revision_data(tool.scm()) | |
| 837 if not revision_data: | |
| 838 return | |
| 839 | |
| 840 min_revision = int(min([item["revision"] for item in revision_data])) | |
| 841 tests, revision, commit, author, bugs, _ = self.tests_to_rebaseline( | |
| 842 tool, min_revision, print_revisions=options.verbose) | |
| 843 | |
| 844 if options.verbose: | |
| 845 _log.info("Min revision across all bots is %s.", min_revision) | |
| 846 for item in revision_data: | |
| 847 _log.info("%s: r%s", item["builder"], item["revision"]) | |
| 848 | |
| 849 if not tests: | |
| 850 _log.debug('No tests to rebaseline.') | |
| 851 return | |
| 852 | |
| 853 if self.tree_status() == 'closed': | |
| 854 _log.info('Cannot proceed. Tree is closed.') | |
| 855 return | |
| 856 | |
| 857 _log.info('Rebaselining %s for r%s by %s.', list(tests), revision, autho
r) | |
| 858 | |
| 859 test_prefix_list, _ = self.get_test_prefix_list(tests) | |
| 860 | |
| 861 did_switch_branches = False | |
| 862 did_finish = False | |
| 863 old_branch_name_or_ref = '' | |
| 864 rebaseline_branch_name = self.AUTO_REBASELINE_BRANCH_NAME | |
| 865 try: | |
| 866 # Save the current branch name and check out a clean branch for the
patch. | |
| 867 old_branch_name_or_ref = tool.scm().current_branch_or_ref() | |
| 868 if old_branch_name_or_ref == self.AUTO_REBASELINE_BRANCH_NAME: | |
| 869 rebaseline_branch_name = self.AUTO_REBASELINE_ALT_BRANCH_NAME | |
| 870 if not options.dry_run: | |
| 871 tool.scm().delete_branch(rebaseline_branch_name) | |
| 872 tool.scm().create_clean_branch(rebaseline_branch_name) | |
| 873 did_switch_branches = True | |
| 874 | |
| 875 if test_prefix_list: | |
| 876 self._rebaseline(options, test_prefix_list) | |
| 877 | |
| 878 if options.dry_run: | |
| 879 return | |
| 880 | |
| 881 tool.scm().commit_locally_with_message( | |
| 882 self.commit_message(author, revision, commit, bugs)) | |
| 883 | |
| 884 # FIXME: It would be nice if we could dcommit the patch without uplo
ading, but still | |
| 885 # go through all the precommit hooks. For rebaselines with lots of f
iles, uploading | |
| 886 # takes a long time and sometimes fails, but we don't want to commit
if, e.g. the | |
| 887 # tree is closed. | |
| 888 did_finish = self._run_git_cl_command(options, ['upload', '-f']) | |
| 889 | |
| 890 if did_finish: | |
| 891 # Uploading can take a very long time. Do another pull to make s
ure TestExpectations is up to date, | |
| 892 # so the dcommit can go through. | |
| 893 # FIXME: Log the pull and dcommit stdout/stderr to the log-serve
r. | |
| 894 tool.executive.run_command(['git', 'pull']) | |
| 895 | |
| 896 self._run_git_cl_command(options, ['land', '-f', '-v']) | |
| 897 except Exception: | |
| 898 traceback.print_exc(file=sys.stderr) | |
| 899 finally: | |
| 900 if did_switch_branches: | |
| 901 if did_finish: | |
| 902 # Close the issue if dcommit failed. | |
| 903 issue_already_closed = tool.executive.run_command( | |
| 904 ['git', 'config', 'branch.%s.rietveldissue' % rebaseline
_branch_name], | |
| 905 return_exit_code=True) | |
| 906 if not issue_already_closed: | |
| 907 self._run_git_cl_command(options, ['set_close']) | |
| 908 | |
| 909 tool.scm().ensure_cleanly_tracking_remote_master() | |
| 910 if old_branch_name_or_ref: | |
| 911 tool.scm().checkout_branch(old_branch_name_or_ref) | |
| 912 tool.scm().delete_branch(rebaseline_branch_name) | |
| OLD | NEW |