Change the separator of the keyboard overlay data to allow labels containing spaces.
[chromium-blink-merge.git] / tools / bisect-builds.py
blob5465f8d8fff08b0016d289b9314a7e1b63a825cb
1 #!/usr/bin/python
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Snapshot Build Bisect Tool
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
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,
12 it will ask you whether it is good or bad before continuing the search.
13 """
15 # Base URL to download snapshots from.
16 BUILD_BASE_URL = 'http://build.chromium.org/f/chromium/snapshots/'
18 # The type (platform) of the build archive. This is what's passed in to the
19 # '-a/--archive' option.
20 BUILD_ARCHIVE_TYPE = ''
22 # The selected archive to bisect.
23 BUILD_ARCHIVE_DIR = ''
25 # The location of the builds.
26 BUILD_ARCHIVE_URL = '/%d/'
28 # Name of the build archive.
29 BUILD_ZIP_NAME = ''
31 # Directory name inside the archive.
32 BUILD_DIR_NAME = ''
34 # Name of the executable.
35 BUILD_EXE_NAME = ''
37 # URL to the ViewVC commit page.
38 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d'
40 # Changelogs URL
41 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
42 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d'
44 ###############################################################################
46 import math
47 import optparse
48 import os
49 import pipes
50 import re
51 import shutil
52 import sys
53 import tempfile
54 import urllib
55 import zipfile
58 def UnzipFilenameToDir(filename, dir):
59 """Unzip |filename| to directory |dir|."""
60 zf = zipfile.ZipFile(filename)
61 # Make base.
62 pushd = os.getcwd()
63 try:
64 if not os.path.isdir(dir):
65 os.mkdir(dir)
66 os.chdir(dir)
67 # Extract files.
68 for info in zf.infolist():
69 name = info.filename
70 if name.endswith('/'): # dir
71 if not os.path.isdir(name):
72 os.makedirs(name)
73 else: # file
74 dir = os.path.dirname(name)
75 if not os.path.isdir(dir):
76 os.makedirs(dir)
77 out = open(name, 'wb')
78 out.write(zf.read(name))
79 out.close()
80 # Set permissions. Permission info in external_attr is shifted 16 bits.
81 os.chmod(name, info.external_attr >> 16L)
82 os.chdir(pushd)
83 except Exception, e:
84 print >>sys.stderr, e
85 sys.exit(1)
88 def SetArchiveVars(archive):
89 """Set a bunch of global variables appropriate for the specified archive."""
90 global BUILD_ARCHIVE_TYPE
91 global BUILD_ARCHIVE_DIR
92 global BUILD_ZIP_NAME
93 global BUILD_DIR_NAME
94 global BUILD_EXE_NAME
95 global BUILD_BASE_URL
97 BUILD_ARCHIVE_TYPE = archive
98 BUILD_ARCHIVE_DIR = 'chromium-rel-' + BUILD_ARCHIVE_TYPE
100 if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64', 'linux-chromiumos'):
101 BUILD_ZIP_NAME = 'chrome-linux.zip'
102 BUILD_DIR_NAME = 'chrome-linux'
103 BUILD_EXE_NAME = 'chrome'
104 elif BUILD_ARCHIVE_TYPE in ('mac'):
105 BUILD_ZIP_NAME = 'chrome-mac.zip'
106 BUILD_DIR_NAME = 'chrome-mac'
107 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium'
108 elif BUILD_ARCHIVE_TYPE in ('xp'):
109 BUILD_ZIP_NAME = 'chrome-win32.zip'
110 BUILD_DIR_NAME = 'chrome-win32'
111 BUILD_EXE_NAME = 'chrome.exe'
113 BUILD_BASE_URL += BUILD_ARCHIVE_DIR
115 def ParseDirectoryIndex(url):
116 """Parses the HTML directory listing into a list of revision numbers."""
117 handle = urllib.urlopen(url)
118 dirindex = handle.read()
119 handle.close()
120 return re.findall(r'<a href="([0-9]*)/">\1/</a>', dirindex)
122 def GetRevList(good, bad):
123 """Gets the list of revision numbers between |good| and |bad|."""
124 # Download the main revlist.
125 revlist = ParseDirectoryIndex(BUILD_BASE_URL)
126 revlist = map(int, revlist)
127 revlist = filter(lambda r: range(good, bad).__contains__(int(r)), revlist)
128 revlist.sort()
129 return revlist
131 def TryRevision(rev, profile, args):
132 """Downloads revision |rev|, unzips it, and opens it for the user to test.
133 |profile| is the profile to use."""
134 # Do this in a temp dir so we don't collide with user files.
135 cwd = os.getcwd()
136 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
137 os.chdir(tempdir)
139 # Download the file.
140 download_url = BUILD_BASE_URL + (BUILD_ARCHIVE_URL % rev) + BUILD_ZIP_NAME
141 def _Reporthook(blocknum, blocksize, totalsize):
142 size = blocknum * blocksize
143 if totalsize == -1: # Total size not known.
144 progress = "Received %d bytes" % size
145 else:
146 size = min(totalsize, size)
147 progress = "Received %d of %d bytes, %.2f%%" % (
148 size, totalsize, 100.0 * size / totalsize)
149 # Send a \r to let all progress messages use just one line of output.
150 sys.stdout.write("\r" + progress)
151 sys.stdout.flush()
152 try:
153 print 'Fetching ' + download_url
154 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _Reporthook)
155 print
156 except Exception, e:
157 print('Could not retrieve the download. Sorry.')
158 sys.exit(-1)
160 # Unzip the file.
161 print 'Unzipping ...'
162 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir)
164 # Tell the system to open the app.
165 args = ['--user-data-dir=%s' % profile] + args
166 flags = ' '.join(map(pipes.quote, args))
167 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME)
168 cmd = '%s %s' % (exe, flags)
169 print 'Running %s' % cmd
170 os.system(cmd)
172 os.chdir(cwd)
173 print 'Cleaning temp dir ...'
174 try:
175 shutil.rmtree(tempdir, True)
176 except Exception, e:
177 pass
180 def AskIsGoodBuild(rev):
181 """Ask the user whether build |rev| is good or bad."""
182 # Loop until we get a response that we can parse.
183 while True:
184 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev))
185 if response and response in ('g', 'b'):
186 return response == 'g'
188 def main():
189 usage = ('%prog [options] [-- chromium-options]\n'
190 'Perform binary search on the snapshot builds.\n'
191 '\n'
192 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
193 parser = optparse.OptionParser(usage=usage)
194 # Strangely, the default help output doesn't include the choice list.
195 choices = ['mac', 'xp', 'linux', 'linux-64', 'linux-chromiumos']
196 parser.add_option('-a', '--archive',
197 choices = choices,
198 help = 'The buildbot archive to bisect [%s].' %
199 '|'.join(choices))
200 parser.add_option('-b', '--bad', type = 'int',
201 help = 'The bad revision to bisect to.')
202 parser.add_option('-g', '--good', type = 'int',
203 help = 'The last known good revision to bisect from.')
204 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
205 help = 'Profile to use; this will not reset every run. ' +
206 'Defaults to a clean profile.')
207 (opts, args) = parser.parse_args()
209 if opts.archive is None:
210 print 'Error: missing required parameter: --archive'
211 print
212 parser.print_help()
213 return 1
215 if opts.bad and opts.good and (opts.good > opts.bad):
216 print ('The good revision (%d) must precede the bad revision (%d).\n' %
217 (opts.good, opts.bad))
218 parser.print_help()
219 return 1
221 SetArchiveVars(opts.archive)
223 # Pick a starting point, try to get HEAD for this.
224 if opts.bad:
225 bad_rev = opts.bad
226 else:
227 bad_rev = 0
228 try:
229 # Location of the latest build revision number
230 BUILD_LATEST_URL = '%s/LATEST' % (BUILD_BASE_URL)
231 nh = urllib.urlopen(BUILD_LATEST_URL)
232 latest = int(nh.read())
233 nh.close()
234 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest)
235 if (bad_rev == ''):
236 bad_rev = latest
237 bad_rev = int(bad_rev)
238 except Exception, e:
239 print('Could not determine latest revision. This could be bad...')
240 bad_rev = int(raw_input('Bad revision: '))
242 # Find out when we were good.
243 if opts.good:
244 good_rev = opts.good
245 else:
246 good_rev = 0
247 try:
248 good_rev = int(raw_input('Last known good [0]: '))
249 except Exception, e:
250 pass
252 # Get a list of revisions to bisect across.
253 revlist = GetRevList(good_rev, bad_rev)
254 if len(revlist) < 2: # Don't have enough builds to bisect
255 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist
256 sys.exit(1)
258 # If we don't have a |good_rev|, set it to be the first revision possible.
259 if good_rev == 0:
260 good_rev = revlist[0]
262 # These are indexes of |revlist|.
263 good = 0
264 bad = len(revlist) - 1
265 last_known_good_rev = revlist[good]
267 # Binary search time!
268 while good < bad:
269 candidates = revlist[good:bad]
270 num_poss = len(candidates)
271 if num_poss > 10:
272 print('%d candidates. %d tries left.' %
273 (num_poss, round(math.log(num_poss, 2))))
274 else:
275 print('Candidates: %s' % revlist[good:bad])
277 # Cut the problem in half...
278 test = int((bad - good) / 2) + good
279 test_rev = revlist[test]
281 # Let the user give this rev a spin (in her own profile, if she wants).
282 profile = opts.profile
283 if not profile:
284 profile = 'profile' # In a temp dir.
285 TryRevision(test_rev, profile, args)
286 if AskIsGoodBuild(test_rev):
287 last_known_good_rev = revlist[good]
288 good = test + 1
289 else:
290 bad = test
292 # We're done. Let the user know the results in an official manner.
293 print('You are probably looking for build %d.' % revlist[bad])
294 print('CHANGELOG URL:')
295 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad]))
296 print('Built at revision:')
297 print(BUILD_VIEWVC_URL % revlist[bad])
299 if __name__ == '__main__':
300 sys.exit(main())