1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
#!/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]} <commands_file> <index_file> <output_file>")
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 = [
"""<table class="table is-fullwidth">
<thead><tr>
<th>Crate name</th>
<th class="has-text-centered" colspan="2">Line coverage</th>
</tr></thead>
<tbody>
"""
]
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"""
<tr>
<th>{crate}</th>
<td class="{tclass}">{pct}</td>
<td class="{tclass}">{numerator} / {denominator}</td>
</tr>"""
)
table_text.append("</tbody></table>")
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())
|