The formats
Detection is content-based
Filenames lie: Pester writes JaCoCo XML intocoverage.xml, coverage.py writes Cobertura XML into
the same name, and .info files are occasionally GNU documentation. mehen therefore sniffs the
first 4 KiB of every candidate and requires format-specific content markers before believing a
file, in a fixed priority order chosen so no format can false-positive on another’s output:
- LCOV —
SF:plusDA:/FN:records. - Go coverprofile — a
mode: set|count|atomicheader orfile.go:N.N,N.N …block lines. - Istanbul — a JSON object containing
"statementMap"and"fnMap". - JaCoCo —
<reportplus a JaCoCo DTD reference or<package. - Clover —
<coveragecarrying aclover="…"version attribute. - Cobertura —
<coveragewithout the Clover marker.
--coverage=<path> argument goes through the same detection; a file no parser
recognizes is a hard error, while an auto-discovered candidate that fails sniffing is silently
recorded as rejected (that is business as usual for e.g. coverage.txt text summaries).
Normalization and merge semantics
Formats disagree about granularity, so parsed records are normalized into one model — per-file line hits, branch arms, and function records:- Statements → lines. Istanbul statements and Go blocks map onto lines; when several statements share a line, the maximum hit count wins.
- Branch encodings → arms. LCOV
BRDArecords, Coberturacondition-coverage="50% (1/2)"fractions, JaCoComb/cbcounters, Clovertruecount/falsecountpairs, and Istanbulbarrays all become flat branch arms (see branch coverage for what that folding means). - Duplicate records collapse.
lcov -a-merged tracefiles repeatDAlines; Cobertura emits the same line under both<method>and<class>— duplicates keep the maximum. - Cross-report merge. All ingested reports fold into one dataset: union of files, saturating-max hits for records shared between reports (“covered anywhere ⇒ covered”). Max is commutative and associative, so the merged result is independent of discovery order — determinism is a hard requirement for reproducible CI gates. Hit-count summing was rejected because re-running the same suite twice would double every count; newest-file-wins was rejected because git checkouts do not preserve mtimes.
Hardening
Coverage artifacts are ingested from build directories that other tools write into, so the parsers are defensive by construction: branch expansion is capped at 1,024 arms per line, Go block spans at 100,000 lines, report files at 256 MiB; XML parsing never resolves DTDs or external entities (billion-laughs and XXE are structurally inert, and a regression test pins that); and a malformed report is a per-file diagnostic, never a crash.What about raw instrumentation output?
.profraw/.gcda (LLVM/GCC counters), SimpleCov’s .resultset.json, c8’s raw V8 dumps in
coverage/tmp/, and coverlet’s proprietary coverage.json are not report formats — decoding
them requires the compiled binaries or the producing tool’s internals. Export a report instead
(cargo llvm-cov --lcov, coverage xml, --coverageReporters=lcov, …); grcov takes the same
report-level stance for Firefox-scale ingestion.
References
- geninfo(1) — the LCOV tracefile format. Linux Test Project.
- The cover story — Go’s coverage design and the coverprofile format. The Go Blog.
- Istanbul.js and the istanbuljs coverage object.
- JaCoCo XML report documentation.
- OpenClover documentation — the Clover XML schema.
- Cobertura — the original tool whose XML schema became the de-facto interchange format.
- mozilla/grcov — prior art for multi-format coverage aggregation at scale.