Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 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, |
| 11 unzipping, and opening Chromium for you. After testing the specific revision, | 11 unzipping, and opening Chromium for you. After testing the specific revision, |
| 12 it will ask you whether it is good or bad before continuing the search. | 12 it will ask you whether it is good or bad before continuing the search. |
| 13 """ | 13 """ |
| 14 | 14 |
| 15 # Base URL to download snapshots from. | 15 # The root URL for storage. |
| 16 BUILD_BASE_URL = 'http://build.chromium.org/f/chromium/continuous/' | 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-continuous' |
| 17 | |
| 18 # The index file that lists all the builds. This lives in BUILD_BASE_URL. | |
| 19 BUILD_INDEX_FILE = 'all_builds.txt' | |
| 20 | |
| 21 # The type (platform) of the build archive. This is what's passed in to the | |
| 22 # '-a/--archive' option. | |
| 23 BUILD_ARCHIVE_TYPE = '' | |
| 24 | |
| 25 # The location of the builds. Format this with a (date, revision) tuple, which | |
| 26 # can be obtained through ParseIndexLine(). | |
| 27 BUILD_ARCHIVE_URL = '/%s/%d/' | |
| 28 | |
| 29 # Name of the build archive. | |
| 30 BUILD_ZIP_NAME = '' | |
| 31 | |
| 32 # Directory name inside the archive. | |
| 33 BUILD_DIR_NAME = '' | |
| 34 | |
| 35 # Name of the executable. | |
| 36 BUILD_EXE_NAME = '' | |
| 37 | 17 |
| 38 # URL to the ViewVC commit page. | 18 # URL to the ViewVC commit page. |
| 39 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' | 19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' |
| 40 | 20 |
| 41 # Changelogs URL | 21 # Changelogs URL. |
| 42 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ | 22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ |
| 43 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' | 23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' |
| 44 | 24 |
| 45 ############################################################################### | 25 ############################################################################### |
| 46 | 26 |
| 47 import math | 27 import math |
| 48 import optparse | 28 import optparse |
| 49 import os | 29 import os |
| 50 import pipes | 30 import pipes |
| 51 import re | 31 import re |
| 52 import shutil | 32 import shutil |
| 53 import sys | 33 import sys |
| 54 import tempfile | 34 import tempfile |
| 55 import urllib | 35 import urllib |
| 36 from xml.etree import ElementTree | |
| 56 import zipfile | 37 import zipfile |
| 57 | 38 |
| 39 class PathContext(object): | |
| 40 """A PathContext is used to carry the information used to construct URLs and | |
| 41 paths when dealing with the storage server and archives.""" | |
| 42 def __init__(self, platform, good_revision, bad_revision): | |
| 43 super(PathContext, self).__init__() | |
| 44 # Store off the input parameters. | |
| 45 self.platform = platform # What's passed in to the '-a/--archive' option. | |
| 46 self.good_revision = good_revision | |
| 47 self.bad_revision = bad_revision | |
| 48 | |
| 49 # The name of the ZIP file in a revision directory on the server. | |
| 50 self.archive_name = None | |
| 51 | |
| 52 # Set some internal members: | |
| 53 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. | |
| 54 # _archive_extract_dir = Uncompressed directory in the archive_name file. | |
| 55 # _binary_name = The name of the executable to run. | |
| 56 if self.platform == 'linux' or self.platform == 'linux-64': | |
| 57 self._listing_platform_dir = 'Linux/' | |
| 58 self.archive_name = 'chrome-linux.zip' | |
| 59 self._archive_extract_dir = 'chrome-linux' | |
| 60 self._binary_name = 'chrome' | |
| 61 # Linux and x64 share all the same path data except for the archive dir. | |
| 62 if self.platform == 'linux-64': | |
| 63 self._listing_platform_dir = 'Linux_x64/' | |
| 64 elif self.platform == 'mac': | |
| 65 self._listing_platform_dir = 'Mac/' | |
| 66 self.archive_name = 'chrome-mac.zip' | |
| 67 self._archive_extract_dir = 'chrome-mac' | |
| 68 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | |
| 69 elif self.platform == 'win': | |
| 70 self._listing_platform_dir = 'Win/' | |
| 71 self.archive_name = 'chrome-win32.zip' | |
| 72 self._archive_extract_dir = 'chrome-win32' | |
| 73 self._binary_name = 'chrome.exe' | |
| 74 else: | |
| 75 raise Exception("Invalid platform") | |
| 76 | |
| 77 def GetListingURL(self, revision_offset=None): | |
| 78 """Returns the URL for a directory listing, with an optional starting | |
| 79 revision.""" | |
| 80 marker = '' | |
| 81 if revision_offset: | |
| 82 marker = '&marker=' + self._listing_platform_dir + \ | |
| 83 str(revision_offset) + '/' | |
| 84 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | |
| 85 marker | |
| 86 | |
| 87 def GetDownloadURL(self, revision): | |
| 88 """Gets the download URL for a build archive of a specific revision.""" | |
| 89 return BASE_URL + '/' + self._listing_platform_dir + str(revision) + '/' + \ | |
| 90 self.archive_name | |
| 91 | |
| 92 def GetLastChangeURL(self): | |
| 93 """Returns a URL to the LAST_CHANGE file.""" | |
| 94 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | |
| 95 | |
| 96 def GetLaunchPath(self): | |
| 97 """Returns a relative path (presumably from the archive extraction location) | |
| 98 that is used to run the executable.""" | |
| 99 return os.path.join(self._archive_extract_dir, self._binary_name) | |
| 100 | |
| 58 | 101 |
| 59 def UnzipFilenameToDir(filename, dir): | 102 def UnzipFilenameToDir(filename, dir): |
| 60 """Unzip |filename| to directory |dir|.""" | 103 """Unzip |filename| to directory |dir|.""" |
| 61 zf = zipfile.ZipFile(filename) | 104 zf = zipfile.ZipFile(filename) |
| 62 # Make base. | 105 # Make base. |
| 63 pushd = os.getcwd() | 106 pushd = os.getcwd() |
| 64 try: | 107 try: |
| 65 if not os.path.isdir(dir): | 108 if not os.path.isdir(dir): |
| 66 os.mkdir(dir) | 109 os.mkdir(dir) |
| 67 os.chdir(dir) | 110 os.chdir(dir) |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 79 out.write(zf.read(name)) | 122 out.write(zf.read(name)) |
| 80 out.close() | 123 out.close() |
| 81 # Set permissions. Permission info in external_attr is shifted 16 bits. | 124 # Set permissions. Permission info in external_attr is shifted 16 bits. |
| 82 os.chmod(name, info.external_attr >> 16L) | 125 os.chmod(name, info.external_attr >> 16L) |
| 83 os.chdir(pushd) | 126 os.chdir(pushd) |
| 84 except Exception, e: | 127 except Exception, e: |
| 85 print >>sys.stderr, e | 128 print >>sys.stderr, e |
| 86 sys.exit(1) | 129 sys.exit(1) |
| 87 | 130 |
| 88 | 131 |
| 89 def SetArchiveVars(archive): | 132 def ParseDirectoryIndex(context): |
| 90 """Set a bunch of global variables appropriate for the specified archive.""" | 133 """Parses the Google Storage directory listing into a list of revision |
| 91 global BUILD_ARCHIVE_TYPE | 134 numbers. The range starts with context.good_revision and goes until the latest |
| 92 global BUILD_ZIP_NAME | 135 revision.""" |
| 93 global BUILD_DIR_NAME | 136 def _FetchAndParse(url): |
| 94 global BUILD_EXE_NAME | 137 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If |
| 95 global BUILD_BASE_URL | 138 next-marker is not None, then the listing is a partial listing and another |
| 139 fetch should be performed with next-marker being the marker= GET | |
| 140 parameter.""" | |
| 141 handle = urllib.urlopen(url) | |
| 142 document = ElementTree.parse(handle) | |
| 96 | 143 |
| 97 BUILD_ARCHIVE_TYPE = archive | 144 # All nodes in the tree are namespaced. Get the root's tag name to extract |
| 145 # the namespace. Etree does namespaces as |{namespace}tag|. | |
| 146 root_tag = document.getroot().tag | |
| 147 end_ns_pos = root_tag.find('}') | |
| 148 if end_ns_pos == -1: | |
| 149 raise Exception("Could not locate end namespace for directory index") | |
| 150 namespace = root_tag[:end_ns_pos + 1] | |
| 98 | 151 |
| 99 if BUILD_ARCHIVE_TYPE in ('linux', 'linux64', 'linux-chromiumos'): | 152 # Find the prefix (_listing_platform_dir) and whether or not the list is |
| 100 BUILD_ZIP_NAME = 'chrome-linux.zip' | 153 # truncated. |
| 101 BUILD_DIR_NAME = 'chrome-linux' | 154 prefix = document.find(namespace + 'Prefix').text |
| 102 BUILD_EXE_NAME = 'chrome' | 155 next_marker = None |
| 103 elif BUILD_ARCHIVE_TYPE in ('mac'): | 156 is_truncated = document.find(namespace + 'IsTruncated') |
| 104 BUILD_ZIP_NAME = 'chrome-mac.zip' | 157 if is_truncated is not None and is_truncated.text.lower() == 'true': |
| 105 BUILD_DIR_NAME = 'chrome-mac' | 158 next_marker = document.find(namespace + 'NextMarker').text |
| 106 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' | 159 |
| 107 elif BUILD_ARCHIVE_TYPE in ('win'): | 160 # Get a list of all the revisions. |
| 108 BUILD_ZIP_NAME = 'chrome-win32.zip' | 161 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + |
| 109 BUILD_DIR_NAME = 'chrome-win32' | 162 namespace + 'Prefix') |
| 110 BUILD_EXE_NAME = 'chrome.exe' | 163 # The <Prefix> nodes have content of the form of |
| 164 # |_listing_platform_dir/revision/|. Strip off the platform dir and the | |
| 165 # trailing slash to just have a number. | |
| 166 revisions = map(lambda x: x.text[len(prefix):-1], all_prefixes) | |
| 167 return (revisions, next_marker) | |
| 168 | |
| 169 # Set the marker to be the good revision, as bisecting before that is not | |
| 170 # necessary. | |
| 171 (revisions, next_marker) = _FetchAndParse( | |
| 172 context.GetListingURL(context.good_revision)) | |
|
nsylvain
2011/06/09 21:12:03
I'm not sure this works well. I don't think the o
Robert Sesek
2011/06/09 23:38:58
Ah, thanks. I didn't realize that; was just trying
| |
| 173 # If the result list was truncated, refetch with the next marker. Do this | |
| 174 # until an entire directory listing is done. | |
| 175 while next_marker: | |
| 176 (new_revisions, next_marker) = _FetchAndParse( | |
| 177 context.GetListingURL(next_marker)) | |
| 178 revisions.extend(new_revisions) | |
| 179 | |
| 180 return revisions | |
| 111 | 181 |
| 112 | 182 |
| 113 def ParseDirectoryIndex(url): | 183 def GetRevList(context): |
| 114 """Parses the all_builds.txt index file. The format of this file is: | 184 """Gets the list of revision numbers between |good_revision| and |
| 115 mac/2011-02-16/75130 | 185 |bad_revision| of the |context|.""" |
| 116 mac/2011-02-16/75218 | 186 # Download the revlist and filter for just the range between good and bad. |
| 117 mac/2011-02-16/75226 | 187 rev_range = range(context.good_revision, context.bad_revision) |
| 118 mac/2011-02-16/75234 | 188 revlist = map(int, ParseDirectoryIndex(context)) |
| 119 mac/2011-02-16/75184 | 189 revlist = filter(lambda r: r in rev_range, revlist) |
| 120 This function will return a list of DATE/REVISION strings for the platform | |
| 121 specified by BUILD_ARCHIVE_TYPE. | |
| 122 """ | |
| 123 handle = urllib.urlopen(url) | |
| 124 dirindex = handle.readlines() | |
| 125 handle.close() | |
| 126 | |
| 127 # Only return values for the specified platform. Include the trailing slash to | |
| 128 # not confuse linux and linux64. | |
| 129 archtype = BUILD_ARCHIVE_TYPE + '/' | |
| 130 dirindex = filter(lambda l: l.startswith(archtype), dirindex) | |
| 131 | |
| 132 # Remove the newline separator and the platform token. | |
| 133 dirindex = map(lambda l: l[len(archtype):].strip(), dirindex) | |
| 134 dirindex.sort() | |
| 135 return dirindex | |
| 136 | |
| 137 | |
| 138 def ParseIndexLine(iline): | |
| 139 """Takes an index line returned by ParseDirectoryIndex() and returns a | |
| 140 2-tuple of (date, revision). |date| is a string and |revision| is an int.""" | |
| 141 split = iline.split('/') | |
| 142 assert(len(split) == 2) | |
| 143 return (split[0], int(split[1])) | |
| 144 | |
| 145 | |
| 146 def GetRevision(iline): | |
| 147 """Takes an index line, parses it, and returns the revision.""" | |
| 148 return ParseIndexLine(iline)[1] | |
| 149 | |
| 150 | |
| 151 def GetRevList(good, bad): | |
| 152 """Gets the list of revision numbers between |good| and |bad|.""" | |
| 153 # Download the main revlist. | |
| 154 revlist = ParseDirectoryIndex(BUILD_BASE_URL + BUILD_INDEX_FILE) | |
| 155 revrange = range(good, bad) | |
| 156 revlist = filter(lambda r: GetRevision(r) in revrange, revlist) | |
| 157 revlist.sort() | 190 revlist.sort() |
| 158 return revlist | 191 return revlist |
| 159 | 192 |
| 160 | 193 |
| 161 def TryRevision(iline, profile, args): | 194 def TryRevision(context, rev, profile, args): |
| 162 """Downloads revision from |iline|, unzips it, and opens it for the user to | 195 """Downloads revision |rev|, unzips it, and opens it for the user to test. |
| 163 test. |profile| is the profile to use.""" | 196 |profile| is the profile to use.""" |
| 164 # Do this in a temp dir so we don't collide with user files. | 197 # Do this in a temp dir so we don't collide with user files. |
| 165 cwd = os.getcwd() | 198 cwd = os.getcwd() |
| 166 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 199 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
| 167 os.chdir(tempdir) | 200 os.chdir(tempdir) |
| 168 | 201 |
| 169 # Download the file. | 202 # Download the file. |
| 170 download_url = BUILD_BASE_URL + BUILD_ARCHIVE_TYPE + \ | 203 download_url = context.GetDownloadURL(rev) |
| 171 (BUILD_ARCHIVE_URL % ParseIndexLine(iline)) + BUILD_ZIP_NAME | |
| 172 def _ReportHook(blocknum, blocksize, totalsize): | 204 def _ReportHook(blocknum, blocksize, totalsize): |
| 173 size = blocknum * blocksize | 205 size = blocknum * blocksize |
| 174 if totalsize == -1: # Total size not known. | 206 if totalsize == -1: # Total size not known. |
| 175 progress = "Received %d bytes" % size | 207 progress = "Received %d bytes" % size |
| 176 else: | 208 else: |
| 177 size = min(totalsize, size) | 209 size = min(totalsize, size) |
| 178 progress = "Received %d of %d bytes, %.2f%%" % ( | 210 progress = "Received %d of %d bytes, %.2f%%" % ( |
| 179 size, totalsize, 100.0 * size / totalsize) | 211 size, totalsize, 100.0 * size / totalsize) |
| 180 # Send a \r to let all progress messages use just one line of output. | 212 # Send a \r to let all progress messages use just one line of output. |
| 181 sys.stdout.write("\r" + progress) | 213 sys.stdout.write("\r" + progress) |
| 182 sys.stdout.flush() | 214 sys.stdout.flush() |
| 183 try: | 215 try: |
| 184 print 'Fetching ' + download_url | 216 print 'Fetching ' + download_url |
| 185 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _ReportHook) | 217 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) |
| 186 print | 218 print |
| 187 except Exception, e: | 219 except Exception, e: |
| 188 print('Could not retrieve the download. Sorry.') | 220 print('Could not retrieve the download. Sorry.') |
| 189 sys.exit(-1) | 221 sys.exit(-1) |
| 190 | 222 |
| 191 # Unzip the file. | 223 # Unzip the file. |
| 192 print 'Unzipping ...' | 224 print 'Unzipping ...' |
| 193 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) | 225 UnzipFilenameToDir(context.archive_name, os.curdir) |
| 194 | 226 |
| 195 # Tell the system to open the app. | 227 # Tell the system to open the app. |
| 196 args = ['--user-data-dir=%s' % profile] + args | 228 args = ['--user-data-dir=%s' % profile] + args |
| 197 flags = ' '.join(map(pipes.quote, args)) | 229 flags = ' '.join(map(pipes.quote, args)) |
| 198 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) | 230 cmd = '%s %s' % (context.GetLaunchPath(), flags) |
| 199 cmd = '%s %s' % (exe, flags) | |
| 200 print 'Running %s' % cmd | 231 print 'Running %s' % cmd |
| 201 os.system(cmd) | 232 os.system(cmd) |
| 202 | 233 |
| 203 os.chdir(cwd) | 234 os.chdir(cwd) |
| 204 print 'Cleaning temp dir ...' | 235 print 'Cleaning temp dir ...' |
| 205 try: | 236 try: |
| 206 shutil.rmtree(tempdir, True) | 237 shutil.rmtree(tempdir, True) |
| 207 except Exception, e: | 238 except Exception, e: |
| 208 pass | 239 pass |
| 209 | 240 |
| 210 | 241 |
| 211 def AskIsGoodBuild(iline): | 242 def AskIsGoodBuild(rev): |
| 212 """Ask the user whether build from index line |iline| is good or bad.""" | 243 """Ask the user whether build |rev| is good or bad.""" |
| 213 # Loop until we get a response that we can parse. | 244 # Loop until we get a response that we can parse. |
| 214 while True: | 245 while True: |
| 215 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % GetRevision(iline)) | 246 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) |
| 216 if response and response in ('g', 'b'): | 247 if response and response in ('g', 'b'): |
| 217 return response == 'g' | 248 return response == 'g' |
| 218 | 249 |
| 250 | |
| 219 def main(): | 251 def main(): |
| 220 usage = ('%prog [options] [-- chromium-options]\n' | 252 usage = ('%prog [options] [-- chromium-options]\n' |
| 221 'Perform binary search on the snapshot builds.\n' | 253 'Perform binary search on the snapshot builds.\n' |
| 222 '\n' | 254 '\n' |
| 223 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 255 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
| 224 parser = optparse.OptionParser(usage=usage) | 256 parser = optparse.OptionParser(usage=usage) |
| 225 # Strangely, the default help output doesn't include the choice list. | 257 # Strangely, the default help output doesn't include the choice list. |
| 226 choices = ['mac', 'win', 'linux', 'linux64'] | 258 choices = ['mac', 'win', 'linux', 'linux64'] |
| 227 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 259 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
| 228 parser.add_option('-a', '--archive', | 260 parser.add_option('-a', '--archive', |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 243 print | 275 print |
| 244 parser.print_help() | 276 parser.print_help() |
| 245 return 1 | 277 return 1 |
| 246 | 278 |
| 247 if opts.bad and opts.good and (opts.good > opts.bad): | 279 if opts.bad and opts.good and (opts.good > opts.bad): |
| 248 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 280 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
| 249 (opts.good, opts.bad)) | 281 (opts.good, opts.bad)) |
| 250 parser.print_help() | 282 parser.print_help() |
| 251 return 1 | 283 return 1 |
| 252 | 284 |
| 253 SetArchiveVars(opts.archive) | 285 # Create the context. Initialize 0 for the revisions as they are set below. |
| 286 context = PathContext(opts.archive, 0, 0) | |
| 254 | 287 |
| 255 # Pick a starting point, try to get HEAD for this. | 288 # Pick a starting point, try to get HEAD for this. |
| 256 if opts.bad: | 289 if opts.bad: |
| 257 bad_rev = opts.bad | 290 bad_rev = opts.bad |
| 258 else: | 291 else: |
| 259 bad_rev = 0 | 292 bad_rev = 0 |
| 260 try: | 293 try: |
| 261 # Location of the latest build revision number | 294 # Location of the latest build revision number |
| 262 BUILD_LATEST_URL = '%s/LATEST/REVISION' % (BUILD_BASE_URL) | 295 nh = urllib.urlopen(context.GetLastChangeURL()) |
| 263 nh = urllib.urlopen(BUILD_LATEST_URL) | |
| 264 latest = int(nh.read()) | 296 latest = int(nh.read()) |
| 265 nh.close() | 297 nh.close() |
| 266 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) | 298 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) |
| 267 if (bad_rev == ''): | 299 if (bad_rev == ''): |
| 268 bad_rev = latest | 300 bad_rev = latest |
| 269 bad_rev = int(bad_rev) | 301 bad_rev = int(bad_rev) |
| 270 except Exception, e: | 302 except Exception, e: |
| 271 print('Could not determine latest revision. This could be bad...') | 303 print('Could not determine latest revision. This could be bad...') |
| 272 bad_rev = int(raw_input('Bad revision: ')) | 304 bad_rev = int(raw_input('Bad revision: ')) |
| 273 | 305 |
| 274 # Find out when we were good. | 306 # Find out when we were good. |
| 275 if opts.good: | 307 if opts.good: |
| 276 good_rev = opts.good | 308 good_rev = opts.good |
| 277 else: | 309 else: |
| 278 good_rev = 0 | 310 good_rev = 0 |
| 279 try: | 311 try: |
| 280 good_rev = int(raw_input('Last known good [0]: ')) | 312 good_rev = int(raw_input('Last known good [0]: ')) |
| 281 except Exception, e: | 313 except Exception, e: |
| 282 pass | 314 pass |
| 283 | 315 |
| 316 # Set the input parameters now that they've been validated. | |
| 317 context.good_revision = good_rev | |
| 318 context.bad_revision = bad_rev | |
| 319 | |
| 284 # Get a list of revisions to bisect across. | 320 # Get a list of revisions to bisect across. |
| 285 revlist = GetRevList(good_rev, bad_rev) | 321 revlist = GetRevList(context) |
| 286 if len(revlist) < 2: # Don't have enough builds to bisect | 322 if len(revlist) < 2: # Don't have enough builds to bisect |
| 287 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 323 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
| 288 sys.exit(1) | 324 sys.exit(1) |
| 289 | 325 |
| 290 # If we don't have a |good_rev|, set it to be the first revision possible. | 326 # If we don't have a |good_rev|, set it to be the first revision possible. |
| 291 if good_rev == 0: | 327 if good_rev == 0: |
| 292 good_rev = revlist[0] | 328 good_rev = revlist[0] |
| 293 | 329 |
| 294 # These are indexes of |revlist|. | 330 # These are indexes of |revlist|. |
| 295 good = 0 | 331 good = 0 |
| 296 bad = len(revlist) - 1 | 332 bad = len(revlist) - 1 |
| 297 last_known_good_rev = revlist[good] | 333 last_known_good_rev = revlist[good] |
| 298 | 334 |
| 299 # Binary search time! | 335 # Binary search time! |
| 300 while good < bad: | 336 while good < bad: |
| 301 candidates = revlist[good:bad] | 337 candidates = revlist[good:bad] |
| 302 num_poss = len(candidates) | 338 num_poss = len(candidates) |
| 303 if num_poss > 10: | 339 if num_poss > 10: |
| 304 print('%d candidates. %d tries left.' % | 340 print('%d candidates. %d tries left.' % |
| 305 (num_poss, round(math.log(num_poss, 2)))) | 341 (num_poss, round(math.log(num_poss, 2)))) |
| 306 else: | 342 else: |
| 307 print('Candidates: %s' % map(GetRevision, revlist[good:bad])) | 343 print('Candidates: %s' % revlist[good:bad]) |
| 308 | 344 |
| 309 # Cut the problem in half... | 345 # Cut the problem in half... |
| 310 test = int((bad - good) / 2) + good | 346 test = int((bad - good) / 2) + good |
| 311 test_rev = revlist[test] | 347 test_rev = revlist[test] |
| 312 | 348 |
| 313 # Let the user give this rev a spin (in her own profile, if she wants). | 349 # Let the user give this rev a spin (in her own profile, if she wants). |
| 314 profile = opts.profile | 350 profile = opts.profile |
| 315 if not profile: | 351 if not profile: |
| 316 profile = 'profile' # In a temp dir. | 352 profile = 'profile' # In a temp dir. |
| 317 TryRevision(test_rev, profile, args) | 353 TryRevision(context, test_rev, profile, args) |
| 318 if AskIsGoodBuild(test_rev): | 354 if AskIsGoodBuild(test_rev): |
| 319 last_known_good_rev = revlist[good] | 355 last_known_good_rev = revlist[good] |
| 320 good = test + 1 | 356 good = test + 1 |
| 321 else: | 357 else: |
| 322 bad = test | 358 bad = test |
| 323 | 359 |
| 324 # We're done. Let the user know the results in an official manner. | 360 # We're done. Let the user know the results in an official manner. |
| 325 bad_revision = GetRevision(revlist[bad]) | 361 print('You are probably looking for build %d.' % revlist[bad]) |
| 326 print('You are probably looking for build %d.' % bad_revision) | |
| 327 print('CHANGELOG URL:') | 362 print('CHANGELOG URL:') |
| 328 print(CHANGELOG_URL % (GetRevision(last_known_good_rev), bad_revision)) | 363 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad])) |
| 329 print('Built at revision:') | 364 print('Built at revision:') |
| 330 print(BUILD_VIEWVC_URL % bad_revision) | 365 print(BUILD_VIEWVC_URL % revlist[bad]) |
| 331 | 366 |
| 332 if __name__ == '__main__': | 367 if __name__ == '__main__': |
| 333 sys.exit(main()) | 368 sys.exit(main()) |
| OLD | NEW |