abtest.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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
  7. # list of files which should be linked together and result tested. "link_test"
  8. # should 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. # ...
  37. from fnmatch import filter
  38. from sys import stderr
  39. import argparse
  40. import filecmp
  41. import os
  42. import subprocess
  43. import sys
  44. LINKTEST = "./link_test"
  45. ESCAPE = "\033[%sm"
  46. BOLD = ESCAPE % "1"
  47. RED = ESCAPE % "31"
  48. NORMAL = ESCAPE % "0"
  49. FAILED = RED + "failed" + NORMAL
  50. def find(dir, file_filter=None):
  51. files = [
  52. walkdir[0]+"/"+file
  53. for walkdir in os.walk(dir)
  54. for file in walkdir[2]
  55. ]
  56. if file_filter is not None:
  57. files = filter(files, file_filter)
  58. return sorted(files)
  59. def error(message):
  60. stderr.write("Error: %s\n" % (message,))
  61. def warn(message):
  62. stderr.write("Warning: %s\n" % (message,))
  63. def info(message):
  64. stderr.write("Info: %s\n" % (message,))
  65. def announce_test(name):
  66. stderr.write("%s%s%s: " % (BOLD, name, NORMAL))
  67. stderr.flush()
  68. def announce_result(result):
  69. stderr.write(result)
  70. stderr.write("\n")
  71. stderr.flush()
  72. def format_namelist(l):
  73. result = ", ".join(l[0:3])
  74. if len(l) > 3:
  75. result += "... (%d total)" % len(l)
  76. return result
  77. def check_sanity(choices, perform_test):
  78. announce_test("sanity check A")
  79. all_a = {name: a_b[0] for name, a_b in choices}
  80. res_a = perform_test(all_a)
  81. if res_a is not True:
  82. error("Picking all choices from A failed to pass the test")
  83. sys.exit(1)
  84. announce_test("sanity check B (expecting failure)")
  85. all_b = {name: a_b[1] for name, a_b in choices}
  86. res_b = perform_test(all_b)
  87. if res_b is not False:
  88. error("Picking all choices from B did unexpectedly pass the test")
  89. sys.exit(1)
  90. def check_sequentially(choices, perform_test):
  91. known_good = set()
  92. all_a = {name: a_b[0] for name, a_b in choices}
  93. n = 1
  94. for name, a_b in sorted(choices):
  95. picks = dict(all_a)
  96. picks[name] = a_b[1]
  97. announce_test("checking %s [%d/%d]" % (name, n, len(choices)))
  98. n += 1
  99. res = perform_test(picks)
  100. if res is True:
  101. known_good.add(name)
  102. return known_good
  103. def check_bisect(choices, perform_test):
  104. known_good = set()
  105. if len(choices) == 0:
  106. return known_good
  107. choice_map = dict(choices)
  108. all_a = {name: a_b[0] for name, a_b in choices}
  109. def test_partition(partition, upcoming_partition):
  110. # Compute the maximum number of checks we have to do in the worst case.
  111. max_remaining_steps = len(partition) * 2 - 1
  112. if upcoming_partition is not None:
  113. max_remaining_steps += len(upcoming_partition) * 2 - 1
  114. for x in partitions_to_split:
  115. max_remaining_steps += (len(x) - 1) * 2
  116. picks = dict(all_a)
  117. for x in partition:
  118. picks[x] = choice_map[x][1]
  119. announce_test("checking %s [<=%d remaining]" %
  120. (format_namelist(partition), max_remaining_steps))
  121. res = perform_test(picks)
  122. if res is True:
  123. known_good.update(partition)
  124. elif len(partition) > 1:
  125. partitions_to_split.insert(0, partition)
  126. # TODO:
  127. # - We could optimize based on the knowledge that when splitting a failed
  128. # partition into two and one side checks out okay then we can deduce that
  129. # the other partition must be a failure.
  130. all_choice_names = [name for name, _ in choices]
  131. partitions_to_split = [all_choice_names]
  132. while len(partitions_to_split) > 0:
  133. partition = partitions_to_split.pop()
  134. middle = len(partition) // 2
  135. left = partition[0:middle]
  136. right = partition[middle:]
  137. if len(left) > 0:
  138. test_partition(left, right)
  139. assert len(right) > 0
  140. test_partition(right, None)
  141. return known_good
  142. def extract_functions(file):
  143. functions = []
  144. in_function = None
  145. for line in open(file):
  146. marker = line.find(" -- Begin function ")
  147. if marker != -1:
  148. if in_function is not None:
  149. warn("Missing end of function %s" % (in_function,))
  150. funcname = line[marker + 19:-1]
  151. in_function = funcname
  152. text = line
  153. continue
  154. marker = line.find(" -- End function")
  155. if marker != -1:
  156. text += line
  157. functions.append((in_function, text))
  158. in_function = None
  159. continue
  160. if in_function is not None:
  161. text += line
  162. return functions
  163. def replace_functions(source, dest, replacements):
  164. out = open(dest, "w")
  165. skip = False
  166. in_function = None
  167. for line in open(source):
  168. marker = line.find(" -- Begin function ")
  169. if marker != -1:
  170. if in_function is not None:
  171. warn("Missing end of function %s" % (in_function,))
  172. funcname = line[marker + 19:-1]
  173. in_function = funcname
  174. replacement = replacements.get(in_function)
  175. if replacement is not None:
  176. out.write(replacement)
  177. skip = True
  178. else:
  179. marker = line.find(" -- End function")
  180. if marker != -1:
  181. in_function = None
  182. if skip:
  183. skip = False
  184. continue
  185. if not skip:
  186. out.write(line)
  187. def testrun(files):
  188. linkline = "%s %s" % (LINKTEST, " ".join(files),)
  189. res = subprocess.call(linkline, shell=True)
  190. if res != 0:
  191. announce_result(FAILED + ": '%s' exitcode != 0" % LINKTEST)
  192. return False
  193. else:
  194. announce_result("ok")
  195. return True
  196. def prepare_files(gooddir, baddir):
  197. files_a = find(gooddir, "*")
  198. files_b = find(baddir, "*")
  199. basenames_a = set(map(os.path.basename, files_a))
  200. basenames_b = set(map(os.path.basename, files_b))
  201. for name in files_b:
  202. basename = os.path.basename(name)
  203. if basename not in basenames_a:
  204. warn("There is no corresponding file to '%s' in %s" %
  205. (name, gooddir))
  206. choices = []
  207. skipped = []
  208. for name in files_a:
  209. basename = os.path.basename(name)
  210. if basename not in basenames_b:
  211. warn("There is no corresponding file to '%s' in %s" %
  212. (name, baddir))
  213. file_a = gooddir + "/" + basename
  214. file_b = baddir + "/" + basename
  215. if filecmp.cmp(file_a, file_b):
  216. skipped.append(basename)
  217. continue
  218. choice = (basename, (file_a, file_b))
  219. choices.append(choice)
  220. if len(skipped) > 0:
  221. info("Skipped (same content): %s" % format_namelist(skipped))
  222. def perform_test(picks):
  223. files = []
  224. # Note that we iterate over files_a so we don't change the order
  225. # (cannot use `picks` as it is a dictionary without order)
  226. for x in files_a:
  227. basename = os.path.basename(x)
  228. picked = picks.get(basename)
  229. if picked is None:
  230. assert basename in skipped
  231. files.append(x)
  232. else:
  233. files.append(picked)
  234. return testrun(files)
  235. return perform_test, choices
  236. def prepare_functions(to_check, gooddir, goodfile, badfile):
  237. files_good = find(gooddir, "*")
  238. functions_a = extract_functions(goodfile)
  239. functions_a_map = dict(functions_a)
  240. functions_b_map = dict(extract_functions(badfile))
  241. for name in functions_b_map.keys():
  242. if name not in functions_a_map:
  243. warn("Function '%s' missing from good file" % name)
  244. choices = []
  245. skipped = []
  246. for name, candidate_a in functions_a:
  247. candidate_b = functions_b_map.get(name)
  248. if candidate_b is None:
  249. warn("Function '%s' missing from bad file" % name)
  250. continue
  251. if candidate_a == candidate_b:
  252. skipped.append(name)
  253. continue
  254. choice = name, (candidate_a, candidate_b)
  255. choices.append(choice)
  256. if len(skipped) > 0:
  257. info("Skipped (same content): %s" % format_namelist(skipped))
  258. combined_file = '/tmp/combined2.s'
  259. files = []
  260. found_good_file = False
  261. for c in files_good:
  262. if os.path.basename(c) == to_check:
  263. found_good_file = True
  264. files.append(combined_file)
  265. continue
  266. files.append(c)
  267. assert found_good_file
  268. def perform_test(picks):
  269. for name, x in picks.items():
  270. assert x == functions_a_map[name] or x == functions_b_map[name]
  271. replace_functions(goodfile, combined_file, picks)
  272. return testrun(files)
  273. return perform_test, choices
  274. def main():
  275. parser = argparse.ArgumentParser()
  276. parser.add_argument('--a', dest='dir_a', default='before')
  277. parser.add_argument('--b', dest='dir_b', default='after')
  278. parser.add_argument('--insane', help='Skip sanity check',
  279. action='store_true')
  280. parser.add_argument('--seq',
  281. help='Check sequentially instead of bisection',
  282. action='store_true')
  283. parser.add_argument('file', metavar='file', nargs='?')
  284. config = parser.parse_args()
  285. gooddir = config.dir_a
  286. baddir = config.dir_b
  287. # Preparation phase: Creates a dictionary mapping names to a list of two
  288. # choices each. The bisection algorithm will pick one choice for each name
  289. # and then run the perform_test function on it.
  290. if config.file is not None:
  291. goodfile = gooddir + "/" + config.file
  292. badfile = baddir + "/" + config.file
  293. perform_test, choices = prepare_functions(config.file, gooddir,
  294. goodfile, badfile)
  295. else:
  296. perform_test, choices = prepare_files(gooddir, baddir)
  297. info("%d bisection choices" % len(choices))
  298. # "Checking whether build environment is sane ..."
  299. if not config.insane:
  300. if not os.access(LINKTEST, os.X_OK):
  301. error("Expect '%s' to be present and executable" % (LINKTEST,))
  302. exit(1)
  303. check_sanity(choices, perform_test)
  304. if config.seq:
  305. known_good = check_sequentially(choices, perform_test)
  306. else:
  307. known_good = check_bisect(choices, perform_test)
  308. stderr.write("")
  309. if len(known_good) != len(choices):
  310. stderr.write("== Failing ==\n")
  311. for name, _ in choices:
  312. if name not in known_good:
  313. stderr.write("%s\n" % name)
  314. else:
  315. # This shouldn't happen when the sanity check works...
  316. # Maybe link_test isn't deterministic?
  317. stderr.write("Could not identify failing parts?!?")
  318. if __name__ == '__main__':
  319. main()