OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 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-snapshots' | 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' |
17 | 17 |
18 # The root URL for official builds. | |
19 OFFICIAL_BASE_URL = 'http://chrome4linux.mtv.corp.google.com/archives' | |
20 | |
18 # URL to the ViewVC commit page. | 21 # URL to the ViewVC commit page. |
19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' | 22 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' |
20 | 23 |
21 # Changelogs URL. | 24 # Changelogs URL. |
22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ | 25 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ |
23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' | 26 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' |
24 | 27 |
28 # Official Changelogs URL. | |
29 OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\ | |
30 'changelog?old_version=%s&new_version=%s' | |
31 | |
25 # DEPS file URL. | 32 # DEPS file URL. |
26 DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' | 33 DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' |
27 | |
28 # WebKit Changelogs URL. | 34 # WebKit Changelogs URL. |
29 WEBKIT_CHANGELOG_URL = 'http://trac.webkit.org/log/' \ | 35 WEBKIT_CHANGELOG_URL = 'http://trac.webkit.org/log/' \ |
30 'trunk/?rev=%d&stop_rev=%d&verbose=on' | 36 'trunk/?rev=%d&stop_rev=%d&verbose=on' |
31 | 37 |
32 ############################################################################### | 38 ############################################################################### |
33 | 39 |
34 import math | 40 import math |
35 import optparse | 41 import optparse |
36 import os | 42 import os |
37 import pipes | 43 import pipes |
38 import re | 44 import re |
39 import shutil | 45 import shutil |
40 import subprocess | 46 import subprocess |
41 import sys | 47 import sys |
42 import tempfile | 48 import tempfile |
43 import threading | 49 import threading |
44 import urllib | 50 import urllib |
51 from distutils.version import LooseVersion | |
45 from xml.etree import ElementTree | 52 from xml.etree import ElementTree |
46 import zipfile | 53 import zipfile |
47 | 54 |
48 | 55 |
49 class PathContext(object): | 56 class PathContext(object): |
50 """A PathContext is used to carry the information used to construct URLs and | 57 """A PathContext is used to carry the information used to construct URLs and |
51 paths when dealing with the storage server and archives.""" | 58 paths when dealing with the storage server and archives.""" |
52 def __init__(self, platform, good_revision, bad_revision): | 59 def __init__(self, platform, good_revision, bad_revision, is_official): |
53 super(PathContext, self).__init__() | 60 super(PathContext, self).__init__() |
54 # Store off the input parameters. | 61 # Store off the input parameters. |
55 self.platform = platform # What's passed in to the '-a/--archive' option. | 62 self.platform = platform # What's passed in to the '-a/--archive' option. |
56 self.good_revision = good_revision | 63 self.good_revision = good_revision |
57 self.bad_revision = bad_revision | 64 self.bad_revision = bad_revision |
65 self.is_official = is_official | |
58 | 66 |
59 # The name of the ZIP file in a revision directory on the server. | 67 # The name of the ZIP file in a revision directory on the server. |
60 self.archive_name = None | 68 self.archive_name = None |
61 | 69 |
62 # Set some internal members: | 70 # Set some internal members: |
63 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. | 71 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. |
64 # _archive_extract_dir = Uncompressed directory in the archive_name file. | 72 # _archive_extract_dir = Uncompressed directory in the archive_name file. |
65 # _binary_name = The name of the executable to run. | 73 # _binary_name = The name of the executable to run. |
66 if self.platform == 'linux' or self.platform == 'linux64': | 74 if self.platform == 'linux' or self.platform == 'linux64': |
67 self._listing_platform_dir = 'Linux/' | |
68 self.archive_name = 'chrome-linux.zip' | |
69 self._archive_extract_dir = 'chrome-linux' | |
70 self._binary_name = 'chrome' | 75 self._binary_name = 'chrome' |
71 # Linux and x64 share all the same path data except for the archive dir. | |
72 if self.platform == 'linux64': | |
73 self._listing_platform_dir = 'Linux_x64/' | |
74 elif self.platform == 'mac': | 76 elif self.platform == 'mac': |
75 self._listing_platform_dir = 'Mac/' | |
76 self.archive_name = 'chrome-mac.zip' | 77 self.archive_name = 'chrome-mac.zip' |
77 self._archive_extract_dir = 'chrome-mac' | 78 self._archive_extract_dir = 'chrome-mac' |
78 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | |
79 elif self.platform == 'win': | 79 elif self.platform == 'win': |
80 self._listing_platform_dir = 'Win/' | |
81 self.archive_name = 'chrome-win32.zip' | 80 self.archive_name = 'chrome-win32.zip' |
82 self._archive_extract_dir = 'chrome-win32' | 81 self._archive_extract_dir = 'chrome-win32' |
83 self._binary_name = 'chrome.exe' | 82 self._binary_name = 'chrome.exe' |
84 else: | 83 else: |
85 raise Exception('Invalid platform: %s' % self.platform) | 84 raise Exception('Invalid platform: %s' % self.platform) |
86 | 85 |
86 if is_official: | |
87 if self.platform == 'linux': | |
88 self._listing_platform_dir = 'lucid32bit/' | |
Robert Sesek
2012/05/24 19:12:29
nit: these ifs (and below) are indented 3 spaces,
anantha
2012/05/24 20:31:28
Done.
| |
89 self.archive_name = 'chrome-lucid32bit.zip' | |
90 self._archive_extract_dir = 'chrome-lucid32bit' | |
91 elif self.platform == 'linux64': | |
92 self._listing_platform_dir = 'lucid64bit/' | |
93 self.archive_name = 'chrome-lucid64bit.zip' | |
94 self._archive_extract_dir = 'chrome-lucid64bit' | |
95 elif self.platform == 'mac': | |
96 self._listing_platform_dir = 'mac/' | |
97 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome' | |
98 elif self.platform == 'win': | |
99 self._listing_platform_dir = 'win/' | |
100 else: | |
101 if self.platform == 'linux' or self.platform == 'linux64': | |
102 self.archive_name = 'chrome-linux.zip' | |
103 self._archive_extract_dir = 'chrome-linux' | |
104 if self.platform == 'linux': | |
105 self._listing_platform_dir = 'Linux/' | |
106 elif self.platform == 'linux64': | |
107 self._listing_platform_dir = 'Linux_x64/' | |
108 elif self.platform == 'mac': | |
109 self._listing_platform_dir = 'Mac/' | |
110 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | |
111 elif self.platform == 'win': | |
112 self._listing_platform_dir = 'Win/' | |
113 | |
87 def GetListingURL(self, marker=None): | 114 def GetListingURL(self, marker=None): |
88 """Returns the URL for a directory listing, with an optional marker.""" | 115 """Returns the URL for a directory listing, with an optional marker.""" |
89 marker_param = '' | 116 marker_param = '' |
90 if marker: | 117 if marker: |
91 marker_param = '&marker=' + str(marker) | 118 marker_param = '&marker=' + str(marker) |
92 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | 119 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ |
93 marker_param | 120 marker_param |
94 | 121 |
95 def GetDownloadURL(self, revision): | 122 def GetDownloadURL(self, revision): |
96 """Gets the download URL for a build archive of a specific revision.""" | 123 """Gets the download URL for a build archive of a specific revision.""" |
97 return "%s/%s%d/%s" % ( | 124 if self.is_official: |
98 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | 125 return "%s/%s/%s%s" % ( |
126 OFFICIAL_BASE_URL, revision, self._listing_platform_dir, | |
127 self.archive_name) | |
128 else: | |
129 return "%s/%s%s/%s" % ( | |
130 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | |
99 | 131 |
100 def GetLastChangeURL(self): | 132 def GetLastChangeURL(self): |
101 """Returns a URL to the LAST_CHANGE file.""" | 133 """Returns a URL to the LAST_CHANGE file.""" |
102 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 134 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
103 | 135 |
104 def GetLaunchPath(self): | 136 def GetLaunchPath(self): |
105 """Returns a relative path (presumably from the archive extraction location) | 137 """Returns a relative path (presumably from the archive extraction location) |
106 that is used to run the executable.""" | 138 that is used to run the executable.""" |
107 return os.path.join(self._archive_extract_dir, self._binary_name) | 139 return os.path.join(self._archive_extract_dir, self._binary_name) |
108 | 140 |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
143 # trailing slash to just have a number. | 175 # trailing slash to just have a number. |
144 revisions = [] | 176 revisions = [] |
145 for prefix in all_prefixes: | 177 for prefix in all_prefixes: |
146 revnum = prefix.text[prefix_len:-1] | 178 revnum = prefix.text[prefix_len:-1] |
147 try: | 179 try: |
148 revnum = int(revnum) | 180 revnum = int(revnum) |
149 revisions.append(revnum) | 181 revisions.append(revnum) |
150 except ValueError: | 182 except ValueError: |
151 pass | 183 pass |
152 return (revisions, next_marker) | 184 return (revisions, next_marker) |
153 | |
154 # Fetch the first list of revisions. | 185 # Fetch the first list of revisions. |
155 (revisions, next_marker) = _FetchAndParse(self.GetListingURL()) | 186 (revisions, next_marker) = _FetchAndParse(self.GetListingURL()) |
156 | 187 |
157 # If the result list was truncated, refetch with the next marker. Do this | 188 # If the result list was truncated, refetch with the next marker. Do this |
158 # until an entire directory listing is done. | 189 # until an entire directory listing is done. |
159 while next_marker: | 190 while next_marker: |
160 next_url = self.GetListingURL(next_marker) | 191 next_url = self.GetListingURL(next_marker) |
161 (new_revisions, next_marker) = _FetchAndParse(next_url) | 192 (new_revisions, next_marker) = _FetchAndParse(next_url) |
162 revisions.extend(new_revisions) | 193 revisions.extend(new_revisions) |
163 | |
164 return revisions | 194 return revisions |
165 | 195 |
166 def GetRevList(self): | 196 def GetRevList(self): |
167 """Gets the list of revision numbers between self.good_revision and | 197 """Gets the list of revision numbers between self.good_revision and |
168 self.bad_revision.""" | 198 self.bad_revision.""" |
169 # Download the revlist and filter for just the range between good and bad. | 199 # Download the revlist and filter for just the range between good and bad. |
170 minrev = self.good_revision | 200 minrev = self.good_revision |
171 maxrev = self.bad_revision | 201 maxrev = self.bad_revision |
172 revlist = map(int, self.ParseDirectoryIndex()) | 202 revlist = map(int, self.ParseDirectoryIndex()) |
173 revlist = [x for x in revlist if x >= minrev and x <= maxrev] | 203 revlist = [x for x in revlist if x >= int(minrev) and x <= int(maxrev)] |
174 revlist.sort() | 204 revlist.sort() |
175 return revlist | 205 return revlist |
176 | 206 |
207 def GetOfficialBuildsList(self): | |
208 """Gets the list of official build numbers between self.good_revision and | |
209 self.bad_revision.""" | |
210 # Download the revlist and filter for just the range between good and bad. | |
211 minrev = self.good_revision | |
212 maxrev = self.bad_revision | |
213 handle = urllib.urlopen(OFFICIAL_BASE_URL) | |
214 dirindex = handle.read() | |
215 handle.close() | |
216 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex) | |
217 final_list = [] | |
218 start_index = '0' | |
219 end_index = '0' | |
220 i = 0 | |
221 | |
222 parsed_build_numbers = [LooseVersion(x) for x in build_numbers] | |
223 for build_number in sorted(parsed_build_numbers): | |
224 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \ | |
225 self._listing_platform_dir + self.archive_name | |
226 i = i + 1 | |
227 try: | |
228 connection = urllib.urlopen(path) | |
229 connection.close() | |
230 final_list.append(str(build_number)) | |
231 if str(build_number) == minrev: | |
232 start_index = i | |
233 if str(build_number) == maxrev: | |
234 end_index = i | |
235 except urllib.HTTPError, e: | |
236 pass | |
237 return final_list[start_index:end_index] | |
177 | 238 |
178 def UnzipFilenameToDir(filename, dir): | 239 def UnzipFilenameToDir(filename, dir): |
179 """Unzip |filename| to directory |dir|.""" | 240 """Unzip |filename| to directory |dir|.""" |
180 cwd = os.getcwd() | 241 cwd = os.getcwd() |
181 if not os.path.isabs(filename): | 242 if not os.path.isabs(filename): |
182 filename = os.path.join(cwd, filename) | 243 filename = os.path.join(cwd, filename) |
183 zf = zipfile.ZipFile(filename) | 244 zf = zipfile.ZipFile(filename) |
184 # Make base. | 245 # Make base. |
185 try: | 246 try: |
186 if not os.path.isdir(dir): | 247 if not os.path.isdir(dir): |
(...skipping 26 matching lines...) Expand all Loading... | |
213 @param rev The Chromium revision number/tag to download. | 274 @param rev The Chromium revision number/tag to download. |
214 @param filename The destination for the downloaded file. | 275 @param filename The destination for the downloaded file. |
215 @param quit_event A threading.Event which will be set by the master thread to | 276 @param quit_event A threading.Event which will be set by the master thread to |
216 indicate that the download should be aborted. | 277 indicate that the download should be aborted. |
217 @param progress_event A threading.Event which will be set by the master thread | 278 @param progress_event A threading.Event which will be set by the master thread |
218 to indicate that the progress of the download should be | 279 to indicate that the progress of the download should be |
219 displayed. | 280 displayed. |
220 """ | 281 """ |
221 def ReportHook(blocknum, blocksize, totalsize): | 282 def ReportHook(blocknum, blocksize, totalsize): |
222 if quit_event and quit_event.isSet(): | 283 if quit_event and quit_event.isSet(): |
223 raise RuntimeError("Aborting download of revision %d" % rev) | 284 raise RuntimeError("Aborting download of revision %s" % str(rev)) |
224 if progress_event and progress_event.isSet(): | 285 if progress_event and progress_event.isSet(): |
225 size = blocknum * blocksize | 286 size = blocknum * blocksize |
226 if totalsize == -1: # Total size not known. | 287 if totalsize == -1: # Total size not known. |
227 progress = "Received %d bytes" % size | 288 progress = "Received %d bytes" % size |
228 else: | 289 else: |
229 size = min(totalsize, size) | 290 size = min(totalsize, size) |
230 progress = "Received %d of %d bytes, %.2f%%" % ( | 291 progress = "Received %d of %d bytes, %.2f%%" % ( |
231 size, totalsize, 100.0 * size / totalsize) | 292 size, totalsize, 100.0 * size / totalsize) |
232 # Send a \r to let all progress messages use just one line of output. | 293 # Send a \r to let all progress messages use just one line of output. |
233 sys.stdout.write("\r" + progress) | 294 sys.stdout.write("\r" + progress) |
234 sys.stdout.flush() | 295 sys.stdout.flush() |
235 | 296 |
236 download_url = context.GetDownloadURL(rev) | 297 download_url = context.GetDownloadURL(rev) |
237 try: | 298 try: |
238 urllib.urlretrieve(download_url, filename, ReportHook) | 299 urllib.urlretrieve(download_url, filename, ReportHook) |
239 if progress_event and progress_event.isSet(): | 300 if progress_event and progress_event.isSet(): |
240 print | 301 print |
241 except RuntimeError, e: | 302 except RuntimeError, e: |
242 pass | 303 pass |
243 | 304 |
244 | 305 |
245 def RunRevision(context, revision, zipfile, profile, num_runs, args): | 306 def RunRevision(context, revision, zipfile, profile, num_runs, args): |
246 """Given a zipped revision, unzip it and run the test.""" | 307 """Given a zipped revision, unzip it and run the test.""" |
247 print "Trying revision %d..." % revision | 308 print "Trying revision %s..." % str(revision) |
248 | 309 |
249 # Create a temp directory and unzip the revision into it. | 310 # Create a temp directory and unzip the revision into it. |
250 cwd = os.getcwd() | 311 cwd = os.getcwd() |
251 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 312 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
252 UnzipFilenameToDir(zipfile, tempdir) | 313 UnzipFilenameToDir(zipfile, tempdir) |
253 os.chdir(tempdir) | 314 os.chdir(tempdir) |
254 | 315 |
255 # Run the build as many times as specified. | 316 # Run the build as many times as specified. |
256 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args | 317 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args |
318 # The sandbox must be run as root on Official Chrome, so bypass it. | |
319 if context.is_official and (context.platform == 'linux' or | |
320 context.platform == 'linux64'): | |
Robert Sesek
2012/05/24 19:12:29
nit: indent one more space (total 4)
anantha
2012/05/24 20:31:28
Done.
| |
321 testargs.append('--no-sandbox') | |
322 | |
Robert Sesek
2012/05/24 19:12:29
nit: remove extra blank line
anantha
2012/05/24 20:31:28
Done.
| |
323 | |
257 for i in range(0, num_runs): | 324 for i in range(0, num_runs): |
258 subproc = subprocess.Popen(testargs, | 325 subproc = subprocess.Popen(testargs, |
259 bufsize=-1, | 326 bufsize=-1, |
260 stdout=subprocess.PIPE, | 327 stdout=subprocess.PIPE, |
261 stderr=subprocess.PIPE) | 328 stderr=subprocess.PIPE) |
262 (stdout, stderr) = subproc.communicate() | 329 (stdout, stderr) = subproc.communicate() |
263 | 330 |
264 os.chdir(cwd) | 331 os.chdir(cwd) |
265 try: | 332 try: |
266 shutil.rmtree(tempdir, True) | 333 shutil.rmtree(tempdir, True) |
267 except Exception, e: | 334 except Exception, e: |
268 pass | 335 pass |
269 | 336 |
270 return (subproc.returncode, stdout, stderr) | 337 return (subproc.returncode, stdout, stderr) |
271 | 338 |
272 | 339 |
273 def AskIsGoodBuild(rev, status, stdout, stderr): | 340 def AskIsGoodBuild(rev, official_builds, status, stdout, stderr): |
274 """Ask the user whether build |rev| is good or bad.""" | 341 """Ask the user whether build |rev| is good or bad.""" |
275 # Loop until we get a response that we can parse. | 342 # Loop until we get a response that we can parse. |
276 while True: | 343 while True: |
277 response = raw_input('Revision %d is [(g)ood/(b)ad/(q)uit]: ' % int(rev)) | 344 response = raw_input('Revision %s is [(g)ood/(b)ad/(q)uit]: ' % str(rev)) |
278 if response and response in ('g', 'b'): | 345 if response and response in ('g', 'b'): |
279 return response == 'g' | 346 return response == 'g' |
280 if response and response == 'q': | 347 if response and response == 'q': |
281 raise SystemExit() | 348 raise SystemExit() |
282 | 349 |
283 | 350 |
284 def Bisect(platform, | 351 def Bisect(platform, |
352 official_builds, | |
285 good_rev=0, | 353 good_rev=0, |
286 bad_rev=0, | 354 bad_rev=0, |
287 num_runs=1, | 355 num_runs=1, |
288 try_args=(), | 356 try_args=(), |
289 profile=None, | 357 profile=None, |
290 predicate=AskIsGoodBuild): | 358 predicate=AskIsGoodBuild): |
291 """Given known good and known bad revisions, run a binary search on all | 359 """Given known good and known bad revisions, run a binary search on all |
292 archived revisions to determine the last known good revision. | 360 archived revisions to determine the last known good revision. |
293 | 361 |
294 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). | 362 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). |
363 @param official_builds Specify build type(Chromium or Official build). | |
295 @param good_rev Number/tag of the last known good revision. | 364 @param good_rev Number/tag of the last known good revision. |
296 @param bad_rev Number/tag of the first known bad revision. | 365 @param bad_rev Number/tag of the first known bad revision. |
297 @param num_runs Number of times to run each build for asking good/bad. | 366 @param num_runs Number of times to run each build for asking good/bad. |
298 @param try_args A tuple of arguments to pass to the test application. | 367 @param try_args A tuple of arguments to pass to the test application. |
299 @param profile The name of the user profile to run with. | 368 @param profile The name of the user profile to run with. |
300 @param predicate A predicate function which returns True iff the argument | 369 @param predicate A predicate function which returns True iff the argument |
301 chromium revision is good. | 370 chromium revision is good. |
302 | 371 |
303 Threading is used to fetch Chromium revisions in the background, speeding up | 372 Threading is used to fetch Chromium revisions in the background, speeding up |
304 the user's experience. For example, suppose the bounds of the search are | 373 the user's experience. For example, suppose the bounds of the search are |
305 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on | 374 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on |
306 whether revision 50 is good or bad, the next revision to check will be either | 375 whether revision 50 is good or bad, the next revision to check will be either |
307 25 or 75. So, while revision 50 is being checked, the script will download | 376 25 or 75. So, while revision 50 is being checked, the script will download |
308 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is | 377 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is |
309 known: | 378 known: |
310 | 379 |
311 - If rev 50 is good, the download of rev 25 is cancelled, and the next test | 380 - If rev 50 is good, the download of rev 25 is cancelled, and the next test |
312 is run on rev 75. | 381 is run on rev 75. |
313 | 382 |
314 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test | 383 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test |
315 is run on rev 25. | 384 is run on rev 25. |
316 """ | 385 """ |
317 | 386 |
318 if not profile: | 387 if not profile: |
319 profile = 'profile' | 388 profile = 'profile' |
320 | 389 |
321 context = PathContext(platform, good_rev, bad_rev) | 390 context = PathContext(platform, good_rev, bad_rev, official_builds) |
322 cwd = os.getcwd() | 391 cwd = os.getcwd() |
323 | 392 |
324 _GetDownloadPath = lambda rev: os.path.join(cwd, | 393 |
325 '%d-%s' % (rev, context.archive_name)) | |
326 | 394 |
327 print "Downloading list of known revisions..." | 395 print "Downloading list of known revisions..." |
328 | 396 _GetDownloadPath = lambda rev: os.path.join(cwd, |
329 revlist = context.GetRevList() | 397 '%s-%s' % (str(rev), context.archive_name)) |
398 if official_builds: | |
399 revlist = context.GetOfficialBuildsList() | |
400 else: | |
401 revlist = context.GetRevList() | |
330 | 402 |
331 # Get a list of revisions to bisect across. | 403 # Get a list of revisions to bisect across. |
332 if len(revlist) < 2: # Don't have enough builds to bisect. | 404 if len(revlist) < 2: # Don't have enough builds to bisect. |
333 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 405 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
334 raise RuntimeError(msg) | 406 raise RuntimeError(msg) |
335 | 407 |
336 # Figure out our bookends and first pivot point; fetch the pivot revision. | 408 # Figure out our bookends and first pivot point; fetch the pivot revision. |
337 good = 0 | 409 good = 0 |
338 bad = len(revlist) - 1 | 410 bad = len(revlist) - 1 |
339 pivot = bad / 2 | 411 pivot = bad / 2 |
340 rev = revlist[pivot] | 412 rev = revlist[pivot] |
341 zipfile = _GetDownloadPath(rev) | 413 zipfile = _GetDownloadPath(rev) |
342 progress_event = threading.Event() | 414 progress_event = threading.Event() |
343 progress_event.set() | 415 progress_event.set() |
344 print "Downloading revision %d..." % rev | 416 print "Downloading revision %s..." % str(rev) |
345 FetchRevision(context, rev, zipfile, | 417 FetchRevision(context, rev, zipfile, |
346 quit_event=None, progress_event=progress_event) | 418 quit_event=None, progress_event=progress_event) |
347 | 419 |
348 # Binary search time! | 420 # Binary search time! |
349 while zipfile and bad - good > 1: | 421 while zipfile and bad - good > 1: |
350 # Pre-fetch next two possible pivots | 422 # Pre-fetch next two possible pivots |
351 # - down_pivot is the next revision to check if the current revision turns | 423 # - down_pivot is the next revision to check if the current revision turns |
352 # out to be bad. | 424 # out to be bad. |
353 # - up_pivot is the next revision to check if the current revision turns | 425 # - up_pivot is the next revision to check if the current revision turns |
354 # out to be good. | 426 # out to be good. |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
393 profile, | 465 profile, |
394 num_runs, | 466 num_runs, |
395 try_args) | 467 try_args) |
396 os.unlink(zipfile) | 468 os.unlink(zipfile) |
397 zipfile = None | 469 zipfile = None |
398 | 470 |
399 # Call the predicate function to see if the current revision is good or bad. | 471 # Call the predicate function to see if the current revision is good or bad. |
400 # On that basis, kill one of the background downloads and complete the | 472 # On that basis, kill one of the background downloads and complete the |
401 # other, as described in the comments above. | 473 # other, as described in the comments above. |
402 try: | 474 try: |
403 if predicate(rev, status, stdout, stderr): | 475 if predicate(rev, official_builds, status, stdout, stderr): |
404 good = pivot | 476 good = pivot |
405 if down_thread: | 477 if down_thread: |
406 down_quit_event.set() # Kill the download of older revision. | 478 down_quit_event.set() # Kill the download of older revision. |
407 down_thread.join() | 479 down_thread.join() |
408 os.unlink(down_zipfile) | 480 os.unlink(down_zipfile) |
409 if up_thread: | 481 if up_thread: |
410 print "Downloading revision %d..." % up_rev | 482 print "Downloading revision %s..." % str(up_rev) |
411 up_progress_event.set() # Display progress of download. | 483 up_progress_event.set() # Display progress of download. |
412 up_thread.join() # Wait for newer revision to finish downloading. | 484 up_thread.join() # Wait for newer revision to finish downloading. |
413 pivot = up_pivot | 485 pivot = up_pivot |
414 zipfile = up_zipfile | 486 zipfile = up_zipfile |
415 else: | 487 else: |
416 bad = pivot | 488 bad = pivot |
417 if up_thread: | 489 if up_thread: |
418 up_quit_event.set() # Kill download of newer revision. | 490 up_quit_event.set() # Kill download of newer revision. |
419 up_thread.join() | 491 up_thread.join() |
420 os.unlink(up_zipfile) | 492 os.unlink(up_zipfile) |
421 if down_thread: | 493 if down_thread: |
422 print "Downloading revision %d..." % down_rev | 494 print "Downloading revision %s..." % str(down_rev) |
423 down_progress_event.set() # Display progress of download. | 495 down_progress_event.set() # Display progress of download. |
424 down_thread.join() # Wait for older revision to finish downloading. | 496 down_thread.join() # Wait for older revision to finish downloading. |
425 pivot = down_pivot | 497 pivot = down_pivot |
426 zipfile = down_zipfile | 498 zipfile = down_zipfile |
427 except SystemExit: | 499 except SystemExit: |
428 print "Cleaning up..." | 500 print "Cleaning up..." |
429 for f in [_GetDownloadPath(revlist[down_pivot]), | 501 for f in [_GetDownloadPath(revlist[down_pivot]), |
430 _GetDownloadPath(revlist[up_pivot])]: | 502 _GetDownloadPath(revlist[up_pivot])]: |
431 try: | 503 try: |
432 os.unlink(f) | 504 os.unlink(f) |
(...skipping 26 matching lines...) Expand all Loading... | |
459 '\n' | 531 '\n' |
460 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 532 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
461 parser = optparse.OptionParser(usage=usage) | 533 parser = optparse.OptionParser(usage=usage) |
462 # Strangely, the default help output doesn't include the choice list. | 534 # Strangely, the default help output doesn't include the choice list. |
463 choices = ['mac', 'win', 'linux', 'linux64'] | 535 choices = ['mac', 'win', 'linux', 'linux64'] |
464 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 536 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
465 parser.add_option('-a', '--archive', | 537 parser.add_option('-a', '--archive', |
466 choices = choices, | 538 choices = choices, |
467 help = 'The buildbot archive to bisect [%s].' % | 539 help = 'The buildbot archive to bisect [%s].' % |
468 '|'.join(choices)) | 540 '|'.join(choices)) |
469 parser.add_option('-b', '--bad', type = 'int', | 541 parser.add_option('-o', action="store_true", dest='official_builds', |
542 help = 'Bisect across official' + | |
Robert Sesek
2012/05/24 19:12:29
nit: space before ending '
anantha
2012/05/24 20:31:28
Done.
| |
543 'Chrome builds (internal only) instead of' + | |
Robert Sesek
2012/05/24 19:12:29
same here
anantha
2012/05/24 20:31:28
Done.
| |
544 'Chromium archives.') | |
545 parser.add_option('-b', '--bad', type = 'str', | |
470 help = 'The bad revision to bisect to.') | 546 help = 'The bad revision to bisect to.') |
471 parser.add_option('-g', '--good', type = 'int', | 547 parser.add_option('-g', '--good', type = 'str', |
472 help = 'The last known good revision to bisect from.') | 548 help = 'The last known good revision to bisect from.') |
473 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 549 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
474 help = 'Profile to use; this will not reset every run. ' + | 550 help = 'Profile to use; this will not reset every run. ' + |
475 'Defaults to a clean profile.', default = 'profile') | 551 'Defaults to a clean profile.', default = 'profile') |
476 parser.add_option('-t', '--times', type = 'int', | 552 parser.add_option('-t', '--times', type = 'int', |
477 help = 'Number of times to run each build before asking ' + | 553 help = 'Number of times to run each build before asking ' + |
478 'if it\'s good or bad. Temporary profiles are reused.', | 554 'if it\'s good or bad. Temporary profiles are reused.', |
479 default = 1) | 555 default = 1) |
480 (opts, args) = parser.parse_args() | 556 (opts, args) = parser.parse_args() |
481 | 557 |
482 if opts.archive is None: | 558 if opts.archive is None: |
483 print 'Error: missing required parameter: --archive' | 559 print 'Error: missing required parameter: --archive' |
484 print | 560 print |
485 parser.print_help() | 561 parser.print_help() |
486 return 1 | 562 return 1 |
487 | 563 |
488 if opts.bad and opts.good and (opts.good > opts.bad): | 564 if opts.bad and opts.good and (opts.good > opts.bad): |
489 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 565 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
490 (opts.good, opts.bad)) | 566 (opts.good, opts.bad)) |
491 parser.print_help() | 567 parser.print_help() |
492 return 1 | 568 return 1 |
493 | 569 |
494 # Create the context. Initialize 0 for the revisions as they are set below. | 570 # Create the context. Initialize 0 for the revisions as they are set below. |
495 context = PathContext(opts.archive, 0, 0) | 571 context = PathContext(opts.archive, 0, 0, opts.official_builds) |
572 | |
573 if opts.official_builds and opts.bad is None: | |
574 print >>sys.stderr, 'Bisecting official builds requires a bad build number.' | |
575 print | |
Robert Sesek
2012/05/24 19:12:29
Remove this extra print line, or add >>sys.stderr.
anantha
2012/05/24 20:31:28
Done.
| |
576 parser.print_help() | |
577 return 1 | |
496 | 578 |
497 # Pick a starting point, try to get HEAD for this. | 579 # Pick a starting point, try to get HEAD for this. |
498 if opts.bad: | 580 if opts.bad: |
499 bad_rev = opts.bad | 581 bad_rev = opts.bad |
500 else: | 582 else: |
501 bad_rev = 0 | 583 bad_rev = 0 |
502 try: | 584 try: |
503 # Location of the latest build revision number | 585 # Location of the latest build revision number |
504 nh = urllib.urlopen(context.GetLastChangeURL()) | 586 nh = urllib.urlopen(context.GetLastChangeURL()) |
505 latest = int(nh.read()) | 587 latest = int(nh.read()) |
(...skipping 16 matching lines...) Expand all Loading... | |
522 except Exception, e: | 604 except Exception, e: |
523 pass | 605 pass |
524 | 606 |
525 if opts.times < 1: | 607 if opts.times < 1: |
526 print('Number of times to run (%d) must be greater than or equal to 1.' % | 608 print('Number of times to run (%d) must be greater than or equal to 1.' % |
527 opts.times) | 609 opts.times) |
528 parser.print_help() | 610 parser.print_help() |
529 return 1 | 611 return 1 |
530 | 612 |
531 (last_known_good_rev, first_known_bad_rev) = Bisect( | 613 (last_known_good_rev, first_known_bad_rev) = Bisect( |
532 opts.archive, good_rev, bad_rev, opts.times, args, opts.profile) | 614 opts.archive, opts.official_builds, good_rev, bad_rev, opts.times, args, |
615 opts.profile) | |
533 | 616 |
534 # Get corresponding webkit revisions. | 617 # Get corresponding webkit revisions. |
535 try: | 618 try: |
536 last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( | 619 last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( |
537 last_known_good_rev) | 620 last_known_good_rev) |
538 first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision( | 621 first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision( |
539 first_known_bad_rev) | 622 first_known_bad_rev) |
540 except Exception, e: | 623 except Exception, e: |
541 # Silently ignore the failure. | 624 # Silently ignore the failure. |
542 last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 | 625 last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 |
543 | 626 |
544 # We're done. Let the user know the results in an official manner. | 627 # We're done. Let the user know the results in an official manner. |
545 print('You are probably looking for build %d.' % first_known_bad_rev) | 628 print('You are probably looking for build %s.' % str(first_known_bad_rev)) |
629 | |
546 if last_known_good_webkit_rev != first_known_bad_webkit_rev: | 630 if last_known_good_webkit_rev != first_known_bad_webkit_rev: |
547 print 'WEBKIT CHANGELOG URL:' | 631 print 'WEBKIT CHANGELOG URL:' |
548 print WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, | 632 print WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, |
549 last_known_good_webkit_rev) | 633 last_known_good_webkit_rev) |
550 print 'CHANGELOG URL:' | 634 print 'CHANGELOG URL:' |
551 print CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 635 if opts.official_builds: |
552 print 'Built at revision:' | 636 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
553 print BUILD_VIEWVC_URL % first_known_bad_rev | 637 else: |
638 print CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | |
639 print 'Built at revision:' | |
640 print BUILD_VIEWVC_URL % first_known_bad_rev | |
554 | 641 |
555 | 642 |
556 if __name__ == '__main__': | 643 if __name__ == '__main__': |
557 sys.exit(main()) | 644 sys.exit(main()) |
OLD | NEW |