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 |