712 lines · python
1#!/usr/bin/env python2#3# Debugify summary for the original debug info testing.4#5 6from __future__ import print_function7import argparse8import os9import re10import sys11from json import loads12from collections import defaultdict13from collections import OrderedDict14 15class DILocBug:16 def __init__(self, origin, action, bb_name, fn_name, instr):17 self.origin = origin18 self.action = action19 self.bb_name = bb_name20 self.fn_name = fn_name21 self.instr = instr22 23 def key(self):24 return self.action + self.bb_name + self.fn_name + self.instr25 26 def reduced_key(self, bug_pass):27 if self.origin is not None:28 # If we have the origin stacktrace available, we can use it to efficiently deduplicate identical errors. We29 # just need to remove the pointer values from the string first, so that we can deduplicate across files.30 origin_no_addr = re.sub(r"0x[0-9a-fA-F]+", "", self.origin)31 return origin_no_addr32 return bug_pass + self.instr33 34 def to_dict(self):35 result = {36 "instr": self.instr,37 "fn_name": self.fn_name,38 "bb_name": self.bb_name,39 "action": self.action,40 }41 if self.origin:42 result["origin"] = self.origin43 return result44 45 46class DISPBug:47 def __init__(self, action, fn_name):48 self.action = action49 self.fn_name = fn_name50 51 def key(self):52 return self.action + self.fn_name53 54 def reduced_key(self, bug_pass):55 return bug_pass + self.fn_name56 57 def to_dict(self):58 return {59 "fn_name": self.fn_name,60 "action": self.action,61 }62 63 64class DIVarBug:65 def __init__(self, action, name, fn_name):66 self.action = action67 self.name = name68 self.fn_name = fn_name69 70 def key(self):71 return self.action + self.name + self.fn_name72 73 def reduced_key(self, bug_pass):74 return bug_pass + self.name75 76 def to_dict(self):77 return {78 "fn_name": self.fn_name,79 "name": self.name,80 "action": self.action,81 }82 83 84def print_bugs_yaml(name, bugs_dict, indent=2):85 def get_bug_line(indent_level: int, text: str, margin_mark: bool = False):86 if margin_mark:87 return "- ".rjust(indent_level * indent) + text88 return " " * indent * indent_level + text89 90 print(f"{name}:")91 for bugs_file, bugs_pass_dict in sorted(iter(bugs_dict.items())):92 print(get_bug_line(1, f"{bugs_file}:"))93 for bugs_pass, bugs_list in sorted(iter(bugs_pass_dict.items())):94 print(get_bug_line(2, f"{bugs_pass}:"))95 for bug in bugs_list:96 bug_dict = bug.to_dict()97 first_line = True98 # First item needs a '-' in the margin.99 for key, val in sorted(iter(bug_dict.items())):100 if "\n" in val:101 # Output block text for any multiline string.102 print(get_bug_line(3, f"{key}: |", first_line))103 for line in val.splitlines():104 print(get_bug_line(4, line))105 else:106 print(get_bug_line(3, f"{key}: {val}", first_line))107 first_line = False108 109# Report the bugs in form of html.110def generate_html_report(111 di_location_bugs,112 di_subprogram_bugs,113 di_var_bugs,114 di_location_bugs_summary,115 di_sp_bugs_summary,116 di_var_bugs_summary,117 html_file,118):119 fileout = open(html_file, "w")120 121 html_header = """ <html>122 <head>123 <style>124 table, th, td {125 border: 1px solid black;126 }127 table.center {128 margin-left: auto;129 margin-right: auto;130 }131 </style>132 </head>133 <body>134 """135 136 # Create the table for Location bugs.137 table_title_di_loc = "Location Bugs found by the Debugify"138 139 table_di_loc = """<table>140 <caption><b>{}</b></caption>141 <tr>142 """.format(143 table_title_di_loc144 )145 146 # If any DILocation bug has an origin stack trace, we emit an extra column in the table, which we must therefore147 # determine up-front.148 has_origin_col = any(149 x.origin is not None150 for per_file_bugs in di_location_bugs.values()151 for per_pass_bugs in per_file_bugs.values()152 for x in per_pass_bugs153 )154 155 header_di_loc = [156 "File",157 "LLVM Pass Name",158 "LLVM IR Instruction",159 "Function Name",160 "Basic Block Name",161 "Action",162 ]163 if has_origin_col:164 header_di_loc.append("Origin")165 166 for column in header_di_loc:167 table_di_loc += " <th>{0}</th>\n".format(column.strip())168 table_di_loc += " </tr>\n"169 170 at_least_one_bug_found = False171 172 # Handle loction bugs.173 for file, per_file_bugs in di_location_bugs.items():174 for llvm_pass, per_pass_bugs in per_file_bugs.items():175 # No location bugs for the pass.176 if len(per_pass_bugs) == 0:177 continue178 at_least_one_bug_found = True179 row = []180 table_di_loc += " </tr>\n"181 # Get the bugs info.182 for x in per_pass_bugs:183 row.append(" <tr>\n")184 row.append(file)185 row.append(llvm_pass)186 row.append(x.instr)187 row.append(x.fn_name)188 row.append(x.bb_name)189 row.append(x.action)190 if has_origin_col:191 if x.origin is not None:192 row.append(193 f"<details><summary>View Origin StackTrace</summary><pre>{x.origin}</pre></details>"194 )195 else:196 row.append("")197 row.append(" </tr>\n")198 # Dump the bugs info into the table.199 for column in row:200 # The same file-pass pair can have multiple bugs.201 if column == " <tr>\n" or column == " </tr>\n":202 table_di_loc += column203 continue204 table_di_loc += " <td>{0}</td>\n".format(column.strip())205 table_di_loc += " <tr>\n"206 207 if not at_least_one_bug_found:208 table_di_loc += """ <tr>209 <td colspan='7'> No bugs found </td>210 </tr>211 """212 table_di_loc += "</table>\n"213 214 # Create the summary table for the loc bugs.215 table_title_di_loc_sum = "Summary of Location Bugs"216 table_di_loc_sum = """<table>217 <caption><b>{}</b></caption>218 <tr>219 """.format(220 table_title_di_loc_sum221 )222 223 header_di_loc_sum = ["LLVM Pass Name", "Number of bugs"]224 225 for column in header_di_loc_sum:226 table_di_loc_sum += " <th>{0}</th>\n".format(column.strip())227 table_di_loc_sum += " </tr>\n"228 229 # Print the summary.230 row = []231 for llvm_pass, num in sorted(di_location_bugs_summary.items()):232 row.append(" <tr>\n")233 row.append(llvm_pass)234 row.append(str(num))235 row.append(" </tr>\n")236 for column in row:237 if column == " <tr>\n" or column == " </tr>\n":238 table_di_loc_sum += column239 continue240 table_di_loc_sum += " <td>{0}</td>\n".format(column.strip())241 table_di_loc_sum += " <tr>\n"242 243 if not at_least_one_bug_found:244 table_di_loc_sum += """<tr>245 <td colspan='2'> No bugs found </td>246 </tr>247 """248 table_di_loc_sum += "</table>\n"249 250 # Create the table for SP bugs.251 table_title_di_sp = "SP Bugs found by the Debugify"252 table_di_sp = """<table>253 <caption><b>{}</b></caption>254 <tr>255 """.format(256 table_title_di_sp257 )258 259 header_di_sp = ["File", "LLVM Pass Name", "Function Name", "Action"]260 261 for column in header_di_sp:262 table_di_sp += " <th>{0}</th>\n".format(column.strip())263 table_di_sp += " </tr>\n"264 265 at_least_one_bug_found = False266 267 # Handle fn bugs.268 for file, per_file_bugs in di_subprogram_bugs.items():269 for llvm_pass, per_pass_bugs in per_file_bugs.items():270 # No SP bugs for the pass.271 if len(per_pass_bugs) == 0:272 continue273 at_least_one_bug_found = True274 row = []275 table_di_sp += " </tr>\n"276 # Get the bugs info.277 for x in per_pass_bugs:278 row.append(" <tr>\n")279 row.append(file)280 row.append(llvm_pass)281 row.append(x.fn_name)282 row.append(x.action)283 row.append(" </tr>\n")284 # Dump the bugs info into the table.285 for column in row:286 # The same file-pass pair can have multiple bugs.287 if column == " <tr>\n" or column == " </tr>\n":288 table_di_sp += column289 continue290 table_di_sp += " <td>{0}</td>\n".format(column.strip())291 table_di_sp += " <tr>\n"292 293 if not at_least_one_bug_found:294 table_di_sp += """<tr>295 <td colspan='4'> No bugs found </td>296 </tr>297 """298 table_di_sp += "</table>\n"299 300 # Create the summary table for the sp bugs.301 table_title_di_sp_sum = "Summary of SP Bugs"302 table_di_sp_sum = """<table>303 <caption><b>{}</b></caption>304 <tr>305 """.format(306 table_title_di_sp_sum307 )308 309 header_di_sp_sum = ["LLVM Pass Name", "Number of bugs"]310 311 for column in header_di_sp_sum:312 table_di_sp_sum += " <th>{0}</th>\n".format(column.strip())313 table_di_sp_sum += " </tr>\n"314 315 # Print the summary.316 row = []317 for llvm_pass, num in sorted(di_sp_bugs_summary.items()):318 row.append(" <tr>\n")319 row.append(llvm_pass)320 row.append(str(num))321 row.append(" </tr>\n")322 for column in row:323 if column == " <tr>\n" or column == " </tr>\n":324 table_di_sp_sum += column325 continue326 table_di_sp_sum += " <td>{0}</td>\n".format(column.strip())327 table_di_sp_sum += " <tr>\n"328 329 if not at_least_one_bug_found:330 table_di_sp_sum += """<tr>331 <td colspan='2'> No bugs found </td>332 </tr>333 """334 table_di_sp_sum += "</table>\n"335 336 # Create the table for Variable bugs.337 table_title_di_var = "Variable Location Bugs found by the Debugify"338 table_di_var = """<table>339 <caption><b>{}</b></caption>340 <tr>341 """.format(342 table_title_di_var343 )344 345 header_di_var = ["File", "LLVM Pass Name", "Variable", "Function", "Action"]346 347 for column in header_di_var:348 table_di_var += " <th>{0}</th>\n".format(column.strip())349 table_di_var += " </tr>\n"350 351 at_least_one_bug_found = False352 353 # Handle var bugs.354 for file, per_file_bugs in di_var_bugs.items():355 for llvm_pass, per_pass_bugs in per_file_bugs.items():356 # No SP bugs for the pass.357 if len(per_pass_bugs) == 0:358 continue359 at_least_one_bug_found = True360 row = []361 table_di_var += " </tr>\n"362 # Get the bugs info.363 for x in per_pass_bugs:364 row.append(" <tr>\n")365 row.append(file)366 row.append(llvm_pass)367 row.append(x.name)368 row.append(x.fn_name)369 row.append(x.action)370 row.append(" </tr>\n")371 # Dump the bugs info into the table.372 for column in row:373 # The same file-pass pair can have multiple bugs.374 if column == " <tr>\n" or column == " </tr>\n":375 table_di_var += column376 continue377 table_di_var += " <td>{0}</td>\n".format(column.strip())378 table_di_var += " <tr>\n"379 380 if not at_least_one_bug_found:381 table_di_var += """<tr>382 <td colspan='4'> No bugs found </td>383 </tr>384 """385 table_di_var += "</table>\n"386 387 # Create the summary table for the sp bugs.388 table_title_di_var_sum = "Summary of Variable Location Bugs"389 table_di_var_sum = """<table>390 <caption><b>{}</b></caption>391 <tr>392 """.format(393 table_title_di_var_sum394 )395 396 header_di_var_sum = ["LLVM Pass Name", "Number of bugs"]397 398 for column in header_di_var_sum:399 table_di_var_sum += " <th>{0}</th>\n".format(column.strip())400 table_di_var_sum += " </tr>\n"401 402 # Print the summary.403 row = []404 for llvm_pass, num in sorted(di_var_bugs_summary.items()):405 row.append(" <tr>\n")406 row.append(llvm_pass)407 row.append(str(num))408 row.append(" </tr>\n")409 for column in row:410 if column == " <tr>\n" or column == " </tr>\n":411 table_di_var_sum += column412 continue413 table_di_var_sum += " <td>{0}</td>\n".format(column.strip())414 table_di_var_sum += " <tr>\n"415 416 if not at_least_one_bug_found:417 table_di_var_sum += """<tr>418 <td colspan='2'> No bugs found </td>419 </tr>420 """421 table_di_var_sum += "</table>\n"422 423 # Finish the html page.424 html_footer = """</body>425 </html>"""426 427 new_line = "<br>\n"428 429 fileout.writelines(html_header)430 fileout.writelines(table_di_loc)431 fileout.writelines(new_line)432 fileout.writelines(table_di_loc_sum)433 fileout.writelines(new_line)434 fileout.writelines(new_line)435 fileout.writelines(table_di_sp)436 fileout.writelines(new_line)437 fileout.writelines(table_di_sp_sum)438 fileout.writelines(new_line)439 fileout.writelines(new_line)440 fileout.writelines(table_di_var)441 fileout.writelines(new_line)442 fileout.writelines(table_di_var_sum)443 fileout.writelines(html_footer)444 fileout.close()445 446 print("The " + html_file + " generated.")447 448 449# Read the JSON file in chunks.450def get_json_chunk(file, start, size):451 json_parsed = None452 di_checker_data = []453 skipped_lines = 0454 line = 0455 456 # The file contains json object per line.457 # An example of the line (formatted json):458 # {459 # "file": "simple.c",460 # "pass": "Deduce function attributes in RPO",461 # "bugs": [462 # [463 # {464 # "action": "drop",465 # "metadata": "DISubprogram",466 # "name": "fn2"467 # },468 # {469 # "action": "drop",470 # "metadata": "DISubprogram",471 # "name": "fn1"472 # }473 # ]474 # ]475 # }476 with open(file) as json_objects_file:477 for json_object_line in json_objects_file:478 line += 1479 if line < start:480 continue481 if line >= start + size:482 break483 try:484 json_object = loads(json_object_line)485 except:486 skipped_lines += 1487 else:488 di_checker_data.append(json_object)489 490 return (di_checker_data, skipped_lines, line)491 492 493# Parse the program arguments.494def parse_program_args(parser):495 parser.add_argument("file_name", type=str, help="json file to process")496 parser.add_argument(497 "--reduce",498 action="store_true",499 help="create reduced report by deduplicating bugs within and across files",500 )501 502 report_type_group = parser.add_mutually_exclusive_group(required=True)503 report_type_group.add_argument(504 "--report-html-file", type=str, help="output HTML file for the generated report"505 )506 report_type_group.add_argument(507 "--acceptance-test",508 action="store_true",509 help="if set, produce terminal-friendly output and return 0 iff the input file is empty or does not exist",510 )511 512 return parser.parse_args()513 514 515def Main():516 parser = argparse.ArgumentParser()517 opts = parse_program_args(parser)518 519 if opts.report_html_file is not None and not opts.report_html_file.endswith(520 ".html"521 ):522 print("error: The output file must be '.html'.")523 sys.exit(1)524 525 if opts.acceptance_test:526 if os.path.isdir(opts.file_name):527 print(f"error: Directory passed as input file: '{opts.file_name}'")528 sys.exit(1)529 if not os.path.exists(opts.file_name):530 # We treat an empty input file as a success, as debugify will generate an output file iff any errors are531 # found, meaning we expect 0 errors to mean that the expected file does not exist.532 print(f"No errors detected for: {opts.file_name}")533 sys.exit(0)534 535 # Use the defaultdict in order to make multidim dicts.536 di_location_bugs = defaultdict(lambda: defaultdict(list))537 di_subprogram_bugs = defaultdict(lambda: defaultdict(list))538 di_variable_bugs = defaultdict(lambda: defaultdict(list))539 540 # Use the ordered dict to make a summary.541 di_location_bugs_summary = OrderedDict()542 di_sp_bugs_summary = OrderedDict()543 di_var_bugs_summary = OrderedDict()544 545 # If we are using --reduce, use these sets to deduplicate similar bugs within and across files.546 di_loc_reduced_set = set()547 di_sp_reduced_set = set()548 di_var_reduced_set = set()549 550 start_line = 0551 chunk_size = 1000000552 end_line = chunk_size - 1553 skipped_lines = 0554 skipped_bugs = 0555 # Process each chunk of 1 million JSON lines.556 while True:557 if start_line > end_line:558 break559 (debug_info_bugs, skipped, end_line) = get_json_chunk(560 opts.file_name, start_line, chunk_size561 )562 start_line += chunk_size563 skipped_lines += skipped564 565 # Map the bugs into the file-pass pairs.566 for bugs_per_pass in debug_info_bugs:567 try:568 bugs_file = bugs_per_pass["file"]569 bugs_pass = bugs_per_pass["pass"]570 bugs = bugs_per_pass["bugs"][0]571 except:572 skipped_lines += 1573 continue574 575 di_loc_bugs = di_location_bugs.get("bugs_file", {}).get("bugs_pass", [])576 di_sp_bugs = di_subprogram_bugs.get("bugs_file", {}).get("bugs_pass", [])577 di_var_bugs = di_variable_bugs.get("bugs_file", {}).get("bugs_pass", [])578 579 # Omit duplicated bugs.580 di_loc_set = set()581 di_sp_set = set()582 di_var_set = set()583 for bug in bugs:584 try:585 bugs_metadata = bug["metadata"]586 except:587 skipped_bugs += 1588 continue589 590 if bugs_metadata == "DILocation":591 try:592 origin = bug.get("origin")593 action = bug["action"]594 bb_name = bug["bb-name"]595 fn_name = bug["fn-name"]596 instr = bug["instr"]597 except:598 skipped_bugs += 1599 continue600 di_loc_bug = DILocBug(origin, action, bb_name, fn_name, instr)601 if not di_loc_bug.key() in di_loc_set:602 di_loc_set.add(di_loc_bug.key())603 if opts.reduce:604 reduced_key = di_loc_bug.reduced_key(bugs_pass)605 if not reduced_key in di_loc_reduced_set:606 di_loc_reduced_set.add(reduced_key)607 di_loc_bugs.append(di_loc_bug)608 else:609 di_loc_bugs.append(di_loc_bug)610 611 # Fill the summary dict.612 if bugs_pass in di_location_bugs_summary:613 di_location_bugs_summary[bugs_pass] += 1614 else:615 di_location_bugs_summary[bugs_pass] = 1616 elif bugs_metadata == "DISubprogram":617 try:618 action = bug["action"]619 name = bug["name"]620 except:621 skipped_bugs += 1622 continue623 di_sp_bug = DISPBug(action, name)624 if not di_sp_bug.key() in di_sp_set:625 di_sp_set.add(di_sp_bug.key())626 if opts.reduce:627 reduced_key = di_sp_bug.reduced_key(bugs_pass)628 if not reduced_key in di_sp_reduced_set:629 di_sp_reduced_set.add(reduced_key)630 di_sp_bugs.append(di_sp_bug)631 else:632 di_sp_bugs.append(di_sp_bug)633 634 # Fill the summary dict.635 if bugs_pass in di_sp_bugs_summary:636 di_sp_bugs_summary[bugs_pass] += 1637 else:638 di_sp_bugs_summary[bugs_pass] = 1639 elif bugs_metadata == "dbg-var-intrinsic":640 try:641 action = bug["action"]642 fn_name = bug["fn-name"]643 name = bug["name"]644 except:645 skipped_bugs += 1646 continue647 di_var_bug = DIVarBug(action, name, fn_name)648 if not di_var_bug.key() in di_var_set:649 di_var_set.add(di_var_bug.key())650 if opts.reduce:651 reduced_key = di_var_bug.reduced_key(bugs_pass)652 if not reduced_key in di_var_reduced_set:653 di_var_reduced_set.add(reduced_key)654 di_var_bugs.append(di_var_bug)655 else:656 di_var_bugs.append(di_var_bug)657 658 # Fill the summary dict.659 if bugs_pass in di_var_bugs_summary:660 di_var_bugs_summary[bugs_pass] += 1661 else:662 di_var_bugs_summary[bugs_pass] = 1663 else:664 # Unsupported metadata.665 skipped_bugs += 1666 continue667 668 if di_loc_bugs:669 di_location_bugs[bugs_file][bugs_pass] = di_loc_bugs670 if di_sp_bugs:671 di_subprogram_bugs[bugs_file][bugs_pass] = di_sp_bugs672 if di_var_bugs:673 di_variable_bugs[bugs_file][bugs_pass] = di_var_bugs674 675 if opts.report_html_file is not None:676 generate_html_report(677 di_location_bugs,678 di_subprogram_bugs,679 di_variable_bugs,680 di_location_bugs_summary,681 di_sp_bugs_summary,682 di_var_bugs_summary,683 opts.report_html_file,684 )685 else:686 # Pretty(ish) print the detected bugs, but check if any exist first so that we don't print an empty dict.687 if di_location_bugs:688 print_bugs_yaml("DILocation Bugs", di_location_bugs)689 if di_subprogram_bugs:690 print_bugs_yaml("DISubprogram Bugs", di_subprogram_bugs)691 if di_variable_bugs:692 print_bugs_yaml("DIVariable Bugs", di_variable_bugs)693 694 if opts.acceptance_test:695 if any((di_location_bugs, di_subprogram_bugs, di_variable_bugs)):696 # Add a newline gap after printing at least one error.697 print()698 print(f"Errors detected for: {opts.file_name}")699 sys.exit(1)700 else:701 print(f"No errors detected for: {opts.file_name}")702 703 if skipped_lines > 0:704 print("Skipped lines: " + str(skipped_lines))705 if skipped_bugs > 0:706 print("Skipped bugs: " + str(skipped_bugs))707 708 709if __name__ == "__main__":710 Main()711 sys.exit(0)712