Files
plotter/tools/coverage.sh
T

97 lines
3.9 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Reproducible coverage for src/**, with a floor.
#
# One command: configure and build the instrumented tree, run the whole suite,
# capture src/** with lcov into build/cov/coverage.info, render the HTML report
# under build/cov/html/, print the per-file table, and exit non-zero when
# tools/coverage_floor.py finds a file below 80% lines or the src/ aggregate
# below 90%. The numbers and the floors are the contract docs/coverage.md
# states; `docs/coverage.md` also records how to re-read the table from here.
#
# Idempotent by construction: gcov counters accumulate in .gcda files, so a
# second run that inherited the first run's counters would report a superset of
# what it exercised (and a source file deleted since the last run would report
# stale data for a file that no longer exists). This script therefore deletes
# every .gcda under the build tree *before* it runs the suite, so the captured
# counters are exactly the ones this run produced. Run it twice on an unchanged
# tree and build/cov/coverage.info is byte-for-byte identical.
#
# Usage: tools/coverage.sh [build-dir]
# build-dir defaults to build/cov, the linux-coverage preset's binaryDir.
# Any other directory is used as-is and must already be configured with
# --coverage (the `coverage` CMake target passes its own build directory).
set -euo pipefail
readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly PRESET_BUILD="$ROOT/build/cov"
readonly BUILD="${1:-$PRESET_BUILD}"
readonly FLOOR="$ROOT/tools/coverage_floor.py"
readonly RAW="$BUILD/coverage.raw.info"
readonly INFO="$BUILD/coverage.info"
readonly HTML="$BUILD/html"
log() { printf '==> %s\n' "$*"; }
for tool in cmake ctest lcov genhtml python3; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf 'coverage: %s is required (Arch: pacman -S cmake ninja lcov python)\n' "$tool" >&2
exit 2
fi
done
if [[ "$BUILD" == "$PRESET_BUILD" ]]; then
# The preset file lives at the repository root, but the `coverage` target runs
# this script with the build directory as its working directory (that is how
# ninja invokes a target's command), so the configure step states where to look
# rather than assuming. Every other path in this script is absolute for the
# same reason.
log "configuring $BUILD from the linux-coverage preset"
cd "$ROOT"
cmake --preset linux-coverage --log-level=WARNING
elif [[ ! -f "$BUILD/CMakeCache.txt" ]]; then
printf 'coverage: %s is not a configured build tree; use --preset linux-coverage\n' "$BUILD" >&2
exit 2
fi
log "building"
cmake --build "$BUILD"
# Before the suite, not after: see the idempotency note at the top of the file.
log "clearing stale gcov counters under $BUILD"
find "$BUILD" -name '*.gcda' -delete
log "running the suite"
ctest --test-dir "$BUILD" --output-on-failure
log "capturing src/** with lcov into $INFO"
# The unexecuted-block warning comes from libstdc++ headers on this toolchain;
# geninfo_unexecuted_blocks=1 records those blocks as unexecuted instead of
# warning about them, which is what the report means anyway.
lcov --capture --directory "$BUILD" --output-file "$RAW" \
--rc branch_coverage=0 --rc geninfo_unexecuted_blocks=1 \
--ignore-errors inconsistent,inconsistent
lcov --extract "$RAW" "$ROOT/src/*" --output-file "$INFO" --ignore-errors unused
rm -f "$RAW"
log "rendering the HTML report into $HTML/index.html"
rm -rf "$HTML"
genhtml "$INFO" --output-directory "$HTML" --prefix "$ROOT" \
--title "plot -- line coverage of src/**" --rc branch_coverage=0 \
--ignore-errors inconsistent,inconsistent
log "per-file coverage for src/**"
set +e
python3 "$FLOOR" --table "$INFO"
readonly FLOOR_STATUS=$?
set -e
if (( FLOOR_STATUS != 0 )); then
printf '\ncoverage: FLOOR FAILED (exit %d) -- report %s, HTML %s/index.html\n' \
"$FLOOR_STATUS" "$INFO" "$HTML" >&2
exit "$FLOOR_STATUS"
fi
printf '\ncoverage: floor satisfied -- report %s, HTML %s/index.html\n' "$INFO" "$HTML"