| 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 |