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 # Keep only one revision before and after if no exact match found. |
Nico
2012/07/20 18:04:02
Now the comment is no longer true, right?
Vitaly Buka (NO REVIEWS)
2012/07/20 18:11:25
Done.
| |
232 if str(build_number) == minrev: | 229 if build_number > maxrev: |
233 start_index = i | 230 break |
234 if str(build_number) == maxrev: | 231 if build_number >= minrev: |
235 end_index = i | 232 final_list.append(str(build_number)) |
236 except urllib.HTTPError, e: | 233 except urllib.HTTPError, e: |
237 pass | 234 pass |
238 return final_list[start_index:end_index] | 235 return final_list |
239 | 236 |
240 def UnzipFilenameToDir(filename, dir): | 237 def UnzipFilenameToDir(filename, dir): |
241 """Unzip |filename| to directory |dir|.""" | 238 """Unzip |filename| to directory |dir|.""" |
242 cwd = os.getcwd() | 239 cwd = os.getcwd() |
243 if not os.path.isabs(filename): | 240 if not os.path.isabs(filename): |
244 filename = os.path.join(cwd, filename) | 241 filename = os.path.join(cwd, filename) |
245 zf = zipfile.ZipFile(filename) | 242 zf = zipfile.ZipFile(filename) |
246 # Make base. | 243 # Make base. |
247 try: | 244 try: |
248 if not os.path.isdir(dir): | 245 if not os.path.isdir(dir): |
(...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
437 if official_builds: | 434 if official_builds: |
438 revlist = context.GetOfficialBuildsList() | 435 revlist = context.GetOfficialBuildsList() |
439 else: | 436 else: |
440 revlist = context.GetRevList() | 437 revlist = context.GetRevList() |
441 | 438 |
442 # Get a list of revisions to bisect across. | 439 # Get a list of revisions to bisect across. |
443 if len(revlist) < 2: # Don't have enough builds to bisect. | 440 if len(revlist) < 2: # Don't have enough builds to bisect. |
444 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 441 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
445 raise RuntimeError(msg) | 442 raise RuntimeError(msg) |
446 | 443 |
444 print 'Bisecting range [%s, %s].' % (revlist[0], revlist[-1]) | |
445 | |
447 # Figure out our bookends and first pivot point; fetch the pivot revision. | 446 # Figure out our bookends and first pivot point; fetch the pivot revision. |
448 good = 0 | 447 good = 0 |
449 bad = len(revlist) - 1 | 448 bad = len(revlist) - 1 |
450 pivot = bad / 2 | 449 pivot = bad / 2 |
451 rev = revlist[pivot] | 450 rev = revlist[pivot] |
452 zipfile = _GetDownloadPath(rev) | 451 zipfile = _GetDownloadPath(rev) |
453 initial_fetch = DownloadJob(context, 'initial_fetch', rev, zipfile) | 452 initial_fetch = DownloadJob(context, 'initial_fetch', rev, zipfile) |
454 initial_fetch.Start() | 453 initial_fetch.Start() |
455 initial_fetch.WaitFor() | 454 initial_fetch.WaitFor() |
456 | 455 |
(...skipping 104 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
561 webkit_re = re.compile(r'webkit_revision.:\D*(\d+)') | 560 webkit_re = re.compile(r'webkit_revision.:\D*(\d+)') |
562 url = urllib.urlopen(DEPS_FILE % rev) | 561 url = urllib.urlopen(DEPS_FILE % rev) |
563 m = webkit_re.search(url.read()) | 562 m = webkit_re.search(url.read()) |
564 url.close() | 563 url.close() |
565 if m: | 564 if m: |
566 return int(m.group(1)) | 565 return int(m.group(1)) |
567 else: | 566 else: |
568 raise Exception('Could not get webkit revision for cr rev %d' % rev) | 567 raise Exception('Could not get webkit revision for cr rev %d' % rev) |
569 | 568 |
570 | 569 |
570 def GetChromiumRevision(url): | |
571 """Returns the chromium revision read from given URL.""" | |
572 try: | |
573 # Location of the latest build revision number | |
574 return int(urllib.urlopen(url).read()) | |
575 except Exception, e: | |
576 print('Could not determine latest revision. This could be bad...') | |
577 return 999999999 | |
578 | |
579 | |
571 def main(): | 580 def main(): |
572 usage = ('%prog [options] [-- chromium-options]\n' | 581 usage = ('%prog [options] [-- chromium-options]\n' |
573 'Perform binary search on the snapshot builds.\n' | 582 'Perform binary search on the snapshot builds.\n' |
574 '\n' | 583 '\n' |
575 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 584 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
576 parser = optparse.OptionParser(usage=usage) | 585 parser = optparse.OptionParser(usage=usage) |
577 # Strangely, the default help output doesn't include the choice list. | 586 # Strangely, the default help output doesn't include the choice list. |
578 choices = ['mac', 'win', 'linux', 'linux64'] | 587 choices = ['mac', 'win', 'linux', 'linux64'] |
579 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 588 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
580 parser.add_option('-a', '--archive', | 589 parser.add_option('-a', '--archive', |
581 choices = choices, | 590 choices = choices, |
582 help = 'The buildbot archive to bisect [%s].' % | 591 help = 'The buildbot archive to bisect [%s].' % |
583 '|'.join(choices)) | 592 '|'.join(choices)) |
584 parser.add_option('-o', action="store_true", dest='official_builds', | 593 parser.add_option('-o', action="store_true", dest='official_builds', |
585 help = 'Bisect across official ' + | 594 help = 'Bisect across official ' + |
586 'Chrome builds (internal only) instead of ' + | 595 'Chrome builds (internal only) instead of ' + |
587 'Chromium archives.') | 596 'Chromium archives.') |
588 parser.add_option('-b', '--bad', type = 'str', | 597 parser.add_option('-b', '--bad', type = 'str', |
589 help = 'The bad revision to bisect to.') | 598 help = 'The bad revision to bisect to.') |
Robert Sesek
2012/07/20 18:12:27
Update the help to say it defaults to HEAD.
Vitaly Buka (NO REVIEWS)
2012/07/20 18:20:12
Done.
| |
590 parser.add_option('-g', '--good', type = 'str', | 599 parser.add_option('-g', '--good', type = 'str', |
591 help = 'The last known good revision to bisect from.') | 600 help = 'The last known good revision to bisect from.') |
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 |