123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- #!/usr/bin/env python
- #
- # 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.
- #
- # 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.
- #
- # Additionally you can perform the same steps with a single .s file. In this
- # mode functions are identified by " -- Begin function FunctionName" and
- # " -- End function" markers. The abtest.py then takes all
- # function from the file in the "before" directory and replaces one function
- # with the corresponding function from the "bad" file in each step.
- #
- # Example usage to identify miscompiled files:
- # 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
- # somefile.s: ok
- # someotherfile.s: skipped: same content
- # anotherfile.s: failed: './link_test' exitcode != 0
- # ...
- # Example usage to identify miscompiled functions inside a file:
- # 3. Run the tests on a single file (assuming before/file.s and
- # after/file.s exist)
- # > ./abtest.py file.s
- # funcname1 [0/XX]: ok
- # funcname2 [1/XX]: ok
- # funcname3 [2/XX]: skipped: same content
- # funcname4 [3/XX]: failed: './link_test' exitcode != 0
- # ...
- from fnmatch import filter
- from sys import stderr
- import argparse
- import filecmp
- 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
- def find(dir, 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 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 is not None:
- warn("Missing end of function %s" % (in_function,))
- funcname = line[marker + 19:-1]
- in_function = funcname
- text = line
- continue
- marker = line.find(" -- End function")
- if marker != -1:
- text += line
- functions.append((in_function, text))
- in_function = None
- continue
- if in_function is not None:
- text += line
- return functions
- def replace_functions(source, dest, replacements):
- out = open(dest, "w")
- skip = False
- in_function = None
- for line in open(source):
- marker = line.find(" -- Begin function ")
- if marker != -1:
- if in_function is not None:
- warn("Missing end of function %s" % (in_function,))
- funcname = line[marker + 19:-1]
- in_function = funcname
- replacement = replacements.get(in_function)
- if replacement is not None:
- out.write(replacement)
- skip = True
- else:
- marker = line.find(" -- End function")
- if marker != -1:
- in_function = None
- if skip:
- skip = False
- continue
- if not skip:
- out.write(line)
- def testrun(files):
- linkline = "%s %s" % (LINKTEST, " ".join(files),)
- res = subprocess.call(linkline, shell=True)
- if res != 0:
- announce_result(FAILED + ": '%s' exitcode != 0" % LINKTEST)
- return False
- else:
- announce_result("ok")
- return True
- 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:
- 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
- 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?!?")
- if __name__ == '__main__':
- main()
|