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 |