/ maint / postprocess_coverage_html
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())