OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2010 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 # Base URL to download snapshots from. |
16 BUILD_BASE_URL = 'http://build.chromium.org/f/chromium/snapshots/' | 16 BUILD_BASE_URL = 'http://build.chromium.org/f/chromium/continuous/' |
17 | |
18 # The index file that lists all the builds. This lives in BUILD_BASE_URL. | |
19 BUILD_INDEX_FILE = 'all_builds.txt' | |
17 | 20 |
18 # The type (platform) of the build archive. This is what's passed in to the | 21 # The type (platform) of the build archive. This is what's passed in to the |
19 # '-a/--archive' option. | 22 # '-a/--archive' option. |
20 BUILD_ARCHIVE_TYPE = '' | 23 BUILD_ARCHIVE_TYPE = '' |
21 | 24 |
22 # The selected archive to bisect. | 25 # The location of the builds. Format this with a (date, revision) tuple, which |
23 BUILD_ARCHIVE_DIR = '' | 26 # can be obtained through ParseIndexLine(). |
Evan Martin
2011/05/02 18:29:17
Might be more maintainable to use something like
| |
24 | 27 BUILD_ARCHIVE_URL = '/%s/%d/' |
25 # The location of the builds. | |
26 BUILD_ARCHIVE_URL = '/%d/' | |
27 | 28 |
28 # Name of the build archive. | 29 # Name of the build archive. |
29 BUILD_ZIP_NAME = '' | 30 BUILD_ZIP_NAME = '' |
30 | 31 |
31 # Directory name inside the archive. | 32 # Directory name inside the archive. |
32 BUILD_DIR_NAME = '' | 33 BUILD_DIR_NAME = '' |
33 | 34 |
34 # Name of the executable. | 35 # Name of the executable. |
35 BUILD_EXE_NAME = '' | 36 BUILD_EXE_NAME = '' |
36 | 37 |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
81 os.chmod(name, info.external_attr >> 16L) | 82 os.chmod(name, info.external_attr >> 16L) |
82 os.chdir(pushd) | 83 os.chdir(pushd) |
83 except Exception, e: | 84 except Exception, e: |
84 print >>sys.stderr, e | 85 print >>sys.stderr, e |
85 sys.exit(1) | 86 sys.exit(1) |
86 | 87 |
87 | 88 |
88 def SetArchiveVars(archive): | 89 def SetArchiveVars(archive): |
89 """Set a bunch of global variables appropriate for the specified archive.""" | 90 """Set a bunch of global variables appropriate for the specified archive.""" |
90 global BUILD_ARCHIVE_TYPE | 91 global BUILD_ARCHIVE_TYPE |
91 global BUILD_ARCHIVE_DIR | |
92 global BUILD_ZIP_NAME | 92 global BUILD_ZIP_NAME |
93 global BUILD_DIR_NAME | 93 global BUILD_DIR_NAME |
94 global BUILD_EXE_NAME | 94 global BUILD_EXE_NAME |
95 global BUILD_BASE_URL | 95 global BUILD_BASE_URL |
96 | 96 |
97 BUILD_ARCHIVE_TYPE = archive | 97 BUILD_ARCHIVE_TYPE = archive |
98 BUILD_ARCHIVE_DIR = 'chromium-rel-' + BUILD_ARCHIVE_TYPE | |
99 | 98 |
100 if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64', 'linux-chromiumos'): | 99 if BUILD_ARCHIVE_TYPE in ('linux', 'linux64', 'linux-chromiumos'): |
101 BUILD_ZIP_NAME = 'chrome-linux.zip' | 100 BUILD_ZIP_NAME = 'chrome-linux.zip' |
102 BUILD_DIR_NAME = 'chrome-linux' | 101 BUILD_DIR_NAME = 'chrome-linux' |
103 BUILD_EXE_NAME = 'chrome' | 102 BUILD_EXE_NAME = 'chrome' |
104 elif BUILD_ARCHIVE_TYPE in ('mac'): | 103 elif BUILD_ARCHIVE_TYPE in ('mac'): |
105 BUILD_ZIP_NAME = 'chrome-mac.zip' | 104 BUILD_ZIP_NAME = 'chrome-mac.zip' |
106 BUILD_DIR_NAME = 'chrome-mac' | 105 BUILD_DIR_NAME = 'chrome-mac' |
107 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' | 106 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' |
108 elif BUILD_ARCHIVE_TYPE in ('xp'): | 107 elif BUILD_ARCHIVE_TYPE in ('xp'): |
109 BUILD_ZIP_NAME = 'chrome-win32.zip' | 108 BUILD_ZIP_NAME = 'chrome-win32.zip' |
110 BUILD_DIR_NAME = 'chrome-win32' | 109 BUILD_DIR_NAME = 'chrome-win32' |
111 BUILD_EXE_NAME = 'chrome.exe' | 110 BUILD_EXE_NAME = 'chrome.exe' |
112 | 111 |
113 BUILD_BASE_URL += BUILD_ARCHIVE_DIR | |
114 | 112 |
115 def ParseDirectoryIndex(url): | 113 def ParseDirectoryIndex(url): |
116 """Parses the HTML directory listing into a list of revision numbers.""" | 114 """Parses the all_builds.txt index file. The format of this file is: |
115 mac/2011-02-16/75130 | |
116 mac/2011-02-16/75218 | |
117 mac/2011-02-16/75226 | |
118 mac/2011-02-16/75234 | |
119 mac/2011-02-16/75184 | |
120 This function will return a list of DATE/REVISION strings for the platform | |
121 specified by BUILD_ARCHIVE_TYPE. | |
122 """ | |
117 handle = urllib.urlopen(url) | 123 handle = urllib.urlopen(url) |
118 dirindex = handle.read() | 124 dirindex = handle.readlines() |
119 handle.close() | 125 handle.close() |
120 return re.findall(r'<a href="([0-9]*)/">\1/</a>', dirindex) | 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 | |
121 | 150 |
122 def GetRevList(good, bad): | 151 def GetRevList(good, bad): |
123 """Gets the list of revision numbers between |good| and |bad|.""" | 152 """Gets the list of revision numbers between |good| and |bad|.""" |
124 # Download the main revlist. | 153 # Download the main revlist. |
125 revlist = ParseDirectoryIndex(BUILD_BASE_URL) | 154 revlist = ParseDirectoryIndex(BUILD_BASE_URL + BUILD_INDEX_FILE) |
126 revlist = map(int, revlist) | 155 |
127 revlist = filter(lambda r: range(good, bad).__contains__(int(r)), revlist) | 156 revrange = range(good, bad) |
157 revlist = filter(lambda r: GetRevision(r) in revrange, revlist) | |
128 revlist.sort() | 158 revlist.sort() |
129 return revlist | 159 return revlist |
130 | 160 |
131 def TryRevision(rev, profile, args): | 161 |
132 """Downloads revision |rev|, unzips it, and opens it for the user to test. | 162 def TryRevision(iline, profile, args): |
133 |profile| is the profile to use.""" | 163 """Downloads revision from |iline|, unzips it, and opens it for the user to |
164 test. |profile| is the profile to use.""" | |
134 # Do this in a temp dir so we don't collide with user files. | 165 # Do this in a temp dir so we don't collide with user files. |
135 cwd = os.getcwd() | 166 cwd = os.getcwd() |
136 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 167 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
137 os.chdir(tempdir) | 168 os.chdir(tempdir) |
138 | 169 |
139 # Download the file. | 170 # Download the file. |
140 download_url = BUILD_BASE_URL + (BUILD_ARCHIVE_URL % rev) + BUILD_ZIP_NAME | 171 download_url = BUILD_BASE_URL + BUILD_ARCHIVE_TYPE + \ |
141 def _Reporthook(blocknum, blocksize, totalsize): | 172 (BUILD_ARCHIVE_URL % ParseIndexLine(iline)) + BUILD_ZIP_NAME |
173 def _ReportHook(blocknum, blocksize, totalsize): | |
142 size = blocknum * blocksize | 174 size = blocknum * blocksize |
143 if totalsize == -1: # Total size not known. | 175 if totalsize == -1: # Total size not known. |
144 progress = "Received %d bytes" % size | 176 progress = "Received %d bytes" % size |
145 else: | 177 else: |
146 size = min(totalsize, size) | 178 size = min(totalsize, size) |
147 progress = "Received %d of %d bytes, %.2f%%" % ( | 179 progress = "Received %d of %d bytes, %.2f%%" % ( |
148 size, totalsize, 100.0 * size / totalsize) | 180 size, totalsize, 100.0 * size / totalsize) |
149 # Send a \r to let all progress messages use just one line of output. | 181 # Send a \r to let all progress messages use just one line of output. |
150 sys.stdout.write("\r" + progress) | 182 sys.stdout.write("\r" + progress) |
151 sys.stdout.flush() | 183 sys.stdout.flush() |
152 try: | 184 try: |
153 print 'Fetching ' + download_url | 185 print 'Fetching ' + download_url |
154 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _Reporthook) | 186 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _ReportHook) |
155 print | 187 print |
156 except Exception, e: | 188 except Exception, e: |
157 print('Could not retrieve the download. Sorry.') | 189 print('Could not retrieve the download. Sorry.') |
158 sys.exit(-1) | 190 sys.exit(-1) |
159 | 191 |
160 # Unzip the file. | 192 # Unzip the file. |
161 print 'Unzipping ...' | 193 print 'Unzipping ...' |
162 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) | 194 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) |
163 | 195 |
164 # Tell the system to open the app. | 196 # Tell the system to open the app. |
165 args = ['--user-data-dir=%s' % profile] + args | 197 args = ['--user-data-dir=%s' % profile] + args |
166 flags = ' '.join(map(pipes.quote, args)) | 198 flags = ' '.join(map(pipes.quote, args)) |
167 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) | 199 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) |
168 cmd = '%s %s' % (exe, flags) | 200 cmd = '%s %s' % (exe, flags) |
169 print 'Running %s' % cmd | 201 print 'Running %s' % cmd |
170 os.system(cmd) | 202 os.system(cmd) |
171 | 203 |
172 os.chdir(cwd) | 204 os.chdir(cwd) |
173 print 'Cleaning temp dir ...' | 205 print 'Cleaning temp dir ...' |
174 try: | 206 try: |
175 shutil.rmtree(tempdir, True) | 207 shutil.rmtree(tempdir, True) |
176 except Exception, e: | 208 except Exception, e: |
177 pass | 209 pass |
178 | 210 |
179 | 211 |
180 def AskIsGoodBuild(rev): | 212 def AskIsGoodBuild(iline): |
181 """Ask the user whether build |rev| is good or bad.""" | 213 """Ask the user whether build from index line |iline| is good or bad.""" |
182 # Loop until we get a response that we can parse. | 214 # Loop until we get a response that we can parse. |
183 while True: | 215 while True: |
184 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) | 216 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % GetRevision(iline)) |
185 if response and response in ('g', 'b'): | 217 if response and response in ('g', 'b'): |
186 return response == 'g' | 218 return response == 'g' |
187 | 219 |
188 def main(): | 220 def main(): |
189 usage = ('%prog [options] [-- chromium-options]\n' | 221 usage = ('%prog [options] [-- chromium-options]\n' |
190 'Perform binary search on the snapshot builds.\n' | 222 'Perform binary search on the snapshot builds.\n' |
191 '\n' | 223 '\n' |
192 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 224 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
193 parser = optparse.OptionParser(usage=usage) | 225 parser = optparse.OptionParser(usage=usage) |
194 # Strangely, the default help output doesn't include the choice list. | 226 # Strangely, the default help output doesn't include the choice list. |
195 choices = ['mac', 'xp', 'linux', 'linux-64', 'linux-chromiumos'] | 227 choices = ['mac', 'xp', 'linux', 'linux64'] |
228 # 'linux-chromiumos' lacks a continuous archive http://crbug.com/781 58 | |
Evan Martin
2011/05/02 18:29:17
80 cols
| |
196 parser.add_option('-a', '--archive', | 229 parser.add_option('-a', '--archive', |
197 choices = choices, | 230 choices = choices, |
198 help = 'The buildbot archive to bisect [%s].' % | 231 help = 'The buildbot archive to bisect [%s].' % |
199 '|'.join(choices)) | 232 '|'.join(choices)) |
200 parser.add_option('-b', '--bad', type = 'int', | 233 parser.add_option('-b', '--bad', type = 'int', |
201 help = 'The bad revision to bisect to.') | 234 help = 'The bad revision to bisect to.') |
202 parser.add_option('-g', '--good', type = 'int', | 235 parser.add_option('-g', '--good', type = 'int', |
203 help = 'The last known good revision to bisect from.') | 236 help = 'The last known good revision to bisect from.') |
204 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 237 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
205 help = 'Profile to use; this will not reset every run. ' + | 238 help = 'Profile to use; this will not reset every run. ' + |
(...skipping 14 matching lines...) Expand all Loading... | |
220 | 253 |
221 SetArchiveVars(opts.archive) | 254 SetArchiveVars(opts.archive) |
222 | 255 |
223 # Pick a starting point, try to get HEAD for this. | 256 # Pick a starting point, try to get HEAD for this. |
224 if opts.bad: | 257 if opts.bad: |
225 bad_rev = opts.bad | 258 bad_rev = opts.bad |
226 else: | 259 else: |
227 bad_rev = 0 | 260 bad_rev = 0 |
228 try: | 261 try: |
229 # Location of the latest build revision number | 262 # Location of the latest build revision number |
230 BUILD_LATEST_URL = '%s/LATEST' % (BUILD_BASE_URL) | 263 BUILD_LATEST_URL = '%s/LATEST/REVISION' % (BUILD_BASE_URL) |
231 nh = urllib.urlopen(BUILD_LATEST_URL) | 264 nh = urllib.urlopen(BUILD_LATEST_URL) |
232 latest = int(nh.read()) | 265 latest = int(nh.read()) |
233 nh.close() | 266 nh.close() |
234 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) | 267 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) |
235 if (bad_rev == ''): | 268 if (bad_rev == ''): |
236 bad_rev = latest | 269 bad_rev = latest |
237 bad_rev = int(bad_rev) | 270 bad_rev = int(bad_rev) |
238 except Exception, e: | 271 except Exception, e: |
239 print('Could not determine latest revision. This could be bad...') | 272 print('Could not determine latest revision. This could be bad...') |
240 bad_rev = int(raw_input('Bad revision: ')) | 273 bad_rev = int(raw_input('Bad revision: ')) |
(...skipping 24 matching lines...) Expand all Loading... | |
265 last_known_good_rev = revlist[good] | 298 last_known_good_rev = revlist[good] |
266 | 299 |
267 # Binary search time! | 300 # Binary search time! |
268 while good < bad: | 301 while good < bad: |
269 candidates = revlist[good:bad] | 302 candidates = revlist[good:bad] |
270 num_poss = len(candidates) | 303 num_poss = len(candidates) |
271 if num_poss > 10: | 304 if num_poss > 10: |
272 print('%d candidates. %d tries left.' % | 305 print('%d candidates. %d tries left.' % |
273 (num_poss, round(math.log(num_poss, 2)))) | 306 (num_poss, round(math.log(num_poss, 2)))) |
274 else: | 307 else: |
275 print('Candidates: %s' % revlist[good:bad]) | 308 print('Candidates: %s' % map(GetRevision, revlist[good:bad])) |
276 | 309 |
277 # Cut the problem in half... | 310 # Cut the problem in half... |
278 test = int((bad - good) / 2) + good | 311 test = int((bad - good) / 2) + good |
279 test_rev = revlist[test] | 312 test_rev = revlist[test] |
280 | 313 |
281 # Let the user give this rev a spin (in her own profile, if she wants). | 314 # Let the user give this rev a spin (in her own profile, if she wants). |
282 profile = opts.profile | 315 profile = opts.profile |
283 if not profile: | 316 if not profile: |
284 profile = 'profile' # In a temp dir. | 317 profile = 'profile' # In a temp dir. |
285 TryRevision(test_rev, profile, args) | 318 TryRevision(test_rev, profile, args) |
286 if AskIsGoodBuild(test_rev): | 319 if AskIsGoodBuild(test_rev): |
287 last_known_good_rev = revlist[good] | 320 last_known_good_rev = revlist[good] |
288 good = test + 1 | 321 good = test + 1 |
289 else: | 322 else: |
290 bad = test | 323 bad = test |
291 | 324 |
292 # We're done. Let the user know the results in an official manner. | 325 # We're done. Let the user know the results in an official manner. |
293 print('You are probably looking for build %d.' % revlist[bad]) | 326 bad_revision = GetRevision(revlist[bad]) |
327 print('You are probably looking for build %d.' % bad_revision) | |
294 print('CHANGELOG URL:') | 328 print('CHANGELOG URL:') |
295 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad])) | 329 print(CHANGELOG_URL % (GetRevision(last_known_good_rev), bad_revision)) |
296 print('Built at revision:') | 330 print('Built at revision:') |
297 print(BUILD_VIEWVC_URL % revlist[bad]) | 331 print(BUILD_VIEWVC_URL % bad_revision) |
298 | 332 |
299 if __name__ == '__main__': | 333 if __name__ == '__main__': |
300 sys.exit(main()) | 334 sys.exit(main()) |
OLD | NEW |