#!/bin/bash # Build fold with coverage instrumentation, run the test suite, and emit a # line/function coverage report (terminal summary + HTML). Vendored third-party # code (Ghidra, Capstone) is excluded. Uses gcovr via `uvx` (no system install) # and clang's `llvm-cov gcov` to parse clang's coverage data. # # ./scripts/coverage.sh # # Report lands in build/coverage/coverage.{html,txt}. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" BUILD_DIR="${COVERAGE_BUILD_DIR:-$ROOT/build/coverage}" OS="$(uname -s)" # Reviewed local baseline: 72.8% line coverage (39,498 / 54,270 lines). # Keep the required floor below that baseline to tolerate small platform variance. COVERAGE_MIN_LINE="${COVERAGE_MIN_LINE:-70}" # --- locate LLVM (honor LLVM_BIN / LLVM_CONFIG; else Homebrew/PATH) --- if [ -n "${LLVM_BIN:-}" ] && [ -d "$LLVM_BIN" ]; then LLVM_CONFIG_PATH="$LLVM_BIN/llvm-config" elif [ -n "${LLVM_CONFIG:-}" ] && command -v "$LLVM_CONFIG" >/dev/null 2>&1; then LLVM_CONFIG_PATH="$(command -v "$LLVM_CONFIG")" elif [ "$OS" = "Darwin" ] && command -v brew >/dev/null 2>&1; then LLVM_CONFIG_PATH="$(brew --prefix llvm 2>/dev/null)/bin/llvm-config" elif command -v llvm-config >/dev/null 2>&1; then LLVM_CONFIG_PATH="$(command -v llvm-config)" fi if [ -z "${LLVM_CONFIG_PATH:-}" ] || [ ! -x "$LLVM_CONFIG_PATH" ]; then echo "ERROR: LLVM not found. Set LLVM_BIN or LLVM_CONFIG to a usable llvm-config executable." >&2 exit 1 fi LLVM_BIN="$(dirname "$LLVM_CONFIG_PATH")" LLVM_SUFFIX="${LLVM_CONFIG_PATH##*/llvm-config}" LLVM_COV_PATH="${LLVM_COV:-$LLVM_BIN/llvm-cov$LLVM_SUFFIX}" LLVM_AR_PATH="${LLVM_AR:-$LLVM_BIN/llvm-ar$LLVM_SUFFIX}" if [ ! -x "$LLVM_COV_PATH" ]; then echo "ERROR: llvm-cov not found next to $LLVM_CONFIG_PATH." >&2 exit 1 fi export PATH="$LLVM_BIN:$PATH" mkdir -p "$BUILD_DIR" # llvm-ar native file (same trick as setup.sh) so static archiving works. MESON_NATIVE_ARGS=(--native-file "$BUILD_DIR/conan_meson_native.ini") if [ -x "$LLVM_AR_PATH" ]; then printf "[binaries]\nar = '%s'\n" "$LLVM_AR_PATH" >"$BUILD_DIR/llvm_ar.ini" MESON_NATIVE_ARGS+=(--native-file "$BUILD_DIR/llvm_ar.ini") fi echo "==> Conan install (coverage)" conan install "$ROOT" -of "$BUILD_DIR" --build=missing \ -s build_type=Debug -s compiler.cppstd=23 -o "build_tests=True" || echo "Conan install had warnings (non-fatal)" # z3.pc symlink (macOS Homebrew / Linux multiarch), mirroring setup.sh. if [ "$OS" = "Darwin" ] && command -v brew >/dev/null 2>&1; then Z3_PC="$(brew --prefix z3 2>/dev/null)/lib/pkgconfig/z3.pc" [ -f "$Z3_PC" ] && ln -sf "$Z3_PC" "$BUILD_DIR/z3.pc" else for d in "/usr/lib/$(uname -m)-linux-gnu" /usr/lib/x86_64-linux-gnu /usr/lib; do [ -f "$d/pkgconfig/z3.pc" ] && ln -sf "$d/pkgconfig/z3.pc" "$BUILD_DIR/z3.pc" && break done fi echo "==> Meson setup (-Db_coverage=true, debug, ghidra+capstone)" meson setup "$BUILD_DIR" \ --reconfigure \ "${MESON_NATIVE_ARGS[@]}" \ --wrap-mode=default \ -Dcpp_std=c++23 \ -Dbuildtype=debug \ -Db_coverage=true \ -Dbuild-tests=true \ -Dghidra=auto \ -Dcapstone=auto \ -Dllvm-config-path="$LLVM_CONFIG_PATH" echo "==> Build + test (generates coverage data)" ninja -C "$BUILD_DIR" test_status=0 meson test -C "$BUILD_DIR" || test_status=$? if [ "$test_status" -ne 0 ]; then echo "WARNING: test suite failed (exit $test_status); generating coverage from completed tests" >&2 fi echo "==> gcovr report (uvx; excludes third_party/ + subprojects/)" GCOVR_COVERAGE_GATE=() if [ -n "$COVERAGE_MIN_LINE" ]; then case "$COVERAGE_MIN_LINE" in *[!0-9.]* | "") echo "ERROR: COVERAGE_MIN_LINE must be a percentage in [0, 100]" >&2 exit 2 ;; esac if ! awk "BEGIN { exit !($COVERAGE_MIN_LINE >= 0 && $COVERAGE_MIN_LINE <= 100) }"; then echo "ERROR: COVERAGE_MIN_LINE must be a percentage in [0, 100]" >&2 exit 2 fi GCOVR_COVERAGE_GATE=(--fail-under-line "$COVERAGE_MIN_LINE") fi coverage_status=0 uvx gcovr \ --root "$ROOT" \ --gcov-executable "$LLVM_COV_PATH gcov" \ --exclude '.*third_party/.*' \ --exclude '.*subprojects/.*' \ --exclude '.*/tests/.*' \ --txt "$BUILD_DIR/coverage.txt" \ --html-details "$BUILD_DIR/coverage.html" \ --print-summary \ "${GCOVR_COVERAGE_GATE[@]}" \ "$BUILD_DIR" || coverage_status=$? if [ "$coverage_status" -eq 0 ]; then echo "" echo "Coverage report: $BUILD_DIR/coverage.txt (summary above) + $BUILD_DIR/coverage.html" else echo "ERROR: coverage report or line-coverage gate failed (exit $coverage_status)" >&2 fi if [ "$test_status" -ne 0 ] || [ "$coverage_status" -ne 0 ]; then echo "ERROR: coverage gate failed: tests=$test_status coverage=$coverage_status" >&2 exit 1 fi