| 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 """Snapshot Build Bisect Tool | 6 """Snapshot Build Bisect Tool |
| 7 | 7 |
| 8 This script bisects a snapshot archive using binary search. It starts at | 8 This script bisects a snapshot archive using binary search. It starts at |
| 9 a bad revision (it will try to guess HEAD) and asks for a last known-good | 9 a bad revision (it will try to guess HEAD) and asks for a last known-good |
| 10 revision. It will then binary search across this revision range by downloading, | 10 revision. It will then binary search across this revision range by downloading, |
| (...skipping 298 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 309 # until an entire directory listing is done. | 309 # until an entire directory listing is done. |
| 310 while next_marker: | 310 while next_marker: |
| 311 next_url = self.GetListingURL(next_marker) | 311 next_url = self.GetListingURL(next_marker) |
| 312 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url) | 312 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url) |
| 313 revisions.extend(new_revisions) | 313 revisions.extend(new_revisions) |
| 314 self.githash_svn_dict.update(new_dict) | 314 self.githash_svn_dict.update(new_dict) |
| 315 return revisions | 315 return revisions |
| 316 | 316 |
| 317 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot): | 317 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot): |
| 318 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1 | 318 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1 |
| 319 try: | 319 response = urllib.urlopen(json_url) |
| 320 response = urllib.urlopen(json_url) | 320 if response.getcode() == 200: |
| 321 except urllib.HTTPError as error: | 321 try: |
| 322 msg = 'HTTP Error %d for %s' % (error.getcode(), git_sha1) | 322 data = json.loads(response.read()[4:]) |
| 323 return None | 323 except ValueError: |
| 324 data = json.loads(response.read()[4:]) | 324 print 'ValueError for JSON URL: %s' % json_url |
| 325 raise ValueError |
| 326 else: |
| 327 raise ValueError |
| 325 if 'message' in data: | 328 if 'message' in data: |
| 326 message = data['message'].split('\n') | 329 message = data['message'].split('\n') |
| 327 message = [line for line in message if line.strip()] | 330 message = [line for line in message if line.strip()] |
| 328 search_pattern = re.compile(SEARCH_PATTERN[depot]) | 331 search_pattern = re.compile(SEARCH_PATTERN[depot]) |
| 329 result = search_pattern.search(message[len(message)-1]) | 332 result = search_pattern.search(message[len(message)-1]) |
| 330 if result: | 333 if result: |
| 331 return result.group(1) | 334 return result.group(1) |
| 332 print 'Failed to get svn revision number for %s' % git_sha1 | 335 print 'Failed to get svn revision number for %s' % git_sha1 |
| 333 raise ValueError | 336 raise ValueError |
| 334 | 337 |
| (...skipping 294 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 629 | 632 |
| 630 def WaitFor(self): | 633 def WaitFor(self): |
| 631 """Prints a message and waits for the download to complete. The download | 634 """Prints a message and waits for the download to complete. The download |
| 632 must have been started previously.""" | 635 must have been started previously.""" |
| 633 assert self.thread, 'DownloadJob must be started before WaitFor is called.' | 636 assert self.thread, 'DownloadJob must be started before WaitFor is called.' |
| 634 print 'Downloading revision %s...' % str(self.rev) | 637 print 'Downloading revision %s...' % str(self.rev) |
| 635 self.progress_event.set() # Display progress of download. | 638 self.progress_event.set() # Display progress of download. |
| 636 self.thread.join() | 639 self.thread.join() |
| 637 | 640 |
| 638 | 641 |
| 639 def Bisect(base_url, | 642 def Bisect(context, |
| 640 platform, | |
| 641 official_builds, | |
| 642 is_aura, | |
| 643 is_asan, | |
| 644 use_local_repo, | |
| 645 good_rev=0, | |
| 646 bad_rev=0, | |
| 647 num_runs=1, | 643 num_runs=1, |
| 648 command='%p %a', | 644 command='%p %a', |
| 649 try_args=(), | 645 try_args=(), |
| 650 profile=None, | 646 profile=None, |
| 651 flash_path=None, | |
| 652 pdf_path=None, | |
| 653 interactive=True, | 647 interactive=True, |
| 654 evaluate=AskIsGoodBuild): | 648 evaluate=AskIsGoodBuild): |
| 655 """Given known good and known bad revisions, run a binary search on all | 649 """Given known good and known bad revisions, run a binary search on all |
| 656 archived revisions to determine the last known good revision. | 650 archived revisions to determine the last known good revision. |
| 657 | 651 |
| 658 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). | 652 @param context PathContext object initialized with user provided parameters. |
| 659 @param official_builds Specify build type (Chromium or Official build). | |
| 660 @param good_rev Number/tag of the known good revision. | |
| 661 @param bad_rev Number/tag of the known bad revision. | |
| 662 @param num_runs Number of times to run each build for asking good/bad. | 653 @param num_runs Number of times to run each build for asking good/bad. |
| 663 @param try_args A tuple of arguments to pass to the test application. | 654 @param try_args A tuple of arguments to pass to the test application. |
| 664 @param profile The name of the user profile to run with. | 655 @param profile The name of the user profile to run with. |
| 665 @param interactive If it is false, use command exit code for good or bad | 656 @param interactive If it is false, use command exit code for good or bad |
| 666 judgment of the argument build. | 657 judgment of the argument build. |
| 667 @param evaluate A function which returns 'g' if the argument build is good, | 658 @param evaluate A function which returns 'g' if the argument build is good, |
| 668 'b' if it's bad or 'u' if unknown. | 659 'b' if it's bad or 'u' if unknown. |
| 669 | 660 |
| 670 Threading is used to fetch Chromium revisions in the background, speeding up | 661 Threading is used to fetch Chromium revisions in the background, speeding up |
| 671 the user's experience. For example, suppose the bounds of the search are | 662 the user's experience. For example, suppose the bounds of the search are |
| 672 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on | 663 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on |
| 673 whether revision 50 is good or bad, the next revision to check will be either | 664 whether revision 50 is good or bad, the next revision to check will be either |
| 674 25 or 75. So, while revision 50 is being checked, the script will download | 665 25 or 75. So, while revision 50 is being checked, the script will download |
| 675 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is | 666 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is |
| 676 known: | 667 known: |
| 677 | 668 |
| 678 - If rev 50 is good, the download of rev 25 is cancelled, and the next test | 669 - If rev 50 is good, the download of rev 25 is cancelled, and the next test |
| 679 is run on rev 75. | 670 is run on rev 75. |
| 680 | 671 |
| 681 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test | 672 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test |
| 682 is run on rev 25. | 673 is run on rev 25. |
| 683 """ | 674 """ |
| 684 | 675 |
| 685 if not profile: | 676 if not profile: |
| 686 profile = 'profile' | 677 profile = 'profile' |
| 687 | 678 |
| 688 context = PathContext(base_url, platform, good_rev, bad_rev, | 679 good_rev = context.good_revision |
| 689 official_builds, is_aura, is_asan, use_local_repo, | 680 bad_rev = context.bad_revision |
| 690 flash_path, pdf_path) | |
| 691 cwd = os.getcwd() | 681 cwd = os.getcwd() |
| 692 | 682 |
| 693 print 'Downloading list of known revisions...', | 683 print 'Downloading list of known revisions...', |
| 694 if not use_local_repo: | 684 if not context.use_local_repo: |
| 695 print '(use --use-local-repo for speed if you have a local checkout)' | 685 print '(use --use-local-repo for speed if you have a local checkout)' |
| 696 else: | 686 else: |
| 697 print | 687 print |
| 698 _GetDownloadPath = lambda rev: os.path.join(cwd, | 688 _GetDownloadPath = lambda rev: os.path.join(cwd, |
| 699 '%s-%s' % (str(rev), context.archive_name)) | 689 '%s-%s' % (str(rev), context.archive_name)) |
| 700 if official_builds: | 690 if context.is_official: |
| 701 revlist = context.GetOfficialBuildsList() | 691 revlist = context.GetOfficialBuildsList() |
| 702 else: | 692 else: |
| 703 revlist = context.GetRevList() | 693 revlist = context.GetRevList() |
| 704 | 694 |
| 705 # Get a list of revisions to bisect across. | 695 # Get a list of revisions to bisect across. |
| 706 if len(revlist) < 2: # Don't have enough builds to bisect. | 696 if len(revlist) < 2: # Don't have enough builds to bisect. |
| 707 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 697 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
| 708 raise RuntimeError(msg) | 698 raise RuntimeError(msg) |
| 709 | 699 |
| 710 # Figure out our bookends and first pivot point; fetch the pivot revision. | 700 # Figure out our bookends and first pivot point; fetch the pivot revision. |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 767 # other, as described in the comments above. | 757 # other, as described in the comments above. |
| 768 try: | 758 try: |
| 769 if not interactive: | 759 if not interactive: |
| 770 if status: | 760 if status: |
| 771 answer = 'b' | 761 answer = 'b' |
| 772 print 'Bad revision: %s' % rev | 762 print 'Bad revision: %s' % rev |
| 773 else: | 763 else: |
| 774 answer = 'g' | 764 answer = 'g' |
| 775 print 'Good revision: %s' % rev | 765 print 'Good revision: %s' % rev |
| 776 else: | 766 else: |
| 777 answer = evaluate(rev, official_builds, status, stdout, stderr) | 767 answer = evaluate(rev, context.is_official, status, stdout, stderr) |
| 778 if ((answer == 'g' and good_rev < bad_rev) | 768 if ((answer == 'g' and good_rev < bad_rev) |
| 779 or (answer == 'b' and bad_rev < good_rev)): | 769 or (answer == 'b' and bad_rev < good_rev)): |
| 780 fetch.Stop() | 770 fetch.Stop() |
| 781 minrev = pivot | 771 minrev = pivot |
| 782 if down_fetch: | 772 if down_fetch: |
| 783 down_fetch.Stop() # Kill the download of the older revision. | 773 down_fetch.Stop() # Kill the download of the older revision. |
| 784 fetch = None | 774 fetch = None |
| 785 if up_fetch: | 775 if up_fetch: |
| 786 up_fetch.WaitFor() | 776 up_fetch.WaitFor() |
| 787 pivot = up_pivot | 777 pivot = up_pivot |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 835 for f in [_GetDownloadPath(revlist[down_pivot]), | 825 for f in [_GetDownloadPath(revlist[down_pivot]), |
| 836 _GetDownloadPath(revlist[up_pivot])]: | 826 _GetDownloadPath(revlist[up_pivot])]: |
| 837 try: | 827 try: |
| 838 os.unlink(f) | 828 os.unlink(f) |
| 839 except OSError: | 829 except OSError: |
| 840 pass | 830 pass |
| 841 sys.exit(0) | 831 sys.exit(0) |
| 842 | 832 |
| 843 rev = revlist[pivot] | 833 rev = revlist[pivot] |
| 844 | 834 |
| 845 return (revlist[minrev], revlist[maxrev]) | 835 return (revlist[minrev], revlist[maxrev], context) |
| 846 | 836 |
| 847 | 837 |
| 848 def GetBlinkDEPSRevisionForChromiumRevision(rev): | 838 def GetBlinkDEPSRevisionForChromiumRevision(rev): |
| 849 """Returns the blink revision that was in REVISIONS file at | 839 """Returns the blink revision that was in REVISIONS file at |
| 850 chromium revision |rev|.""" | 840 chromium revision |rev|.""" |
| 851 # . doesn't match newlines without re.DOTALL, so this is safe. | 841 # . doesn't match newlines without re.DOTALL, so this is safe. |
| 852 blink_re = re.compile(r'webkit_revision\D*(\d+)') | 842 blink_re = re.compile(r'webkit_revision\D*(\d+)') |
| 853 url = urllib.urlopen(DEPS_FILE % rev) | 843 url = urllib.urlopen(DEPS_FILE % rev) |
| 854 m = blink_re.search(url.read()) | 844 m = blink_re.search(url.read()) |
| 855 url.close() | 845 url.close() |
| 856 if m: | 846 if m: |
| 857 return int(m.group(1)) | 847 return int(m.group(1)) |
| 858 else: | 848 else: |
| 859 raise Exception('Could not get Blink revision for Chromium rev %d' % rev) | 849 raise Exception('Could not get Blink revision for Chromium rev %d' % rev) |
| 860 | 850 |
| 861 | 851 |
| 862 def GetBlinkRevisionForChromiumRevision(self, rev): | 852 def GetBlinkRevisionForChromiumRevision(context, rev): |
| 863 """Returns the blink revision that was in REVISIONS file at | 853 """Returns the blink revision that was in REVISIONS file at |
| 864 chromium revision |rev|.""" | 854 chromium revision |rev|.""" |
| 865 def _IsRevisionNumber(revision): | 855 def _IsRevisionNumber(revision): |
| 866 if isinstance(revision, int): | 856 if isinstance(revision, int): |
| 867 return True | 857 return True |
| 868 else: | 858 else: |
| 869 return revision.isdigit() | 859 return revision.isdigit() |
| 870 if str(rev) in self.githash_svn_dict: | 860 if str(rev) in context.githash_svn_dict: |
| 871 rev = self.githash_svn_dict[str(rev)] | 861 rev = context.githash_svn_dict[str(rev)] |
| 872 file_url = '%s/%s%s/REVISIONS' % (self.base_url, | 862 file_url = '%s/%s%s/REVISIONS' % (context.base_url, |
| 873 self._listing_platform_dir, rev) | 863 context._listing_platform_dir, rev) |
| 874 url = urllib.urlopen(file_url) | 864 url = urllib.urlopen(file_url) |
| 875 data = json.loads(url.read()) | 865 if url.getcode() == 200: |
| 866 try: |
| 867 data = json.loads(url.read()) |
| 868 except ValueError: |
| 869 print 'ValueError for JSON URL: %s' % file_url |
| 870 raise ValueError |
| 871 else: |
| 872 raise ValueError |
| 876 url.close() | 873 url.close() |
| 877 if 'webkit_revision' in data: | 874 if 'webkit_revision' in data: |
| 878 blink_rev = data['webkit_revision'] | 875 blink_rev = data['webkit_revision'] |
| 879 if not _IsRevisionNumber(blink_rev): | 876 if not _IsRevisionNumber(blink_rev): |
| 880 blink_rev = self.GetSVNRevisionFromGitHash(blink_rev, 'blink') | 877 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink')) |
| 881 return blink_rev | 878 return blink_rev |
| 882 else: | 879 else: |
| 883 raise Exception('Could not get blink revision for cr rev %d' % rev) | 880 raise Exception('Could not get blink revision for cr rev %d' % rev) |
| 884 | 881 |
| 885 | 882 |
| 886 def FixChromiumRevForBlink(revisions_final, revisions, self, rev): | 883 def FixChromiumRevForBlink(revisions_final, revisions, self, rev): |
| 887 """Returns the chromium revision that has the correct blink revision | 884 """Returns the chromium revision that has the correct blink revision |
| 888 for blink bisect, DEPS and REVISIONS file might not match since | 885 for blink bisect, DEPS and REVISIONS file might not match since |
| 889 blink snapshots point to tip of tree blink. | 886 blink snapshots point to tip of tree blink. |
| 890 Note: The revisions_final variable might get modified to include | 887 Note: The revisions_final variable might get modified to include |
| (...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1037 return 1 | 1034 return 1 |
| 1038 | 1035 |
| 1039 if opts.asan: | 1036 if opts.asan: |
| 1040 base_url = ASAN_BASE_URL | 1037 base_url = ASAN_BASE_URL |
| 1041 elif opts.blink: | 1038 elif opts.blink: |
| 1042 base_url = WEBKIT_BASE_URL | 1039 base_url = WEBKIT_BASE_URL |
| 1043 else: | 1040 else: |
| 1044 base_url = CHROMIUM_BASE_URL | 1041 base_url = CHROMIUM_BASE_URL |
| 1045 | 1042 |
| 1046 # Create the context. Initialize 0 for the revisions as they are set below. | 1043 # Create the context. Initialize 0 for the revisions as they are set below. |
| 1047 context = PathContext(base_url, opts.archive, 0, 0, | 1044 context = PathContext(base_url, opts.archive, opts.good, opts.bad, |
| 1048 opts.official_builds, opts.aura, opts.asan, | 1045 opts.official_builds, opts.aura, opts.asan, |
| 1049 opts.use_local_repo, None) | 1046 opts.use_local_repo, opts.flash_path, opts.pdf_path) |
| 1050 # Pick a starting point, try to get HEAD for this. | 1047 # Pick a starting point, try to get HEAD for this. |
| 1051 if opts.bad: | 1048 if not opts.bad: |
| 1052 bad_rev = opts.bad | 1049 context.bad_revision = '999.0.0.0' |
| 1053 else: | 1050 context.bad_revision = GetChromiumRevision( |
| 1054 bad_rev = '999.0.0.0' | 1051 context, context.GetLastChangeURL()) |
| 1055 if not opts.official_builds: | |
| 1056 bad_rev = GetChromiumRevision(context, context.GetLastChangeURL()) | |
| 1057 | 1052 |
| 1058 # Find out when we were good. | 1053 # Find out when we were good. |
| 1059 if opts.good: | 1054 if not opts.good: |
| 1060 good_rev = opts.good | 1055 context.good_revision = '0.0.0.0' if opts.official_builds else 0 |
| 1061 else: | |
| 1062 good_rev = '0.0.0.0' if opts.official_builds else 0 | |
| 1063 | 1056 |
| 1064 if opts.flash_path: | 1057 if opts.flash_path: |
| 1065 flash_path = opts.flash_path | 1058 msg = 'Could not find Flash binary at %s' % opts.flash_path |
| 1066 msg = 'Could not find Flash binary at %s' % flash_path | 1059 assert os.path.exists(opts.flash_path), msg |
| 1067 assert os.path.exists(flash_path), msg | |
| 1068 | 1060 |
| 1069 if opts.pdf_path: | 1061 if opts.pdf_path: |
| 1070 pdf_path = opts.pdf_path | 1062 msg = 'Could not find PDF binary at %s' % opts.pdf_path |
| 1071 msg = 'Could not find PDF binary at %s' % pdf_path | 1063 assert os.path.exists(opts.pdf_path), msg |
| 1072 assert os.path.exists(pdf_path), msg | |
| 1073 | 1064 |
| 1074 if opts.official_builds: | 1065 if opts.official_builds: |
| 1075 good_rev = LooseVersion(good_rev) | 1066 context.good_revision = LooseVersion(context.good_revision) |
| 1076 bad_rev = LooseVersion(bad_rev) | 1067 context.bad_revision = LooseVersion(context.bad_revision) |
| 1077 else: | 1068 else: |
| 1078 good_rev = int(good_rev) | 1069 context.good_revision = int(context.good_revision) |
| 1079 bad_rev = int(bad_rev) | 1070 context.bad_revision = int(context.bad_revision) |
| 1080 | 1071 |
| 1081 if opts.times < 1: | 1072 if opts.times < 1: |
| 1082 print('Number of times to run (%d) must be greater than or equal to 1.' % | 1073 print('Number of times to run (%d) must be greater than or equal to 1.' % |
| 1083 opts.times) | 1074 opts.times) |
| 1084 parser.print_help() | 1075 parser.print_help() |
| 1085 return 1 | 1076 return 1 |
| 1086 | 1077 |
| 1087 if opts.asan: | 1078 if opts.asan: |
| 1088 evaluator = IsGoodASANBuild | 1079 evaluator = IsGoodASANBuild |
| 1089 else: | 1080 else: |
| 1090 evaluator = AskIsGoodBuild | 1081 evaluator = AskIsGoodBuild |
| 1091 | 1082 |
| 1092 (min_chromium_rev, max_chromium_rev) = Bisect( | 1083 # Save these revision numbers to compare when showing the changelog URL |
| 1093 base_url, opts.archive, opts.official_builds, opts.aura, opts.asan, | 1084 # after the bisect. |
| 1094 opts.use_local_repo, good_rev, bad_rev, opts.times, opts.command, | 1085 good_rev = context.good_revision |
| 1095 args, opts.profile, opts.flash_path, opts.pdf_path, | 1086 bad_rev = context.bad_revision |
| 1087 |
| 1088 (min_chromium_rev, max_chromium_rev, context) = Bisect( |
| 1089 context, opts.times, opts.command, args, opts.profile, |
| 1096 not opts.not_interactive, evaluator) | 1090 not opts.not_interactive, evaluator) |
| 1097 | 1091 |
| 1098 # Get corresponding blink revisions. | 1092 # Get corresponding blink revisions. |
| 1099 try: | 1093 try: |
| 1100 min_blink_rev = GetBlinkRevisionForChromiumRevision(context, | 1094 min_blink_rev = GetBlinkRevisionForChromiumRevision(context, |
| 1101 min_chromium_rev) | 1095 min_chromium_rev) |
| 1102 max_blink_rev = GetBlinkRevisionForChromiumRevision(context, | 1096 max_blink_rev = GetBlinkRevisionForChromiumRevision(context, |
| 1103 max_chromium_rev) | 1097 max_chromium_rev) |
| 1104 except Exception: | 1098 except Exception: |
| 1105 # Silently ignore the failure. | 1099 # Silently ignore the failure. |
| (...skipping 23 matching lines...) Expand all Loading... |
| 1129 | 1123 |
| 1130 print 'CHANGELOG URL:' | 1124 print 'CHANGELOG URL:' |
| 1131 if opts.official_builds: | 1125 if opts.official_builds: |
| 1132 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) | 1126 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) |
| 1133 else: | 1127 else: |
| 1134 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) | 1128 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) |
| 1135 | 1129 |
| 1136 | 1130 |
| 1137 if __name__ == '__main__': | 1131 if __name__ == '__main__': |
| 1138 sys.exit(main()) | 1132 sys.exit(main()) |
| OLD | NEW |