Chromium Code Reviews| 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(). |
| 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 10 matching lines...) Expand all Loading... | |
| 47 import optparse | 48 import optparse |
| 48 import os | 49 import os |
| 49 import pipes | 50 import pipes |
| 50 import re | 51 import re |
| 51 import shutil | 52 import shutil |
| 52 import sys | 53 import sys |
| 53 import tempfile | 54 import tempfile |
| 54 import urllib | 55 import urllib |
| 55 import zipfile | 56 import zipfile |
| 56 | 57 |
| 57 | |
| 58 def UnzipFilenameToDir(filename, dir): | 58 def UnzipFilenameToDir(filename, dir): |
| 59 """Unzip |filename| to directory |dir|.""" | 59 """Unzip |filename| to directory |dir|.""" |
| 60 zf = zipfile.ZipFile(filename) | 60 zf = zipfile.ZipFile(filename) |
| 61 # Make base. | 61 # Make base. |
| 62 pushd = os.getcwd() | 62 pushd = os.getcwd() |
| 63 try: | 63 try: |
| 64 if not os.path.isdir(dir): | 64 if not os.path.isdir(dir): |
| 65 os.mkdir(dir) | 65 os.mkdir(dir) |
| 66 os.chdir(dir) | 66 os.chdir(dir) |
| 67 # Extract files. | 67 # Extract files. |
| 68 for info in zf.infolist(): | 68 for info in zf.infolist(): |
| 69 name = info.filename | 69 name = info.filename |
| 70 if name.endswith('/'): # dir | 70 if name.endswith('/'): # dir |
| 71 if not os.path.isdir(name): | 71 if not os.path.isdir(name): |
| 72 os.makedirs(name) | 72 os.makedirs(name) |
| 73 else: # file | 73 else: # file |
| 74 dir = os.path.dirname(name) | 74 dir = os.path.dirname(name) |
| 75 if not os.path.isdir(dir): | 75 if not os.path.isdir(dir): |
| 76 os.makedirs(dir) | 76 os.makedirs(dir) |
| 77 out = open(name, 'wb') | 77 out = open(name, 'wb') |
| 78 out.write(zf.read(name)) | 78 out.write(zf.read(name)) |
| 79 out.close() | 79 out.close() |
| 80 # Set permissions. Permission info in external_attr is shifted 16 bits. | 80 # Set permissions. Permission info in external_attr is shifted 16 bits. |
| 81 os.chmod(name, info.external_attr >> 16L) | 81 os.chmod(name, info.external_attr >> 16L) |
| 82 os.chdir(pushd) | 82 os.chdir(pushd) |
| 83 except Exception, e: | 83 except Exception, e: |
| 84 print >>sys.stderr, e | 84 print >>sys.stderr, e |
| 85 sys.exit(1) | 85 sys.exit(1) |
| 86 | 86 |
|
Evan Martin
2011/04/14 19:03:56
fwiw, double-newline after function bodies is reco
Robert Sesek
2011/04/29 20:25:44
Done.
| |
| 87 | |
| 88 def SetArchiveVars(archive): | 87 def SetArchiveVars(archive): |
| 89 """Set a bunch of global variables appropriate for the specified archive.""" | 88 """Set a bunch of global variables appropriate for the specified archive.""" |
| 90 global BUILD_ARCHIVE_TYPE | 89 global BUILD_ARCHIVE_TYPE |
| 91 global BUILD_ARCHIVE_DIR | |
| 92 global BUILD_ZIP_NAME | 90 global BUILD_ZIP_NAME |
| 93 global BUILD_DIR_NAME | 91 global BUILD_DIR_NAME |
| 94 global BUILD_EXE_NAME | 92 global BUILD_EXE_NAME |
| 95 global BUILD_BASE_URL | 93 global BUILD_BASE_URL |
| 96 | 94 |
| 97 BUILD_ARCHIVE_TYPE = archive | 95 BUILD_ARCHIVE_TYPE = archive |
| 98 BUILD_ARCHIVE_DIR = 'chromium-rel-' + BUILD_ARCHIVE_TYPE | |
| 99 | 96 |
| 100 if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64', 'linux-chromiumos'): | 97 if BUILD_ARCHIVE_TYPE in ('linux', 'linux64', 'linux-chromiumos'): |
| 101 BUILD_ZIP_NAME = 'chrome-linux.zip' | 98 BUILD_ZIP_NAME = 'chrome-linux.zip' |
| 102 BUILD_DIR_NAME = 'chrome-linux' | 99 BUILD_DIR_NAME = 'chrome-linux' |
| 103 BUILD_EXE_NAME = 'chrome' | 100 BUILD_EXE_NAME = 'chrome' |
| 104 elif BUILD_ARCHIVE_TYPE in ('mac'): | 101 elif BUILD_ARCHIVE_TYPE in ('mac'): |
| 105 BUILD_ZIP_NAME = 'chrome-mac.zip' | 102 BUILD_ZIP_NAME = 'chrome-mac.zip' |
| 106 BUILD_DIR_NAME = 'chrome-mac' | 103 BUILD_DIR_NAME = 'chrome-mac' |
| 107 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' | 104 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' |
| 108 elif BUILD_ARCHIVE_TYPE in ('xp'): | 105 elif BUILD_ARCHIVE_TYPE in ('xp'): |
| 109 BUILD_ZIP_NAME = 'chrome-win32.zip' | 106 BUILD_ZIP_NAME = 'chrome-win32.zip' |
| 110 BUILD_DIR_NAME = 'chrome-win32' | 107 BUILD_DIR_NAME = 'chrome-win32' |
| 111 BUILD_EXE_NAME = 'chrome.exe' | 108 BUILD_EXE_NAME = 'chrome.exe' |
| 112 | 109 |
| 113 BUILD_BASE_URL += BUILD_ARCHIVE_DIR | 110 def ParseDirectoryIndex(url): |
| 111 """Parses the all_builds.txt index file. The format of this file is: | |
| 112 mac/2011-02-16/75130 | |
| 113 mac/2011-02-16/75218 | |
| 114 mac/2011-02-16/75226 | |
| 115 mac/2011-02-16/75234 | |
| 116 mac/2011-02-16/75184 | |
| 117 This function will return a list of DATE/REVISION strings for the platform | |
| 118 specified by BUILD_ARCHIVE_TYPE. | |
| 119 """ | |
| 120 handle = urllib.urlopen(url) | |
| 121 dirindex = handle.readlines() | |
| 122 handle.close() | |
| 114 | 123 |
| 115 def ParseDirectoryIndex(url): | 124 # Only return values for the specified platform. |
| 116 """Parses the HTML directory listing into a list of revision numbers.""" | 125 archtype = BUILD_ARCHIVE_TYPE |
| 117 handle = urllib.urlopen(url) | 126 dirindex = filter(lambda l: l.startswith(archtype), dirindex) |
| 118 dirindex = handle.read() | 127 |
| 119 handle.close() | 128 # Remove the newline separator and the platform token. |
| 120 return re.findall(r'<a href="([0-9]*)/">\1/</a>', dirindex) | 129 dirindex = map(lambda l: l[len(archtype) + 1:].strip(), dirindex) |
| 130 dirindex.sort() | |
| 131 return dirindex | |
| 132 | |
| 133 def ParseIndexLine(iline): | |
| 134 """Takes an index line returned by ParseDirectoryIndex() and returns a | |
| 135 2-tuple of (date, revision). |date| is a string and |revision| is an int.""" | |
| 136 split = iline.split('/') | |
| 137 assert(len(split) == 2) | |
| 138 return (split[0], int(split[1])) | |
| 139 | |
| 140 def GetRevision(iline): | |
| 141 """Takes an index line, parses it, and returns the revision.""" | |
| 142 return ParseIndexLine(iline)[1] | |
| 121 | 143 |
| 122 def GetRevList(good, bad): | 144 def GetRevList(good, bad): |
| 123 """Gets the list of revision numbers between |good| and |bad|.""" | 145 """Gets the list of revision numbers between |good| and |bad|.""" |
| 124 # Download the main revlist. | 146 # Download the main revlist. |
| 125 revlist = ParseDirectoryIndex(BUILD_BASE_URL) | 147 revlist = ParseDirectoryIndex(BUILD_BASE_URL + BUILD_INDEX_FILE) |
| 126 revlist = map(int, revlist) | 148 |
| 127 revlist = filter(lambda r: range(good, bad).__contains__(int(r)), revlist) | 149 revrange = range(good, bad) |
| 150 revlist = filter(lambda r: revrange.__contains__(GetRevision(r)), | |
|
Evan Martin
2011/04/14 19:03:56
GetRevision(r) in revrange
?
Robert Sesek
2011/04/29 20:25:44
Done.
| |
| 151 revlist) | |
| 128 revlist.sort() | 152 revlist.sort() |
| 129 return revlist | 153 return revlist |
| 130 | 154 |
| 131 def TryRevision(rev, profile, args): | 155 def TryRevision(iline, profile, args): |
| 132 """Downloads revision |rev|, unzips it, and opens it for the user to test. | 156 """Downloads revision from |iline|, unzips it, and opens it for the user to |
| 133 |profile| is the profile to use.""" | 157 test. |profile| is the profile to use.""" |
| 134 # Do this in a temp dir so we don't collide with user files. | 158 # Do this in a temp dir so we don't collide with user files. |
| 135 cwd = os.getcwd() | 159 cwd = os.getcwd() |
| 136 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 160 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
| 137 os.chdir(tempdir) | 161 os.chdir(tempdir) |
| 138 | 162 |
| 139 # Download the file. | 163 # Download the file. |
| 140 download_url = BUILD_BASE_URL + (BUILD_ARCHIVE_URL % rev) + BUILD_ZIP_NAME | 164 download_url = BUILD_BASE_URL + BUILD_ARCHIVE_TYPE + \ |
| 141 def _Reporthook(blocknum, blocksize, totalsize): | 165 (BUILD_ARCHIVE_URL % ParseIndexLine(iline)) + BUILD_ZIP_NAME |
| 166 def _ReportHook(blocknum, blocksize, totalsize): | |
| 142 size = blocknum * blocksize | 167 size = blocknum * blocksize |
| 143 if totalsize == -1: # Total size not known. | 168 if totalsize == -1: # Total size not known. |
| 144 progress = "Received %d bytes" % size | 169 progress = "Received %d bytes" % size |
| 145 else: | 170 else: |
| 146 size = min(totalsize, size) | 171 size = min(totalsize, size) |
| 147 progress = "Received %d of %d bytes, %.2f%%" % ( | 172 progress = "Received %d of %d bytes, %.2f%%" % ( |
| 148 size, totalsize, 100.0 * size / totalsize) | 173 size, totalsize, 100.0 * size / totalsize) |
| 149 # Send a \r to let all progress messages use just one line of output. | 174 # Send a \r to let all progress messages use just one line of output. |
| 150 sys.stdout.write("\r" + progress) | 175 sys.stdout.write("\r" + progress) |
| 151 sys.stdout.flush() | 176 sys.stdout.flush() |
| 152 try: | 177 try: |
| 153 print 'Fetching ' + download_url | 178 print 'Fetching ' + download_url |
| 154 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _Reporthook) | 179 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _ReportHook) |
| 155 print | 180 print |
| 156 except Exception, e: | 181 except Exception, e: |
| 157 print('Could not retrieve the download. Sorry.') | 182 print('Could not retrieve the download. Sorry.') |
| 158 sys.exit(-1) | 183 sys.exit(-1) |
| 159 | 184 |
| 160 # Unzip the file. | 185 # Unzip the file. |
| 161 print 'Unzipping ...' | 186 print 'Unzipping ...' |
| 162 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) | 187 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) |
| 163 | 188 |
| 164 # Tell the system to open the app. | 189 # Tell the system to open the app. |
| 165 args = ['--user-data-dir=%s' % profile] + args | 190 args = ['--user-data-dir=%s' % profile] + args |
| 166 flags = ' '.join(map(pipes.quote, args)) | 191 flags = ' '.join(map(pipes.quote, args)) |
| 167 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) | 192 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) |
| 168 cmd = '%s %s' % (exe, flags) | 193 cmd = '%s %s' % (exe, flags) |
| 169 print 'Running %s' % cmd | 194 print 'Running %s' % cmd |
| 170 os.system(cmd) | 195 os.system(cmd) |
| 171 | 196 |
| 172 os.chdir(cwd) | 197 os.chdir(cwd) |
| 173 print 'Cleaning temp dir ...' | 198 print 'Cleaning temp dir ...' |
| 174 try: | 199 try: |
| 175 shutil.rmtree(tempdir, True) | 200 shutil.rmtree(tempdir, True) |
| 176 except Exception, e: | 201 except Exception, e: |
| 177 pass | 202 pass |
| 178 | 203 |
| 179 | 204 def AskIsGoodBuild(iline): |
| 180 def AskIsGoodBuild(rev): | 205 """Ask the user whether build from index line |iline| is good or bad.""" |
| 181 """Ask the user whether build |rev| is good or bad.""" | |
| 182 # Loop until we get a response that we can parse. | 206 # Loop until we get a response that we can parse. |
| 183 while True: | 207 while True: |
| 184 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) | 208 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % GetRevision(iline)) |
| 185 if response and response in ('g', 'b'): | 209 if response and response in ('g', 'b'): |
| 186 return response == 'g' | 210 return response == 'g' |
| 187 | 211 |
| 188 def main(): | 212 def main(): |
| 189 usage = ('%prog [options] [-- chromium-options]\n' | 213 usage = ('%prog [options] [-- chromium-options]\n' |
| 190 'Perform binary search on the snapshot builds.\n' | 214 'Perform binary search on the snapshot builds.\n' |
| 191 '\n' | 215 '\n' |
| 192 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 216 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
| 193 parser = optparse.OptionParser(usage=usage) | 217 parser = optparse.OptionParser(usage=usage) |
| 194 # Strangely, the default help output doesn't include the choice list. | 218 # Strangely, the default help output doesn't include the choice list. |
| 195 choices = ['mac', 'xp', 'linux', 'linux-64', 'linux-chromiumos'] | 219 choices = ['mac', 'xp', 'linux', 'linux64']#, 'linux-chromiumos'] http://crbug .com/78158 |
|
Evan Martin
2011/04/14 19:03:56
typo here, extra pasted url
Robert Sesek
2011/04/29 20:25:44
No, it's actually a bug that chromiumos doesn't ha
| |
| 196 parser.add_option('-a', '--archive', | 220 parser.add_option('-a', '--archive', |
| 197 choices = choices, | 221 choices = choices, |
| 198 help = 'The buildbot archive to bisect [%s].' % | 222 help = 'The buildbot archive to bisect [%s].' % |
| 199 '|'.join(choices)) | 223 '|'.join(choices)) |
| 200 parser.add_option('-b', '--bad', type = 'int', | 224 parser.add_option('-b', '--bad', type = 'int', |
| 201 help = 'The bad revision to bisect to.') | 225 help = 'The bad revision to bisect to.') |
| 202 parser.add_option('-g', '--good', type = 'int', | 226 parser.add_option('-g', '--good', type = 'int', |
| 203 help = 'The last known good revision to bisect from.') | 227 help = 'The last known good revision to bisect from.') |
| 204 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 228 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
| 205 help = 'Profile to use; this will not reset every run. ' + | 229 help = 'Profile to use; this will not reset every run. ' + |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 220 | 244 |
| 221 SetArchiveVars(opts.archive) | 245 SetArchiveVars(opts.archive) |
| 222 | 246 |
| 223 # Pick a starting point, try to get HEAD for this. | 247 # Pick a starting point, try to get HEAD for this. |
| 224 if opts.bad: | 248 if opts.bad: |
| 225 bad_rev = opts.bad | 249 bad_rev = opts.bad |
| 226 else: | 250 else: |
| 227 bad_rev = 0 | 251 bad_rev = 0 |
| 228 try: | 252 try: |
| 229 # Location of the latest build revision number | 253 # Location of the latest build revision number |
| 230 BUILD_LATEST_URL = '%s/LATEST' % (BUILD_BASE_URL) | 254 BUILD_LATEST_URL = '%s/LATEST/REVISION' % (BUILD_BASE_URL) |
| 231 nh = urllib.urlopen(BUILD_LATEST_URL) | 255 nh = urllib.urlopen(BUILD_LATEST_URL) |
| 232 latest = int(nh.read()) | 256 latest = int(nh.read()) |
| 233 nh.close() | 257 nh.close() |
| 234 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) | 258 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) |
| 235 if (bad_rev == ''): | 259 if (bad_rev == ''): |
| 236 bad_rev = latest | 260 bad_rev = latest |
| 237 bad_rev = int(bad_rev) | 261 bad_rev = int(bad_rev) |
| 238 except Exception, e: | 262 except Exception, e: |
| 239 print('Could not determine latest revision. This could be bad...') | 263 print('Could not determine latest revision. This could be bad...') |
| 240 bad_rev = int(raw_input('Bad revision: ')) | 264 bad_rev = int(raw_input('Bad revision: ')) |
| (...skipping 24 matching lines...) Expand all Loading... | |
| 265 last_known_good_rev = revlist[good] | 289 last_known_good_rev = revlist[good] |
| 266 | 290 |
| 267 # Binary search time! | 291 # Binary search time! |
| 268 while good < bad: | 292 while good < bad: |
| 269 candidates = revlist[good:bad] | 293 candidates = revlist[good:bad] |
| 270 num_poss = len(candidates) | 294 num_poss = len(candidates) |
| 271 if num_poss > 10: | 295 if num_poss > 10: |
| 272 print('%d candidates. %d tries left.' % | 296 print('%d candidates. %d tries left.' % |
| 273 (num_poss, round(math.log(num_poss, 2)))) | 297 (num_poss, round(math.log(num_poss, 2)))) |
| 274 else: | 298 else: |
| 275 print('Candidates: %s' % revlist[good:bad]) | 299 print('Candidates: %s' % map(GetRevision, revlist[good:bad])) |
| 276 | 300 |
| 277 # Cut the problem in half... | 301 # Cut the problem in half... |
| 278 test = int((bad - good) / 2) + good | 302 test = int((bad - good) / 2) + good |
| 279 test_rev = revlist[test] | 303 test_rev = revlist[test] |
| 280 | 304 |
| 281 # Let the user give this rev a spin (in her own profile, if she wants). | 305 # Let the user give this rev a spin (in her own profile, if she wants). |
| 282 profile = opts.profile | 306 profile = opts.profile |
| 283 if not profile: | 307 if not profile: |
| 284 profile = 'profile' # In a temp dir. | 308 profile = 'profile' # In a temp dir. |
| 285 TryRevision(test_rev, profile, args) | 309 TryRevision(test_rev, profile, args) |
| 286 if AskIsGoodBuild(test_rev): | 310 if AskIsGoodBuild(test_rev): |
| 287 last_known_good_rev = revlist[good] | 311 last_known_good_rev = revlist[good] |
| 288 good = test + 1 | 312 good = test + 1 |
| 289 else: | 313 else: |
| 290 bad = test | 314 bad = test |
| 291 | 315 |
| 292 # We're done. Let the user know the results in an official manner. | 316 # We're done. Let the user know the results in an official manner. |
| 293 print('You are probably looking for build %d.' % revlist[bad]) | 317 bad_revision = GetRevision(revlist[bad]) |
| 318 print('You are probably looking for build %d.' % bad_revision) | |
| 294 print('CHANGELOG URL:') | 319 print('CHANGELOG URL:') |
| 295 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad])) | 320 print(CHANGELOG_URL % (GetRevision(last_known_good_rev), bad_revision)) |
| 296 print('Built at revision:') | 321 print('Built at revision:') |
| 297 print(BUILD_VIEWVC_URL % revlist[bad]) | 322 print(BUILD_VIEWVC_URL % bad_revision) |
| 298 | 323 |
| 299 if __name__ == '__main__': | 324 if __name__ == '__main__': |
| 300 sys.exit(main()) | 325 sys.exit(main()) |
| OLD | NEW |