embedded-coverage-strategy-reference
Pure-reference catalog of code-coverage strategy for embedded C/C++: the criteria hierarchy (statement / branch / decision / condition / MC/DC), the gcov toolchain (.gcno/.gcda, --coverage), the LLVM source-based toolchain (llvm-profdata / llvm-cov), host-build vs QEMU-build instrumentation, MISRA-C:2012 and DO-178C structural-coverage expectations by safety level (DAL A maps to MC/DC), and report-format choices. Use when choosing what structural-coverage level to require and wiring gcov / llvm-cov into the build; physical .gcda retrieval from hardware is in hardware-in-loop-reference, QEMU machine flags in qemu-system-test-runner, and to author the embedded tests themselves use googletest-embedded-arm or unity-test-framework-c.
Install with skills.sh (any agent)
npx skills add testland/qa --skill embedded-coverage-strategy-referenceembedded-coverage-strategy-reference
Overview
This skill is a pure reference consumed by the per-tool skills (googletest-embedded-arm, unity-test-framework-c, ceedling-build-runner, qemu-system-test-runner) and by the HIL reference (hardware-in-loop-reference).
When to use
How to use
The full gcov-to-HTML path for a single module is in Worked example below.
Coverage criteria hierarchy
The standard hierarchy, from weakest to strongest. ISTQB glossary terms (cite by stable term ID; the glossary is a JS SPA and not WebFetchable):
| Criterion | What it requires | ISTQB term ID |
|---|---|---|
| Statement coverage | Every executable statement executed at least once | "statement coverage" |
| Branch coverage | Each branch of every decision point taken in both directions | "branch coverage" |
| Decision coverage | Each decision (e.g. if (x)) evaluated both true and false | "decision coverage" |
| Condition coverage | Each atomic boolean condition in a compound decision evaluated both true and false | "condition coverage" |
| MC/DC (Modified Condition/Decision Coverage) | Each condition independently affects the decision outcome | "modified condition decision coverage" |
| Multiple-condition coverage | All combinations of conditions in a decision exercised | "multiple condition coverage" |
The escalation matters because higher criteria find different defect classes - branch coverage finds an unreached else; MC/DC finds a short-circuit-evaluated condition whose change never alters the decision.
gcov toolchain (GCC, ARM-GCC, AVR-GCC)
Per gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html (opens in new window):
Compilation
Build the program (or test harness) with --coverage - a convenience alias that "tells the compiler to generate additional information needed by gcov (basically a flow graph of the program) and also includes additional code in the object files for generating the extra profiling information" (per the GCC gcov invocation page).
arm-none-eabi-gcc --coverage -O0 -g \
main.c parser.c tests.c -o test_binary
# Equivalent to: -fprofile-arcs -ftest-coverage -lgcovCompilation produces a .gcno file (flow-graph) per source file "at compile time". Running the binary produces a .gcda file per source file with the accumulated execution counts (both per the GCC docs).
Running gcov
./test_binary # writes parser.gcda etc.
gcov -b -c parser.c # produces parser.c.gcov text reportThe full gcov flag table and the .gcov annotation sentinels (##### for unexecuted, - for non-executable) are in references/gcov-flag-reference.md.
LCOV info format
For HTML reports + CI integration, post-process with lcov/genhtml:
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage-html/LCOV's .info is the de-facto interchange format consumed by Codecov, Coveralls, SonarQube. (LCOV is not GCC; it is a separate Linux Test Project tool that wraps gcov.)
LLVM source-based coverage (clang, arm-linux-clang)
Per clang.llvm.org/docs/SourceBasedCodeCoverage.html (opens in new window):
Compilation
clang --target=arm-none-eabi \
-fprofile-instr-generate -fcoverage-mapping \
-O0 -g main.c parser.c tests.c -o test_binaryThe two flags are independent: -fprofile-instr-generate enables "instrumentation based profiling"; -fcoverage-mapping emits the mapping that "describes the mapping between the source ranges and the profiling instrumentation counters" (per the LLVM Coverage Mapping Format docs at llvm.org/docs/CoverageMappingFormat.html (opens in new window)).
For MC/DC, add -fcoverage-mcdc per the Clang docs.
Running + profile merge
Running the binary writes default.profraw in the current directory (or to the path in LLVM_PROFILE_FILE, with pattern strings %p for PID, %h for hostname, %Nm for merge-pool - per the Clang docs).
LLVM_PROFILE_FILE="raw/%p.profraw" ./test_binary
llvm-profdata merge -sparse raw/*.profraw -o test.profdata-sparse "produces smaller indexed profiles" (per Clang docs).
Reports
llvm-cov show ./test_binary -instr-profile=test.profdata \
-format=html -output-dir=cov-html/
llvm-cov report ./test_binary -instr-profile=test.profdata
llvm-cov export ./test_binary -instr-profile=test.profdata \
-format=lcov > coverage.infoshow emits per-line annotations, report emits the file-level summary table, export -format=lcov produces a file compatible with the gcov-flavoured LCOV .info consumed by Codecov / SonarQube. (Per the same Clang Source-Based Code Coverage page.)
LLVM coverage mapping regions
The LLVM coverage-mapping format (per llvm.org/docs/CoverageMappingFormat.html (opens in new window)) distinguishes:
This region taxonomy is why llvm-cov can show separate counts inside macro expansions - gcov cannot.
On-target vs host vs QEMU instrumentation
A practical trade-off for embedded teams:
| Approach | Coverage accuracy | Cost | Notes |
|---|---|---|---|
| Host build (same source, x86_64 toolchain, no MCU) | Misses MCU-specific paths | Lowest | Default for Ceedling per throwtheswitch.org/ceedling (opens in new window); use when business logic dominates |
| QEMU system emulation | Catches arch-specific paths (endianness, alignment) | Medium | See qemu-system-test-runner; reports written to host filesystem via virtio / semihosting |
| On-target with semihosting | Highest fidelity | Highest (flash space, RAM for counters) | .gcda files written back via semihosting; needs ARM --specs=rdimon.specs (librdimon is the gcc-arm-none-eabi semihosting library - see developer.arm.com toolchain docs (opens in new window)) |
| On-target with file-system shim | High | High | Counters streamed over UART / SWO; host re-assembles .gcda |
For most safety-critical projects, the standard recipe is host build for fast loop, QEMU build for arch sanity, on-target build for the certification artefact.
Worked example
gcov branch coverage on one module, host build (the fast-loop default from the instrumentation table).
# 1. instrument + build on the host at -O0 (writes parser.gcno at compile time)
gcc --coverage -O0 -g parser.c parser_test.c -o parser_test
# 2. run - writes parser.gcda next to parser.gcno
./parser_test
# 3. branch counts as a text report
gcov -b -c parser.c # -> parser.c.gcov (scan for ##### lines)
# 4. interchange artefact + HTML for the team
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage-html/
# 5. gate the PR on coverage.info branch totalsThe host loop is fastest, but it misses MCU-specific paths - pair it with at least one QEMU or on-target run before claiming the number for the MCU (see the instrumentation table and the "coverage measured on host then claimed for the MCU" anti-pattern below).
Safety-standard coverage expectations
Coverage targets are set by the project's safety standard, not by taste. DAL A / ASIL D demand MC/DC; DAL B demands decision coverage; DAL C / ASIL A demand statement coverage; MISRA-C:2012 and IEC 62304 Class C prescribe no numeric target but expect a structural-coverage justification. The full per-standard table (DO-178C DAL A-D, ISO 26262 ASIL A-D, MISRA-C:2012 §8, IEC 62304) is in references/safety-standards-and-formats.md. Treat every listed level as the floor, not a turnkey recipe.
Coverage report formats
Produce lcov.info as the durable artefact, render HTML for the developer, and gate on the .info totals. gcov also emits text .gcov and .gcov.json.gz; llvm-cov emits .profdata, HTML, and lcov / JSON exports; gcovr --xml produces Cobertura for Jenkins. The full producer / consumer matrix is in references/safety-standards-and-formats.md.
Coverage gates that work
| Gate | Why |
|---|---|
Per-file branch coverage minimum (e.g. 80% per file, 90% per function with __attribute__((critical))) | Catches unreviewed new code without holding back legacy files |
| No regressions on changed lines | PR-scoped; lets the absolute number drift down only for code not touched |
MC/DC on annotated decisions (clang -fcoverage-mcdc + a _MCDC decorator) | Targets the cost where the standard demands it |
A flat "global ≥85% branch" gate is the failure mode - it penalises the team for unreviewed legacy code and rewards removing tests for hard-to-cover error paths.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Optimising the test build with -O2 | gcov / llvm-cov measure post-optimisation flow; branches collapse | Use -O0 for the coverage build per GCC gcov docs guidance |
| Mixing gcov and llvm-cov artefacts | .gcno and .profraw come from different toolchains; tools can't merge them | Pick one toolchain per build; document why |
| Coverage of test code counted as product coverage | Inflates numbers | Exclude tests/, mocks/, framework/ directories in lcov / llvm-cov filter |
| MC/DC reported from gcov | gcov doesn't measure MC/DC | Use clang -fcoverage-mcdc per the Clang Source-Based Coverage page; gcov gives at best condition coverage via -b |
| Coverage measured on host then claimed for the MCU | Endianness / alignment / weak-symbol paths uncovered | Pair host coverage with at least one QEMU or on-target run |
| Counters compiled but never written back from MCU | .gcda missing; gcov sees only the .gcno flow-graph and reports 0% | Implement _write / _exit semihosting hook; ARM --specs=rdimon.specs per developer.arm.com GNU toolchain (opens in new window) |
Limitations
References
Cited inline above. Foundational documents:
gcov flag reference for embedded coverage
View source (opens in new window)gcov flag reference for embedded coverage
Deep reference for the embedded-coverage-strategy-reference SKILL.md. Consult when reading a .gcov report or reaching past the --coverage / gcov -b -c basics shown in the SKILL.
gcov command-line flags
Key flags from the GCC gcov invocation page (gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html (opens in new window)):
| Flag | Long form | Effect |
|---|---|---|
-a | --all-blocks | Write per-basic-block counts |
-b | --branch-probabilities | Write branch frequencies + summary to stdout |
-c | --branch-counts | Branch frequencies as counts not percentages |
-f | --function-summaries | Per-function coverage on top of file-level |
-n | --no-output | Suppress the .gcov file |
-p | --preserve-paths | Preserve full path in generated filenames |
-u | --unconditional-branches | Include unconditional branches in -b output |
--json-format | - | Emit .gcov.json.gz (gzip-compressed JSON, "does not require source code for generation") |
Reading the .gcov text report
The text .gcov file annotates each source line with an execution count. Two sentinels matter:
-b -c together give branch coverage as raw counts, which is what an embedded branch-coverage gate reads. For per-operand visibility on short-circuit && / ||, gcov is not enough - use clang -fcoverage-mcdc (see the SKILL's LLVM section).
Safety-standard coverage expectations and report formats
View source (opens in new window)Safety-standard coverage expectations and report formats
Deep reference for the embedded-coverage-strategy-reference SKILL.md. Consult when negotiating a coverage target against a safety standard, or when choosing which report format to produce and gate on.
Safety-standard coverage expectations
These are cited by stable ID - the standards themselves are gated and not WebFetchable.
| Standard / level | Minimum structural coverage |
|---|---|
| MISRA-C:2012 Coverage Guidance | No prescribed numeric target; the rule set requires defined control flow and explicit default: in switch, which makes branch coverage achievable. See "MISRA-C:2012 §8 Coverage" |
| DO-178C / DAL A (catastrophic failure) | MC/DC required for every condition (see "DO-178C §6.4.4 Structural Coverage") |
| DO-178C / DAL B | Decision coverage |
| DO-178C / DAL C | Statement coverage |
| DO-178C / DAL D | None mandated |
| ISO 26262 ASIL D | MC/DC strongly recommended for unit verification (see "ISO 26262-6:2018 Table 12") |
| ISO 26262 ASIL A / B / C | Branch (B/C) or statement (A) coverage |
| IEC 62304 Class C (medical, life-supporting) | No numeric target, but bidirectional traceability + structural coverage justification expected |
The number "100% MC/DC" in aviation is famously expensive; the standard accepts "MC/DC of the integrated executable object code" which is interpreted differently by certifiers. Treat these as the floor, not a turnkey recipe.
Coverage report formats
| Format | Producer | Consumer |
|---|---|---|
.gcov (text) | gcov | Human reading; line-level annotation |
.gcov.json.gz | gcov --json-format | CI parser; no source-code dependency per GCC docs |
.info (LCOV) | lcov --capture or llvm-cov export -format=lcov | Codecov / Coveralls / SonarQube |
.profdata | llvm-profdata merge | Input only to llvm-cov |
| HTML | genhtml (LCOV) or llvm-cov show -format=html | Humans; not for diff'ing |
| JSON | llvm-cov export -format=text | Custom CI dashboards |
| Cobertura XML | gcovr --xml (gcovr is a third-party gcov wrapper) | Jenkins coverage plugin |
For embedded CI, the rule of thumb is: produce lcov.info as the durable artefact; render HTML for the developer; gate the PR on the .info totals.
Related skills
ceedling-build-runner
Author and run the Ceedling build system for C unit testing - the canonical build orchestration on top of Unity (assertions) + CMock (mocks) + CException (exceptions). Covers ceedling new project scaffolding, the project.yml schema (:project / :paths / :files / :defines / :flags / :tools / :test_runner / :cmock / :unity / :cexception / :gcov / :plugins), the task surface (ceedling test:all, ceedling test:{name}, ceedling test:pattern, ceedling test:path, ceedling release, ceedling clean / clobber, ceedling gcov:all, ceedling module:create, ceedling environment, ceedling dumpconfig), JUnit XML output via the report_tests_pretty_stdout / report_tests_junit_xml plugins, gcov plugin integration, host vs cross-build flow, and CI wiring. Use when a C project wants the standard ThrowTheSwitch trio bundled by one build command. For the Unity assertion API see unity-test-framework-c; for CMock semantics see cmock-reference.
cmock-reference
Pure-reference catalog of CMock and Ceedling mocking semantics for C. Defines what CMock generates from a C header (the full Expect / ExpectAndReturn / ExpectAnyArgs / ExpectWithArray / Ignore / IgnoreAndReturn / IgnoreArg_{param} / ReturnThruPtr_{param} / AddCallback / Stub / ExpectAndThrow naming family), the cmock.yml :plugins list (ignore, ignore_stateless, ignore_arg, expect_any_args, array, callback, cexception, return_thru_ptr) and what each enables, mock-suffix and mock-prefix conventions, how Unity teardown validates expectations, the resetTest mid-test verification, strict vs ignore argument-matching modes, and the trade-offs between mock / stub / spy / fake. Use as the CMock semantics reference when authoring Ceedling tests with mocks or when reading an unfamiliar mock-driven test suite.
googletest-embedded-arm
Author and run GoogleTest 1.17+ for embedded C++ on ARM targets - TEST() / TEST_F() / TEST_P() / TYPED_TEST(), EXPECT_* vs ASSERT_* assertions, fixtures with SetUp() / TearDown(), value-parameterised tests, GoogleMock when paired, cross-compile with arm-none-eabi-g++, run on host or under QEMU via the qemu-system-test-runner skill, --gtest_filter / --gtest_output=xml:results.xml / --gtest_shuffle / --gtest_repeat command-line flags, and XML / JSON output parsing for CI. Use when the unit-under-test is C++ (modern C++17+) and the team wants the de-facto C++ test framework instead of the C-only Unity. For C use unity-test-framework-c; for pure mocks use cmock-reference.
hardware-in-loop-reference
Pure-reference catalog of hardware-in-the-loop (HIL) testing for embedded systems. Defines the HIL pattern (ECU-under-test + real-time plant simulator + I/O cards emulating sensors / actuators / buses), the V-cycle progression (MIL → SIL → PIL → HIL), the canonical vendor stack (NI VeriStand + PXI / CompactRIO, dSPACE SCALEXIO / MicroAutoBox, Vector CANoe + VT System, Speedgoat real-time targets), bus emulation per protocol (CAN / CAN FD / LIN / FlexRay / Automotive Ethernet / SOME-IP), fault-injection patterns (short-to-ground, open-circuit, signal corruption), DO-178C / ISO 26262 / IEC 61508 alignment, and the test-evidence chain HIL produces. Use as the HIL terminology + vendor + standard reference when scoping an embedded test rig or interpreting an automotive / aerospace / industrial HIL test report.
qemu-system-test-runner
Author and run QEMU system emulation as an embedded-test target - qemu-system-arm / qemu-system-aarch64 / qemu-system-riscv32 launching cross-compiled ELF binaries on virtual MCUs and SoCs. Covers machine selection (-M virt / mps2-an385 / mps2-an386 / mps2-an500 / mps2-an511 / mps3-an524 / lm3s6965evb / raspi3b / xilinx-zynq-a9), CPU selection (-cpu cortex-m0 / cortex-m3 / cortex-m4 / cortex-m33 / cortex-a15 / cortex-a57 / max), -kernel ELF load, -nographic + -serial stdio, ARM semihosting via -semihosting-config enable=on,target=native (so cross-compiled GoogleTest / Unity binaries print to host stdio and exit with the test return code), GDB stub via -S -gdb tcp::1234, QMP monitor via -qmp tcp:host:port for automated test orchestration, and CI wiring. Use when host-only test runs are insufficient and the team wants arch-correct (endianness / alignment / interrupt-vector) behaviour on a virtual MCU without committing to physical hardware-in-loop.
unity-test-framework-c
Author and run ThrowTheSwitch Unity (the C unit-testing library) for bare-metal and RTOS C code. Distinct from the Unity game-engine Test Framework at docs.unity3d.com: this is the ThrowTheSwitch C testing library at throwtheswitch.org/unity, a single C file plus headers that runs on 8-bit MCUs through 64-bit hosts. Anchored on the Unity assertion API and configuration macros regardless of execution environment: the TEST_ASSERT_EQUAL_* / _FLOAT / _DOUBLE / _STRING / _MEMORY / _BITS assertion families, setUp/tearDown/RUN_TEST/UNITY_BEGIN/UNITY_END semantics and the exit-code contract, the generate_test_runner.rb generator, build-time config defines (UNITY_INCLUDE_DOUBLE, UNITY_OUTPUT_CHAR, UNITY_EXCLUDE_SETJMP), and CI integration via Ceedling JUnit XML; applies to host builds, cross-builds, and QEMU-run targets alike. For QEMU machine flags, semihosting, and exit-code capture, see qemu-system-test-runner. Use when the unit-under-test is pure C and the target ranges from 8-bit AVR to Cortex-M0 to Linux ARM.