awk Part III · Module 11 · Synthesis and Coverage Report Analysis
Line Coverage
Area is a measurement. A coverage percentage is a ratio, and that changes what you can do with it: two ratios cannot be averaged, a ratio with a zero denominator is not zero, and a ratio printed with a percent sign is not a number as far as awk is concerned. This chapter extracts line coverage per design unit and then works through all three, ending on the comparison that matters most in practice — a unit at 100.00% that fails a 90% coverage gate, and the one character that causes it.
Intermediate17 min readawkPart IIICoveragePercentagesComparison
Part III · Module 11 · Chapter 11.4 · Line Coverage
1. Why This Matters
Coverage closure is a gate. Somewhere in every verification flow there is a script deciding whether a number clears a threshold, and the number comes out of a report like the one below.
The three chapters before this one parsed synthesis reports, where every value was a measured quantity on its own line. Coverage values are ratios, and three properties of ratios break the habits that worked on areas: they cannot be meaningfully averaged, a zero denominator does not mean zero, and the convention of printing them with a % makes them something awk will not compare as numbers.
2. The Report
========================================
Coverage Report : alu_core
Tests merged : 148
========================================
Design Unit: alu_core
Line Coverage 96.55% (28 of 29 lines)
Toggle Coverage 74.19% (46 of 62 bits)
Branch Coverage 91.67% (11 of 12 branches)
Design Unit: u_regfile
Line Coverage 100.00% (41 of 41 lines)
Toggle Coverage 98.44% (126 of 128 bits)
Branch Coverage 100.00% (6 of 6 branches)
Design Unit: u_alu
Line Coverage 88.24% (75 of 85 lines)
Toggle Coverage 61.90% (78 of 126 bits)
Branch Coverage 76.47% (13 of 17 branches)
Design Unit: u_ctrl
Line Coverage 72.00% (36 of 50 lines)
Toggle Coverage 45.31% (29 of 64 bits)
Branch Coverage 64.29% (9 of 14 branches)
Design Unit: u_io
Line Coverage 100.00% (12 of 12 lines)
Toggle Coverage 100.00% (16 of 16 bits)
Branch Coverage n/a (0 of 0 branches)
Design Unit: u_debug
Line Coverage 0.00% (0 of 18 lines)
Toggle Coverage 0.00% (0 of 24 bits)
Branch Coverage 0.00% (0 of 4 branches)
========================================
TOTAL Line Coverage 81.70% (192 of 235)
TOTAL Toggle Coverage 70.24% (295 of 420)
TOTAL Branch Coverage 73.58% (39 of 53)
========================================Three things to notice before parsing anything: the unit name is on a separate line from its metrics, every percentage is followed by the counts that produced it, and u_io has n/a for branch coverage because it has (0 of 0 branches).
3. Carrying the Unit Name
The metric lines do not say which unit they belong to, so this is Chapter 10.2's pattern again:
awk '/^Design Unit:/ { u = $3 } /^ Line Coverage/ { printf " %-10s %8s %s of %s\n", u, $3, substr($4,2), $6; u = "" }' cov.rpt alu_core 96.55% 28 of 29
u_regfile 100.00% 41 of 41
u_alu 88.24% 75 of 85
u_ctrl 72.00% 36 of 50
u_io 100.00% 12 of 12
u_debug 0.00% 0 of 18substr($4, 2) drops the opening parenthesis from (28. The u = "" after consuming is the clear-on-use rule from Chapter 10.2 section 7 — a metric line with no preceding Design Unit: would otherwise inherit the previous unit's name.
Patterns need anchoring here as much as in Chapter 11.1:
for pat in '/Line Coverage/' '/^ Line Coverage/' '/Coverage/'; do
printf " %-22s " "$pat"; awk "$pat { n++ } END { print n+0 \" line(s)\" }" cov.rpt
done /Line Coverage/ 6 line(s)
/^ Line Coverage/ 6 line(s)
/Coverage/ 22 line(s)/Coverage/ sweeps up all eighteen metric lines, the three totals and the report title. The two Line Coverage forms agree at six here — the TOTAL rows read TOTAL Line Coverage with the words separated, so they do not match. That is a property of this report's column alignment and not something to rely on; anchor anyway.
4. A Unit at 100% Fails a 90% Gate
This is the chapter's central result. A small file makes it unambiguous:
printf 'u_alu 100.00%%\nu_ctrl 87.50%%\nu_io 9.00%%\nu_dma 100.00\n' > pf.txt
awk '{ printf " %-8s field [%-8s] $2 >= 90 -> %-5s $2 + 0 >= 90 -> %-5s $2+0 = %s\n", $1, $2, ($2 >= 90 ? "TRUE" : "false"), ($2 + 0 >= 90 ? "TRUE" : "false"), $2 + 0 }' pf.txt u_alu field [100.00% ] $2 >= 90 -> false $2 + 0 >= 90 -> TRUE $2+0 = 100
u_ctrl field [87.50% ] $2 >= 90 -> false $2 + 0 >= 90 -> false $2+0 = 87.5
u_io field [9.00% ] $2 >= 90 -> false $2 + 0 >= 90 -> false $2+0 = 9
u_dma field [100.00 ] $2 >= 90 -> TRUE $2 + 0 >= 90 -> TRUE $2+0 = 100u_alu is at 100.00% and $2 >= 90 is false. u_dma is also at 100 and passes. The only difference between the two rows is that one value carries a percent sign.
As gate lists:
printf " passes 90%% without +0: "; awk '$2 >= 90 { printf "[%s]", $1 } END { print "" }' pf.txt
printf " passes 90%% with +0: "; awk '$2 + 0 >= 90 { printf "[%s]", $1 } END { print "" }' pf.txt
printf " FAILS 90%% without +0: "; awk '$2 < 90 { printf "[%s]", $1 } END { print "" }' pf.txt
printf " FAILS 90%% with +0: "; awk '$2 + 0 < 90 { printf "[%s]", $1 } END { print "" }' pf.txt passes 90% without +0: [u_dma]
passes 90% with +0: [u_alu][u_dma]
FAILS 90% without +0: [u_alu][u_ctrl][u_io]
FAILS 90% with +0: [u_ctrl][u_io]u_alu appears on the failure list at 100% coverage.
+ 0 handles the whole range of forms a report might print:
awk 'BEGIN { split("87.50% 100.00% 0.00% n/a -- 100% 7.5%", v, " ")
for (i = 1; i <= 7; i++) printf " [%-8s] +0 -> %-8s >=90 -> %s\n", v[i], v[i]+0, (v[i]+0>=90?"yes":"no") }' [87.50% ] +0 -> 87.5 >=90 -> no
[100.00% ] +0 -> 100 >=90 -> yes
[0.00% ] +0 -> 0 >=90 -> no
[n/a ] +0 -> 0 >=90 -> no
[-- ] +0 -> 0 >=90 -> no
[100% ] +0 -> 100 >=90 -> yes
[7.5% ] +0 -> 7.5 >=90 -> noBoth 100% forms now agree, and 7.5% is correctly below the gate rather than lexically above it. The conversion has to appear in the comparison itself — + 0 applied only to a printed column leaves the test comparing the raw string, which is the defect section 4 opened with.
The three rows that convert to 0 are the subject of the next section, because only one of them means zero coverage.
5. n/a Is Not Zero
Rows three, four and five of that table all converted to 0, and only one of them means zero coverage. u_io in the report has Branch Coverage n/a (0 of 0 branches) — it contains no branches, so there is nothing to cover.
awk '/^ Branch Coverage/ { p = $3; printf " [%-9s] +0 = [%s]\n", p, p + 0 }' cov.rpt [91.67% ] +0 = [91.67]
[100.00% ] +0 = [100]
[76.47% ] +0 = [76.47]
[64.29% ] +0 = [64.29]
[n/a ] +0 = [0]
[0.00% ] +0 = [0]The fifth and sixth rows are indistinguishable after conversion, and they are different facts: u_io has no branches, u_debug has four and covered none.
Which gives branch coverage three different answers:
awk '/^ Branch Coverage/ { p = $3; s += p + 0; n++ } END { printf " n/a as 0%%: mean of %d units = %.2f%%\n", n, s/n }' cov.rpt
awk '/^ Branch Coverage/ { if ($3 == "n/a") { skipped++; next } p = $3; s += p + 0; n++ } END { printf " n/a excluded: mean of %d units = %.2f%% (%d skipped)\n", n, s/n, skipped }' cov.rpt
awk '/^TOTAL Branch/ { printf " report TOTAL: %s\n", $4 }' cov.rpt n/a as 0%: mean of 6 units = 55.41%
n/a excluded: mean of 5 units = 66.49% (1 skipped)
report TOTAL: 73.58%55.41%, 66.49% and 73.58% from one report, and the first two are both wrong.
6. Mean of Percentages Is Not the Coverage
The third answer is right, and the report proves it:
awk '/^ Branch Coverage/ { c += substr($4,2) + 0; t += $6 + 0 } END { printf " recomputed from counts: %d of %d = %.2f%%\n", c, t, 100*c/t }' cov.rpt recomputed from counts: 39 of 53 = 73.58%Aggregating the counts reproduces the report's own total exactly. Averaging the percentages does not, and the gap is not small:
awk '/^ Line Coverage/ { p = $3; sub(/%$/,"",p); s += p+0; n++; c += substr($4,2)+0; t += $6+0 }
END { printf " mean of %d per-unit percentages : %.2f%%\n", n, s/n; printf " aggregate %d of %d lines : %.2f%%\n", c, t, 100*c/t }' cov.rpt
awk '/^TOTAL Line/ { printf " report TOTAL : %s\n", $4 }' cov.rpt mean of 6 per-unit percentages : 76.13%
aggregate 192 of 235 lines : 81.70%
report TOTAL : 81.70%76.13% against 81.70%, with the report agreeing on the latter. The mean treats u_io's twelve lines as worth the same as u_alu's eighty-five; the aggregate does not.
7. Verifying the Report Against Itself
Every percentage against its own counts:
awk '/^ (Line|Toggle|Branch) Coverage/ { p = $3
if (p == "n/a") { printf " %-8s n/a (%s of %s) -> no coverable items\n", $1, substr($4,2), $6; next }
sub(/%$/, "", p); n = substr($4, 2) + 0; m = $6 + 0
if (m == 0) { printf " %-8s %7s%% with a zero denominator -- cannot check\n", $1, p; next }
r = 100 * n / m
printf " %-8s %7s%% %4d of %4d -> recomputed %6.2f%% %s\n", $1, p, n, m, r, (p+0 - r < 0.005 && r - (p+0) < 0.005 ? "ok" : "MISMATCH") }' cov.rpt | head -7 Line 96.55% 28 of 29 -> recomputed 96.55% ok
Toggle 74.19% 46 of 62 -> recomputed 74.19% ok
Branch 91.67% 11 of 12 -> recomputed 91.67% ok
Line 100.00% 41 of 41 -> recomputed 100.00% ok
Toggle 98.44% 126 of 128 -> recomputed 98.44% ok
Branch 100.00% 6 of 6 -> recomputed 100.00% ok
Line 88.24% 75 of 85 -> recomputed 88.24% okThe m == 0 guard comes before the division, not after — Chapter 11.1 section 8's division by zero is fatal, so a guard that runs afterwards never runs.
And the three totals:
awk '/^ Line Coverage/ { lc += substr($4,2)+0; lt += $6+0 } /^ Toggle Coverage/ { tc += substr($4,2)+0; tt += $6+0 } /^ Branch Coverage/ { bc += substr($4,2)+0; bt += $6+0 }
END { printf " Line %d of %d = %.2f%%\n Toggle %d of %d = %.2f%%\n Branch %d of %d = %.2f%%\n", lc, lt, 100*lc/lt, tc, tt, 100*tc/tt, bc, bt, 100*bc/bt }' cov.rpt
awk '/^TOTAL/ { printf " report: %-6s %s %s %s %s\n", $2, $4, substr($5,2), $6, substr($7,1,length($7)-1) }' cov.rpt Line 192 of 235 = 81.70%
Toggle 295 of 420 = 70.24%
Branch 39 of 53 = 73.58%
report: Line 81.70% 192 of 235
report: Toggle 70.24% 295 of 420
report: Branch 73.58% 39 of 53Every figure agrees. That is the check worth running on any real coverage report before using a single number from it.
8. Common Mistakes
Comparing a percentage field against a threshold without + 0. The % makes it a string, so 100.00% >= 90 is false — section 4.
Applying + 0 to the printed column but not to the test. Convert into a variable and compare the variable.
Treating n/a as 0%. It means no coverable items — section 5.
Averaging per-unit percentages. 76.13% against a true 81.70% — section 6.
Keeping the percentage and discarding the counts. The weights are then unrecoverable.
/Coverage/ as the metric pattern. It matches 22 lines including the report title — section 3.
Not carrying the unit name, or not clearing it. The name is on a separate line from the metrics.
Dividing before guarding a zero denominator. The division is fatal, so the guard must come first.
9. Exercises
Exercise 1 — Line coverage per unit
awk '/^Design Unit:/ { u = $3 } /^ Line Coverage/ { printf "%-10s %7.2f%% %s of %s\n", u, $3 + 0, substr($4,2), $6; u = "" }' cov.rptalu_core 96.55% 28 of 29
u_regfile 100.00% 41 of 41
u_alu 88.24% 75 of 85
u_ctrl 72.00% 36 of 50
u_io 100.00% 12 of 12
u_debug 0.00% 0 of 18Exercise 2 — Units below a threshold
awk -v gate=90 '/^Design Unit:/ { u = $3 } /^ Line Coverage/ { p = $3 + 0; if (p < gate) printf "%-10s %7.2f%% below %d%%\n", u, p, gate; u = "" }' cov.rptu_alu 88.24% below 90%
u_ctrl 72.00% below 90%
u_debug 0.00% below 90%Three units, and neither of the two at 100.00% is among them.
Exercise 3 — Debug a gate that fails a fully covered unit
A coverage gate reports u_alu failing at 100% line coverage.
printf 'u_alu 100.00%%\nu_dma 100.00\n' > two.txt
awk '{ printf "%-8s [%s] >= 90 without +0 -> %-5s with +0 -> %s\n", $1, $2, ($2 >= 90 ? "TRUE" : "false"), ($2 + 0 >= 90 ? "TRUE" : "false") }' two.txtu_alu [100.00%] >= 90 without +0 -> false with +0 -> TRUE
u_dma [100.00] >= 90 without +0 -> TRUE with +0 -> TRUEBoth are at 100. The one with the percent sign is not a number, so its comparison is lexical and 1 sorts below 9.
Exercise 4 — Debug a coverage figure that is too low
A dashboard reports branch coverage at 55.41% where the report says 73.58%.
awk '/^ Branch Coverage/ { printf "[%s]", $3 } END { print "" }' cov.rpt
awk '/^ Branch Coverage/ { s += $3 + 0; n++ } END { printf " mean of %d = %.2f%%\n", n, s/n }' cov.rpt
awk '/^ Branch Coverage/ { c += substr($4,2)+0; t += $6+0 } END { printf " aggregate %d of %d = %.2f%%\n", c, t, 100*c/t }' cov.rpt[91.67%][100.00%][76.47%][64.29%][n/a][0.00%]
mean of 6 = 55.41%
aggregate 39 of 53 = 73.58%The dashboard counted the n/a unit as 0% and averaged the six percentages. Aggregating the counts instead gives 39 of 53, which is the report's own figure. And excluding the n/a row changes nothing, because (0 of 0) contributes zero to both the numerator and the denominator — the aggregate handles the undefined case for free, which averaging cannot.
Exercise 5 — Engineering application
The coverage gate a regression actually runs: counts aggregated, n/a distinguished, every percentage checked against its own counts, and a non-zero status when a unit is below the threshold.
# covgate.awk — per-unit coverage gate with aggregate totals.
#
# Usage: awk -f covgate.awk cov.rpt
# awk -v gate=90 -v metric=Line -f covgate.awk cov.rpt
# Exit: 0 all units at or above the gate
# 1 at least one unit below the gate
# 2 a percentage disagrees with its own counts
BEGIN { if (gate == "") gate = 90; if (metric == "") metric = "Line"; TOL = 0.005 }
/^Design Unit:/ { u = $3 }
$1 == metric && $2 == "Coverage" && /^ / {
pct = $3
n = substr($4, 2) + 0
m = $6 + 0
nu++
if (pct == "n/a" || m == 0) {
printf "%-10s %8s %4d of %4d no coverable items\n", u, "n/a", n, m
skipped++
u = ""
next
}
p = pct + 0
r = 100 * n / m
if (!(p - r < TOL && r - p < TOL)) { printf "MISMATCH: %s reports %s but %d of %d is %.2f%%\n", u, pct, n, m, r > "/dev/stderr"; bad++ }
cov += n; tot += m
below = (p < gate)
if (below) nbelow++
printf "%-10s %8.2f%% %4d of %4d %s\n", u, p, n, m, (below ? "BELOW " gate "%" : "ok")
u = ""
}
END {
if (bad) exit 2
if (tot == 0) { print "ERROR: no coverable items at all" > "/dev/stderr"; exit 2 }
printf "#\n# %s coverage, aggregated: %d of %d = %.2f%%\n", metric, cov, tot, 100 * cov / tot
printf "# %d unit(s), %d below %d%%, %d with nothing to cover\n", nu, nbelow + 0, gate, skipped + 0
if (nbelow) exit 1
}awk -f covgate.awk cov.rpt
echo "status: $?"alu_core 96.55% 28 of 29 ok
u_regfile 100.00% 41 of 41 ok
u_alu 88.24% 75 of 85 BELOW 90%
u_ctrl 72.00% 36 of 50 BELOW 90%
u_io 100.00% 12 of 12 ok
u_debug 0.00% 0 of 18 BELOW 90%
#
# Line coverage, aggregated: 192 of 235 = 81.70%
# 6 unit(s), 3 below 90%, 0 with nothing to cover
status: 1The aggregate 81.70% matches the report's own TOTAL, which is the check that the parse was right rather than merely plausible. Status 1 means at least one unit is below the gate — a result for a human to act on, not an error, and the same polarity as every gate script in Module 10.
On branch coverage, where the n/a unit appears:
awk -v metric=Branch -v gate=75 -f covgate.awk cov.rpt
echo "status: $?"alu_core 91.67% 11 of 12 ok
u_regfile 100.00% 6 of 6 ok
u_alu 76.47% 13 of 17 ok
u_ctrl 64.29% 9 of 14 BELOW 75%
u_io n/a 0 of 0 no coverable items
u_debug 0.00% 0 of 4 BELOW 75%
#
# Branch coverage, aggregated: 39 of 53 = 73.58%
# 6 unit(s), 2 below 75%, 1 with nothing to cover
status: 1u_io is reported as having nothing to cover rather than as a 0% failure, and it contributes 0 of 0 to the aggregate, which changes neither the numerator nor the denominator. The aggregate 73.58% again matches the report.
Exercise 6 — Challenge: show that the gate's own total is the only available check
covgate.awk compares its aggregate against nothing — it just prints it. Show that a plausible parsing error produces a plausible aggregate, and that only the report's TOTAL row distinguishes them.
awk '/^ Line Coverage/ { c += substr($4,2)+0; t += $6+0 } END { printf "correct: %d of %d = %.2f%%\n", c, t, 100*c/t }' cov.rpt
awk '/^ Line Coverage/ { c += $4+0; t += $6+0 } END { printf "forgot substr: %d of %d = %.2f%%\n", c, t, 100*c/t }' cov.rptcorrect: 192 of 235 = 81.70%
forgot substr: 0 of 235 = 0.00%Forgetting substr gives 0.00%, because (28 converts to 0 — a visibly broken answer. Now a subtler slip, reading the denominator one field over:
awk '/^ Line Coverage/ { c += substr($4,2)+0; t += $5+0 } END { printf "wrong field: %d of %d = %s\n", c, t, (t ? sprintf("%.2f%%", 100*c/t) : "undefined") }' cov.rptwrong field: 192 of 0 = undefinedAlso visibly broken — $5 is the word of. The dangerous slip is one that still produces numbers:
awk '/^ (Line|Toggle) Coverage/ { c += substr($4,2)+0; t += $6+0 } END { printf "line+toggle: %d of %d = %.2f%%\n", c, t, 100*c/t }' cov.rptline+toggle: 487 of 655 = 74.35%74.35% is a completely plausible coverage figure and it is the line and toggle metrics added together — two different kinds of item in one ratio. Nothing about the number suggests it, and it sits between the true line figure of 81.70% and the true toggle figure of 70.24%, which is exactly where a reviewer's intuition would put a believable answer.
The report's own total is what refuses it:
awk '/^TOTAL Line/ { d = $7; sub(/\)$/, "", d); printf " report TOTAL Line: %s (%s of %s)\n", $4, substr($5,2), d }' cov.rpt report TOTAL Line: 81.70% (192 of 235)487 of 655 is not 192 of 235, and no amount of inspecting 74.35% would have told you. A coverage parser should always print its aggregate counts and compare them against the report's, because the percentage alone cannot be checked — the fourth time in two modules that the conclusion has been to validate against the input's own arithmetic rather than the output's plausibility.
10. Summary
- A coverage value is a ratio, so it cannot be averaged, and a zero denominator is not zero.
- The
%sign makes the field non-numeric, so a comparison against a threshold is lexical even with no quoted constant — and100.00% >= 90is false. - This is a genuine exception to Chapter 10.5's rule, which held for fields that look numeric.
- Convert into a variable, then compare the variable.
+ 0in the printed column does not protect the test. n/aconverts to 0 and means no coverable items.(0 of 0)and(0 of 4)are different facts.- Branch coverage has three answers — 55.41% with
n/aas zero, 66.49% excluding it, 73.58% aggregating the counts. Only the last matches the report. - Averaging per-unit line percentages gives 76.13% against a true 81.70%.
- Extract the counts, not the percentage. The percentage has already discarded the weights, and the counts handle
(0 of 0)for free. - Guard a zero denominator before dividing, because the division is fatal.
- Print your aggregate counts and compare them against the report's
TOTAL. Mixing line and toggle items gave a thoroughly plausible 74.35%.
Chapter 11.5 — Toggle Coverage stays on the same report and takes the metric whose meaning is most often misread: an untoggled bit and an unexecuted line are different failures, and a bus reported at 61.90% may have every bit of its data path stuck.
Where this fits
Part of the awk curriculum.
