OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2011 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, |
(...skipping 224 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
235 | 235 |
236 download_url = context.GetDownloadURL(rev) | 236 download_url = context.GetDownloadURL(rev) |
237 try: | 237 try: |
238 urllib.urlretrieve(download_url, filename, ReportHook) | 238 urllib.urlretrieve(download_url, filename, ReportHook) |
239 if progress_event and progress_event.isSet(): | 239 if progress_event and progress_event.isSet(): |
240 print | 240 print |
241 except RuntimeError, e: | 241 except RuntimeError, e: |
242 pass | 242 pass |
243 | 243 |
244 | 244 |
245 def RunRevision(context, revision, zipfile, profile, args): | 245 def RunRevision(context, revision, zipfile, profile, num_runs, args): |
246 """Given a zipped revision, unzip it and run the test.""" | 246 """Given a zipped revision, unzip it and run the test.""" |
247 print "Trying revision %d..." % revision | 247 print "Trying revision %d..." % revision |
248 | 248 |
249 # Create a temp directory and unzip the revision into it. | 249 # Create a temp directory and unzip the revision into it. |
250 cwd = os.getcwd() | 250 cwd = os.getcwd() |
251 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 251 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
252 UnzipFilenameToDir(zipfile, tempdir) | 252 UnzipFilenameToDir(zipfile, tempdir) |
253 os.chdir(tempdir) | 253 os.chdir(tempdir) |
254 | 254 |
255 # Run the build. | 255 # Run the build as many times as specified. |
256 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args | 256 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args |
257 subproc = subprocess.Popen(testargs, | 257 for i in range(0, num_runs): |
258 bufsize=-1, | 258 subproc = subprocess.Popen(testargs, |
259 stdout=subprocess.PIPE, | 259 bufsize=-1, |
260 stderr=subprocess.PIPE) | 260 stdout=subprocess.PIPE, |
261 (stdout, stderr) = subproc.communicate() | 261 stderr=subprocess.PIPE) |
| 262 (stdout, stderr) = subproc.communicate() |
262 | 263 |
263 os.chdir(cwd) | 264 os.chdir(cwd) |
264 try: | 265 try: |
265 shutil.rmtree(tempdir, True) | 266 shutil.rmtree(tempdir, True) |
266 except Exception, e: | 267 except Exception, e: |
267 pass | 268 pass |
268 | 269 |
269 return (subproc.returncode, stdout, stderr) | 270 return (subproc.returncode, stdout, stderr) |
270 | 271 |
271 | 272 |
272 def AskIsGoodBuild(rev, status, stdout, stderr): | 273 def AskIsGoodBuild(rev, status, stdout, stderr): |
273 """Ask the user whether build |rev| is good or bad.""" | 274 """Ask the user whether build |rev| is good or bad.""" |
274 # Loop until we get a response that we can parse. | 275 # Loop until we get a response that we can parse. |
275 while True: | 276 while True: |
276 response = raw_input('Revision %d is [(g)ood/(b)ad/(q)uit]: ' % int(rev)) | 277 response = raw_input('Revision %d is [(g)ood/(b)ad/(q)uit]: ' % int(rev)) |
277 if response and response in ('g', 'b'): | 278 if response and response in ('g', 'b'): |
278 return response == 'g' | 279 return response == 'g' |
279 if response and response == 'q': | 280 if response and response == 'q': |
280 raise SystemExit() | 281 raise SystemExit() |
281 | 282 |
282 | 283 |
283 def Bisect(platform, | 284 def Bisect(platform, |
284 good_rev=0, | 285 good_rev=0, |
285 bad_rev=0, | 286 bad_rev=0, |
| 287 num_runs=1, |
286 try_args=(), | 288 try_args=(), |
287 profile=None, | 289 profile=None, |
288 predicate=AskIsGoodBuild): | 290 predicate=AskIsGoodBuild): |
289 """Given known good and known bad revisions, run a binary search on all | 291 """Given known good and known bad revisions, run a binary search on all |
290 archived revisions to determine the last known good revision. | 292 archived revisions to determine the last known good revision. |
291 | 293 |
292 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). | 294 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). |
293 @param good_rev Number/tag of the last known good revision. | 295 @param good_rev Number/tag of the last known good revision. |
294 @param bad_rev Number/tag of the first known bad revision. | 296 @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. |
295 @param try_args A tuple of arguments to pass to the test application. | 298 @param try_args A tuple of arguments to pass to the test application. |
296 @param profile The name of the user profile to run with. | 299 @param profile The name of the user profile to run with. |
297 @param predicate A predicate function which returns True iff the argument | 300 @param predicate A predicate function which returns True iff the argument |
298 chromium revision is good. | 301 chromium revision is good. |
299 | 302 |
300 Threading is used to fetch Chromium revisions in the background, speeding up | 303 Threading is used to fetch Chromium revisions in the background, speeding up |
301 the user's experience. For example, suppose the bounds of the search are | 304 the user's experience. For example, suppose the bounds of the search are |
302 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on | 305 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on |
303 whether revision 50 is good or bad, the next revision to check will be either | 306 whether revision 50 is good or bad, the next revision to check will be either |
304 25 or 75. So, while revision 50 is being checked, the script will download | 307 25 or 75. So, while revision 50 is being checked, the script will download |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
381 up_thread = threading.Thread(target=FetchRevision, | 384 up_thread = threading.Thread(target=FetchRevision, |
382 name='up_fetch', | 385 name='up_fetch', |
383 args=fetchargs) | 386 args=fetchargs) |
384 up_thread.start() | 387 up_thread.start() |
385 | 388 |
386 # Run test on the pivot revision. | 389 # Run test on the pivot revision. |
387 (status, stdout, stderr) = RunRevision(context, | 390 (status, stdout, stderr) = RunRevision(context, |
388 rev, | 391 rev, |
389 zipfile, | 392 zipfile, |
390 profile, | 393 profile, |
| 394 num_runs, |
391 try_args) | 395 try_args) |
392 os.unlink(zipfile) | 396 os.unlink(zipfile) |
393 zipfile = None | 397 zipfile = None |
394 | 398 |
395 # Call the predicate function to see if the current revision is good or bad. | 399 # Call the predicate function to see if the current revision is good or bad. |
396 # On that basis, kill one of the background downloads and complete the | 400 # On that basis, kill one of the background downloads and complete the |
397 # other, as described in the comments above. | 401 # other, as described in the comments above. |
398 try: | 402 try: |
399 if predicate(rev, status, stdout, stderr): | 403 if predicate(rev, status, stdout, stderr): |
400 good = pivot | 404 good = pivot |
(...skipping 14 matching lines...) Expand all Loading... |
415 up_thread.join() | 419 up_thread.join() |
416 os.unlink(up_zipfile) | 420 os.unlink(up_zipfile) |
417 if down_thread: | 421 if down_thread: |
418 print "Downloading revision %d..." % down_rev | 422 print "Downloading revision %d..." % down_rev |
419 down_progress_event.set() # Display progress of download. | 423 down_progress_event.set() # Display progress of download. |
420 down_thread.join() # Wait for older revision to finish downloading. | 424 down_thread.join() # Wait for older revision to finish downloading. |
421 pivot = down_pivot | 425 pivot = down_pivot |
422 zipfile = down_zipfile | 426 zipfile = down_zipfile |
423 except SystemExit: | 427 except SystemExit: |
424 print "Cleaning up..." | 428 print "Cleaning up..." |
425 for f in [down_zipfile, up_zipfile]: | 429 for f in [_GetDownloadPath(revlist[down_pivot]), |
| 430 _GetDownloadPath(revlist[up_pivot])]: |
426 try: | 431 try: |
427 os.unlink(f) | 432 os.unlink(f) |
428 except OSError: | 433 except OSError: |
429 pass | 434 pass |
430 sys.exit(0) | 435 sys.exit(0) |
431 | 436 |
432 rev = revlist[pivot] | 437 rev = revlist[pivot] |
433 | 438 |
434 return (revlist[good], revlist[bad]) | 439 return (revlist[good], revlist[bad]) |
435 | 440 |
(...skipping 25 matching lines...) Expand all Loading... |
461 choices = choices, | 466 choices = choices, |
462 help = 'The buildbot archive to bisect [%s].' % | 467 help = 'The buildbot archive to bisect [%s].' % |
463 '|'.join(choices)) | 468 '|'.join(choices)) |
464 parser.add_option('-b', '--bad', type = 'int', | 469 parser.add_option('-b', '--bad', type = 'int', |
465 help = 'The bad revision to bisect to.') | 470 help = 'The bad revision to bisect to.') |
466 parser.add_option('-g', '--good', type = 'int', | 471 parser.add_option('-g', '--good', type = 'int', |
467 help = 'The last known good revision to bisect from.') | 472 help = 'The last known good revision to bisect from.') |
468 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 473 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
469 help = 'Profile to use; this will not reset every run. ' + | 474 help = 'Profile to use; this will not reset every run. ' + |
470 'Defaults to a clean profile.', default = 'profile') | 475 'Defaults to a clean profile.', default = 'profile') |
| 476 parser.add_option('-t', '--times', type = 'int', |
| 477 help = 'Number of times to run each build before asking ' + |
| 478 'if it\'s good or bad. Temporary profiles are reused.', |
| 479 default = 1) |
471 (opts, args) = parser.parse_args() | 480 (opts, args) = parser.parse_args() |
472 | 481 |
473 if opts.archive is None: | 482 if opts.archive is None: |
474 print 'Error: missing required parameter: --archive' | 483 print 'Error: missing required parameter: --archive' |
475 print | 484 print |
476 parser.print_help() | 485 parser.print_help() |
477 return 1 | 486 return 1 |
478 | 487 |
479 if opts.bad and opts.good and (opts.good > opts.bad): | 488 if opts.bad and opts.good and (opts.good > opts.bad): |
480 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 489 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
(...skipping 25 matching lines...) Expand all Loading... |
506 # Find out when we were good. | 515 # Find out when we were good. |
507 if opts.good: | 516 if opts.good: |
508 good_rev = opts.good | 517 good_rev = opts.good |
509 else: | 518 else: |
510 good_rev = 0 | 519 good_rev = 0 |
511 try: | 520 try: |
512 good_rev = int(raw_input('Last known good [0]: ')) | 521 good_rev = int(raw_input('Last known good [0]: ')) |
513 except Exception, e: | 522 except Exception, e: |
514 pass | 523 pass |
515 | 524 |
| 525 if opts.times < 1: |
| 526 print('Number of times to run (%d) must be greater than or equal to 1.' % |
| 527 opts.times) |
| 528 parser.print_help() |
| 529 return 1 |
| 530 |
516 (last_known_good_rev, first_known_bad_rev) = Bisect( | 531 (last_known_good_rev, first_known_bad_rev) = Bisect( |
517 opts.archive, good_rev, bad_rev, args, opts.profile) | 532 opts.archive, good_rev, bad_rev, opts.times, args, opts.profile) |
518 | 533 |
519 # Get corresponding webkit revisions. | 534 # Get corresponding webkit revisions. |
520 try: | 535 try: |
521 last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( | 536 last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( |
522 last_known_good_rev) | 537 last_known_good_rev) |
523 first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision( | 538 first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision( |
524 first_known_bad_rev) | 539 first_known_bad_rev) |
525 except Exception, e: | 540 except Exception, e: |
526 # Silently ignore the failure. | 541 # Silently ignore the failure. |
527 last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 | 542 last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 |
528 | 543 |
529 # We're done. Let the user know the results in an official manner. | 544 # We're done. Let the user know the results in an official manner. |
530 print('You are probably looking for build %d.' % first_known_bad_rev) | 545 print('You are probably looking for build %d.' % first_known_bad_rev) |
531 if last_known_good_webkit_rev != first_known_bad_webkit_rev: | 546 if last_known_good_webkit_rev != first_known_bad_webkit_rev: |
532 print 'WEBKIT CHANGELOG URL:' | 547 print 'WEBKIT CHANGELOG URL:' |
533 print WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, | 548 print WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, |
534 last_known_good_webkit_rev) | 549 last_known_good_webkit_rev) |
535 print 'CHANGELOG URL:' | 550 print 'CHANGELOG URL:' |
536 print CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 551 print CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
537 print 'Built at revision:' | 552 print 'Built at revision:' |
538 print BUILD_VIEWVC_URL % first_known_bad_rev | 553 print BUILD_VIEWVC_URL % first_known_bad_rev |
539 | 554 |
540 | 555 |
541 if __name__ == '__main__': | 556 if __name__ == '__main__': |
542 sys.exit(main()) | 557 sys.exit(main()) |
OLD | NEW |