Testland
Browse all skills & agents

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-reference
View source

embedded-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

  • Choosing a coverage criterion for a new embedded test suite - what does "enough coverage" mean for this project?
  • Wiring gcov or llvm-cov into the cross-compile build.
  • Reading a coverage report and translating between gcov, LCOV, and llvm-cov formats.
  • Negotiating coverage requirements against a safety standard (MISRA-C, DO-178C, ISO 26262, IEC 62304).
  • Deciding whether to instrument the host build, the QEMU build, or the on-target build.

How to use

  1. Pick the coverage criterion the project's safety level demands (DAL A / ASIL D map to MC/DC; DAL B to decision; DAL C / ASIL A to statement) from the criteria hierarchy - full standards mapping in references/safety-standards-and-formats.md.
  2. Pick the toolchain: gcov for GCC / ARM-GCC / AVR-GCC; LLVM source-based coverage for clang (MC/DC needs clang -fcoverage-mcdc).
  3. Instrument the build at -O0 (--coverage for gcc; -fprofile-instr-generate -fcoverage-mapping for clang) - never optimise a coverage build.
  4. Pick where to run (host / QEMU / on-target) from the instrumentation trade-off table.
  5. Produce lcov.info as the durable artefact, render HTML for the developer, and gate the PR on changed-line coverage - see Coverage gates that work.

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):

CriterionWhat it requiresISTQB term ID
Statement coverageEvery executable statement executed at least once"statement coverage"
Branch coverageEach branch of every decision point taken in both directions"branch coverage"
Decision coverageEach decision (e.g. if (x)) evaluated both true and false"decision coverage"
Condition coverageEach 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 coverageAll 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 -lgcov

Compilation 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 report

The 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_binary

The 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.info

show 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:

  • Code regions - associate source ranges with counters.
  • Skipped regions - preprocessor-excluded code (e.g. #ifdef branches not taken at compile time).
  • Expansion regions - macro expansions, so a macro that fires from multiple call sites has separate coverage per site.
  • Branch regions - true / false condition paths (added with -fcoverage-mapping).

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:

ApproachCoverage accuracyCostNotes
Host build (same source, x86_64 toolchain, no MCU)Misses MCU-specific pathsLowestDefault for Ceedling per throwtheswitch.org/ceedling (opens in new window); use when business logic dominates
QEMU system emulationCatches arch-specific paths (endianness, alignment)MediumSee qemu-system-test-runner; reports written to host filesystem via virtio / semihosting
On-target with semihostingHighest fidelityHighest (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 shimHighHighCounters 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 totals

The 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

GateWhy
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 linesPR-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-patternWhy it failsFix
Optimising the test build with -O2gcov / llvm-cov measure post-optimisation flow; branches collapseUse -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 themPick one toolchain per build; document why
Coverage of test code counted as product coverageInflates numbersExclude tests/, mocks/, framework/ directories in lcov / llvm-cov filter
MC/DC reported from gcovgcov doesn't measure MC/DCUse 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 MCUEndianness / alignment / weak-symbol paths uncoveredPair 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

  • Statement coverage is the weakest meaningful target - a 100%-statement-covered suite can miss every else branch.
  • Branch coverage from gcov has known imprecision on short-circuit && / || - these compile to two branches; the count attributes to the source line, not the individual operand. For per-operand visibility, use clang -fcoverage-mcdc per the Clang docs.
  • .gcda files accumulate across runs. Re-run without deleting them and counters keep climbing. Use __gcov_reset() (provided by libgcov) to zero between scenarios - per the gcov source.
  • Counter overflow. Default counters are 64-bit on modern GCC but were 32-bit historically - on a long-running on-target run, check gcov-tool overlap for saturated counts.
  • No path coverage from either toolchain. Path coverage ("path coverage" per ISTQB) is exponential and neither gcov nor llvm-cov measures it. For path-sensitive testing pair with fuzzing or symbolic execution.

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)):

FlagLong formEffect
-a--all-blocksWrite per-basic-block counts
-b--branch-probabilitiesWrite branch frequencies + summary to stdout
-c--branch-countsBranch frequencies as counts not percentages
-f--function-summariesPer-function coverage on top of file-level
-n--no-outputSuppress the .gcov file
-p--preserve-pathsPreserve full path in generated filenames
-u--unconditional-branchesInclude 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:

  • - marks a non-executable line (declaration, blank, comment).
  • ##### marks an executable line that was never run - the reads worth chasing.

-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 / levelMinimum structural coverage
MISRA-C:2012 Coverage GuidanceNo 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 BDecision coverage
DO-178C / DAL CStatement coverage
DO-178C / DAL DNone mandated
ISO 26262 ASIL DMC/DC strongly recommended for unit verification (see "ISO 26262-6:2018 Table 12")
ISO 26262 ASIL A / B / CBranch (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

FormatProducerConsumer
.gcov (text)gcovHuman reading; line-level annotation
.gcov.json.gzgcov --json-formatCI parser; no source-code dependency per GCC docs
.info (LCOV)lcov --capture or llvm-cov export -format=lcovCodecov / Coveralls / SonarQube
.profdatallvm-profdata mergeInput only to llvm-cov
HTMLgenhtml (LCOV) or llvm-cov show -format=htmlHumans; not for diff'ing
JSONllvm-cov export -format=textCustom CI dashboards
Cobertura XMLgcovr --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.