https://github.com/taiki-e/cargo-llvm-cov
In order to gather code coverage reports for fpgad, we need to run the daemon separately to the test executable and
to gather the results after the fact. llvm-cov can be used to assemble a
coverage report by combining raw profile data (.profraw extensions are typical) and with source code and executable
binaries. It does this using internal path discovery which will be explained within this document.
We cannot use tarpaulin because
- it does not have support for subprocess/external profiling
- it does not run on arm
We can't just run the direct llvm-cov because that
- doesn't start fpgad
- does not build the daemon with the right flags
- requires running cargo with sudo (bad)
Instead, we need to use llvm-cov to assemble a coverage report from separate profiles generated by each test stream,
and cross-reference that with the binaries we ran. These binaries need to expose llvm coverage information so we need
to build them with special flags.
The llvm-cov tool can be installed using cargo:
cargo install cargo-llvm-covthen on first run it prompts you to install the llvm-tools-preview component:
$ cargo llvm-cov show-env
info: cargo-llvm-cov currently setting cfg(coverage); you can opt-out it by passing --no-cfg-coverage
I will run `rustup component add llvm-tools-preview --toolchain <aarch triple>` to install the `llvm-tools-preview` component for the selected toolchain.
Proceed? [Y/n] y
info: downloading component 'llvm-tools'
info: installing component 'llvm-tools'This can be avoided by running
rustup component add llvm-tools-preview
manually.
llvm-cov provides a way to conveniently collect results from external binaries as described very briefly in the docs.
So following that, in order to build the tests and also gather results from unit tests, certain environment variables must be set. Handily, this can be done using their method:
source <(cargo llvm-cov show-env --export-prefix)or alternatively
eval "$(cargo llvm-cov show-env --export-prefix)"the body of which (without eval $(...)) will print something like
RUSTFLAGS='-C instrument-coverage --cfg=coverage --cfg=trybuild_no_target'
LLVM_PROFILE_FILE='<working dir>/target/<working dir name>-%p-%4m.profraw'
CARGO_LLVM_COV=1
CARGO_LLVM_COV_SHOW_ENV=1
CARGO_LLVM_COV_TARGET_DIR=<working dir>/targetand for most approaches, this is enough. In our case, we need to append one additional flag
(-C llvm-args=-runtime-counter-relocation) to allow for continuous printing of code coverage profile data while
executing our daemon. Without this flag, the .profraw file can only be written to after a successful exit code
(exit(0)) and our daemon does not catch signals and exit safely, it instead must be terminated. Therefore, we can do
export RUSTFLAGS="$RUSTFLAGS -C llvm-args=-runtime-counter-relocation"to append this flag. This means that all of our binaries (including test binaries) will be capable of continuous output,
but it must be enabled with a llvm flag within LLVM_PROFILE_FILE (explained further below).
Now we must build our executable binaries using
cargo buildand our test binaries using
cargo test --no-runThen we can run our unit tests by running, for example,
cargo test --binsBuild/test targets can be specified here as normal. In our case,
cargo test --bin fpgadwill run only fpgad's unit tests, and we don't want to gather the results of the target which we do not currently test
so instead of cargo build we can select only our daemon like cargo build --bin fpgad so that cli target is never
built.
There are --exclude-* flags available for llvm-cov but these cannot be used here, nor in the cargo llvm-cov report
steps later, so the only way I found to avoid having unwanted binaries in the coverage report misrepresenting results,
is to never build them (at least not with llvm related compiler flags) in the first place.
With the flags being set by show-env as with above, the .profraw files are dumped into ./target unless the
CARGO_TARGET_DIR and/or CARGO_LLVM_COV_TARGET_DIR variables are overridden. If you change these directories,
the executable and test binaries must be present in the proper places inside these target dirs. It is easiest to use
the defaults.
The test executables for integration tests are produced with a hash string appended to the binary name. Some bash magic can be used to extract the path to these binaries from the cargo output by doing something like
<target>_test=$(
cargo test --no-run --test <target> | \
grep 'Executable tests/<target>.rs' | \
awk '{ gsub(/[()]/, ""); print $3 }'
)in our case we need to run a daemon with continuous collection (enabled by having the aforementioned %c flag
present in the LLVM_PROFILE_FILE variable) before running out integration tests so we can do that like
sudo env LLVM_PROFILE_FILE="${CARGO_LLVM_COV_TARGET_DIR}/<daemon>-%p%c.profraw" <daemon> [--options] &> <daemon>.log &
DAEMON_PID=$! # to use with kill when doneand in our case we can check our dbus is up and running by polling <daemon>.log for a known string like
timeout 5 bash -c '
while ! grep -q "<known string>" <daemon>.log; do
sleep 0.1
done
'Once that is up and running, we can run our integration test binaries:
sudo env LLVM_PROFILE_FILE="${CARGO_LLVM_COV_TARGET_DIR}/test_<target>>-%p.profraw" "$<target>_test" [--test-options]where LLVM_PROFILE_FILE is set to output to the same directory as the already generated .profraw files
(from running unit tests).
If you want to edit the filename but otherwise not change the LLVM_PROFILE_FILE, you can use bash magic to do that.
Once all our integration tests are done, we can kill the daemon
sudo kill $DAEMON_PID
wait $DAEMON_PID
# print leftover `Terminated....` stdout buffer
echo $()
unset DAEMON_PIDNow that we have the collection of .profraw files (typically floating about in ./target), we can use llvm-cov to
gather them up, cross-reference them with the original binaries, and work out what lines of code were never run. To do
this, from whatever directory contains the top level Cargo.toml (where we have been for this entire doc), run
cargo llvm-cov reportto print the coverage report summary in the terminal (or pipe to file or whatever you want).
If you're not using ssh (using a window manager) you can run
cargo llvm-cov report --opento generate and open the html report in a browser.
For other formats, specify the appropriate flag (and probably --output-path too ) i.e.
cargo llvm-cov report --json --output-path=coverage.json
cargo llvm-cov report --lcov --output-path=coverage.lcov
cargo llvm-cov report --text > coverage.txt
cargo llvm-cov report --html # doesn't support --output-path :(If you do generate html output and want to copy it to a host machine just do, for example
scp -r <user>@<device>:<contents of CARGO_LLVM_COV_TARGET_DIR>/llvm-cov/html/* ./htmlfrom the host system. Note that $CARGO_LLVM_COV_TARGET_DIR is an environment variable set on the machine so cannot be
used directly.
The docs provide these options for the llvm-cov report to enable a script to fail (useful for GitHub actions,
for example) under certain conditions
--fail-under-functions <MIN>
Exit with a status of 1 if the total function coverage is less than MIN percent
--fail-under-lines <MIN>
Exit with a status of 1 if the total line coverage is less than MIN percent
--fail-under-regions <MIN>
Exit with a status of 1 if the total region coverage is less than MIN percent
--fail-uncovered-lines <MAX>
Exit with a status of 1 if the uncovered lines are greater than MAX
--fail-uncovered-regions <MAX>
Exit with a status of 1 if the uncovered regions are greater than MAX
--fail-uncovered-functions <MAX>
Exit with a status of 1 if the uncovered functions are greater than MAX