1#!/usr/bin/env python 2# 3# Given a previous good compile narrow down miscompiles. 4# Expects two directories named "before" and "after" each containing a set of 5# assembly or object files where the "after" version is assumed to be broken. 6# You also have to provide a script called "link_test". It is called with a list 7# of files which should be linked together and result tested. "link_test" should 8# returns with exitcode 0 if the linking and testing succeeded. 9# 10# abtest.py operates by taking all files from the "before" directory and 11# in each step replacing one of them with a file from the "bad" directory. 12# 13# Additionally you can perform the same steps with a single .s file. In this 14# mode functions are identified by " -- Begin function FunctionName" and 15# " -- End function" markers. The abtest.py then takes all 16# function from the file in the "before" directory and replaces one function 17# with the corresponding function from the "bad" file in each step. 18# 19# Example usage to identify miscompiled files: 20# 1. Create a link_test script, make it executable. Simple Example: 21# clang "$@" -o /tmp/test && /tmp/test || echo "PROBLEM" 22# 2. Run the script to figure out which files are miscompiled: 23# > ./abtest.py 24# somefile.s: ok 25# someotherfile.s: skipped: same content 26# anotherfile.s: failed: './link_test' exitcode != 0 27# ... 28# Example usage to identify miscompiled functions inside a file: 29# 3. Run the tests on a single file (assuming before/file.s and 30# after/file.s exist) 31# > ./abtest.py file.s 32# funcname1 [0/XX]: ok 33# funcname2 [1/XX]: ok 34# funcname3 [2/XX]: skipped: same content 35# funcname4 [3/XX]: failed: './link_test' exitcode != 0 36# ... 37from fnmatch import filter 38from sys import stderr 39import argparse 40import filecmp 41import os 42import subprocess 43import sys 44 45LINKTEST="./link_test" 46ESCAPE="\033[%sm" 47BOLD=ESCAPE % "1" 48RED=ESCAPE % "31" 49NORMAL=ESCAPE % "0" 50FAILED=RED+"failed"+NORMAL 51 52def find(dir, file_filter=None): 53 files = [walkdir[0]+"/"+file for walkdir in os.walk(dir) for file in walkdir[2]] 54 if file_filter != None: 55 files = filter(files, file_filter) 56 return files 57 58def error(message): 59 stderr.write("Error: %s\n" % (message,)) 60 61def warn(message): 62 stderr.write("Warning: %s\n" % (message,)) 63 64def extract_functions(file): 65 functions = [] 66 in_function = None 67 for line in open(file): 68 marker = line.find(" -- Begin function ") 69 if marker != -1: 70 if in_function != None: 71 warn("Missing end of function %s" % (in_function,)) 72 funcname = line[marker + 19:-1] 73 in_function = funcname 74 text = line 75 continue 76 77 marker = line.find(" -- End function") 78 if marker != -1: 79 text += line 80 functions.append( (in_function, text) ) 81 in_function = None 82 continue 83 84 if in_function != None: 85 text += line 86 return functions 87 88def replace_function(file, function, replacement, dest): 89 out = open(dest, "w") 90 skip = False 91 found = False 92 in_function = None 93 for line in open(file): 94 marker = line.find(" -- Begin function ") 95 if marker != -1: 96 if in_function != None: 97 warn("Missing end of function %s" % (in_function,)) 98 funcname = line[marker + 19:-1] 99 in_function = funcname 100 if in_function == function: 101 out.write(replacement) 102 skip = True 103 else: 104 marker = line.find(" -- End function") 105 if marker != -1: 106 in_function = None 107 if skip: 108 skip = False 109 continue 110 111 if not skip: 112 out.write(line) 113 114def announce_test(name): 115 stderr.write("%s%s%s: " % (BOLD, name, NORMAL)) 116 stderr.flush() 117 118def announce_result(result, info): 119 stderr.write(result) 120 if info != "": 121 stderr.write(": %s" % info) 122 stderr.write("\n") 123 stderr.flush() 124 125def testrun(files): 126 linkline="%s %s" % (LINKTEST, " ".join(files),) 127 res = subprocess.call(linkline, shell=True) 128 if res != 0: 129 announce_result(FAILED, "'%s' exitcode != 0" % LINKTEST) 130 return False 131 else: 132 announce_result("ok", "") 133 return True 134 135def check_files(): 136 """Check files mode""" 137 for i in range(0, len(NO_PREFIX)): 138 f = NO_PREFIX[i] 139 b=baddir+"/"+f 140 if b not in BAD_FILES: 141 warn("There is no corresponding file to '%s' in %s" \ 142 % (gooddir+"/"+f, baddir)) 143 continue 144 145 announce_test(f + " [%s/%s]" % (i+1, len(NO_PREFIX))) 146 147 # combine files (everything from good except f) 148 testfiles=[] 149 skip=False 150 for c in NO_PREFIX: 151 badfile = baddir+"/"+c 152 goodfile = gooddir+"/"+c 153 if c == f: 154 testfiles.append(badfile) 155 if filecmp.cmp(goodfile, badfile): 156 announce_result("skipped", "same content") 157 skip = True 158 break 159 else: 160 testfiles.append(goodfile) 161 if skip: 162 continue 163 testrun(testfiles) 164 165def check_functions_in_file(base, goodfile, badfile): 166 functions = extract_functions(goodfile) 167 if len(functions) == 0: 168 warn("Couldn't find any function in %s, missing annotations?" % (goodfile,)) 169 return 170 badfunctions = dict(extract_functions(badfile)) 171 if len(functions) == 0: 172 warn("Couldn't find any function in %s, missing annotations?" % (badfile,)) 173 return 174 175 COMBINED="/tmp/combined.s" 176 i = 0 177 for (func,func_text) in functions: 178 announce_test(func + " [%s/%s]" % (i+1, len(functions))) 179 i+=1 180 if func not in badfunctions: 181 warn("Function '%s' missing from bad file" % func) 182 continue 183 if badfunctions[func] == func_text: 184 announce_result("skipped", "same content") 185 continue 186 replace_function(goodfile, func, badfunctions[func], COMBINED) 187 testfiles=[] 188 for c in NO_PREFIX: 189 if c == base: 190 testfiles.append(COMBINED) 191 continue 192 testfiles.append(gooddir + "/" + c) 193 194 testrun(testfiles) 195 196parser = argparse.ArgumentParser() 197parser.add_argument('--a', dest='dir_a', default='before') 198parser.add_argument('--b', dest='dir_b', default='after') 199parser.add_argument('--insane', help='Skip sanity check', action='store_true') 200parser.add_argument('file', metavar='file', nargs='?') 201config = parser.parse_args() 202 203gooddir=config.dir_a 204baddir=config.dir_b 205 206BAD_FILES=find(baddir, "*") 207GOOD_FILES=find(gooddir, "*") 208NO_PREFIX=sorted([x[len(gooddir)+1:] for x in GOOD_FILES]) 209 210# "Checking whether build environment is sane ..." 211if not config.insane: 212 announce_test("sanity check") 213 if not os.access(LINKTEST, os.X_OK): 214 error("Expect '%s' to be present and executable" % (LINKTEST,)) 215 exit(1) 216 217 res = testrun(GOOD_FILES) 218 if not res: 219 # "build environment is grinning and holding a spatula. Guess not." 220 linkline="%s %s" % (LINKTEST, " ".join(GOOD_FILES),) 221 stderr.write("\n%s\n\n" % linkline) 222 stderr.write("Returned with exitcode != 0\n") 223 sys.exit(1) 224 225if config.file is not None: 226 # File exchange mode 227 goodfile = gooddir+"/"+config.file 228 badfile = baddir+"/"+config.file 229 check_functions_in_file(config.file, goodfile, badfile) 230else: 231 # Function exchange mode 232 check_files() 233