|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# run_coverage.sh — F-Stack lib/ unit-test coverage one-shot runner. |
| 4 | +# |
| 5 | +# Usage: |
| 6 | +# ./run_coverage.sh # full coverage: build + test + report + summary |
| 7 | +# ./run_coverage.sh --quick # skip rebuild if .gcda already exist |
| 8 | +# ./run_coverage.sh --file <c> # show per-line gcov detail for one file |
| 9 | +# # (e.g. --file ff_log.c) |
| 10 | +# ./run_coverage.sh --serve # also start an HTTP server on :8080 to view |
| 11 | +# # the HTML report (Ctrl-C to stop) |
| 12 | +# ./run_coverage.sh --clean # remove all coverage artifacts and exit |
| 13 | +# ./run_coverage.sh -h | --help # show this help |
| 14 | +# |
| 15 | +# Exit codes: |
| 16 | +# 0 success, all G8 thresholds met |
| 17 | +# 1 coverage build/test failure or G8 threshold violation |
| 18 | +# 2 bad CLI arguments |
| 19 | +# |
| 20 | + |
| 21 | +set -u |
| 22 | + |
| 23 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 24 | +cd "$SCRIPT_DIR" |
| 25 | + |
| 26 | +# --- ANSI helpers --------------------------------------------------------- |
| 27 | +if [ -t 1 ] && [ "${TERM:-}" != "dumb" ]; then |
| 28 | + C_BOLD=$'\033[1m' |
| 29 | + C_GREEN=$'\033[32m' |
| 30 | + C_YELLOW=$'\033[33m' |
| 31 | + C_RED=$'\033[31m' |
| 32 | + C_CYAN=$'\033[36m' |
| 33 | + C_RESET=$'\033[0m' |
| 34 | +else |
| 35 | + C_BOLD=""; C_GREEN=""; C_YELLOW=""; C_RED=""; C_CYAN=""; C_RESET="" |
| 36 | +fi |
| 37 | + |
| 38 | +banner() { printf "\n${C_BOLD}${C_CYAN}===== %s =====${C_RESET}\n" "$1"; } |
| 39 | +ok() { printf "${C_GREEN}[ OK ]${C_RESET} %s\n" "$1"; } |
| 40 | +warn() { printf "${C_YELLOW}[WARN]${C_RESET} %s\n" "$1"; } |
| 41 | +err() { printf "${C_RED}[FAIL]${C_RESET} %s\n" "$1" >&2; } |
| 42 | + |
| 43 | +# --- preflight ------------------------------------------------------------ |
| 44 | +check_tools() { |
| 45 | + local missing=0 |
| 46 | + for t in gcc make pkg-config lcov genhtml awk; do |
| 47 | + if ! command -v "$t" >/dev/null 2>&1; then |
| 48 | + err "required tool missing: $t" |
| 49 | + missing=1 |
| 50 | + fi |
| 51 | + done |
| 52 | + if ! pkg-config --exists cmocka; then |
| 53 | + err "cmocka pkg-config not found (try: dnf install -y libcmocka-devel)" |
| 54 | + missing=1 |
| 55 | + fi |
| 56 | + if ! pkg-config --exists libdpdk; then |
| 57 | + warn "libdpdk pkg-config not found — test_ff_dpdk_{if,kni} may fail" |
| 58 | + fi |
| 59 | + [ "$missing" -eq 0 ] |
| 60 | +} |
| 61 | + |
| 62 | +# --- per-file gcov detail ------------------------------------------------- |
| 63 | +show_file_detail() { |
| 64 | + local f="$1" |
| 65 | + if [ -z "$f" ]; then |
| 66 | + err "--file requires a filename (e.g. --file ff_log.c)" |
| 67 | + exit 2 |
| 68 | + fi |
| 69 | + |
| 70 | + local lib_obj_dir="$SCRIPT_DIR/lib_objs" |
| 71 | + if [ ! -f "$lib_obj_dir/${f%.c}.gcno" ]; then |
| 72 | + warn "$lib_obj_dir/${f%.c}.gcno not found; running 'make coverage' first..." |
| 73 | + run_coverage_full |
| 74 | + fi |
| 75 | + banner "Per-line gcov detail for $f" |
| 76 | + cd "$lib_obj_dir" |
| 77 | + gcov "$f" 2>/dev/null | head -20 |
| 78 | + if [ -f "${f}.gcov" ]; then |
| 79 | + echo "" |
| 80 | + printf "${C_BOLD}Showing first 60 lines of ${f}.gcov:${C_RESET}\n" |
| 81 | + printf " ${C_GREEN}<n>${C_RESET} : executed n times\n" |
| 82 | + printf " ${C_RED}#####${C_RESET} : NOT covered\n" |
| 83 | + printf " ${C_YELLOW}-${C_RESET} : not executable (comment / decl)\n\n" |
| 84 | + head -60 "${f}.gcov" |
| 85 | + else |
| 86 | + warn "no .gcov output produced (maybe ${f%.c}.gcda missing)" |
| 87 | + fi |
| 88 | +} |
| 89 | + |
| 90 | +# --- full coverage flow --------------------------------------------------- |
| 91 | +run_coverage_full() { |
| 92 | + banner "1/4 Run make coverage (build + test + lcov + threshold check)" |
| 93 | + if make coverage; then |
| 94 | + ok "make coverage exit=0" |
| 95 | + else |
| 96 | + local rv=$? |
| 97 | + err "make coverage exit=$rv" |
| 98 | + return $rv |
| 99 | + fi |
| 100 | + |
| 101 | + banner "2/4 Project-wide summary (all files including P2/P3 partials)" |
| 102 | + lcov --summary coverage.info --rc branch_coverage=1 2>&1 \ |
| 103 | + | grep -E "lines|functions|branches" \ |
| 104 | + | sed 's/^/ /' |
| 105 | + |
| 106 | + banner "3/4 Per-file thresholds (spec 06 §6.1, P0/P1 only)" |
| 107 | + bash ./coverage_threshold.sh coverage.info 2>&1 \ |
| 108 | + | grep -E "PASS|FAIL|threshold|G8" \ |
| 109 | + | sed 's/^/ /' |
| 110 | + |
| 111 | + banner "4/4 Reports" |
| 112 | + if [ -f "$SCRIPT_DIR/coverage_report/index.html" ]; then |
| 113 | + ok "HTML : $SCRIPT_DIR/coverage_report/index.html" |
| 114 | + else |
| 115 | + warn "HTML report not produced" |
| 116 | + fi |
| 117 | + [ -f "$SCRIPT_DIR/coverage.info" ] && ok "lcov : $SCRIPT_DIR/coverage.info" |
| 118 | + return 0 |
| 119 | +} |
| 120 | + |
| 121 | +# --- quick: reuse existing .gcda -------------------------------------------- |
| 122 | +run_coverage_quick() { |
| 123 | + if ! find lib_objs -name '*.gcda' -print -quit 2>/dev/null | grep -q '.'; then |
| 124 | + warn "no .gcda found; falling back to full coverage flow" |
| 125 | + run_coverage_full |
| 126 | + return |
| 127 | + fi |
| 128 | + banner "Quick mode: reusing existing .gcda (no rebuild)" |
| 129 | + rm_via_wrapper coverage.info |
| 130 | + rm_via_wrapper coverage_report |
| 131 | + lcov --capture --directory lib_objs --directory . \ |
| 132 | + --output-file coverage.info --rc branch_coverage=1 \ |
| 133 | + --ignore-errors mismatch,negative,inconsistent,empty,unused,source 2>&1 \ |
| 134 | + | tail -3 |
| 135 | + lcov --remove coverage.info \ |
| 136 | + '/usr/*' '*/cmocka.h' '*/tests/unit/test_*.c' '*/tests/unit/common/*' \ |
| 137 | + --output-file coverage.info.clean --rc branch_coverage=1 \ |
| 138 | + --ignore-errors unused,inconsistent,empty 2>&1 | tail -3 |
| 139 | + mv coverage.info.clean coverage.info |
| 140 | + genhtml coverage.info --output-directory coverage_report \ |
| 141 | + --branch-coverage \ |
| 142 | + --title "F-Stack lib/ unit-test coverage" \ |
| 143 | + --ignore-errors source,inconsistent,corrupt 2>&1 | tail -3 |
| 144 | + bash ./coverage_threshold.sh coverage.info |
| 145 | +} |
| 146 | + |
| 147 | +# --- helper: remove via workspace wrapper (NFR-U-7) ------------------------ |
| 148 | +rm_via_wrapper() { |
| 149 | + local tgt="$1" |
| 150 | + if [ -e "$tgt" ]; then |
| 151 | + /data/workspace/rm_tmp_file.sh "$(realpath "$tgt")" >/dev/null 2>&1 |
| 152 | + fi |
| 153 | +} |
| 154 | + |
| 155 | +run_clean() { |
| 156 | + banner "Cleaning coverage artifacts" |
| 157 | + make coverage_clean |
| 158 | + ok "done" |
| 159 | +} |
| 160 | + |
| 161 | +# --- argument parsing ------------------------------------------------------ |
| 162 | +ACTION="full" |
| 163 | +FILE_ARG="" |
| 164 | +SERVE=0 |
| 165 | + |
| 166 | +while [ $# -gt 0 ]; do |
| 167 | + case "$1" in |
| 168 | + --quick) ACTION="quick" ;; |
| 169 | + --file) ACTION="file"; FILE_ARG="${2:-}"; shift ;; |
| 170 | + --serve) SERVE=1 ;; |
| 171 | + --clean) ACTION="clean" ;; |
| 172 | + -h|--help) |
| 173 | + sed -n '2,20p' "$0" | sed 's/^# \?//' |
| 174 | + exit 0 |
| 175 | + ;; |
| 176 | + *) |
| 177 | + err "unknown argument: $1" |
| 178 | + echo "Try: $0 --help" >&2 |
| 179 | + exit 2 |
| 180 | + ;; |
| 181 | + esac |
| 182 | + shift |
| 183 | +done |
| 184 | + |
| 185 | +# --- dispatch -------------------------------------------------------------- |
| 186 | +check_tools || exit 1 |
| 187 | + |
| 188 | +case "$ACTION" in |
| 189 | + full) run_coverage_full || exit 1 ;; |
| 190 | + quick) run_coverage_quick || exit 1 ;; |
| 191 | + file) show_file_detail "$FILE_ARG" ;; |
| 192 | + clean) run_clean; exit 0 ;; |
| 193 | +esac |
| 194 | + |
| 195 | +# --- optional HTTP server for HTML viewing -------------------------------- |
| 196 | +if [ "$SERVE" -eq 1 ] && [ -d "$SCRIPT_DIR/coverage_report" ]; then |
| 197 | + PORT="${COVERAGE_PORT:-8080}" |
| 198 | + banner "Serving HTML report on http://0.0.0.0:${PORT} (Ctrl-C to stop)" |
| 199 | + cd "$SCRIPT_DIR/coverage_report" |
| 200 | + if command -v python3 >/dev/null 2>&1; then |
| 201 | + python3 -m http.server "$PORT" |
| 202 | + elif command -v python2 >/dev/null 2>&1; then |
| 203 | + python2 -m SimpleHTTPServer "$PORT" |
| 204 | + else |
| 205 | + err "no python found; cannot start HTTP server" |
| 206 | + exit 1 |
| 207 | + fi |
| 208 | +fi |
0 commit comments