#!/usr/bin/env python3 # # Use BeautifulSoup to mangle a grov HTML output file and insert other # things we care about. import sys import os.path try: from bs4 import BeautifulSoup except ImportError: print("Sorry, BeautifulSoup 4 is not installed.", file=sys.stderr) sys.exit(1) if len(sys.argv) != 4: print(f"Usage: {sys.argv[0]} ") print(" Post-process a grcov index.html file") sys.exit(1) # Read the commands file: with open(sys.argv[1]) as f: commands = f.read() # Parse the coverage file with open(sys.argv[2]) as f: document = BeautifulSoup(f, "html.parser") # Print summary of overall line, function, and branch coverage for level in document.find_all("div", class_="level-item"): heading = level.p.text percentage = level.abbr.text print(f"{heading}: {percentage}") def parse_frac(s): "Parse a 'num / den' fraction into a 2-tuple." elts = s.split() if len(elts) != 3 or elts[1] != "/": return (0, 0) return (int(elts[0]), int(elts[2])) def find_crate(s): "Extract a crate name from a path string." while "/" in s: s, rest = os.path.split(s) if s == "crates": return rest return s # Generate a summary of per-crate coverage. crate_lines: dict[str, list[int]] = dict() def get_or_fail(obj, field): """ Like obj.field, but raise a KeyError if obj.field is None. Insisting on an exception in this case helps mypy typecheck this code. """ val = getattr(obj, field) if val is None: raise KeyError(field) return val for row in get_or_fail(document, "table").find_all("tr"): path = row.th.text.strip() cells = row.find_all("td") if not cells or len(cells) < 3: continue pct = cells[1].text.strip() numerator, denominator = parse_frac(cells[2].text) if pct.endswith("%"): pct = pct[:-1] pct = float(pct) if abs(pct - (100 * numerator / denominator)) > 0.01: print(f"Whoops, mismatched percentage for {path}. Am I parsing right?") crate = find_crate(path) entry = crate_lines.setdefault(crate, [0, 0]) entry[0] += numerator entry[1] += denominator # Insert a command summary before the main table. commands_tag = document.new_tag("pre") commands_tag.string = commands get_or_fail(document, "nav").insert_after(commands_tag) # Construct a crate-coverage table to go after the main table. # # We build this as a string and parse it because it's simpler that way. table_text = [""" """] danger_threshold = 0.7 warning_threshold = 0.9 # Add a row for each crate... for crate, (numerator, denominator) in crate_lines.items(): if denominator == 0: frac = 0.0 pct = "n/a" else: frac = numerator / denominator pct = "%.02f%%" % (100 * numerator / denominator) # Choose what class to put the text in if frac < danger_threshold: bg = "danger" elif frac < warning_threshold: bg = "warning" else: bg = "success" tclass = f"has-text-centered has-background-{bg} p-2" table_text.append(f""" """) table_text.append("
Crate name Line coverage
{crate} {pct} {numerator} / {denominator}
") newtable = BeautifulSoup("\n".join(table_text), "html.parser") # Insert the table! get_or_fail(document, "table").insert_after(newtable) with open(sys.argv[3], "w") as out: out.write(document.prettify())