postprocess_coverage_html
1 #!/usr/bin/env python3 2 # 3 # Use BeautifulSoup to mangle a grov HTML output file and insert other 4 # things we care about. 5 6 import sys 7 import os.path 8 9 try: 10 from bs4 import BeautifulSoup 11 except ImportError: 12 print("Sorry, BeautifulSoup 4 is not installed.", file=sys.stderr) 13 sys.exit(1) 14 15 if len(sys.argv) != 4: 16 print(f"Usage: {sys.argv[0]} <commands_file> <index_file> <output_file>") 17 print(" Post-process a grcov index.html file") 18 sys.exit(1) 19 20 # Read the commands file: 21 with open(sys.argv[1]) as f: 22 commands = f.read() 23 24 # Parse the coverage file 25 with open(sys.argv[2]) as f: 26 document = BeautifulSoup(f, "html.parser") 27 28 # Print summary of overall line, function, and branch coverage 29 for level in document.find_all("div", class_="level-item"): 30 heading = level.p.text 31 percentage = level.abbr.text 32 print(f"{heading}: {percentage}") 33 34 35 def parse_frac(s): 36 "Parse a 'num / den' fraction into a 2-tuple." 37 elts = s.split() 38 if len(elts) != 3 or elts[1] != "/": 39 return (0, 0) 40 return (int(elts[0]), int(elts[2])) 41 42 43 def find_crate(s): 44 "Extract a crate name from a path string." 45 while "/" in s: 46 s, rest = os.path.split(s) 47 if s == "crates": 48 return rest 49 return s 50 51 52 # Generate a summary of per-crate coverage. 53 crate_lines: dict[str, list[int]] = dict() 54 55 56 def get_or_fail(obj, field): 57 """ 58 Like obj.field, but raise a KeyError if obj.field is None. 59 60 Insisting on an exception in this case helps mypy typecheck this code. 61 """ 62 val = getattr(obj, field) 63 if val is None: 64 raise KeyError(field) 65 return val 66 67 68 for row in get_or_fail(document, "table").find_all("tr"): 69 path = row.th.text.strip() 70 cells = row.find_all("td") 71 if not cells or len(cells) < 3: 72 continue 73 pct = cells[1].text.strip() 74 numerator, denominator = parse_frac(cells[2].text) 75 if pct.endswith("%"): 76 pct = pct[:-1] 77 pct = float(pct) 78 if abs(pct - (100 * numerator / denominator)) > 0.01: 79 print(f"Whoops, mismatched percentage for {path}. Am I parsing right?") 80 81 crate = find_crate(path) 82 entry = crate_lines.setdefault(crate, [0, 0]) 83 entry[0] += numerator 84 entry[1] += denominator 85 86 87 # Insert a command summary before the main table. 88 commands_tag = document.new_tag("pre") 89 commands_tag.string = commands 90 get_or_fail(document, "nav").insert_after(commands_tag) 91 92 # Construct a crate-coverage table to go after the main table. 93 # 94 # We build this as a string and parse it because it's simpler that way. 95 table_text = [ 96 """<table class="table is-fullwidth"> 97 <thead><tr> 98 <th>Crate name</th> 99 <th class="has-text-centered" colspan="2">Line coverage</th> 100 </tr></thead> 101 <tbody> 102 """ 103 ] 104 danger_threshold = 0.7 105 warning_threshold = 0.9 106 # Add a row for each crate... 107 for crate, (numerator, denominator) in crate_lines.items(): 108 if denominator == 0: 109 frac = 0 110 pct = "n/a" 111 else: 112 frac = numerator / denominator 113 pct = "%.02f%%" % (100 * numerator / denominator) 114 # Choose what class to put the text in 115 if frac < danger_threshold: 116 bg = "danger" 117 elif frac < warning_threshold: 118 bg = "warning" 119 else: 120 bg = "success" 121 tclass = f"has-text-centered has-background-{bg} p-2" 122 table_text.append( 123 f""" 124 <tr> 125 <th>{crate}</th> 126 <td class="{tclass}">{pct}</td> 127 <td class="{tclass}">{numerator} / {denominator}</td> 128 </tr>""" 129 ) 130 table_text.append("</tbody></table>") 131 newtable = BeautifulSoup("\n".join(table_text), "html.parser") 132 # Insert the table! 133 get_or_fail(document, "table").insert_after(newtable) 134 135 with open(sys.argv[3], "w") as out: 136 out.write(document.prettify())