Test coverage
Measured 2026-09-02 on e0a5427, with make coverage (see the end for how to
reproduce). Both suites green: 627/627 on mongoc, 577/577 on corDB.
The figure covers the broker, and the broker is four repositories
corRest, corNgsild and corJsonld are static archives whole-archived into the
binary — corNgsild alone is 12k lines, most of the NGSI-LD rulebook, three quarters
the size of coraine/src. Until 2026-09-01 make coverage linked them exactly as
make libs had left them: ordinary flags, no gcov counters. They were therefore not
reported as uncovered, they were absent — in neither numerator nor denominator —
and --root coraine/src kept the omission out of sight. Roughly half the broker's C
sat outside a number presented as the broker's.
| Run | Tests | Lines | Functions | Branches |
|---|---|---|---|---|
make coverage DB=mongoc |
627 / 627 pass | 82.9% (26180/31573) | 95.7% (1312/1371) | 64.6% (18132/28078) |
make coverage (corDB) |
577 / 577 pass | 79.2% (23496/29677) | 91.5% (1217/1330) | 61.8% (16717/27030) |
...and per repository, in the mongoc run, which is where it gets interesting:
| Repository | Lines | Functions | Branches |
|---|---|---|---|
corNgsild |
84.3% (10240/12153) | 94.4% (589/624) | 67.4% (8705/12915) |
coraine/src |
83.0% (13376/16115) | 97.6% (564/578) | 61.9% (7784/12567) |
corJsonld |
80.0% (825/1031) | 96.5% (55/57) | 67.8% (629/928) |
corRest |
76.5% (1739/2274) | 92.9% (104/112) | 60.8% (1014/1668) |
The same four in the corDB run:
| Repository | Lines | Functions | Branches |
|---|---|---|---|
corNgsild |
82.3% (10000/12153) | 92.5% (577/624) | 66.3% (8558/12915) |
corJsonld |
79.5% (820/1031) | 96.5% (55/57) | 67.3% (625/928) |
coraine/src |
76.9% (10939/14219) | 89.6% (481/537) | 56.7% (6526/11519) |
corRest |
76.4% (1737/2274) | 92.9% (104/112) | 60.4% (1008/1668) |
corNgsild — the library nobody was measuring until 2026-09-01 — is still the
best-covered of the four, because the NGSI-LD rules the functests hammer hardest
live there.
The two runs disagree almost entirely in one place. corRest and corJsonld barely
move between them (76.5% → 76.4%, 80.0% → 79.5%) because nothing in them knows which
database is underneath; the gap is coraine/src (83.0% → 76.9%) and, through the 56
mongoc-only tests against corDB's 6, corNgsild (84.3% → 82.3%).
What moved since 2026-09-01, and why it was not new tests
Four tests were added (623 → 627 on mongoc) and every headline number rose: 82.7% → 82.9% lines, 95.1% → 95.7% functions, 64.3% → 64.6% branches.
Almost none of that is the new tests. It is corRest, which went
73.6% → 76.5% on lines and 85.6% → 92.9% on functions without gaining a
single test of its own — 253 lines were deleted from it: the convenience client
layer no caller could use, and a CorRestMetrics block that duplicated what the
broker already measures from corRestPostResponseHook. Nearly all of it was
uncovered, so removing it moved the fraction from the denominator side.
⭐ A rise can be dead code removed rather than behaviour newly tested, and the two are indistinguishable in the headline figure. The whole-broker denominator only fell 31613 → 31573 across the same week, because the metrics and query-encoding work put most of those lines back.
⚠️ The figure depends on the environment, and the HA paths are the proof
The HA cache sync (--high-availability mongo) rides on a mongo change stream,
and a change stream reads the oplog — which a standalone mongod does not have.
corTestParams.sh probes isMaster.setName and decides: on a replica set
ha_cache_sync.test is in the run set, on a standalone it is not. It is not reported
as skipped. Nothing in the output says the HA paths went unexercised.
The measurements above were taken against a single-node replica set, so they
include the HA code — 108/149 lines, 10/10 functions, 72/114 branches across
haInit.c, haEventApply.c and mongocHaWatch.c.
CI runs a standalone mongo:8.0, so the nightly's published figure does not:
those 108 lines and 10 functions read as entirely unexecuted there, worth
−0.34 pp on lines, −0.73 pp on functions and −0.26 pp on branches against the numbers
in this file. The two are not comparable until CI grows an oplog.
⚠️ Every earlier revision of this file called those lines untested code needing an environment the harness does not stand up. That sentence was describing our own CI, not the broker, and it is the reason the distinction is spelled out here.
The corDB run does not enter them either, and correctly so — ha_cache_sync.test
declares REQUIRE_DB: mongoc, because the sync it tests is a mongo mechanism.
Why two runs, and why the totals differ
They are separate measurements, not two views of one. A test that pins a
backend-specific answer — mongo's earth model in the fourth digit of a distance, or
the tenant-wide 2dsphere index that makes a GeoProperty and a Property refuse to
share an Attribute name — declares REQUIRE_DB and belongs to one run only. Hence
627 tests against mongoc and 577 against corDB.
Each report also excludes the DB plugin that is not under test: a corDB run cannot
execute a line of mongoc.so, and counting it would measure the choice of backend
rather than the state of the suite.
Branches is the number that matters
A line is covered if it ran once. if (a && b) that only ever runs with both true is
a covered line and two uncovered branches — and the second case is where untested
behaviour hides. That is why branch coverage sits close to twenty points below line
coverage here, and why it is the figure to move.
"Anything less than 100% is laziness"
It is worth being precise about what the missing 17.1% actually is, because the reflex answer — it's all unreachable error handling — is not what the data says.
Of the 5393 uncovered lines in the mongoc run — 2739 in coraine/src, 1913 in
corNgsild, 535 in corRest, 206 in corJsonld:
| Share | Lines | What it is |
|---|---|---|
| 60.2% | 3249 | ordinary code with no failure guard at all |
| 25.5% | 1374 | inside a NULL / invalid-argument guard |
| 9.8% | 527 | inside 59 functions the suite never enters at all |
| 2.4% | 132 | guarded by a DB / driver failure — bson_error_t, a cursor that fails, != DB_OK |
| 1.2% | 64 | the NULL-driver-method → 501/422 convention |
| 0.5% | 25 | defensive paths — KT_X, default: on an exhaustive switch, "cannot happen" |
| 0.2% | 12 | pthread_create failing, a short fread, an allocator returning NULL |
| 0.2% | 10 | network / socket failure |
The part that genuinely needs fault injection — a database that fails on demand,
a socket that dies mid-write, an allocator that returns NULL — is 154 lines, under
3% of what is uncovered. The largest group by far is ordinary behaviour nobody has
written a test for. Counted in this run: the success/errors assembly for a batch
operation that half-works (64 lines), idPattern handling across subscription
validation, CSR notification and DistOp matching (23), the LRU eviction that runs
when the @context cache is full and the expiry sweep for volatile contexts
(corLdCache.c, 14 and 10), $minDistance on a geo query (4) and expiresAt on a
Context-Source subscription (4).
The 59 functions that are never entered
This is the one bucket that needs no heuristic — gcov reports an execution count per
function — and the first time it has been measured across all four repositories
rather than coraine/src alone.
| Lines | Functions | What they are |
|---|---|---|
| 246 | 25 | genuinely untested behaviour |
| 136 | 19 | shutdown and cleanup — troeStop, timescaleClose, corRestStop, the three cache …Release functions, ldMqttCleanup, onCrash. They run when the process is going away and assert nothing a test can read |
| 94 | 10 | parked, or with no caller at all — see below |
| 42 | 1 | httpEndpointDetect, startup auto-detection of the broker's own externally-reachable endpoint |
| 9 | 4 | null-object defaults — hookNoop, preServiceHookNoop and two setters nothing calls |
The largest single entries in the untested-behaviour group are geoEntityValidate
(23), attrToNormalized (19), stripInfoAttrsFromEntity and
stripInfoAttrsFromTemporal (17 each), vocabCompactInPlace (17),
temporalLatestInstance (13) and isNumberString (13).
The parked group is two families, and they are not the same thing:
ringSelfIntersectsand its four helpers, 51 lines. Deliberately parked, with the reason written at the call site:(void) ringSelfIntersects;— real fixtures have near-coincident vertices that produce mathematically-valid self-intersections, and the geo backend resolves interior by the right-hand rule anyway. Kept for a strict-validation mode.ldDatasetIdDedupand its four helpers, 43 lines. Declared in corNgsild's public header, named in its README as part of the entity API — and called from nowhere in the four repositories. That is the same shape as the corRest client layer deleted above: advertised library surface with no consumer. ▶ Worth a decision: wire it up or delete it.
The corDB run has 113 never-entered functions and 1541 lines rather than 59 and 527. The difference is the 56 mongoc-only tests plus the HA test, and it is a property of the run, not of the code.
On the method: the never-entered bucket comes straight from the gcov JSON. The
other buckets are machine-classified — each uncovered line is attributed to the
nearest enclosing guard by indentation, and the guard's text decides the bucket. Two
limits worth knowing before quoting them. The NULL / invalid-argument bucket is
coarse: it matches == NULL, != 0 and < 0 alike, so it mixes real input
validation with ordinary logic, which makes it an upper bound on "defensive" and the
"ordinary code" figure a lower bound. And line numbers move the moment a file is
edited, so the classification must be recomputed from a coverage run of the same
source rather than re-scaled.
⚠️ The percentages here are not comparable with the hand-sampled ones this file
carried before 2026-09-02. Those covered coraine/src only, against a denominator
that no longer exists, and were bucketed by a different reading of the same idea.
Reproducing it
make coverage # corDB → coverage-corDB/index.html
make coverage DB=mongoc # mongoc → coverage-mongoc/index.html
make coverage-etsi # ETSI TP suite, instrumenting the libs too
Six details, each of which produced a wrong number before it was handled:
- The coverage tree is rebuilt from scratch.
.gcnofiles of a renamed or deleted source are never cleaned up, and gcovr reports those vanished files as entirely unexecuted — aftercorRamDBbecamecorDB, a stale tree added ~700 phantom uncovered lines and moved the figure from 80.7% to 77.5%. SEAMWARE_PLUGIN_DIRis exported alongsideCOR_PLUGIN_DIR. The first is how the harness spells a plugin path; the second is how the broker resolves a short name a test passes itself (coraineStart --apiPlugins admin). Without it the run loads the installed, uninstrumentedadmin.soand the whole admin plugin reads 0% in a run whose tests exercise it.- gcovr is told not to abort on GCC bug 68080. gcov occasionally reports a
negative hit count in a threaded binary; without
--gcov-ignore-parse-errors=negative_hits.warn_once_per_filea twenty-minute run ends with no report at all. The flag needs gcovr >= 8.3, pinned incorLibs/docker/Dockerfile.ci-nightly: on 8.2 it was read only by the text.gcovparser, and on >= 8.3 without it gcovr does not fail — it silently drops the whole affected file from the report and exits 0. - The libs are instrumented and the root moves up to the sibling directory.
Otherwise
corRest,corNgsildandcorJsonldlink in with no counters and vanish from both sides of the fraction, as they did until 2026-09-01. - The flags go in
EXTRA_CFLAGS, neverDFLAGS.DFLAGSis a plain variable in each lib's makefile, so setting it on the command line replaces the lib's own defaults — and aDFLAGS +=inside the makefile is then ignored too, because+=never appends to a command-line variable.corNgsildlost-DANSIand-DCOR_WITH_ICUthat way and compiled its non-ICU collation fallback while the broker went on linking libicu.EXTRA_CFLAGSis appended last, so-O0beats-O2and-Wno-errorbeats-Werrorwithout displacing anything. - The mongod must be a replica set for the run to include the HA paths, and a
single-node set is enough —
mongod --replSet rs0, thenrs.initiate()once. On a standalone the numbers are quietly lower and nothing says why; see the section above.
Afterwards the libs are left instrumented, and getting out of that takes
make libs-rebuild, not make libs: libs is each lib's own incremental build, and
a change of compiler flags is invisible to it — the objects are newer than their
sources, so it rebuilds nothing and corNgsild stays instrumented inside a build
that calls itself ordinary.
The per-spec-statement view — which statements of TS 104-175 have a test asserting
them — is a different question, tracked in
spec-coverage-gaps.md.