Background
The heavy CI gate runs cargo llvm-cov nextest --fail-under-lines 100 --fail-under-functions 100. When it fails, the existing Show missing coverage on gate failure step emits:
cargo llvm-cov report --show-missing-lines (text)
cargo llvm-cov report --json | jq (per-file summary of files below 100%)
That output tells you which file dropped below 100% — but not which symbol or which line.
Problem (concrete example from #161)
The gate failed on feat/jobs-api-core with the per-file summary:
node/src/router.rs 1232 regions 7 missed 99.43%
84 functions 1 missed 98.81%
1105 lines 1 missed 99.91%
The --show-missing-lines block listed only program-plonky2/... files — router.rs was absent from the per-file uncovered listing despite being the file that failed the gate. This is a quirk of --show-missing-lines interacting with --ignore-filename-regex: the listing surface and the gate surface use different filter semantics, and partial-file failures get elided.
To find the actual gap, the only path forward was:
- Either re-run
cargo llvm-cov locally (≈50 min on an M3 Ultra; ≈hours on developer hardware)
- Or guess based on the diff and add speculative tests, push, wait another 50 min, repeat
Both burn shared self-hosted runner time and operator attention; the second one is unreliable (we tried it: three of the four guesses were wrong).
What this issue tracks
The fix landed alongside #161 (commit 45836ac). Filing as a separate issue so the change is documented as a stand-alone diagnostic improvement — useful in every future coverage failure, not just the one that motivated it.
The failure-only step now also emits:
1. Per-symbol uncovered function list
cargo llvm-cov report --release --json --ignore-filename-regex "$IGNORE" \
| jq -r '.data[0].functions[]
| select(.count == 0)
| "\(.filenames[0]):\(.regions[0][0])\t\(.name)"' \
| sort -u
Emits one line per uncovered function as file:line\tmangled_symbol_name. The mangled symbol is recoverable via rustfilt (or by reading the source at the line). For the #161 failure the relevant line was unambiguous:
/Users/dfxai/.../node/src/router.rs:1465 <closure>
2. Full HTML coverage report uploaded as a CI artifact
cargo llvm-cov report --release --html \
--output-dir target/llvm-cov-html \
--ignore-filename-regex "$IGNORE"
Plus a follow-up step:
- name: Upload coverage HTML report on gate failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: llvm-cov-html-${{ github.run_id }}-${{ github.run_attempt }}
path: target/llvm-cov-html
retention-days: 14
The operator downloads the artifact, opens index.html, clicks through to the failing file, and sees red regions / red lines / red functions at a glance. Same UX as a local cargo llvm-cov --open without paying the reproduction cost.
Result
|
Before |
After |
| Time to locate a missed line |
50 min (local reproduce) or several push-iterations |
30 seconds (download artifact, scroll to red) |
| Operator info on failure |
Per-file percent |
Per-file percent + per-symbol list + clickable HTML |
| Shared runner load on failure-iteration |
1 heavy gate per guess |
1 heavy gate total |
How to use (for the next coverage failure)
- Open the failing
Tests + Coverage Gate job
- Scroll to
--- llvm-cov report: uncovered functions (per-symbol) --- for the symbol list, OR
- Open the run summary page → Artifacts →
llvm-cov-html-{run_id}-{attempt} → unzip → html/coverage/.../<file>.rs.html
- The red regions/lines/functions are the missing coverage
Cost
The two added steps run only on if: failure() — green CI pays nothing. HTML generation + artifact upload add ≈10–30 s to the post-failure path, dwarfed by the gate's existing 44–50 min runtime.
Why this took until now
The 100% line + function coverage gate has been in place for months; the failure diagnostics were originally good enough because gate failures were rare (full file uncovered → obvious). Once we started hitting partial-file failures (closures, single uncovered branches), --show-missing-lines proved insufficient and the absence of an HTML artifact forced expensive local re-runs. The new diagnostics close that gap permanently.
Background
The heavy CI gate runs
cargo llvm-cov nextest --fail-under-lines 100 --fail-under-functions 100. When it fails, the existingShow missing coverage on gate failurestep emits:cargo llvm-cov report --show-missing-lines(text)cargo llvm-cov report --json | jq(per-file summary of files below 100%)That output tells you which file dropped below 100% — but not which symbol or which line.
Problem (concrete example from #161)
The gate failed on
feat/jobs-api-corewith the per-file summary:The
--show-missing-linesblock listed onlyprogram-plonky2/...files —router.rswas absent from the per-file uncovered listing despite being the file that failed the gate. This is a quirk of--show-missing-linesinteracting with--ignore-filename-regex: the listing surface and the gate surface use different filter semantics, and partial-file failures get elided.To find the actual gap, the only path forward was:
cargo llvm-covlocally (≈50 min on an M3 Ultra; ≈hours on developer hardware)Both burn shared self-hosted runner time and operator attention; the second one is unreliable (we tried it: three of the four guesses were wrong).
What this issue tracks
The fix landed alongside #161 (commit
45836ac). Filing as a separate issue so the change is documented as a stand-alone diagnostic improvement — useful in every future coverage failure, not just the one that motivated it.The failure-only step now also emits:
1. Per-symbol uncovered function list
Emits one line per uncovered function as
file:line\tmangled_symbol_name. The mangled symbol is recoverable viarustfilt(or by reading the source at the line). For the #161 failure the relevant line was unambiguous:2. Full HTML coverage report uploaded as a CI artifact
cargo llvm-cov report --release --html \ --output-dir target/llvm-cov-html \ --ignore-filename-regex "$IGNORE"Plus a follow-up step:
The operator downloads the artifact, opens
index.html, clicks through to the failing file, and sees red regions / red lines / red functions at a glance. Same UX as a localcargo llvm-cov --openwithout paying the reproduction cost.Result
How to use (for the next coverage failure)
Tests + Coverage Gatejob--- llvm-cov report: uncovered functions (per-symbol) ---for the symbol list, ORllvm-cov-html-{run_id}-{attempt}→ unzip →html/coverage/.../<file>.rs.htmlCost
The two added steps run only on
if: failure()— green CI pays nothing. HTML generation + artifact upload add ≈10–30 s to the post-failure path, dwarfed by the gate's existing 44–50 min runtime.Why this took until now
The 100% line + function coverage gate has been in place for months; the failure diagnostics were originally good enough because gate failures were rare (full file uncovered → obvious). Once we started hitting partial-file failures (closures, single uncovered branches),
--show-missing-linesproved insufficient and the absence of an HTML artifact forced expensive local re-runs. The new diagnostics close that gap permanently.