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 198 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
209 """Gets the list of official build numbers between self.good_revision and | 209 """Gets the list of official build numbers between self.good_revision and |
210 self.bad_revision.""" | 210 self.bad_revision.""" |
211 # Download the revlist and filter for just the range between good and bad. | 211 # Download the revlist and filter for just the range between good and bad. |
212 minrev = self.good_revision | 212 minrev = self.good_revision |
213 maxrev = self.bad_revision | 213 maxrev = self.bad_revision |
214 handle = urllib.urlopen(OFFICIAL_BASE_URL) | 214 handle = urllib.urlopen(OFFICIAL_BASE_URL) |
215 dirindex = handle.read() | 215 dirindex = handle.read() |
216 handle.close() | 216 handle.close() |
217 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex) | 217 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex) |
218 final_list = [] | 218 final_list = [] |
219 start_index = 0 | |
220 end_index = 0 | |
221 i = 0 | 219 i = 0 |
222 | |
223 parsed_build_numbers = [LooseVersion(x) for x in build_numbers] | 220 parsed_build_numbers = [LooseVersion(x) for x in build_numbers] |
224 for build_number in sorted(parsed_build_numbers): | 221 for build_number in sorted(parsed_build_numbers): |
225 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \ | 222 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \ |
226 self._listing_platform_dir + self.archive_name | 223 self._listing_platform_dir + self.archive_name |
227 i = i + 1 | 224 i = i + 1 |
228 try: | 225 try: |
229 connection = urllib.urlopen(path) | 226 connection = urllib.urlopen(path) |
230 connection.close() | 227 connection.close() |
231 final_list.append(str(build_number)) | 228 if build_number > maxrev: |
232 if str(build_number) == minrev: | 229 break |
233 start_index = i | 230 if build_number >= minrev: |
234 if str(build_number) == maxrev: | 231 final_list.append(str(build_number)) |
235 end_index = i | |
236 except urllib.HTTPError, e: | 232 except urllib.HTTPError, e: |
237 pass | 233 pass |
238 return final_list[start_index:end_index] | 234 return final_list |
239 | 235 |
240 def UnzipFilenameToDir(filename, dir): | 236 def UnzipFilenameToDir(filename, dir): |
241 """Unzip |filename| to directory |dir|.""" | 237 """Unzip |filename| to directory |dir|.""" |
242 cwd = os.getcwd() | 238 cwd = os.getcwd() |
243 if not os.path.isabs(filename): | 239 if not os.path.isabs(filename): |
244 filename = os.path.join(cwd, filename) | 240 filename = os.path.join(cwd, filename) |
245 zf = zipfile.ZipFile(filename) | 241 zf = zipfile.ZipFile(filename) |
246 # Make base. | 242 # Make base. |
247 try: | 243 try: |
248 if not os.path.isdir(dir): | 244 if not os.path.isdir(dir): |
(...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
437 if official_builds: | 433 if official_builds: |
438 revlist = context.GetOfficialBuildsList() | 434 revlist = context.GetOfficialBuildsList() |
439 else: | 435 else: |
440 revlist = context.GetRevList() | 436 revlist = context.GetRevList() |
441 | 437 |
442 # Get a list of revisions to bisect across. | 438 # Get a list of revisions to bisect across. |
443 if len(revlist) < 2: # Don't have enough builds to bisect. | 439 if len(revlist) < 2: # Don't have enough builds to bisect. |
444 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 440 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
445 raise RuntimeError(msg) | 441 raise RuntimeError(msg) |
446 | 442 |
| 443 print 'Bisecting range [%s, %s].' % (revlist[0], revlist[-1]) |
| 444 |
447 # Figure out our bookends and first pivot point; fetch the pivot revision. | 445 # Figure out our bookends and first pivot point; fetch the pivot revision. |
448 good = 0 | 446 good = 0 |
449 bad = len(revlist) - 1 | 447 bad = len(revlist) - 1 |
450 pivot = bad / 2 | 448 pivot = bad / 2 |
451 rev = revlist[pivot] | 449 rev = revlist[pivot] |
452 zipfile = _GetDownloadPath(rev) | 450 zipfile = _GetDownloadPath(rev) |
453 initial_fetch = DownloadJob(context, 'initial_fetch', rev, zipfile) | 451 initial_fetch = DownloadJob(context, 'initial_fetch', rev, zipfile) |
454 initial_fetch.Start() | 452 initial_fetch.Start() |
455 initial_fetch.WaitFor() | 453 initial_fetch.WaitFor() |
456 | 454 |
(...skipping 104 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
561 webkit_re = re.compile(r'webkit_revision.:\D*(\d+)') | 559 webkit_re = re.compile(r'webkit_revision.:\D*(\d+)') |
562 url = urllib.urlopen(DEPS_FILE % rev) | 560 url = urllib.urlopen(DEPS_FILE % rev) |
563 m = webkit_re.search(url.read()) | 561 m = webkit_re.search(url.read()) |
564 url.close() | 562 url.close() |
565 if m: | 563 if m: |
566 return int(m.group(1)) | 564 return int(m.group(1)) |
567 else: | 565 else: |
568 raise Exception('Could not get webkit revision for cr rev %d' % rev) | 566 raise Exception('Could not get webkit revision for cr rev %d' % rev) |
569 | 567 |
570 | 568 |
| 569 def GetChromiumRevision(url): |
| 570 """Returns the chromium revision read from given URL.""" |
| 571 try: |
| 572 # Location of the latest build revision number |
| 573 return int(urllib.urlopen(url).read()) |
| 574 except Exception, e: |
| 575 print('Could not determine latest revision. This could be bad...') |
| 576 return 999999999 |
| 577 |
| 578 |
571 def main(): | 579 def main(): |
572 usage = ('%prog [options] [-- chromium-options]\n' | 580 usage = ('%prog [options] [-- chromium-options]\n' |
573 'Perform binary search on the snapshot builds.\n' | 581 'Perform binary search on the snapshot builds.\n' |
574 '\n' | 582 '\n' |
575 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 583 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
576 parser = optparse.OptionParser(usage=usage) | 584 parser = optparse.OptionParser(usage=usage) |
577 # Strangely, the default help output doesn't include the choice list. | 585 # Strangely, the default help output doesn't include the choice list. |
578 choices = ['mac', 'win', 'linux', 'linux64'] | 586 choices = ['mac', 'win', 'linux', 'linux64'] |
579 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 587 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
580 parser.add_option('-a', '--archive', | 588 parser.add_option('-a', '--archive', |
581 choices = choices, | 589 choices = choices, |
582 help = 'The buildbot archive to bisect [%s].' % | 590 help = 'The buildbot archive to bisect [%s].' % |
583 '|'.join(choices)) | 591 '|'.join(choices)) |
584 parser.add_option('-o', action="store_true", dest='official_builds', | 592 parser.add_option('-o', action="store_true", dest='official_builds', |
585 help = 'Bisect across official ' + | 593 help = 'Bisect across official ' + |
586 'Chrome builds (internal only) instead of ' + | 594 'Chrome builds (internal only) instead of ' + |
587 'Chromium archives.') | 595 'Chromium archives.') |
588 parser.add_option('-b', '--bad', type = 'str', | 596 parser.add_option('-b', '--bad', type = 'str', |
589 help = 'The bad revision to bisect to.') | 597 help = 'The bad revision to bisect to. Default is HEAD.') |
590 parser.add_option('-g', '--good', type = 'str', | 598 parser.add_option('-g', '--good', type = 'str', |
591 help = 'The last known good revision to bisect from.') | 599 help = 'The last known good revision to bisect from. ' + |
| 600 'Default is 0.') |
592 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 601 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
593 help = 'Profile to use; this will not reset every run. ' + | 602 help = 'Profile to use; this will not reset every run. ' + |
594 'Defaults to a clean profile.', default = 'profile') | 603 'Defaults to a clean profile.', default = 'profile') |
595 parser.add_option('-t', '--times', type = 'int', | 604 parser.add_option('-t', '--times', type = 'int', |
596 help = 'Number of times to run each build before asking ' + | 605 help = 'Number of times to run each build before asking ' + |
597 'if it\'s good or bad. Temporary profiles are reused.', | 606 'if it\'s good or bad. Temporary profiles are reused.', |
598 default = 1) | 607 default = 1) |
599 (opts, args) = parser.parse_args() | 608 (opts, args) = parser.parse_args() |
600 | 609 |
601 if opts.archive is None: | 610 if opts.archive is None: |
602 print 'Error: missing required parameter: --archive' | 611 print 'Error: missing required parameter: --archive' |
603 print | 612 print |
604 parser.print_help() | 613 parser.print_help() |
605 return 1 | 614 return 1 |
606 | 615 |
607 if opts.bad and opts.good and (opts.good > opts.bad): | |
608 print ('The good revision (%d) must precede the bad revision (%d).\n' % | |
609 (opts.good, opts.bad)) | |
610 parser.print_help() | |
611 return 1 | |
612 | |
613 # Create the context. Initialize 0 for the revisions as they are set below. | 616 # Create the context. Initialize 0 for the revisions as they are set below. |
614 context = PathContext(opts.archive, 0, 0, opts.official_builds) | 617 context = PathContext(opts.archive, 0, 0, opts.official_builds) |
615 | |
616 if opts.official_builds and opts.bad is None: | |
617 print >>sys.stderr, 'Bisecting official builds requires a bad build number.' | |
618 parser.print_help() | |
619 return 1 | |
620 | |
621 # Pick a starting point, try to get HEAD for this. | 618 # Pick a starting point, try to get HEAD for this. |
622 if opts.bad: | 619 if opts.bad: |
623 bad_rev = opts.bad | 620 bad_rev = opts.bad |
624 else: | 621 else: |
625 bad_rev = 0 | 622 bad_rev = '999.0.0.0' |
626 try: | 623 if not opts.official_builds: |
627 # Location of the latest build revision number | 624 bad_rev = GetChromiumRevision(context.GetLastChangeURL()) |
628 nh = urllib.urlopen(context.GetLastChangeURL()) | |
629 latest = int(nh.read()) | |
630 nh.close() | |
631 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) | |
632 if (bad_rev == ''): | |
633 bad_rev = latest | |
634 bad_rev = int(bad_rev) | |
635 except Exception, e: | |
636 print('Could not determine latest revision. This could be bad...') | |
637 bad_rev = int(raw_input('Bad revision: ')) | |
638 | 625 |
639 # Find out when we were good. | 626 # Find out when we were good. |
640 if opts.good: | 627 if opts.good: |
641 good_rev = opts.good | 628 good_rev = opts.good |
642 else: | 629 else: |
643 good_rev = 0 | 630 good_rev = '0.0.0.0' if opts.official_builds else 0 |
644 try: | 631 |
645 good_rev = int(raw_input('Last known good [0]: ')) | 632 if opts.official_builds: |
646 except Exception, e: | 633 good_rev = LooseVersion(good_rev) |
647 pass | 634 bad_rev = LooseVersion(bad_rev) |
| 635 else: |
| 636 good_rev = int(good_rev) |
| 637 bad_rev = int(bad_rev) |
| 638 |
| 639 if good_rev > bad_rev: |
| 640 print ('The good revision (%s) must precede the bad revision (%s).\n' % |
| 641 (good_rev, bad_rev)) |
| 642 parser.print_help() |
| 643 return 1 |
648 | 644 |
649 if opts.times < 1: | 645 if opts.times < 1: |
650 print('Number of times to run (%d) must be greater than or equal to 1.' % | 646 print('Number of times to run (%d) must be greater than or equal to 1.' % |
651 opts.times) | 647 opts.times) |
652 parser.print_help() | 648 parser.print_help() |
653 return 1 | 649 return 1 |
654 | 650 |
655 (last_known_good_rev, first_known_bad_rev) = Bisect( | 651 (last_known_good_rev, first_known_bad_rev) = Bisect( |
656 opts.archive, opts.official_builds, good_rev, bad_rev, opts.times, args, | 652 opts.archive, opts.official_builds, good_rev, bad_rev, opts.times, args, |
657 opts.profile) | 653 opts.profile) |
(...skipping 15 matching lines...) Expand all Loading... |
673 print ' ' + WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, | 669 print ' ' + WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, |
674 last_known_good_webkit_rev) | 670 last_known_good_webkit_rev) |
675 print 'CHANGELOG URL:' | 671 print 'CHANGELOG URL:' |
676 if opts.official_builds: | 672 if opts.official_builds: |
677 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 673 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
678 else: | 674 else: |
679 print ' ' + CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 675 print ' ' + CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
680 | 676 |
681 if __name__ == '__main__': | 677 if __name__ == '__main__': |
682 sys.exit(main()) | 678 sys.exit(main()) |
OLD | NEW |