ROS 2 C++ Code Coverage
Published
Instructions on setting up C++ code coverage using ROS, colcon and clang based on my experience on Cavalier Autonomous Racing. A problem with a surprisingly simple setup, but took many iterations to get right.

I recent set up test coverage reports for Cavalier Autonomous Racing. Despite me thinking it was simple, it ended up taking me several hours. Read this post to avoid my pain.
I am assuming the stack uses:
- a ROS 2 Monorepo (we have 30+ nodes in our stack)
- clang(++) for C++ Code (note not standard to use this toolchain)
- CMakeLists
- colcon
Tools Used
Setup
First, ensure the following are installed:
sudo apt update && sudo apt install -y \
clang \
llvm \
lld \
llvm-cov \
file
#!/bin/bash
# =========
# Builds and outputs a coverage report using `llvm-cov` to `public` directory and then shows a summary of the coverage report
source /opt/ros/$ROS_DISTRO/setup.sh
PACKAGES_UP_TO="iac_launch"
colcon build \
--merge-install \
--packages-up-to $PACKAGES_UP_TO \
--cmake-force-configure \
--cmake-args \
-DCMAKE_CXX_FLAGS="-g -Og -fprofile-instr-generate -fcoverage-mapping" \
-DCMAKE_C_FLAGS="-g -Og -fprofile-instr-generate -fcoverage-mapping" \
-DCMAKE_CXX_COMPILER="clang++" \
-DCMAKE_C_COMPILER="clang"
# tells LLVM to output the profiles with the process name (`%p`) and the binary id (`%b`),
# used to avoid conflicts between profile names
LLVM_PROFILE_FILE="$PWD/%p.%b.profraw"
colcon test \
--merge-install \
--packages-up-to $PACKAGES_UP_TO \
--event-handlers console_direct-
llvm-profdata merge -sparse *.profraw -o coverage.profdata
FILES=$(find install build -type f \( -executable -o -name '*.so' \) ! -path '*_msgs*' ! -name '*.py' ! -name '*.bin' -exec sh -c 'for f; do case "$(file --brief --mime-type "$f")" in application/x-executable|application/x-sharedlib) printf -- "--object=%s " "$f";; esac; done' _ {} +)
llvm-cov show \
--format=html \
--output-dir=public \
--instr-profile=coverage.profdata \
$FILES
After the report is generated, a summary is printed in the command line. Viewing the HTML report in a browser is as simple as running this locally.
python3 -m http.server -d . 8080
This produces a standard coverage report which everyone is used to seeing
Why is this required?
If you examine the llvm-cov
report it mentions that multiple files can be passed. However, the --object=
is needed for each. Without this, only code coverage for the first passed library or executable and it will not tell you why.
$ llvm-cov report --help
OVERVIEW: LLVM code coverage tool
USAGE: llvm-cov report [options] Covered executable or object file. --sources <Source files>
OPTIONS:
Color Options:
--color - Use colors in output (default=autodetect)
This one issue took me several hours to debug, plus getting the binary filtering criteria strict enough to not cause the script to fail.
I hope this post helped you avoid some frustration.
iMessage Visualizer
Stay in touch
Subscribe to my RSS feed to stay updated
Have any questions
Feel free to contact me! I will answer any and all inquires