Add --verify-range option to bisect-builds.py.
In this mode, we check the "known" good/bad revisions before bisecting, which
saves a lot of time when the range is wrong.
This change also improves the logic for cleaning up downloaded files on exit,
including better handling of Ctrl+C.
Review URL: https://codereview.chromium.org/1692723003
Cr-Commit-Position: refs/heads/master@{#382428}
diff --git a/tools/bisect-builds.py b/tools/bisect-builds.py
index 8b4576a4..2ff6126 100755
--- a/tools/bisect-builds.py
+++ b/tools/bisect-builds.py
@@ -700,7 +700,7 @@
# They are present here because this function is passed to Bisect which then
# calls it with 5 arguments.
# pylint: disable=W0613
-def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
+def AskIsGoodBuild(rev, official_builds, exit_status, stdout, stderr):
"""Asks the user whether build |rev| is good or bad."""
# Loop until we get a response that we can parse.
while True:
@@ -716,7 +716,7 @@
print stderr
-def IsGoodASANBuild(rev, official_builds, status, stdout, stderr):
+def IsGoodASANBuild(rev, official_builds, exit_status, stdout, stderr):
"""Determine if an ASAN build |rev| is good or bad
Will examine stderr looking for the error message emitted by ASAN. If not
@@ -730,7 +730,17 @@
if bad_count > 0:
print 'Revision %d determined to be bad.' % rev
return 'b'
- return AskIsGoodBuild(rev, official_builds, status, stdout, stderr)
+ return AskIsGoodBuild(rev, official_builds, exit_status, stdout, stderr)
+
+
+def DidCommandSucceed(rev, official_builds, exit_status, stdout, stderr):
+ if exit_status:
+ print 'Bad revision: %s' % rev
+ return 'b'
+ else:
+ print 'Good revision: %s' % rev
+ return 'g'
+
class DownloadJob(object):
"""DownloadJob represents a task to download a given Chromium revision."""
@@ -781,13 +791,27 @@
raise
+def VerifyEndpoint(fetch, context, rev, profile, num_runs, command, try_args,
+ evaluate, expected_answer):
+ fetch.WaitFor()
+ try:
+ (exit_status, stdout, stderr) = RunRevision(
+ context, rev, fetch.zip_file, profile, num_runs, command, try_args)
+ except Exception, e:
+ print >> sys.stderr, e
+ if (evaluate(rev, context.is_official, exit_status, stdout, stderr) !=
+ expected_answer):
+ print 'Unexpected result at a range boundary! Your range is not correct.'
+ raise SystemExit
+
+
def Bisect(context,
num_runs=1,
command='%p %a',
try_args=(),
profile=None,
- interactive=True,
- evaluate=AskIsGoodBuild):
+ evaluate=AskIsGoodBuild,
+ verify_range=False):
"""Given known good and known bad revisions, run a binary search on all
archived revisions to determine the last known good revision.
@@ -795,10 +819,10 @@
@param num_runs Number of times to run each build for asking good/bad.
@param try_args A tuple of arguments to pass to the test application.
@param profile The name of the user profile to run with.
- @param interactive If it is false, use command exit code for good or bad
- judgment of the argument build.
@param evaluate A function which returns 'g' if the argument build is good,
'b' if it's bad or 'u' if unknown.
+ @param verify_range If true, tests the first and last revisions in the range
+ before proceeding with the bisect.
Threading is used to fetch Chromium revisions in the background, speeding up
the user's experience. For example, suppose the bounds of the search are
@@ -844,9 +868,31 @@
maxrev = len(revlist) - 1
pivot = maxrev / 2
rev = revlist[pivot]
- zip_file = _GetDownloadPath(rev)
- fetch = DownloadJob(context, 'initial_fetch', rev, zip_file)
+ fetch = DownloadJob(context, 'initial_fetch', rev, _GetDownloadPath(rev))
fetch.Start()
+
+ if verify_range:
+ minrev_fetch = DownloadJob(
+ context, 'minrev_fetch', revlist[minrev],
+ _GetDownloadPath(revlist[minrev]))
+ maxrev_fetch = DownloadJob(
+ context, 'maxrev_fetch', revlist[maxrev],
+ _GetDownloadPath(revlist[maxrev]))
+ minrev_fetch.Start()
+ maxrev_fetch.Start()
+ try:
+ VerifyEndpoint(minrev_fetch, context, revlist[minrev], profile, num_runs,
+ command, try_args, evaluate, 'b' if bad_rev < good_rev else 'g')
+ VerifyEndpoint(maxrev_fetch, context, revlist[maxrev], profile, num_runs,
+ command, try_args, evaluate, 'g' if bad_rev < good_rev else 'b')
+ except (KeyboardInterrupt, SystemExit):
+ print 'Cleaning up...'
+ fetch.Stop()
+ sys.exit(0)
+ finally:
+ minrev_fetch.Stop()
+ maxrev_fetch.Stop()
+
fetch.WaitFor()
# Binary search time!
@@ -880,17 +926,12 @@
up_fetch.Start()
# Run test on the pivot revision.
- status = None
+ exit_status = None
stdout = None
stderr = None
try:
- (status, stdout, stderr) = RunRevision(context,
- rev,
- fetch.zip_file,
- profile,
- num_runs,
- command,
- try_args)
+ (exit_status, stdout, stderr) = RunRevision(
+ context, rev, fetch.zip_file, profile, num_runs, command, try_args)
except Exception, e:
print >> sys.stderr, e
@@ -898,15 +939,7 @@
# On that basis, kill one of the background downloads and complete the
# other, as described in the comments above.
try:
- if not interactive:
- if status:
- answer = 'b'
- print 'Bad revision: %s' % rev
- else:
- answer = 'g'
- print 'Good revision: %s' % rev
- else:
- answer = evaluate(rev, context.is_official, status, stdout, stderr)
+ answer = evaluate(rev, context.is_official, exit_status, stdout, stderr)
if ((answer == 'g' and good_rev < bad_rev)
or (answer == 'b' and bad_rev < good_rev)):
fetch.Stop()
@@ -954,7 +987,6 @@
pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
else:
pivot = down_pivot
- zip_file = fetch.zip_file
if down_fetch and fetch != down_fetch:
down_fetch.Stop()
@@ -962,9 +994,10 @@
up_fetch.Stop()
else:
assert False, 'Unexpected return value from evaluate(): ' + answer
- except SystemExit:
+ except (KeyboardInterrupt, SystemExit):
print 'Cleaning up...'
- for f in [_GetDownloadPath(revlist[down_pivot]),
+ for f in [_GetDownloadPath(rev),
+ _GetDownloadPath(revlist[down_pivot]),
_GetDownloadPath(revlist[up_pivot])]:
try:
os.unlink(f)
@@ -1162,6 +1195,12 @@
help='Use a local file in the current directory to cache '
'a list of known revisions to speed up the '
'initialization of this script.')
+ parser.add_option('--verify-range',
+ dest='verify_range',
+ action='store_true',
+ default=False,
+ help='Test the first and last revisions in the range ' +
+ 'before proceeding with the bisect.')
(opts, args) = parser.parse_args()
@@ -1220,7 +1259,9 @@
parser.print_help()
return 1
- if opts.asan:
+ if opts.not_interactive:
+ evaluator = DidCommandSucceed
+ elif opts.asan:
evaluator = IsGoodASANBuild
else:
evaluator = AskIsGoodBuild
@@ -1232,7 +1273,7 @@
(min_chromium_rev, max_chromium_rev, context) = Bisect(
context, opts.times, opts.command, args, opts.profile,
- not opts.not_interactive, evaluator)
+ evaluator, opts.verify_range)
# Get corresponding blink revisions.
try: