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 FunctionName" and 15 # "# -- End FunctionName" markers. The abtest.py then takes all functions from 16 # the file in the "before" directory and replaces one function with the 17 # 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. First you have to mark begin and end of the functions. 30 # The script comes with some examples called mark_xxx.py. 31 # Unfortunately this is very specific to your environment and it is likely 32 # that you have to write a custom version for your environment. 33 # > for i in before/*.s after/*.s; do mark_xxx.py $i; done 34 # 4. Run the tests on a single file (assuming before/file.s and 35 # after/file.s exist) 36 # > ./abtest.py file.s 37 # funcname1 [0/XX]: ok 38 # funcname2 [1/XX]: ok 39 # funcname3 [2/XX]: skipped: same content 40 # funcname4 [3/XX]: failed: './link_test' exitcode != 0 41 # ... 42 from fnmatch import filter 43 from sys import stderr 44 import argparse 45 import filecmp 46 import os 47 import subprocess 48 import sys 49 50 LINKTEST="./link_test" 51 ESCAPE="\033[%sm" 52 BOLD=ESCAPE % "1" 53 RED=ESCAPE % "31" 54 NORMAL=ESCAPE % "0" 55 FAILED=RED+"failed"+NORMAL 56 57 def find(dir, file_filter=None): 58 files = [walkdir[0]+"/"+file for walkdir in os.walk(dir) for file in walkdir[2]] 59 if file_filter != None: 60 files = filter(files, file_filter) 61 return files 62 63 def error(message): 64 stderr.write("Error: %s\n" % (message,)) 65 66 def warn(message): 67 stderr.write("Warning: %s\n" % (message,)) 68 69 def extract_functions(file): 70 functions = [] 71 in_function = None 72 for line in open(file): 73 if line.startswith("# -- Begin "): 74 if in_function != None: 75 warn("Missing end of function %s" % (in_function,)) 76 funcname = line[12:-1] 77 in_function = funcname 78 text = line 79 elif line.startswith("# -- End "): 80 function_name = line[10:-1] 81 if in_function != function_name: 82 warn("End %s does not match begin %s" % (function_name, in_function)) 83 else: 84 text += line 85 functions.append( (in_function, text) ) 86 in_function = None 87 elif in_function != None: 88 text += line 89 return functions 90 91 def replace_function(file, function, replacement, dest): 92 out = open(dest, "w") 93 skip = False 94 found = False 95 in_function = None 96 for line in open(file): 97 if line.startswith("# -- Begin "): 98 if in_function != None: 99 warn("Missing end of function %s" % (in_function,)) 100 funcname = line[12:-1] 101 in_function = funcname 102 if in_function == function: 103 out.write(replacement) 104 skip = True 105 elif line.startswith("# -- End "): 106 function_name = line[10:-1] 107 if in_function != function_name: 108 warn("End %s does not match begin %s" % (function_name, in_function)) 109 in_function = None 110 if skip: 111 skip = False 112 continue 113 if not skip: 114 out.write(line) 115 116 def announce_test(name): 117 stderr.write("%s%s%s: " % (BOLD, name, NORMAL)) 118 stderr.flush() 119 120 def announce_result(result, info): 121 stderr.write(result) 122 if info != "": 123 stderr.write(": %s" % info) 124 stderr.write("\n") 125 stderr.flush() 126 127 def testrun(files): 128 linkline="%s %s" % (LINKTEST, " ".join(files),) 129 res = subprocess.call(linkline, shell=True) 130 if res != 0: 131 announce_result(FAILED, "'%s' exitcode != 0" % LINKTEST) 132 return False 133 else: 134 announce_result("ok", "") 135 return True 136 137 def check_files(): 138 """Check files mode""" 139 for i in range(0, len(NO_PREFIX)): 140 f = NO_PREFIX[i] 141 b=baddir+"/"+f 142 if b not in BAD_FILES: 143 warn("There is no corresponding file to '%s' in %s" \ 144 % (gooddir+"/"+f, baddir)) 145 continue 146 147 announce_test(f + " [%s/%s]" % (i+1, len(NO_PREFIX))) 148 149 # combine files (everything from good except f) 150 testfiles=[] 151 skip=False 152 for c in NO_PREFIX: 153 badfile = baddir+"/"+c 154 goodfile = gooddir+"/"+c 155 if c == f: 156 testfiles.append(badfile) 157 if filecmp.cmp(goodfile, badfile): 158 announce_result("skipped", "same content") 159 skip = True 160 break 161 else: 162 testfiles.append(goodfile) 163 if skip: 164 continue 165 testrun(testfiles) 166 167 def check_functions_in_file(base, goodfile, badfile): 168 functions = extract_functions(goodfile) 169 if len(functions) == 0: 170 warn("Couldn't find any function in %s, missing annotations?" % (goodfile,)) 171 return 172 badfunctions = dict(extract_functions(badfile)) 173 if len(functions) == 0: 174 warn("Couldn't find any function in %s, missing annotations?" % (badfile,)) 175 return 176 177 COMBINED="/tmp/combined.s" 178 i = 0 179 for (func,func_text) in functions: 180 announce_test(func + " [%s/%s]" % (i+1, len(functions))) 181 i+=1 182 if func not in badfunctions: 183 warn("Function '%s' missing from bad file" % func) 184 continue 185 if badfunctions[func] == func_text: 186 announce_result("skipped", "same content") 187 continue 188 replace_function(goodfile, func, badfunctions[func], COMBINED) 189 testfiles=[] 190 for c in NO_PREFIX: 191 if c == base: 192 testfiles.append(COMBINED) 193 continue 194 testfiles.append(gooddir + "/" + c) 195 196 testrun(testfiles) 197 198 parser = argparse.ArgumentParser() 199 parser.add_argument('--a', dest='dir_a', default='before') 200 parser.add_argument('--b', dest='dir_b', default='after') 201 parser.add_argument('--insane', help='Skip sanity check', action='store_true') 202 parser.add_argument('file', metavar='file', nargs='?') 203 config = parser.parse_args() 204 205 gooddir=config.dir_a 206 baddir=config.dir_b 207 208 BAD_FILES=find(baddir, "*") 209 GOOD_FILES=find(gooddir, "*") 210 NO_PREFIX=sorted([x[len(gooddir)+1:] for x in GOOD_FILES]) 211 212 # "Checking whether build environment is sane ..." 213 if not config.insane: 214 announce_test("sanity check") 215 if not os.access(LINKTEST, os.X_OK): 216 error("Expect '%s' to be present and executable" % (LINKTEST,)) 217 exit(1) 218 219 res = testrun(GOOD_FILES) 220 if not res: 221 # "build environment is grinning and holding a spatula. Guess not." 222 linkline="%s %s" % (LINKTEST, " ".join(GOOD_FILES),) 223 stderr.write("\n%s\n\n" % linkline) 224 stderr.write("Returned with exitcode != 0\n") 225 sys.exit(1) 226 227 if config.file is not None: 228 # File exchange mode 229 goodfile = gooddir+"/"+config.file 230 badfile = baddir+"/"+config.file 231 check_functions_in_file(config.file, goodfile, badfile) 232 else: 233 # Function exchange mode 234 check_files() 235