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 # The root URL for storage. | 15 # The root URL for storage. |
16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-continuous' | 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' |
17 | 17 |
18 # URL to the ViewVC commit page. | 18 # URL to the ViewVC commit page. |
19 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' |
20 | 20 |
21 # Changelogs URL. | 21 # Changelogs URL. |
22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ | 22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ |
23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' | 23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' |
24 | 24 |
25 ############################################################################### | 25 ############################################################################### |
26 | 26 |
27 import math | 27 import math |
28 import optparse | 28 import optparse |
29 import os | 29 import os |
30 import pipes | 30 import pipes |
31 import re | 31 import re |
32 import shutil | 32 import shutil |
33 import sys | 33 import sys |
34 import tempfile | 34 import tempfile |
35 import urllib | 35 import urllib |
36 from xml.etree import ElementTree | 36 from xml.etree import ElementTree |
37 import zipfile | 37 import zipfile |
38 import subprocess | |
39 import threading | |
Evan Martin
2011/07/14 18:57:52
I think this list is alphabetized.
szager
2011/07/14 19:23:03
Done.
| |
38 | 40 |
39 class PathContext(object): | 41 class PathContext(object): |
40 """A PathContext is used to carry the information used to construct URLs and | 42 """A PathContext is used to carry the information used to construct URLs and |
41 paths when dealing with the storage server and archives.""" | 43 paths when dealing with the storage server and archives.""" |
42 def __init__(self, platform, good_revision, bad_revision): | 44 def __init__(self, platform, good_revision, bad_revision): |
43 super(PathContext, self).__init__() | 45 super(PathContext, self).__init__() |
44 # Store off the input parameters. | 46 # Store off the input parameters. |
45 self.platform = platform # What's passed in to the '-a/--archive' option. | 47 self.platform = platform # What's passed in to the '-a/--archive' option. |
46 self.good_revision = good_revision | 48 self.good_revision = good_revision |
47 self.bad_revision = bad_revision | 49 self.bad_revision = bad_revision |
48 | 50 |
49 # The name of the ZIP file in a revision directory on the server. | 51 # The name of the ZIP file in a revision directory on the server. |
50 self.archive_name = None | 52 self.archive_name = None |
51 | 53 |
52 # Set some internal members: | 54 # Set some internal members: |
53 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. | 55 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. |
54 # _archive_extract_dir = Uncompressed directory in the archive_name file. | 56 # _archive_extract_dir = Uncompressed directory in the archive_name file. |
55 # _binary_name = The name of the executable to run. | 57 # _binary_name = The name of the executable to run. |
56 if self.platform == 'linux' or self.platform == 'linux-64': | 58 if self.platform == 'linux' or self.platform == 'linux64': |
57 self._listing_platform_dir = 'Linux/' | 59 self._listing_platform_dir = 'Linux/' |
58 self.archive_name = 'chrome-linux.zip' | 60 self.archive_name = 'chrome-linux.zip' |
59 self._archive_extract_dir = 'chrome-linux' | 61 self._archive_extract_dir = 'chrome-linux' |
60 self._binary_name = 'chrome' | 62 self._binary_name = 'chrome' |
61 # Linux and x64 share all the same path data except for the archive dir. | 63 # Linux and x64 share all the same path data except for the archive dir. |
62 if self.platform == 'linux-64': | 64 if self.platform == 'linux64': |
63 self._listing_platform_dir = 'Linux_x64/' | 65 self._listing_platform_dir = 'Linux_x64/' |
64 elif self.platform == 'mac': | 66 elif self.platform == 'mac': |
65 self._listing_platform_dir = 'Mac/' | 67 self._listing_platform_dir = 'Mac/' |
66 self.archive_name = 'chrome-mac.zip' | 68 self.archive_name = 'chrome-mac.zip' |
67 self._archive_extract_dir = 'chrome-mac' | 69 self._archive_extract_dir = 'chrome-mac' |
68 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | 70 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' |
69 elif self.platform == 'win': | 71 elif self.platform == 'win': |
70 self._listing_platform_dir = 'Win/' | 72 self._listing_platform_dir = 'Win/' |
71 self.archive_name = 'chrome-win32.zip' | 73 self.archive_name = 'chrome-win32.zip' |
72 self._archive_extract_dir = 'chrome-win32' | 74 self._archive_extract_dir = 'chrome-win32' |
73 self._binary_name = 'chrome.exe' | 75 self._binary_name = 'chrome.exe' |
74 else: | 76 else: |
75 raise Exception("Invalid platform") | 77 raise Exception("Invalid platform") |
76 | 78 |
77 def GetListingURL(self, marker=None): | 79 def GetListingURL(self, marker=None): |
78 """Returns the URL for a directory listing, with an optional marker.""" | 80 """Returns the URL for a directory listing, with an optional marker.""" |
79 marker_param = '' | 81 marker_param = '' |
80 if marker: | 82 if marker: |
81 marker_param = '&marker=' + str(marker) | 83 marker_param = '&marker=' + str(marker) |
82 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | 84 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ |
83 marker_param | 85 marker_param |
84 | 86 |
85 def GetDownloadURL(self, revision): | 87 def GetDownloadURL(self, revision): |
86 """Gets the download URL for a build archive of a specific revision.""" | 88 """Gets the download URL for a build archive of a specific revision.""" |
87 return BASE_URL + '/' + self._listing_platform_dir + str(revision) + '/' + \ | 89 return "%s/%s%d/%s" % ( |
88 self.archive_name | 90 BASE_URL, self._listing_platform_dir, revision, self.archive_name) |
89 | 91 |
90 def GetLastChangeURL(self): | 92 def GetLastChangeURL(self): |
91 """Returns a URL to the LAST_CHANGE file.""" | 93 """Returns a URL to the LAST_CHANGE file.""" |
92 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 94 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
93 | 95 |
94 def GetLaunchPath(self): | 96 def GetLaunchPath(self): |
95 """Returns a relative path (presumably from the archive extraction location) | 97 """Returns a relative path (presumably from the archive extraction location) |
96 that is used to run the executable.""" | 98 that is used to run the executable.""" |
97 return os.path.join(self._archive_extract_dir, self._binary_name) | 99 return os.path.join(self._archive_extract_dir, self._binary_name) |
98 | 100 |
99 | 101 |
100 def UnzipFilenameToDir(filename, dir): | 102 def UnzipFilenameToDir(filename, dir): |
101 """Unzip |filename| to directory |dir|.""" | 103 """Unzip |filename| to directory |dir|.""" |
104 pushd = os.getcwd() | |
105 if not os.path.isabs(filename): | |
106 filename = os.path.join(pushd, filename) | |
102 zf = zipfile.ZipFile(filename) | 107 zf = zipfile.ZipFile(filename) |
103 # Make base. | 108 # Make base. |
104 pushd = os.getcwd() | |
105 try: | 109 try: |
106 if not os.path.isdir(dir): | 110 if not os.path.isdir(dir): |
107 os.mkdir(dir) | 111 os.mkdir(dir) |
108 os.chdir(dir) | 112 os.chdir(dir) |
109 # Extract files. | 113 # Extract files. |
110 for info in zf.infolist(): | 114 for info in zf.infolist(): |
111 name = info.filename | 115 name = info.filename |
112 if name.endswith('/'): # dir | 116 if name.endswith('/'): # dir |
113 if not os.path.isdir(name): | 117 if not os.path.isdir(name): |
114 os.makedirs(name) | 118 os.makedirs(name) |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
154 is_truncated = document.find(namespace + 'IsTruncated') | 158 is_truncated = document.find(namespace + 'IsTruncated') |
155 if is_truncated is not None and is_truncated.text.lower() == 'true': | 159 if is_truncated is not None and is_truncated.text.lower() == 'true': |
156 next_marker = document.find(namespace + 'NextMarker').text | 160 next_marker = document.find(namespace + 'NextMarker').text |
157 | 161 |
158 # Get a list of all the revisions. | 162 # Get a list of all the revisions. |
159 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + | 163 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + |
160 namespace + 'Prefix') | 164 namespace + 'Prefix') |
161 # The <Prefix> nodes have content of the form of | 165 # The <Prefix> nodes have content of the form of |
162 # |_listing_platform_dir/revision/|. Strip off the platform dir and the | 166 # |_listing_platform_dir/revision/|. Strip off the platform dir and the |
163 # trailing slash to just have a number. | 167 # trailing slash to just have a number. |
164 revisions = map(lambda x: x.text[len(prefix):-1], all_prefixes) | 168 #revisions = map(lambda x: x.text[len(prefix):-1], all_prefixes) |
Evan Martin
2011/07/14 18:57:52
Did you mean to leave in this commented-out code?
szager
2011/07/14 19:23:03
Deleted!
| |
169 revisions = [] | |
170 for pf in all_prefixes: | |
Robert Sesek
2011/07/15 16:27:14
What's "pf"? |prefix| is fine.
| |
171 revnum = pf.text[len(prefix):-1] | |
172 try: | |
173 revnum = int(revnum) | |
174 revisions.append(revnum) | |
175 except ValueError: | |
176 pass | |
165 return (revisions, next_marker) | 177 return (revisions, next_marker) |
166 | 178 |
167 # Fetch the first list of revisions. | 179 # Fetch the first list of revisions. |
168 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) | 180 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) |
169 # If the result list was truncated, refetch with the next marker. Do this | 181 # If the result list was truncated, refetch with the next marker. Do this |
170 # until an entire directory listing is done. | 182 # until an entire directory listing is done. |
171 while next_marker: | 183 while next_marker: |
172 (new_revisions, next_marker) = _FetchAndParse( | 184 (new_revisions, next_marker) = _FetchAndParse( |
173 context.GetListingURL(next_marker)) | 185 context.GetListingURL(next_marker)) |
174 revisions.extend(new_revisions) | 186 revisions.extend(new_revisions) |
175 | 187 |
176 return revisions | 188 return revisions |
177 | 189 |
178 | 190 |
179 def GetRevList(context): | 191 def GetRevList(context): |
180 """Gets the list of revision numbers between |good_revision| and | 192 """Gets the list of revision numbers between |good_revision| and |
181 |bad_revision| of the |context|.""" | 193 |bad_revision| of the |context|.""" |
182 # Download the revlist and filter for just the range between good and bad. | 194 # Download the revlist and filter for just the range between good and bad. |
183 rev_range = range(context.good_revision, context.bad_revision) | 195 rev_range = range(context.good_revision, context.bad_revision) |
184 revlist = map(int, ParseDirectoryIndex(context)) | 196 revlist = map(int, ParseDirectoryIndex(context)) |
185 revlist = filter(lambda r: r in rev_range, revlist) | 197 revlist = filter(lambda r: r in rev_range, revlist) |
186 revlist.sort() | 198 revlist.sort() |
187 return revlist | 199 return revlist |
188 | 200 |
201 def FetchRevision(context, rev, filename, quit_event=None) : | |
202 """Downloads and unzips revision |rev|""" | |
189 | 203 |
190 def TryRevision(context, rev, profile, args): | 204 def ReportHook(blocknum, blocksize, totalsize): |
191 """Downloads revision |rev|, unzips it, and opens it for the user to test. | 205 if quit_event and quit_event.is_set(): |
192 |profile| is the profile to use.""" | 206 raise RuntimeError("Aborting download of revision %d" % rev) |
193 # Do this in a temp dir so we don't collide with user files. | 207 |
208 download_url = context.GetDownloadURL(rev) | |
209 | |
210 try: | |
211 urllib.urlretrieve(download_url, filename, ReportHook) | |
212 except RuntimeError, e: | |
213 pass | |
214 | |
215 def RunRevision(context, revision, zipfile, profile, args) : | |
216 """Given a zipped revision, unzip it and run the test""" | |
217 | |
218 print "Trying revision %d..." % revision | |
219 | |
220 # Create a temp directory and unzip the revision into it | |
194 cwd = os.getcwd() | 221 cwd = os.getcwd() |
195 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 222 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
223 UnzipFilenameToDir(zipfile, tempdir) | |
196 os.chdir(tempdir) | 224 os.chdir(tempdir) |
197 | 225 |
198 # Download the file. | 226 # Run the test |
199 download_url = context.GetDownloadURL(rev) | 227 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args |
200 def _ReportHook(blocknum, blocksize, totalsize): | 228 subproc = subprocess.Popen(testargs, |
201 size = blocknum * blocksize | 229 bufsize=-1, |
202 if totalsize == -1: # Total size not known. | 230 stdout=subprocess.PIPE, |
203 progress = "Received %d bytes" % size | 231 stderr=subprocess.PIPE) |
204 else: | 232 (stdout, stderr) = subproc.communicate() |
205 size = min(totalsize, size) | |
206 progress = "Received %d of %d bytes, %.2f%%" % ( | |
207 size, totalsize, 100.0 * size / totalsize) | |
208 # Send a \r to let all progress messages use just one line of output. | |
209 sys.stdout.write("\r" + progress) | |
210 sys.stdout.flush() | |
211 try: | |
212 print 'Fetching ' + download_url | |
213 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) | |
214 print | |
215 except Exception, e: | |
216 print('Could not retrieve the download. Sorry.') | |
217 sys.exit(-1) | |
218 | |
219 # Unzip the file. | |
220 print 'Unzipping ...' | |
221 UnzipFilenameToDir(context.archive_name, os.curdir) | |
222 | |
223 # Tell the system to open the app. | |
224 args = ['--user-data-dir=%s' % profile] + args | |
225 flags = ' '.join(map(pipes.quote, args)) | |
226 cmd = '%s %s' % (context.GetLaunchPath(), flags) | |
227 print 'Running %s' % cmd | |
228 os.system(cmd) | |
229 | 233 |
230 os.chdir(cwd) | 234 os.chdir(cwd) |
231 print 'Cleaning temp dir ...' | |
232 try: | 235 try: |
233 shutil.rmtree(tempdir, True) | 236 shutil.rmtree(tempdir, True) |
234 except Exception, e: | 237 except Exception, e: |
235 pass | 238 pass |
236 | 239 |
240 return (subproc.returncode, stdout, stderr) | |
237 | 241 |
238 def AskIsGoodBuild(rev): | 242 def ResultIsGood(status, stdout, stderr) : |
239 """Ask the user whether build |rev| is good or bad.""" | 243 """Ask the user whether build |rev| is good or bad.""" |
240 # Loop until we get a response that we can parse. | 244 # Loop until we get a response that we can parse. |
241 while True: | 245 while True: |
242 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) | 246 response = raw_input('Build is [(g)ood/(b)ad/(q)uit]: ') |
243 if response and response in ('g', 'b'): | 247 if response and response in ('g', 'b'): |
244 return response == 'g' | 248 return response == 'g' |
249 if response and response == 'q': | |
250 raise SystemExit() | |
245 | 251 |
252 def bisect(platform=None, | |
Evan Martin
2011/07/14 18:57:52
Capital B on function names. (Our Python style is
szager
2011/07/14 19:23:03
Done.
| |
253 profile="", | |
254 run_args=(), | |
255 good_rev=0, | |
256 bad_rev=0, | |
257 predicate=ResultIsGood): | |
258 """Given known good and known bad revisions, run a binary search on all | |
259 archived revisions to determine the last known good revision.""" | |
260 | |
261 context = PathContext(platform, good_rev, bad_rev) | |
262 cwd = os.getcwd() | |
263 | |
264 revlist = GetRevList(context) | |
265 | |
266 # Get a list of revisions to bisect across. | |
267 if len(revlist) < 2: # Don't have enough builds to bisect | |
268 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | |
269 raise RuntimeError(msg) | |
270 | |
271 # If we don't have a |good_rev|, set it to be the first revision possible. | |
272 if good_rev == 0: | |
273 good_rev = revlist[0] | |
274 | |
275 # Figure out our bookends and first pivot point; fetch the pivot revision | |
276 good = 0 | |
277 bad = len(revlist) - 1 | |
278 pivot = bad/2 | |
279 rev = revlist[pivot] | |
280 zipfile = os.path.join(cwd, '%d-%s' % (rev, context.archive_name)) | |
281 FetchRevision(context, rev, zipfile) | |
282 | |
283 # Binary search time! | |
284 while zipfile and bad - good > 1: | |
285 print "iterating with good=%d bad=%d pivot=%d" % ( | |
286 revlist[good], revlist[bad], revlist[pivot]) | |
287 | |
288 # Pre-fetch next two possible pivots | |
289 down_pivot = int((pivot - good) / 2) + good | |
290 down_thread = None | |
291 if down_pivot != pivot and down_pivot != good : | |
292 down_rev = revlist[down_pivot] | |
293 zipfile_base = '%d-%s' % (down_rev, context.archive_name) | |
294 down_zipfile = os.path.join(cwd, zipfile_base) | |
295 down_event = threading.Event() | |
296 fetchargs = (context, down_rev, down_zipfile, down_event) | |
297 down_thread = threading.Thread(target=FetchRevision, | |
298 name='down_fetch', | |
299 args=fetchargs) | |
300 down_thread.start() | |
301 | |
302 up_pivot = int((bad - pivot) / 2) + pivot | |
303 up_thread = None | |
304 if up_pivot != pivot and up_pivot != bad : | |
305 up_rev = revlist[up_pivot] | |
306 zipfile_base = '%d-%s' % (up_rev, context.archive_name) | |
307 up_zipfile = os.path.join(cwd, zipfile_base) | |
308 up_event = threading.Event() | |
309 fetchargs = (context, up_rev, up_zipfile, up_event) | |
310 up_thread = threading.Thread(target=FetchRevision, | |
311 name='up_fetch', | |
312 args=fetchargs) | |
313 up_thread.start() | |
314 | |
315 # Run test on the pivot revision | |
316 (status, stdout, stderr) = RunRevision(context, | |
317 rev, | |
318 zipfile, | |
319 profile, | |
320 run_args) | |
321 os.unlink(zipfile) | |
322 zipfile = None | |
323 try: | |
324 if predicate(status, stdout, stderr) : | |
325 good = pivot | |
326 if down_thread : | |
327 down_event.set() # Kill the download of older revision | |
328 down_thread.join() | |
329 os.unlink(down_zipfile) | |
330 if up_thread : | |
331 print "Downloading revision %d..." % up_rev | |
332 up_thread.join() # Wait for newer revision to finish downloading | |
333 pivot = up_pivot | |
334 zipfile = up_zipfile | |
335 else : | |
336 bad = pivot | |
337 if up_thread : | |
338 up_event.set() # Kill download of newer revision | |
339 up_thread.join() | |
340 os.unlink(up_zipfile) | |
341 if down_thread : | |
342 print "Downloading revision %d..." % down_rev | |
343 down_thread.join() # Wait for older revision to finish downloading | |
344 pivot = down_pivot | |
345 zipfile = down_zipfile | |
346 except SystemExit: | |
347 for f in [down_zipfile, up_zipfile]: | |
348 try: | |
349 os.unlink(f) | |
350 except OSError: | |
351 pass | |
352 sys.exit(0) | |
Evan Martin
2011/07/14 18:57:52
Is this both moving the code and making changes to
szager
2011/07/14 19:23:03
I had move the main loop out of main() to enable t
| |
353 | |
354 rev = revlist[pivot] | |
355 | |
356 return (good, bad) | |
357 | |
358 ################################################################################ | |
246 | 359 |
247 def main(): | 360 def main(): |
248 usage = ('%prog [options] [-- chromium-options]\n' | 361 usage = ('%prog [options] [-- chromium-options]\n' |
249 'Perform binary search on the snapshot builds.\n' | 362 'Perform binary search on the snapshot builds.\n' |
250 '\n' | 363 '\n' |
251 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 364 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
252 parser = optparse.OptionParser(usage=usage) | 365 parser = optparse.OptionParser(usage=usage) |
253 # Strangely, the default help output doesn't include the choice list. | 366 # Strangely, the default help output doesn't include the choice list. |
254 choices = ['mac', 'win', 'linux', 'linux64'] | 367 choices = ['mac', 'win', 'linux', 'linux64'] |
255 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 368 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
(...skipping 18 matching lines...) Expand all Loading... | |
274 | 387 |
275 if opts.bad and opts.good and (opts.good > opts.bad): | 388 if opts.bad and opts.good and (opts.good > opts.bad): |
276 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 389 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
277 (opts.good, opts.bad)) | 390 (opts.good, opts.bad)) |
278 parser.print_help() | 391 parser.print_help() |
279 return 1 | 392 return 1 |
280 | 393 |
281 # Create the context. Initialize 0 for the revisions as they are set below. | 394 # Create the context. Initialize 0 for the revisions as they are set below. |
282 context = PathContext(opts.archive, 0, 0) | 395 context = PathContext(opts.archive, 0, 0) |
283 | 396 |
284 # Pick a starting point, try to get HEAD for this. | 397 if not opts.bad : |
285 if opts.bad: | |
286 bad_rev = opts.bad | |
287 else: | |
288 bad_rev = 0 | |
289 try: | 398 try: |
290 # Location of the latest build revision number | 399 # Location of the latest build revision number |
291 nh = urllib.urlopen(context.GetLastChangeURL()) | 400 nh = urllib.urlopen(context.GetLastChangeURL()) |
292 latest = int(nh.read()) | 401 latest = int(nh.read()) |
293 nh.close() | 402 nh.close() |
294 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) | 403 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) |
295 if (bad_rev == ''): | 404 if (bad_rev == ''): |
296 bad_rev = latest | 405 bad_rev = latest |
297 bad_rev = int(bad_rev) | 406 opts.bad = int(bad_rev) |
298 except Exception, e: | 407 except Exception, e: |
299 print('Could not determine latest revision. This could be bad...') | 408 print('Could not determine latest revision. This could be bad...') |
300 bad_rev = int(raw_input('Bad revision: ')) | 409 opts.bad = int(raw_input('Bad revision: ')) |
301 | 410 |
302 # Find out when we were good. | 411 if not opts.good : |
303 if opts.good: | |
304 good_rev = opts.good | |
305 else: | |
306 good_rev = 0 | |
307 try: | 412 try: |
308 good_rev = int(raw_input('Last known good [0]: ')) | 413 opts.good = int(raw_input('Last known good [0]: ')) |
309 except Exception, e: | 414 except Exception, e: |
310 pass | 415 pass |
311 | 416 |
312 # Set the input parameters now that they've been validated. | 417 try : |
313 context.good_revision = good_rev | 418 (good, bad) = bisect(opts.archive, opts.profile, args, opts.good, opts.bad) |
314 context.bad_revision = bad_rev | 419 # We're done. Let the user know the results in an official manner. |
420 print('You are probably looking for build %d.' % bad) | |
421 clog_url = CHANGELOG_URL % (good, bad) | |
422 print('CHANGELOG URL: %s' % clog_url) | |
423 build_url = BUILD_VIEWVC_URL % bad | |
424 print('Built at revision: %s' % build_url) | |
425 return 0 | |
426 except Exception, e: | |
427 print e | |
428 return 1 | |
315 | 429 |
316 # Get a list of revisions to bisect across. | |
317 revlist = GetRevList(context) | |
318 if len(revlist) < 2: # Don't have enough builds to bisect | |
319 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist | |
320 sys.exit(1) | |
321 | |
322 # If we don't have a |good_rev|, set it to be the first revision possible. | |
323 if good_rev == 0: | |
324 good_rev = revlist[0] | |
325 | |
326 # These are indexes of |revlist|. | |
327 good = 0 | |
328 bad = len(revlist) - 1 | |
329 last_known_good_rev = revlist[good] | |
330 | |
331 # Binary search time! | |
332 while good < bad: | |
333 candidates = revlist[good:bad] | |
334 num_poss = len(candidates) | |
335 if num_poss > 10: | |
336 print('%d candidates. %d tries left.' % | |
337 (num_poss, round(math.log(num_poss, 2)))) | |
338 else: | |
339 print('Candidates: %s' % revlist[good:bad]) | |
340 | |
341 # Cut the problem in half... | |
342 test = int((bad - good) / 2) + good | |
343 test_rev = revlist[test] | |
344 | |
345 # Let the user give this rev a spin (in her own profile, if she wants). | |
346 profile = opts.profile | |
347 if not profile: | |
348 profile = 'profile' # In a temp dir. | |
349 TryRevision(context, test_rev, profile, args) | |
350 if AskIsGoodBuild(test_rev): | |
351 last_known_good_rev = revlist[good] | |
352 good = test + 1 | |
353 else: | |
354 bad = test | |
355 | |
356 # We're done. Let the user know the results in an official manner. | |
357 print('You are probably looking for build %d.' % revlist[bad]) | |
358 print('CHANGELOG URL:') | |
359 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad])) | |
360 print('Built at revision:') | |
361 print(BUILD_VIEWVC_URL % revlist[bad]) | |
362 | 430 |
363 if __name__ == '__main__': | 431 if __name__ == '__main__': |
364 sys.exit(main()) | 432 sys.exit(main()) |
OLD | NEW |