Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(109)

Side by Side Diff: tools/bisect-builds.py

Issue 7329005: Add programmatic interface (Closed) Base URL: http://git.chromium.org/git/chromium.git@trunk
Patch Set: Created 9 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698