[llvm] r341679 - utils/abtest: Refactor and add bisection method
Matthias Braun via llvm-commits
llvm-commits at lists.llvm.org
Fri Sep 7 10:08:44 PDT 2018
Author: matze
Date: Fri Sep 7 10:08:44 2018
New Revision: 341679
URL: http://llvm.org/viewvc/llvm-project?rev=341679&view=rev
Log:
utils/abtest: Refactor and add bisection method
- Refactor/rewrite most of the code. Also make sure it passes
pycodestyle/pyflakes now
- Add a new mode that performs bisection on the search space. This
should be faster in the common case where there is only a small number
of files or functions actually leading to failure.
The previous sequential behavior can still be accessed via `--seq`.
Modified:
llvm/trunk/utils/abtest.py
Modified: llvm/trunk/utils/abtest.py
URL: http://llvm.org/viewvc/llvm-project/llvm/trunk/utils/abtest.py?rev=341679&r1=341678&r2=341679&view=diff
==============================================================================
--- llvm/trunk/utils/abtest.py (original)
+++ llvm/trunk/utils/abtest.py Fri Sep 7 10:08:44 2018
@@ -3,9 +3,9 @@
# Given a previous good compile narrow down miscompiles.
# Expects two directories named "before" and "after" each containing a set of
# assembly or object files where the "after" version is assumed to be broken.
-# You also have to provide a script called "link_test". It is called with a list
-# of files which should be linked together and result tested. "link_test" should
-# returns with exitcode 0 if the linking and testing succeeded.
+# You also have to provide a script called "link_test". It is called with a
+# list of files which should be linked together and result tested. "link_test"
+# should returns with exitcode 0 if the linking and testing succeeded.
#
# abtest.py operates by taking all files from the "before" directory and
# in each step replacing one of them with a file from the "bad" directory.
@@ -20,7 +20,7 @@
# 1. Create a link_test script, make it executable. Simple Example:
# clang "$@" -o /tmp/test && /tmp/test || echo "PROBLEM"
# 2. Run the script to figure out which files are miscompiled:
-# > ./abtest.py
+# > ./abtest.py
# somefile.s: ok
# someotherfile.s: skipped: same content
# anotherfile.s: failed: './link_test' exitcode != 0
@@ -42,32 +42,142 @@ import os
import subprocess
import sys
-LINKTEST="./link_test"
-ESCAPE="\033[%sm"
-BOLD=ESCAPE % "1"
-RED=ESCAPE % "31"
-NORMAL=ESCAPE % "0"
-FAILED=RED+"failed"+NORMAL
+
+LINKTEST = "./link_test"
+ESCAPE = "\033[%sm"
+BOLD = ESCAPE % "1"
+RED = ESCAPE % "31"
+NORMAL = ESCAPE % "0"
+FAILED = RED + "failed" + NORMAL
+
def find(dir, file_filter=None):
- files = [walkdir[0]+"/"+file for walkdir in os.walk(dir) for file in walkdir[2]]
- if file_filter != None:
+ files = [
+ walkdir[0]+"/"+file
+ for walkdir in os.walk(dir)
+ for file in walkdir[2]
+ ]
+ if file_filter is not None:
files = filter(files, file_filter)
- return files
+ return sorted(files)
+
def error(message):
stderr.write("Error: %s\n" % (message,))
+
def warn(message):
stderr.write("Warning: %s\n" % (message,))
+
+def info(message):
+ stderr.write("Info: %s\n" % (message,))
+
+
+def announce_test(name):
+ stderr.write("%s%s%s: " % (BOLD, name, NORMAL))
+ stderr.flush()
+
+
+def announce_result(result):
+ stderr.write(result)
+ stderr.write("\n")
+ stderr.flush()
+
+
+def format_namelist(l):
+ result = ", ".join(l[0:3])
+ if len(l) > 3:
+ result += "... (%d total)" % len(l)
+ return result
+
+
+def check_sanity(choices, perform_test):
+ announce_test("sanity check A")
+ all_a = {name: a_b[0] for name, a_b in choices}
+ res_a = perform_test(all_a)
+ if res_a is not True:
+ error("Picking all choices from A failed to pass the test")
+ sys.exit(1)
+
+ announce_test("sanity check B (expecting failure)")
+ all_b = {name: a_b[1] for name, a_b in choices}
+ res_b = perform_test(all_b)
+ if res_b is not False:
+ error("Picking all choices from B did unexpectedly pass the test")
+ sys.exit(1)
+
+
+def check_sequentially(choices, perform_test):
+ known_good = set()
+ all_a = {name: a_b[0] for name, a_b in choices}
+ n = 1
+ for name, a_b in sorted(choices):
+ picks = dict(all_a)
+ picks[name] = a_b[1]
+ announce_test("checking %s [%d/%d]" % (name, n, len(choices)))
+ n += 1
+ res = perform_test(picks)
+ if res is True:
+ known_good.add(name)
+ return known_good
+
+
+def check_bisect(choices, perform_test):
+ known_good = set()
+ if len(choices) == 0:
+ return known_good
+
+ choice_map = dict(choices)
+ all_a = {name: a_b[0] for name, a_b in choices}
+
+ def test_partition(partition, upcoming_partition):
+ # Compute the maximum number of checks we have to do in the worst case.
+ max_remaining_steps = len(partition) * 2 - 1
+ if upcoming_partition is not None:
+ max_remaining_steps += len(upcoming_partition) * 2 - 1
+ for x in partitions_to_split:
+ max_remaining_steps += (len(x) - 1) * 2
+
+ picks = dict(all_a)
+ for x in partition:
+ picks[x] = choice_map[x][1]
+ announce_test("checking %s [<=%d remaining]" %
+ (format_namelist(partition), max_remaining_steps))
+ res = perform_test(picks)
+ if res is True:
+ known_good.update(partition)
+ elif len(partition) > 1:
+ partitions_to_split.insert(0, partition)
+
+ # TODO:
+ # - We could optimize based on the knowledge that when splitting a failed
+ # partition into two and one side checks out okay then we can deduce that
+ # the other partition must be a failure.
+ all_choice_names = [name for name, _ in choices]
+ partitions_to_split = [all_choice_names]
+ while len(partitions_to_split) > 0:
+ partition = partitions_to_split.pop()
+
+ middle = len(partition) // 2
+ left = partition[0:middle]
+ right = partition[middle:]
+
+ if len(left) > 0:
+ test_partition(left, right)
+ assert len(right) > 0
+ test_partition(right, None)
+
+ return known_good
+
+
def extract_functions(file):
functions = []
in_function = None
for line in open(file):
marker = line.find(" -- Begin function ")
if marker != -1:
- if in_function != None:
+ if in_function is not None:
warn("Missing end of function %s" % (in_function,))
funcname = line[marker + 19:-1]
in_function = funcname
@@ -77,27 +187,28 @@ def extract_functions(file):
marker = line.find(" -- End function")
if marker != -1:
text += line
- functions.append( (in_function, text) )
+ functions.append((in_function, text))
in_function = None
continue
- if in_function != None:
+ if in_function is not None:
text += line
return functions
-def replace_function(file, function, replacement, dest):
+
+def replace_functions(source, dest, replacements):
out = open(dest, "w")
skip = False
- found = False
in_function = None
- for line in open(file):
+ for line in open(source):
marker = line.find(" -- Begin function ")
if marker != -1:
- if in_function != None:
+ if in_function is not None:
warn("Missing end of function %s" % (in_function,))
funcname = line[marker + 19:-1]
in_function = funcname
- if in_function == function:
+ replacement = replacements.get(in_function)
+ if replacement is not None:
out.write(replacement)
skip = True
else:
@@ -111,122 +222,164 @@ def replace_function(file, function, rep
if not skip:
out.write(line)
-def announce_test(name):
- stderr.write("%s%s%s: " % (BOLD, name, NORMAL))
- stderr.flush()
-
-def announce_result(result, info):
- stderr.write(result)
- if info != "":
- stderr.write(": %s" % info)
- stderr.write("\n")
- stderr.flush()
def testrun(files):
- linkline="%s %s" % (LINKTEST, " ".join(files),)
+ linkline = "%s %s" % (LINKTEST, " ".join(files),)
res = subprocess.call(linkline, shell=True)
if res != 0:
- announce_result(FAILED, "'%s' exitcode != 0" % LINKTEST)
+ announce_result(FAILED + ": '%s' exitcode != 0" % LINKTEST)
return False
else:
- announce_result("ok", "")
+ announce_result("ok")
return True
-def check_files():
- """Check files mode"""
- for i in range(0, len(NO_PREFIX)):
- f = NO_PREFIX[i]
- b=baddir+"/"+f
- if b not in BAD_FILES:
- warn("There is no corresponding file to '%s' in %s" \
- % (gooddir+"/"+f, baddir))
- continue
-
- announce_test(f + " [%s/%s]" % (i+1, len(NO_PREFIX)))
-
- # combine files (everything from good except f)
- testfiles=[]
- skip=False
- for c in NO_PREFIX:
- badfile = baddir+"/"+c
- goodfile = gooddir+"/"+c
- if c == f:
- testfiles.append(badfile)
- if filecmp.cmp(goodfile, badfile):
- announce_result("skipped", "same content")
- skip = True
- break
+
+def prepare_files(gooddir, baddir):
+ files_a = find(gooddir, "*")
+ files_b = find(baddir, "*")
+
+ basenames_a = set(map(os.path.basename, files_a))
+ basenames_b = set(map(os.path.basename, files_b))
+
+ for name in files_b:
+ basename = os.path.basename(name)
+ if basename not in basenames_a:
+ warn("There is no corresponding file to '%s' in %s" %
+ (name, gooddir))
+ choices = []
+ skipped = []
+ for name in files_a:
+ basename = os.path.basename(name)
+ if basename not in basenames_b:
+ warn("There is no corresponding file to '%s' in %s" %
+ (name, baddir))
+
+ file_a = gooddir + "/" + basename
+ file_b = baddir + "/" + basename
+ if filecmp.cmp(file_a, file_b):
+ skipped.append(basename)
+ continue
+
+ choice = (basename, (file_a, file_b))
+ choices.append(choice)
+
+ if len(skipped) > 0:
+ info("Skipped (same content): %s" % format_namelist(skipped))
+
+ def perform_test(picks):
+ files = []
+ # Note that we iterate over files_a so we don't change the order
+ # (cannot use `picks` as it is a dictionary without order)
+ for x in files_a:
+ basename = os.path.basename(x)
+ picked = picks.get(basename)
+ if picked is None:
+ assert basename in skipped
+ files.append(x)
else:
- testfiles.append(goodfile)
- if skip:
+ files.append(picked)
+ return testrun(files)
+
+ return perform_test, choices
+
+
+def prepare_functions(to_check, gooddir, goodfile, badfile):
+ files_good = find(gooddir, "*")
+
+ functions_a = extract_functions(goodfile)
+ functions_a_map = dict(functions_a)
+ functions_b_map = dict(extract_functions(badfile))
+
+ for name in functions_b_map.keys():
+ if name not in functions_a_map:
+ warn("Function '%s' missing from good file" % name)
+ choices = []
+ skipped = []
+ for name, candidate_a in functions_a:
+ candidate_b = functions_b_map.get(name)
+ if candidate_b is None:
+ warn("Function '%s' missing from bad file" % name)
continue
- testrun(testfiles)
+ if candidate_a == candidate_b:
+ skipped.append(name)
+ continue
+ choice = name, (candidate_a, candidate_b)
+ choices.append(choice)
+
+ if len(skipped) > 0:
+ info("Skipped (same content): %s" % format_namelist(skipped))
+
+ combined_file = '/tmp/combined2.s'
+ files = []
+ found_good_file = False
+ for c in files_good:
+ if os.path.basename(c) == to_check:
+ found_good_file = True
+ files.append(combined_file)
+ continue
+ files.append(c)
+ assert found_good_file
+
+ def perform_test(picks):
+ for name, x in picks.items():
+ assert x == functions_a_map[name] or x == functions_b_map[name]
+ replace_functions(goodfile, combined_file, picks)
+ return testrun(files)
+ return perform_test, choices
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--a', dest='dir_a', default='before')
+ parser.add_argument('--b', dest='dir_b', default='after')
+ parser.add_argument('--insane', help='Skip sanity check',
+ action='store_true')
+ parser.add_argument('--seq',
+ help='Check sequentially instead of bisection',
+ action='store_true')
+ parser.add_argument('file', metavar='file', nargs='?')
+ config = parser.parse_args()
+
+ gooddir = config.dir_a
+ baddir = config.dir_b
+
+ # Preparation phase: Creates a dictionary mapping names to a list of two
+ # choices each. The bisection algorithm will pick one choice for each name
+ # and then run the perform_test function on it.
+ if config.file is not None:
+ goodfile = gooddir + "/" + config.file
+ badfile = baddir + "/" + config.file
+ perform_test, choices = prepare_functions(config.file, gooddir,
+ goodfile, badfile)
+ else:
+ perform_test, choices = prepare_files(gooddir, baddir)
+
+ info("%d bisection choices" % len(choices))
+
+ # "Checking whether build environment is sane ..."
+ if not config.insane:
+ if not os.access(LINKTEST, os.X_OK):
+ error("Expect '%s' to be present and executable" % (LINKTEST,))
+ exit(1)
+
+ check_sanity(choices, perform_test)
+
+ if config.seq:
+ known_good = check_sequentially(choices, perform_test)
+ else:
+ known_good = check_bisect(choices, perform_test)
+
+ stderr.write("")
+ if len(known_good) != len(choices):
+ stderr.write("== Failing ==\n")
+ for name, _ in choices:
+ if name not in known_good:
+ stderr.write("%s\n" % name)
+ else:
+ # This shouldn't happen when the sanity check works...
+ # Maybe link_test isn't deterministic?
+ stderr.write("Could not identify failing parts?!?")
-def check_functions_in_file(base, goodfile, badfile):
- functions = extract_functions(goodfile)
- if len(functions) == 0:
- warn("Couldn't find any function in %s, missing annotations?" % (goodfile,))
- return
- badfunctions = dict(extract_functions(badfile))
- if len(functions) == 0:
- warn("Couldn't find any function in %s, missing annotations?" % (badfile,))
- return
-
- COMBINED="/tmp/combined.s"
- i = 0
- for (func,func_text) in functions:
- announce_test(func + " [%s/%s]" % (i+1, len(functions)))
- i+=1
- if func not in badfunctions:
- warn("Function '%s' missing from bad file" % func)
- continue
- if badfunctions[func] == func_text:
- announce_result("skipped", "same content")
- continue
- replace_function(goodfile, func, badfunctions[func], COMBINED)
- testfiles=[]
- for c in NO_PREFIX:
- if c == base:
- testfiles.append(COMBINED)
- continue
- testfiles.append(gooddir + "/" + c)
-
- testrun(testfiles)
-
-parser = argparse.ArgumentParser()
-parser.add_argument('--a', dest='dir_a', default='before')
-parser.add_argument('--b', dest='dir_b', default='after')
-parser.add_argument('--insane', help='Skip sanity check', action='store_true')
-parser.add_argument('file', metavar='file', nargs='?')
-config = parser.parse_args()
-
-gooddir=config.dir_a
-baddir=config.dir_b
-
-BAD_FILES=find(baddir, "*")
-GOOD_FILES=find(gooddir, "*")
-NO_PREFIX=sorted([x[len(gooddir)+1:] for x in GOOD_FILES])
-
-# "Checking whether build environment is sane ..."
-if not config.insane:
- announce_test("sanity check")
- if not os.access(LINKTEST, os.X_OK):
- error("Expect '%s' to be present and executable" % (LINKTEST,))
- exit(1)
-
- res = testrun(GOOD_FILES)
- if not res:
- # "build environment is grinning and holding a spatula. Guess not."
- linkline="%s %s" % (LINKTEST, " ".join(GOOD_FILES),)
- stderr.write("\n%s\n\n" % linkline)
- stderr.write("Returned with exitcode != 0\n")
- sys.exit(1)
-if config.file is not None:
- # File exchange mode
- goodfile = gooddir+"/"+config.file
- badfile = baddir+"/"+config.file
- check_functions_in_file(config.file, goodfile, badfile)
-else:
- # Function exchange mode
- check_files()
+if __name__ == '__main__':
+ main()
More information about the llvm-commits
mailing list