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, marker=None): |
| 78 """Returns the URL for a directory listing, with an optional marker.""" |
| 79 marker_param = '' |
| 80 if marker: |
| 81 marker_param = '&marker=' + str(marker) |
| 82 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ |
| 83 marker_param |
| 84 |
| 85 def GetDownloadURL(self, revision): |
| 86 """Gets the download URL for a build archive of a specific revision.""" |
| 87 return BASE_URL + '/' + self._listing_platform_dir + str(revision) + '/' + \ |
| 88 self.archive_name |
| 89 |
| 90 def GetLastChangeURL(self): |
| 91 """Returns a URL to the LAST_CHANGE file.""" |
| 92 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
| 93 |
| 94 def GetLaunchPath(self): |
| 95 """Returns a relative path (presumably from the archive extraction location) |
| 96 that is used to run the executable.""" |
| 97 return os.path.join(self._archive_extract_dir, self._binary_name) |
| 98 |
58 | 99 |
59 def UnzipFilenameToDir(filename, dir): | 100 def UnzipFilenameToDir(filename, dir): |
60 """Unzip |filename| to directory |dir|.""" | 101 """Unzip |filename| to directory |dir|.""" |
61 zf = zipfile.ZipFile(filename) | 102 zf = zipfile.ZipFile(filename) |
62 # Make base. | 103 # Make base. |
63 pushd = os.getcwd() | 104 pushd = os.getcwd() |
64 try: | 105 try: |
65 if not os.path.isdir(dir): | 106 if not os.path.isdir(dir): |
66 os.mkdir(dir) | 107 os.mkdir(dir) |
67 os.chdir(dir) | 108 os.chdir(dir) |
(...skipping 11 matching lines...) Expand all Loading... |
79 out.write(zf.read(name)) | 120 out.write(zf.read(name)) |
80 out.close() | 121 out.close() |
81 # Set permissions. Permission info in external_attr is shifted 16 bits. | 122 # Set permissions. Permission info in external_attr is shifted 16 bits. |
82 os.chmod(name, info.external_attr >> 16L) | 123 os.chmod(name, info.external_attr >> 16L) |
83 os.chdir(pushd) | 124 os.chdir(pushd) |
84 except Exception, e: | 125 except Exception, e: |
85 print >>sys.stderr, e | 126 print >>sys.stderr, e |
86 sys.exit(1) | 127 sys.exit(1) |
87 | 128 |
88 | 129 |
89 def SetArchiveVars(archive): | 130 def ParseDirectoryIndex(context): |
90 """Set a bunch of global variables appropriate for the specified archive.""" | 131 """Parses the Google Storage directory listing into a list of revision |
91 global BUILD_ARCHIVE_TYPE | 132 numbers. The range starts with context.good_revision and goes until the latest |
92 global BUILD_ZIP_NAME | 133 revision.""" |
93 global BUILD_DIR_NAME | 134 def _FetchAndParse(url): |
94 global BUILD_EXE_NAME | 135 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If |
95 global BUILD_BASE_URL | 136 next-marker is not None, then the listing is a partial listing and another |
| 137 fetch should be performed with next-marker being the marker= GET |
| 138 parameter.""" |
| 139 handle = urllib.urlopen(url) |
| 140 document = ElementTree.parse(handle) |
96 | 141 |
97 BUILD_ARCHIVE_TYPE = archive | 142 # All nodes in the tree are namespaced. Get the root's tag name to extract |
| 143 # the namespace. Etree does namespaces as |{namespace}tag|. |
| 144 root_tag = document.getroot().tag |
| 145 end_ns_pos = root_tag.find('}') |
| 146 if end_ns_pos == -1: |
| 147 raise Exception("Could not locate end namespace for directory index") |
| 148 namespace = root_tag[:end_ns_pos + 1] |
98 | 149 |
99 if BUILD_ARCHIVE_TYPE in ('linux', 'linux64', 'linux-chromiumos'): | 150 # Find the prefix (_listing_platform_dir) and whether or not the list is |
100 BUILD_ZIP_NAME = 'chrome-linux.zip' | 151 # truncated. |
101 BUILD_DIR_NAME = 'chrome-linux' | 152 prefix = document.find(namespace + 'Prefix').text |
102 BUILD_EXE_NAME = 'chrome' | 153 next_marker = None |
103 elif BUILD_ARCHIVE_TYPE in ('mac'): | 154 is_truncated = document.find(namespace + 'IsTruncated') |
104 BUILD_ZIP_NAME = 'chrome-mac.zip' | 155 if is_truncated is not None and is_truncated.text.lower() == 'true': |
105 BUILD_DIR_NAME = 'chrome-mac' | 156 next_marker = document.find(namespace + 'NextMarker').text |
106 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' | 157 |
107 elif BUILD_ARCHIVE_TYPE in ('win'): | 158 # Get a list of all the revisions. |
108 BUILD_ZIP_NAME = 'chrome-win32.zip' | 159 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + |
109 BUILD_DIR_NAME = 'chrome-win32' | 160 namespace + 'Prefix') |
110 BUILD_EXE_NAME = 'chrome.exe' | 161 # The <Prefix> nodes have content of the form of |
| 162 # |_listing_platform_dir/revision/|. Strip off the platform dir and the |
| 163 # trailing slash to just have a number. |
| 164 revisions = map(lambda x: x.text[len(prefix):-1], all_prefixes) |
| 165 return (revisions, next_marker) |
| 166 |
| 167 # Fetch the first list of revisions. |
| 168 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) |
| 169 # If the result list was truncated, refetch with the next marker. Do this |
| 170 # until an entire directory listing is done. |
| 171 while next_marker: |
| 172 (new_revisions, next_marker) = _FetchAndParse( |
| 173 context.GetListingURL(next_marker)) |
| 174 revisions.extend(new_revisions) |
| 175 |
| 176 return revisions |
111 | 177 |
112 | 178 |
113 def ParseDirectoryIndex(url): | 179 def GetRevList(context): |
114 """Parses the all_builds.txt index file. The format of this file is: | 180 """Gets the list of revision numbers between |good_revision| and |
115 mac/2011-02-16/75130 | 181 |bad_revision| of the |context|.""" |
116 mac/2011-02-16/75218 | 182 # Download the revlist and filter for just the range between good and bad. |
117 mac/2011-02-16/75226 | 183 rev_range = range(context.good_revision, context.bad_revision) |
118 mac/2011-02-16/75234 | 184 revlist = map(int, ParseDirectoryIndex(context)) |
119 mac/2011-02-16/75184 | 185 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() | 186 revlist.sort() |
158 return revlist | 187 return revlist |
159 | 188 |
160 | 189 |
161 def TryRevision(iline, profile, args): | 190 def TryRevision(context, rev, profile, args): |
162 """Downloads revision from |iline|, unzips it, and opens it for the user to | 191 """Downloads revision |rev|, unzips it, and opens it for the user to test. |
163 test. |profile| is the profile to use.""" | 192 |profile| is the profile to use.""" |
164 # Do this in a temp dir so we don't collide with user files. | 193 # Do this in a temp dir so we don't collide with user files. |
165 cwd = os.getcwd() | 194 cwd = os.getcwd() |
166 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 195 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
167 os.chdir(tempdir) | 196 os.chdir(tempdir) |
168 | 197 |
169 # Download the file. | 198 # Download the file. |
170 download_url = BUILD_BASE_URL + BUILD_ARCHIVE_TYPE + \ | 199 download_url = context.GetDownloadURL(rev) |
171 (BUILD_ARCHIVE_URL % ParseIndexLine(iline)) + BUILD_ZIP_NAME | |
172 def _ReportHook(blocknum, blocksize, totalsize): | 200 def _ReportHook(blocknum, blocksize, totalsize): |
173 size = blocknum * blocksize | 201 size = blocknum * blocksize |
174 if totalsize == -1: # Total size not known. | 202 if totalsize == -1: # Total size not known. |
175 progress = "Received %d bytes" % size | 203 progress = "Received %d bytes" % size |
176 else: | 204 else: |
177 size = min(totalsize, size) | 205 size = min(totalsize, size) |
178 progress = "Received %d of %d bytes, %.2f%%" % ( | 206 progress = "Received %d of %d bytes, %.2f%%" % ( |
179 size, totalsize, 100.0 * size / totalsize) | 207 size, totalsize, 100.0 * size / totalsize) |
180 # Send a \r to let all progress messages use just one line of output. | 208 # Send a \r to let all progress messages use just one line of output. |
181 sys.stdout.write("\r" + progress) | 209 sys.stdout.write("\r" + progress) |
182 sys.stdout.flush() | 210 sys.stdout.flush() |
183 try: | 211 try: |
184 print 'Fetching ' + download_url | 212 print 'Fetching ' + download_url |
185 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _ReportHook) | 213 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) |
186 print | 214 print |
187 except Exception, e: | 215 except Exception, e: |
188 print('Could not retrieve the download. Sorry.') | 216 print('Could not retrieve the download. Sorry.') |
189 sys.exit(-1) | 217 sys.exit(-1) |
190 | 218 |
191 # Unzip the file. | 219 # Unzip the file. |
192 print 'Unzipping ...' | 220 print 'Unzipping ...' |
193 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) | 221 UnzipFilenameToDir(context.archive_name, os.curdir) |
194 | 222 |
195 # Tell the system to open the app. | 223 # Tell the system to open the app. |
196 args = ['--user-data-dir=%s' % profile] + args | 224 args = ['--user-data-dir=%s' % profile] + args |
197 flags = ' '.join(map(pipes.quote, args)) | 225 flags = ' '.join(map(pipes.quote, args)) |
198 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) | 226 cmd = '%s %s' % (context.GetLaunchPath(), flags) |
199 cmd = '%s %s' % (exe, flags) | |
200 print 'Running %s' % cmd | 227 print 'Running %s' % cmd |
201 os.system(cmd) | 228 os.system(cmd) |
202 | 229 |
203 os.chdir(cwd) | 230 os.chdir(cwd) |
204 print 'Cleaning temp dir ...' | 231 print 'Cleaning temp dir ...' |
205 try: | 232 try: |
206 shutil.rmtree(tempdir, True) | 233 shutil.rmtree(tempdir, True) |
207 except Exception, e: | 234 except Exception, e: |
208 pass | 235 pass |
209 | 236 |
210 | 237 |
211 def AskIsGoodBuild(iline): | 238 def AskIsGoodBuild(rev): |
212 """Ask the user whether build from index line |iline| is good or bad.""" | 239 """Ask the user whether build |rev| is good or bad.""" |
213 # Loop until we get a response that we can parse. | 240 # Loop until we get a response that we can parse. |
214 while True: | 241 while True: |
215 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % GetRevision(iline)) | 242 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) |
216 if response and response in ('g', 'b'): | 243 if response and response in ('g', 'b'): |
217 return response == 'g' | 244 return response == 'g' |
218 | 245 |
| 246 |
219 def main(): | 247 def main(): |
220 usage = ('%prog [options] [-- chromium-options]\n' | 248 usage = ('%prog [options] [-- chromium-options]\n' |
221 'Perform binary search on the snapshot builds.\n' | 249 'Perform binary search on the snapshot builds.\n' |
222 '\n' | 250 '\n' |
223 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 251 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
224 parser = optparse.OptionParser(usage=usage) | 252 parser = optparse.OptionParser(usage=usage) |
225 # Strangely, the default help output doesn't include the choice list. | 253 # Strangely, the default help output doesn't include the choice list. |
226 choices = ['mac', 'win', 'linux', 'linux64'] | 254 choices = ['mac', 'win', 'linux', 'linux64'] |
227 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 255 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
228 parser.add_option('-a', '--archive', | 256 parser.add_option('-a', '--archive', |
(...skipping 14 matching lines...) Expand all Loading... |
243 print | 271 print |
244 parser.print_help() | 272 parser.print_help() |
245 return 1 | 273 return 1 |
246 | 274 |
247 if opts.bad and opts.good and (opts.good > opts.bad): | 275 if opts.bad and opts.good and (opts.good > opts.bad): |
248 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 276 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
249 (opts.good, opts.bad)) | 277 (opts.good, opts.bad)) |
250 parser.print_help() | 278 parser.print_help() |
251 return 1 | 279 return 1 |
252 | 280 |
253 SetArchiveVars(opts.archive) | 281 # Create the context. Initialize 0 for the revisions as they are set below. |
| 282 context = PathContext(opts.archive, 0, 0) |
254 | 283 |
255 # Pick a starting point, try to get HEAD for this. | 284 # Pick a starting point, try to get HEAD for this. |
256 if opts.bad: | 285 if opts.bad: |
257 bad_rev = opts.bad | 286 bad_rev = opts.bad |
258 else: | 287 else: |
259 bad_rev = 0 | 288 bad_rev = 0 |
260 try: | 289 try: |
261 # Location of the latest build revision number | 290 # Location of the latest build revision number |
262 BUILD_LATEST_URL = '%s/LATEST/REVISION' % (BUILD_BASE_URL) | 291 nh = urllib.urlopen(context.GetLastChangeURL()) |
263 nh = urllib.urlopen(BUILD_LATEST_URL) | |
264 latest = int(nh.read()) | 292 latest = int(nh.read()) |
265 nh.close() | 293 nh.close() |
266 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) | 294 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) |
267 if (bad_rev == ''): | 295 if (bad_rev == ''): |
268 bad_rev = latest | 296 bad_rev = latest |
269 bad_rev = int(bad_rev) | 297 bad_rev = int(bad_rev) |
270 except Exception, e: | 298 except Exception, e: |
271 print('Could not determine latest revision. This could be bad...') | 299 print('Could not determine latest revision. This could be bad...') |
272 bad_rev = int(raw_input('Bad revision: ')) | 300 bad_rev = int(raw_input('Bad revision: ')) |
273 | 301 |
274 # Find out when we were good. | 302 # Find out when we were good. |
275 if opts.good: | 303 if opts.good: |
276 good_rev = opts.good | 304 good_rev = opts.good |
277 else: | 305 else: |
278 good_rev = 0 | 306 good_rev = 0 |
279 try: | 307 try: |
280 good_rev = int(raw_input('Last known good [0]: ')) | 308 good_rev = int(raw_input('Last known good [0]: ')) |
281 except Exception, e: | 309 except Exception, e: |
282 pass | 310 pass |
283 | 311 |
| 312 # Set the input parameters now that they've been validated. |
| 313 context.good_revision = good_rev |
| 314 context.bad_revision = bad_rev |
| 315 |
284 # Get a list of revisions to bisect across. | 316 # Get a list of revisions to bisect across. |
285 revlist = GetRevList(good_rev, bad_rev) | 317 revlist = GetRevList(context) |
286 if len(revlist) < 2: # Don't have enough builds to bisect | 318 if len(revlist) < 2: # Don't have enough builds to bisect |
287 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 319 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
288 sys.exit(1) | 320 sys.exit(1) |
289 | 321 |
290 # If we don't have a |good_rev|, set it to be the first revision possible. | 322 # If we don't have a |good_rev|, set it to be the first revision possible. |
291 if good_rev == 0: | 323 if good_rev == 0: |
292 good_rev = revlist[0] | 324 good_rev = revlist[0] |
293 | 325 |
294 # These are indexes of |revlist|. | 326 # These are indexes of |revlist|. |
295 good = 0 | 327 good = 0 |
296 bad = len(revlist) - 1 | 328 bad = len(revlist) - 1 |
297 last_known_good_rev = revlist[good] | 329 last_known_good_rev = revlist[good] |
298 | 330 |
299 # Binary search time! | 331 # Binary search time! |
300 while good < bad: | 332 while good < bad: |
301 candidates = revlist[good:bad] | 333 candidates = revlist[good:bad] |
302 num_poss = len(candidates) | 334 num_poss = len(candidates) |
303 if num_poss > 10: | 335 if num_poss > 10: |
304 print('%d candidates. %d tries left.' % | 336 print('%d candidates. %d tries left.' % |
305 (num_poss, round(math.log(num_poss, 2)))) | 337 (num_poss, round(math.log(num_poss, 2)))) |
306 else: | 338 else: |
307 print('Candidates: %s' % map(GetRevision, revlist[good:bad])) | 339 print('Candidates: %s' % revlist[good:bad]) |
308 | 340 |
309 # Cut the problem in half... | 341 # Cut the problem in half... |
310 test = int((bad - good) / 2) + good | 342 test = int((bad - good) / 2) + good |
311 test_rev = revlist[test] | 343 test_rev = revlist[test] |
312 | 344 |
313 # Let the user give this rev a spin (in her own profile, if she wants). | 345 # Let the user give this rev a spin (in her own profile, if she wants). |
314 profile = opts.profile | 346 profile = opts.profile |
315 if not profile: | 347 if not profile: |
316 profile = 'profile' # In a temp dir. | 348 profile = 'profile' # In a temp dir. |
317 TryRevision(test_rev, profile, args) | 349 TryRevision(context, test_rev, profile, args) |
318 if AskIsGoodBuild(test_rev): | 350 if AskIsGoodBuild(test_rev): |
319 last_known_good_rev = revlist[good] | 351 last_known_good_rev = revlist[good] |
320 good = test + 1 | 352 good = test + 1 |
321 else: | 353 else: |
322 bad = test | 354 bad = test |
323 | 355 |
324 # We're done. Let the user know the results in an official manner. | 356 # We're done. Let the user know the results in an official manner. |
325 bad_revision = GetRevision(revlist[bad]) | 357 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:') | 358 print('CHANGELOG URL:') |
328 print(CHANGELOG_URL % (GetRevision(last_known_good_rev), bad_revision)) | 359 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad])) |
329 print('Built at revision:') | 360 print('Built at revision:') |
330 print(BUILD_VIEWVC_URL % bad_revision) | 361 print(BUILD_VIEWVC_URL % revlist[bad]) |
331 | 362 |
332 if __name__ == '__main__': | 363 if __name__ == '__main__': |
333 sys.exit(main()) | 364 sys.exit(main()) |
OLD | NEW |