#!/usr/bin/env bash # scripts/quality/coverage.sh # # Measure line/branch test coverage of code/include/ via gcov + lcov. # # Builds the test suite with `--coverage` (gcc) or `-fprofile-instr-generate # -fcoverage-mapping` (clang), runs ctest, and emits: # * build-coverage/coverage.info — lcov tracefile # * build-coverage/lcov-html/index.html — browseable HTML report # * stdout: per-file summary + grand total # # Local-only. Not gated in CI yet; once a coverage threshold is agreed # with the reviewer (e.g. 80 %), the gate can be a single line in # cpp-tests.yml. # # Usage: # bash scripts/quality/coverage.sh # gcc default # CXX=g++-13 bash scripts/quality/coverage.sh # specific compiler # # Prerequisites: gcov + lcov (apt install lcov / brew install lcov) # # Exit codes: # 0 coverage report generated; prints % # 1 tests failed (no usable trace) # 2 prerequisite missing set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT" BUILD_DIR="build-coverage" command -v lcov >/dev/null 2>&1 || { echo "FAIL: lcov not found in PATH. Install via:" >&2 echo " macOS: brew install lcov" >&2 echo " Linux: sudo apt install lcov" >&2 exit 2 } # On macOS we prefer brew-installed LLVM clang++ because Apple Clang's # GCov-compatible `--coverage` runtime is known to deadlock during # static-initializer profiling on arm64 with template-heavy code # (Eigen + CGAL); the LLVM build does not. Override with `CXX=...`. DEFAULT_CXX="g++" if [ -z "${CXX:-}" ] && [ "$(uname -s)" = "Darwin" ] \ && [ -x /opt/homebrew/opt/llvm/bin/clang++ ]; then DEFAULT_CXX="/opt/homebrew/opt/llvm/bin/clang++" fi CXX_BIN="${CXX:-$DEFAULT_CXX}" command -v "$CXX_BIN" >/dev/null 2>&1 || { echo "FAIL: $CXX_BIN not found" >&2; exit 2; } echo "========================================" echo " Coverage build (gcov + lcov)" echo " CXX: $CXX_BIN ($($CXX_BIN --version | head -1))" echo " build: $BUILD_DIR" echo "========================================" echo cmake -S code -B "$BUILD_DIR" \ -DCMAKE_CXX_COMPILER="$CXX_BIN" \ -DCMAKE_CXX_FLAGS="--coverage -O0 -g" \ -DCMAKE_EXE_LINKER_FLAGS="--coverage" \ -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \ -Wno-dev nice -n 19 cmake --build "$BUILD_DIR" --target conformallab_tests \ -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 2)" # Run the non-CGAL test suite. (CGAL tests under coverage instrumentation # blow up to ~6 GB RAM during compilation; out-of-scope for this gate.) cd "$BUILD_DIR" if ! ctest -E "^cgal\." --output-on-failure; then cd "$ROOT" echo "FAIL: coverage build's test run did not complete cleanly." >&2 exit 1 fi cd "$ROOT" # ── Capture trace ──────────────────────────────────────────────────────────── # lcov ≥ 2.0 became strict about "inconsistent" / "unsupported" / "negative" # diagnostics from gcov data; the GTest sources reliably trigger # "inconsistent" because of their preprocessor gymnastics, and brew clang's # gcov shim is older than the function-end-line tracking lcov wants. # These are noise we cannot fix in our source tree — suppress them. LCOV_TOLERANT=( --ignore-errors inconsistent --ignore-errors unsupported --ignore-errors negative --ignore-errors empty --ignore-errors mismatch --rc lcov_branch_coverage=1 ) lcov --capture --directory "$BUILD_DIR" \ --output-file "$BUILD_DIR/coverage.raw.info" \ --no-external \ "${LCOV_TOLERANT[@]}" \ >/dev/null 2>&1 || true # Restrict to code/include/ (our public API surface; ignore deps/tests). lcov --extract "$BUILD_DIR/coverage.raw.info" \ "*/code/include/*" \ --output-file "$BUILD_DIR/coverage.info" \ "${LCOV_TOLERANT[@]}" \ >/dev/null 2>&1 || true # ── HTML report ────────────────────────────────────────────────────────────── genhtml --branch-coverage --legend \ --output-directory "$BUILD_DIR/lcov-html" \ "${LCOV_TOLERANT[@]}" \ "$BUILD_DIR/coverage.info" >/dev/null 2>&1 || true # ── Summary to stdout ──────────────────────────────────────────────────────── echo echo "── Coverage summary (code/include/) ─────────────────────────" if [ -s "$BUILD_DIR/coverage.info" ]; then lcov --summary "$BUILD_DIR/coverage.info" "${LCOV_TOLERANT[@]}" 2>/dev/null \ | grep -E "lines\.\.\.\.|functions|branches" \ | sed 's/^/ /' echo echo "HTML report: $BUILD_DIR/lcov-html/index.html" echo " open $BUILD_DIR/lcov-html/index.html" else echo " WARN: coverage.info is empty — likely an lcov/gcov version" echo " mismatch. Tests passed; raw .gcda files are in $BUILD_DIR." echo " Inspect with: find $BUILD_DIR -name '*.gcda' | head" fi