diff options
-rwxr-xr-x | tools/bisect-builds.py | 110 | ||||
-rw-r--r-- | tools/bisect_test.py | 53 |
2 files changed, 109 insertions, 54 deletions
diff --git a/tools/bisect-builds.py b/tools/bisect-builds.py index 04b4689..e601e47 100755 --- a/tools/bisect-builds.py +++ b/tools/bisect-builds.py @@ -32,8 +32,10 @@ DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' WEBKIT_CHANGELOG_URL = 'http://trac.webkit.org/log/' \ 'trunk/?rev=%d&stop_rev=%d&verbose=on&limit=10000' -DONE_MESSAGE = 'You are probably looking for a change made after ' \ - '%s (known good), but no later than %s (first known bad).' +DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \ + '(known good), but no later than %s (first known bad).' +DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \ + '(known bad), but no later than %s (first known good).' ############################################################################### @@ -140,8 +142,7 @@ class PathContext(object): def ParseDirectoryIndex(self): """Parses the Google Storage directory listing into a list of revision - numbers. The range starts with self.good_revision and goes until - self.bad_revision.""" + numbers.""" def _FetchAndParse(url): """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If @@ -198,8 +199,8 @@ class PathContext(object): """Gets the list of revision numbers between self.good_revision and self.bad_revision.""" # Download the revlist and filter for just the range between good and bad. - minrev = self.good_revision - maxrev = self.bad_revision + minrev = min(self.good_revision, self.bad_revision) + maxrev = max(self.good_revision, self.bad_revision) revlist = map(int, self.ParseDirectoryIndex()) revlist = [x for x in revlist if x >= int(minrev) and x <= int(maxrev)] revlist.sort() @@ -209,8 +210,8 @@ class PathContext(object): """Gets the list of official build numbers between self.good_revision and self.bad_revision.""" # Download the revlist and filter for just the range between good and bad. - minrev = self.good_revision - maxrev = self.bad_revision + minrev = min(self.good_revision, self.bad_revision) + maxrev = max(self.good_revision, self.bad_revision) handle = urllib.urlopen(OFFICIAL_BASE_URL) dirindex = handle.read() handle.close() @@ -392,8 +393,8 @@ def Bisect(platform, @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). @param official_builds Specify build type (Chromium or Official build). - @param good_rev Number/tag of the last known good revision. - @param bad_rev Number/tag of the first known bad revision. + @param good_rev Number/tag of the known good revision. + @param bad_rev Number/tag of the known bad revision. @param num_runs Number of times to run each build for asking good/bad. @param try_args A tuple of arguments to pass to the test application. @param profile The name of the user profile to run with. @@ -436,36 +437,41 @@ def Bisect(platform, msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist raise RuntimeError(msg) - print 'Bisecting range [%s, %s].' % (revlist[0], revlist[-1]) - # Figure out our bookends and first pivot point; fetch the pivot revision. - good = 0 - bad = len(revlist) - 1 - pivot = bad / 2 + minrev = 0 + maxrev = len(revlist) - 1 + pivot = maxrev / 2 rev = revlist[pivot] zipfile = _GetDownloadPath(rev) - initial_fetch = DownloadJob(context, 'initial_fetch', rev, zipfile) - initial_fetch.Start() - initial_fetch.WaitFor() + fetch = DownloadJob(context, 'initial_fetch', rev, zipfile) + fetch.Start() + fetch.WaitFor() # Binary search time! - while zipfile and bad - good > 1: + while fetch and fetch.zipfile and maxrev - minrev > 1: + if bad_rev < good_rev: + min_str, max_str = "bad", "good" + else: + min_str, max_str = "good", "bad" + print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str, \ + revlist[maxrev], max_str) + # Pre-fetch next two possible pivots # - down_pivot is the next revision to check if the current revision turns # out to be bad. # - up_pivot is the next revision to check if the current revision turns # out to be good. - down_pivot = int((pivot - good) / 2) + good + down_pivot = int((pivot - minrev) / 2) + minrev down_fetch = None - if down_pivot != pivot and down_pivot != good: + if down_pivot != pivot and down_pivot != minrev: down_rev = revlist[down_pivot] down_fetch = DownloadJob(context, 'down_fetch', down_rev, _GetDownloadPath(down_rev)) down_fetch.Start() - up_pivot = int((bad - pivot) / 2) + pivot + up_pivot = int((maxrev - pivot) / 2) + pivot up_fetch = None - if up_pivot != pivot and up_pivot != bad: + if up_pivot != pivot and up_pivot != maxrev: up_rev = revlist[up_pivot] up_fetch = DownloadJob(context, 'up_fetch', up_rev, _GetDownloadPath(up_rev)) @@ -478,43 +484,44 @@ def Bisect(platform, try: (status, stdout, stderr) = RunRevision(context, rev, - zipfile, + fetch.zipfile, profile, num_runs, try_args) except Exception, e: print >>sys.stderr, e - os.unlink(zipfile) - zipfile = None + fetch.Stop() + fetch = None # Call the evaluate function to see if the current revision is good or bad. # On that basis, kill one of the background downloads and complete the # other, as described in the comments above. try: answer = evaluate(rev, official_builds, status, stdout, stderr) - if answer == 'g': - good = pivot + if answer == 'g' and good_rev < bad_rev or \ + answer == 'b' and bad_rev < good_rev: + minrev = pivot if down_fetch: down_fetch.Stop() # Kill the download of the older revision. if up_fetch: up_fetch.WaitFor() pivot = up_pivot - zipfile = up_fetch.zipfile - elif answer == 'b': - bad = pivot + fetch = up_fetch + elif answer == 'b' and good_rev < bad_rev or \ + answer == 'g' and bad_rev < good_rev: + maxrev = pivot if up_fetch: up_fetch.Stop() # Kill the download of the newer revision. if down_fetch: down_fetch.WaitFor() pivot = down_pivot - zipfile = down_fetch.zipfile + fetch = down_fetch elif answer == 'u': # Nuke the revision from the revlist and choose a new pivot. revlist.pop(pivot) - bad -= 1 # Assumes bad >= pivot. + maxrev -= 1 # Assumes maxrev >= pivot. - fetch = None - if bad - good > 1: + if maxrev - minrev > 1: # Alternate between using down_pivot or up_pivot for the new pivot # point, without affecting the range. Do this instead of setting the # pivot to the midpoint of the new range because adjacent revisions @@ -551,7 +558,7 @@ def Bisect(platform, rev = revlist[pivot] - return (revlist[good], revlist[bad]) + return (revlist[minrev], revlist[maxrev]) def GetWebKitRevisionForChromiumRevision(rev): @@ -638,43 +645,38 @@ def main(): good_rev = int(good_rev) bad_rev = int(bad_rev) - if good_rev > bad_rev: - print ('The good revision (%s) must precede the bad revision (%s).\n' % - (good_rev, bad_rev)) - parser.print_help() - return 1 - if opts.times < 1: print('Number of times to run (%d) must be greater than or equal to 1.' % opts.times) parser.print_help() return 1 - (last_known_good_rev, first_known_bad_rev) = Bisect( + (min_chromium_rev, max_chromium_rev) = Bisect( opts.archive, opts.official_builds, good_rev, bad_rev, opts.times, args, opts.profile) # Get corresponding webkit revisions. try: - last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( - last_known_good_rev) - first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision( - first_known_bad_rev) + min_webkit_rev = GetWebKitRevisionForChromiumRevision(min_chromium_rev) + max_webkit_rev = GetWebKitRevisionForChromiumRevision(max_chromium_rev) except Exception, e: # Silently ignore the failure. - last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 + min_webkit_rev, max_webkit_rev = 0, 0 # We're done. Let the user know the results in an official manner. - print DONE_MESSAGE % (str(last_known_good_rev), str(first_known_bad_rev)) - if last_known_good_webkit_rev != first_known_bad_webkit_rev: + if good_rev > bad_rev: + print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), str(max_chromium_rev)) + else: + print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), str(max_chromium_rev)) + + if min_webkit_rev != max_webkit_rev: print 'WEBKIT CHANGELOG URL:' - print ' ' + WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, - last_known_good_webkit_rev) + print ' ' + WEBKIT_CHANGELOG_URL % (max_webkit_rev, min_webkit_rev) print 'CHANGELOG URL:' if opts.official_builds: - print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) + print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) else: - print ' ' + CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) + print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) if __name__ == '__main__': sys.exit(main()) diff --git a/tools/bisect_test.py b/tools/bisect_test.py new file mode 100644 index 0000000..b970f84 --- /dev/null +++ b/tools/bisect_test.py @@ -0,0 +1,53 @@ +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import unittest + +bisect_builds = __import__('bisect-builds') + + +class BisectTest(unittest.TestCase): + + patched = [] + max_rev = 10000 + + def monkey_patch(self, obj, name, new): + self.patched.append((obj, name, getattr(obj, name))) + setattr(obj, name, new) + + def clear_patching(self): + for obj, name, old in self.patched: + setattr(obj, name, old) + self.patched = [] + + def setUp(self): + self.monkey_patch(bisect_builds.DownloadJob, 'Start', lambda *args: None) + self.monkey_patch(bisect_builds.DownloadJob, 'Stop', lambda *args: None) + self.monkey_patch(bisect_builds.DownloadJob, 'WaitFor', lambda *args: None) + self.monkey_patch(bisect_builds, 'RunRevision', lambda *args: (0, "", "")) + self.monkey_patch(bisect_builds.PathContext, 'ParseDirectoryIndex', + lambda *args: range(self.max_rev)) + + def tearDown(self): + self.clear_patching() + + def bisect(self, good_rev, bad_rev, evaluate): + return bisect_builds.Bisect(good_rev=good_rev, + bad_rev=bad_rev, + evaluate=evaluate, + num_runs=1, + official_builds=False, + platform='linux', + profile=None, + try_args=()) + + def testBisectConsistentAnswer(self): + self.assertEqual(self.bisect(1000, 100, lambda *args: 'g'), (100, 101)) + self.assertEqual(self.bisect(100, 1000, lambda *args: 'b'), (100, 101)) + self.assertEqual(self.bisect(2000, 200, lambda *args: 'b'), (1999, 2000)) + self.assertEqual(self.bisect(200, 2000, lambda *args: 'g'), (1999, 2000)) + + +if __name__ == '__main__': + unittest.main() |