#!/usr/bin/env sh # Host workspace coverage. The four strict-core packages require 100% source # lines and branch outcomes; all other package totals are informational. # Run from firmware/ in the locked Nix shell (cargo-llvm-cov 0.8.7). # Install the separate coverage compiler with: # rustup toolchain install nightly-2025-08-01 --profile minimal --component llvm-tools-preview set -eu export RUSTUP_TOOLCHAIN=nightly-2025-08-01 export CARGO_BUILD_JOBS=2 if [ "$(cargo llvm-cov --version)" != "cargo-llvm-cov 0.8.7" ]; then echo "Use cargo-llvm-cov 0.8.7 from nix develop (flake.lock)." >&2 exit 127 fi if [ -n "${RUSTC_BOOTSTRAP:-}" ] || [ -n "${RUSTC:-}" ] || [ -n "${RUSTDOC:-}" ]; then echo "Coverage requires the pinned compiler without RUSTC_BOOTSTRAP/RUSTC/RUSTDOC overrides." >&2 exit 2 fi sysroot=$(rustc --print sysroot) host=$(rustc -vV | awk '/^host:/ { print $2 }') export LLVM_COV="$sysroot/lib/rustlib/$host/bin/llvm-cov" export LLVM_PROFDATA="$sysroot/lib/rustlib/$host/bin/llvm-profdata" rustc -vV cargo llvm-cov --version "$LLVM_COV" --version "$LLVM_PROFDATA" --version echo "Coverage profile: dev; compiler and LLVM tools: $RUSTUP_TOOLCHAIN" python3 scripts/test-summarize-test-coverage.py # Keep coverage out of ordinary build artifacts. Strict-core invocations also # need separate object directories to avoid unexecuted dependency duplicates. : "${CARGO_TARGET_DIR:=$PWD/target/test-coverage}" coverage_root=$CARGO_TARGET_DIR mkdir -p "$coverage_root" # Discover current workspace members, including host platform contracts and # proc-macro runtime harnesses. Only bare-metal boards are excluded. # Put the strict core first so unrelated test failures cannot hide its gaps. cargo metadata --no-deps --format-version 1 --locked --offline >"$coverage_root/metadata.json" python3 - "$coverage_root/metadata.json" >"$coverage_root/packages.txt" <<'PY' import json from pathlib import Path import sys metadata = json.loads(Path(sys.argv[1]).read_text()) excluded = {"kaptan-geprc-f722-v2", "kaptan-kakute-f7-mini-v3"} core = {"kaptan-math", "kaptan-navigation", "kaptan-control", "kaptan-safety"} packages = [p for p in metadata["packages"] if p["id"] in metadata["workspace_members"] and p["name"] not in excluded] if not core <= {p["name"] for p in packages}: raise ValueError("missing strict-core package") for package in sorted(packages, key=lambda p: (p["name"] not in core, p["name"])): source = Path(package["manifest_path"]).parent.relative_to(metadata["workspace_root"]) print(package["name"], f"/{source}/") PY report_package() { package=$1 source_path=$2 shift 2 lcov_report="$coverage_root/$package.lcov" case "$package" in kaptan-math | kaptan-navigation | kaptan-control | kaptan-safety) package_target_dir="$coverage_root/$package" json_report="$coverage_root/$package.json" report_format=strict-core ;; *) package_target_dir="$coverage_root/other" report_format=lcov ;; esac printf '\nCoverage package: %s; extra arguments: %s\n' "$package" "$*" CARGO_TARGET_DIR="$package_target_dir" cargo llvm-cov --profile dev \ --locked --offline --branch --no-report -p "$package" "$@" || return 1 CARGO_TARGET_DIR="$package_target_dir" cargo llvm-cov report --profile dev \ --locked --offline --lcov --output-path "$lcov_report" || return 1 case "$report_format" in strict-core) # LCOV DA defines source lines. JSON spans identify branch outcomes # across generic instances. Both exports use the same compiler/profile. CARGO_TARGET_DIR="$package_target_dir" cargo llvm-cov report --profile dev \ --locked --offline --json --output-path "$json_report" || return 1 totals=$(python3 scripts/summarize-test-coverage.py \ "$lcov_report" "$json_report" "$source_path") || return 1 ;; lcov) totals=$(awk -v source_path="$source_path" ' /^SF:/ { selected = index($0, source_path) != 0 } selected && /^LF:/ { lines_found += substr($0, 4) } selected && /^LH:/ { lines_hit += substr($0, 4) } selected && /^BRF:/ { branches_found += substr($0, 5) } selected && /^BRH:/ { branches_hit += substr($0, 5) } END { printf "%d %d %d %d", lines_hit, lines_found, branches_hit, branches_found } ' "$lcov_report") || return 1 ;; esac # Both summarizers emit exactly four space-separated integer counters. # shellcheck disable=SC2086 set -- $totals printf '%-30s lines %s/%s, branches %s/%s\n' "$package" "$1" "$2" "$3" "$4" if [ "$report_format" = strict-core ]; then if [ "$2" -eq 0 ] || [ "$4" -eq 0 ] || [ "$1" -ne "$2" ] || [ "$3" -ne "$4" ]; then echo "$package: FAIL strict 100% line/branch gate" >&2 return 1 fi elif [ "$2" -eq 0 ]; then echo "$package: no instrumented host source; coverage unavailable" fi } echo "Host test coverage (reports: $coverage_root; strict core unions LCOV DA lines and JSON branches)" echo "UI/compile-fail suites run under Rust 1.90.0 in make test, not under coverage nightly." echo "Proc-macro runtime-harness totals do not establish compiler-phase macro coverage." incomplete=0 while read -r package source_path; do set -- case "$package" in kaptan-navigation) set -- --features sensor-snapshots ;; kaptan-mavlink) set -- --features sensor-telemetry ;; # Diagnostic snapshots describe the production compiler's output. Keep # those suites in stable make test; instrument the runtime harnesses here. kaptan-config-derive) set -- --lib --test section ;; kaptan-err-reportable-macros) set -- --lib ;; esac if ! report_package "$package" "$source_path" "$@"; then echo "$package: coverage/test failure (see diagnostics above)" >&2 incomplete=1 fi done <"$coverage_root/packages.txt" echo "kaptan-geprc-f722-v2, kaptan-kakute-f7-mini-v3: target-only; make boards validates release images" if [ "$incomplete" -ne 0 ]; then echo "Coverage gate failed. Strict core gaps and test/report failures are listed above." >&2 exit 1 fi