Code coverage¶
doppler measures coverage with clang source-based coverage
(-fprofile-instr-generate -fcoverage-mapping). The design leverages doppler's
defining property — every algorithm is in C exactly once — so the same
native/src/<obj>/<obj>_core.c is exercised by three harnesses:
| Harness | reaches _core.c via |
|---|---|
C tests (native/tests/test_*_core.c) |
link the <obj>_core OBJECT lib |
Python tests (src/doppler/**/tests/) |
load the .so (ext binding → core) |
Rust tests (ffi/rust/) |
link libdoppler.a |
Because one DOPPLER_COVERAGE build instruments those OBJECT files once,
every consumer carries instrumentation; each harness emits .profraw,
llvm-profdata merge unifies them, and llvm-cov attributes the merged result
back to _core.c — a single report showing what C ∪ Python ∪ Rust covers.
Status: All four phases are wired — the merged C ∪ Python ∪ Rust report is produced on every PR and gates it via a
diff-coverpatch-coverage check.
Run it locally¶
make coverage # instrumented build → C tests + pytest → merged report
# open the HTML:
xdg-open build-cov/html/index.html # or: python -m http.server -d build-cov/html
Requirements: clang + matching llvm-profdata / llvm-cov (same
LLVM version). Override version-suffixed tools if needed:
The coverage target configures a dedicated build-cov/ tree
(-DDOPPLER_COVERAGE=ON -DCMAKE_C_COMPILER=clang -DBUILD_PYTHON=ON,
Debug/-O0 for accurate line mapping), then:
- runs the CTest suite (
LLVM_PROFILE_FILE→build-cov/prof/c-*.profraw); - stages the instrumented Python extension into a throwaway package
(
build-cov/pkg/) —PYTHON_PACKAGE_DIRis redirected there so the build never clobbers the dev's workingsrc/doppler/.so; the Python source is layered over the staged.sowithtar; - runs pytest against that staged package (
LLVM_PROFILE_FILE→py-*.profraw), so the same cores get coverage from what Python exercises; - runs the Rust FFI tests against the instrumented
libdoppler.so(DOPPLER_BUILD_DIRpointsbuild.rsatbuild-cov); the.soself-writes its C.profrawvia its own profile runtime — no Rust instrumentation needed, and rustc's LLVM matches clang's so the.profrawmerges; llvm-profdata merges C ∪ Python ∪ Rust and emits the HTML +lcov, reported againstlibdoppler.soand every module.so(a line only one harness reaches still attributes correctly).
A live NATS broker on 127.0.0.1:4222 lets the nats:// stream path count too
(the CI job starts one); without it stream_nats.c self-skips and shows 0%.
What's measured¶
- Included: first-party
native/src/**/*_core.c(and the wfm/timing/measure cores) — the hand-written algorithms. - Excluded (
COV_IGNOREin theMakefile):vendor/**, the jm-generated*_ext.cbinding aggregators, and thetests//benchmarks/harnesses.
The report is generated against build-cov/libdoppler.so plus every staged
module .so. Hand-owned binding fragments (<mod>_ext_<obj>.c) count;
jm-generated <mod>_ext.c aggregators are excluded.
Phase 1 (C tests) covered ~72% of the cores it reached; phase 2 adds the Python
harness, which lifts the cores the C tests can't reach (magnitude_db_cf32.c
0%→100%, wfm_synth_bridge.c 0%→96%) while widening the denominator with the
binding fragments and the stream layer. Remaining 0% spots are genuine gaps
worth a test — e.g. obw_from_power.c (no caller in any suite) — or are
environment-gated (stream_nats.c needs the broker).
CI¶
The coverage (C + Python + Rust) job (.github/workflows/ci.yml) runs
make coverage on every PR (with a NATS broker so the nats:// path counts),
uploads the HTML + lcov as the coverage-report artifact (+ a TOTAL line in
the job summary), and then gates the PR:
coverage is in the ci-passed required set, so a PR whose added/changed C
lines fall below COV_PATCH_MIN (90%) is blocked. It's a patch gate, not a
global threshold — the existing tree is never retroactively gated, and pure
.py/.rs diffs (no C coverage data) aren't counted. diff-cover is fetched
on demand via uvx (self-contained — no external service). The lcov's SF:
paths are relativized so they match the git diff.
Notes / caveats¶
- clang only. The gcc OS matrix is untouched; coverage is a separate
clang+Linux build. (
make coveragesetsclang/clang++— the latter for the vendored C++ in the optional stream component.) - x86-64-v2 baseline, not
-march=native. Native would enable FMA, which breakswfm_synth's bit-exactstep()==steps()parity test (FMA contraction). The coverage build stays at the shipped portable baseline.awgn_core.c's weak libmvec_ZGVdN8v_logfdeclaration carries__attribute__((target("avx2")))so its AVX dispatcher still compiles under clang at that baseline (ABI-only; gcc codegen unchanged). - "N functions have mismatched data" from
llvm-covis benign: a few functions' mappings differ between the.soand the C-test exes (inlining); those drop from the report. The Python phase reads the same.soit loaded, so its attribution is exact.
Roadmap¶
- C tests — done.
- + Python — done: instrumented
.sostaged intobuild-cov/pkg/, pytest run against it, its.profrawmerged in. - + Rust — done: the Rust FFI tests link the instrumented
libdoppler.so(viaDOPPLER_BUILD_DIR), which self-writes C.profraw; merged in. - Patch gate — done:
make coverage-gaterunsdiff-coverover the merged report at--fail-under=90vsorigin/main;coverageis inci-passed. A patch gate, never a retroactive global threshold.