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(). |
|
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 |