Problem
scripts/coverage.sh already has a fix for one class of per-template-
instantiation noise in llvm-cov's coverage export
(scripts/aggregate_lcov_branches.py, added for branch/BRDA records —
see that script's own docstring): llvm-cov export -format=lcov emits a
separate coverage record per template instantiation of the same source
line, so a header-only function instantiated with N different type
arguments produces N copies of every line inside it. For branches, the
existing script aggregates these by source location before generating the
final .lcov, so a branch fully covered in aggregate isn't scored
"partial" just because one particular instantiation didn't take an arm.
The same duplication happens for plain line-hit counts (DA: records),
and nothing aggregates those. aggregate_lcov_branches.py's main loop
passes every DA: line straight through unchanged
(scripts/aggregate_lcov_branches.py's final else: out.append(line)
branch) — it only rewrites BRDA/BRF/BRH. If instantiation A of a
function hits line 42 and instantiation B (a different lambda/model type)
never does, llvm-cov — and therefore Codecov's codecov/patch check —
counts line 42 as uncovered, even though the underlying source line is
exercised by real test coverage.
Confirmed real impact
Investigating PR #88's stuck codecov/patch percentage (94-95% against a
97.37% target) surfaced this exact pattern three times, each independently
confirmed via CI logs proving the "uncovered" line's owning test actually
ran and passed:
examples/common/testkit/pump.hpp: pumpUntil<Pred>'s timeout branch
(lines 74-79) and awaitQt's deadline-throw (112-113) each appeared
15+ times in the coverage HTML report — once per distinct lambda-type
instantiation across the whole test suite. Both are genuinely tested
(test_pump.cpp's "pumpUntil returns false on timeout without hanging"
and "awaitQt timeout does not leave dangling references" cases); every
other call site's own instantiation just never happens to time out,
which is correct passing-test behavior, not a gap.
include/morph/core/remote.hpp:747 (acquireSharedInstance's final
"ok" reply) — every line in its enclosing locked block shows covered;
this exact line, immediately after the block, does not, despite an
existing test ("two connections sharing a key reach one instance")
demonstrably reaching it per CI logs.
examples/common/testkit/backend_rig.hpp's client<T>() bounds-check
throw (lines 297-298/301) — a test named exactly
"BackendRig::client() throws out_of_range past nClients in Socket mode"
already exists and passes per CI logs, yet the line shows red; client<T>
is instantiated with several different Model types across the test
file (RigProbeModel, RigCounterModel, RigBlobModel), each a
separate instantiation.
This inflates the real uncovered-line count in any header-only,
heavily-templated file (which describes most of morph's core: bridge.hpp,
completion.hpp, registry.hpp, forms.hpp, the ladder's own testkit
headers) well beyond what real test coverage actually leaves untested,
making codecov/patch's percentage systematically pessimistic for such
files and the target correspondingly harder to hit through test-writing
alone — no additional test can fix a report-level double-count.
Proposed fix
Extend scripts/aggregate_lcov_branches.py (or add a sibling step in
scripts/coverage.sh) to aggregate DA: line-hit records the same way it
already aggregates BRDA: branch records: for each source file, sum hit
counts across every instantiation's copy of the same line number before
emitting the final DA:<line>,<hits> record, instead of passing each
instantiation's own DA: line straight through. The coverage.json export
coverage.sh already generates (llvm-cov export -format=lcov step,
consumed for the BRDA aggregation) has the same per-instantiation
"segments"/hit-count data needed to do this for lines — no new llvm-cov
invocation should be required, only extending the existing script's
line-record handling to match its branch-record handling.
Not in scope here
This issue is about the tooling gap, not about writing more tests. Recent
work (PR #88) found and fixed several genuinely-real coverage gaps in
remote.hpp/backend_rig.hpp/presenter.hpp (deterministic preconditions
and forced concurrency races that had never been exercised) before
concluding the remainder is this reporting artifact — that work should not
be redone once this is fixed; re-running the fixed tooling against the
current test suite should simply show the true, already-adequate coverage
for the lines this issue describes.
Problem
scripts/coverage.shalready has a fix for one class of per-template-instantiation noise in
llvm-cov's coverage export(
scripts/aggregate_lcov_branches.py, added for branch/BRDArecords —see that script's own docstring):
llvm-cov export -format=lcovemits aseparate coverage record per template instantiation of the same source
line, so a header-only function instantiated with N different type
arguments produces N copies of every line inside it. For branches, the
existing script aggregates these by source location before generating the
final
.lcov, so a branch fully covered in aggregate isn't scored"partial" just because one particular instantiation didn't take an arm.
The same duplication happens for plain line-hit counts (
DA:records),and nothing aggregates those.
aggregate_lcov_branches.py's main looppasses every
DA:line straight through unchanged(
scripts/aggregate_lcov_branches.py's finalelse: out.append(line)branch) — it only rewrites
BRDA/BRF/BRH. If instantiation A of afunction hits line 42 and instantiation B (a different lambda/model type)
never does,
llvm-cov— and therefore Codecov'scodecov/patchcheck —counts line 42 as uncovered, even though the underlying source line is
exercised by real test coverage.
Confirmed real impact
Investigating PR #88's stuck
codecov/patchpercentage (94-95% against a97.37% target) surfaced this exact pattern three times, each independently
confirmed via CI logs proving the "uncovered" line's owning test actually
ran and passed:
examples/common/testkit/pump.hpp:pumpUntil<Pred>'s timeout branch(lines 74-79) and
awaitQt's deadline-throw (112-113) each appeared15+ times in the coverage HTML report — once per distinct lambda-type
instantiation across the whole test suite. Both are genuinely tested
(
test_pump.cpp's "pumpUntil returns false on timeout without hanging"and "awaitQt timeout does not leave dangling references" cases); every
other call site's own instantiation just never happens to time out,
which is correct passing-test behavior, not a gap.
include/morph/core/remote.hpp:747(acquireSharedInstance's final"ok" reply) — every line in its enclosing locked block shows covered;
this exact line, immediately after the block, does not, despite an
existing test ("two connections sharing a key reach one instance")
demonstrably reaching it per CI logs.
examples/common/testkit/backend_rig.hpp'sclient<T>()bounds-checkthrow (lines 297-298/301) — a test named exactly
"BackendRig::client() throws out_of_range past nClients in Socket mode"
already exists and passes per CI logs, yet the line shows red;
client<T>is instantiated with several different
Modeltypes across the testfile (
RigProbeModel,RigCounterModel,RigBlobModel), each aseparate instantiation.
This inflates the real uncovered-line count in any header-only,
heavily-templated file (which describes most of morph's core:
bridge.hpp,completion.hpp,registry.hpp,forms.hpp, the ladder's own testkitheaders) well beyond what real test coverage actually leaves untested,
making
codecov/patch's percentage systematically pessimistic for suchfiles and the target correspondingly harder to hit through test-writing
alone — no additional test can fix a report-level double-count.
Proposed fix
Extend
scripts/aggregate_lcov_branches.py(or add a sibling step inscripts/coverage.sh) to aggregateDA:line-hit records the same way italready aggregates
BRDA:branch records: for each source file, sum hitcounts across every instantiation's copy of the same line number before
emitting the final
DA:<line>,<hits>record, instead of passing eachinstantiation's own
DA:line straight through. Thecoverage.jsonexportcoverage.shalready generates (llvm-cov export -format=lcovstep,consumed for the
BRDAaggregation) has the same per-instantiation"segments"/hit-count data needed to do this for lines — no newllvm-covinvocation should be required, only extending the existing script's
line-record handling to match its branch-record handling.
Not in scope here
This issue is about the tooling gap, not about writing more tests. Recent
work (PR #88) found and fixed several genuinely-real coverage gaps in
remote.hpp/backend_rig.hpp/presenter.hpp(deterministic preconditionsand forced concurrency races that had never been exercised) before
concluding the remainder is this reporting artifact — that work should not
be redone once this is fixed; re-running the fixed tooling against the
current test suite should simply show the true, already-adequate coverage
for the lines this issue describes.