lcov 与 genhtml 覆盖率报告通用 Shell 脚本
本文记录一个尽量通用的 Linux/macOS Shell 脚本模板,用于配合 CMake、CTest、GoogleTest、lcov、genhtml 生成 HTML 覆盖率报告。
脚本假设工程已经支持覆盖率构建,例如 CMake 中存在 ENABLE_COVERAGE 选项,或 Debug 覆盖率构建会自动添加 --coverage -O0 -g。如果工程尚未添加覆盖率编译选项,应先参考 07_GoogleTest集成方式CMake与包管理.md 中的覆盖率编译配置。
通用脚本
#!/usr/bin/env bash
set -euo pipefail
# =========================
# User configurable options
# =========================
PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}"
BUILD_DIR="${BUILD_DIR:-${PROJECT_ROOT}/build-coverage}"
REPORT_DIR="${REPORT_DIR:-${PROJECT_ROOT}/coverage-html}"
CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE:-Debug}"
COVERAGE_OPTION_NAME="${COVERAGE_OPTION_NAME:-ENABLE_COVERAGE}"
COVERAGE_OPTION_VALUE="${COVERAGE_OPTION_VALUE:-ON}"
CTEST_LABEL="${CTEST_LABEL:-}"
CTEST_PARALLEL="${CTEST_PARALLEL:-}"
RAW_INFO="${RAW_INFO:-${BUILD_DIR}/coverage.raw.info}"
BASE_INFO="${BASE_INFO:-${BUILD_DIR}/coverage.base.info}"
TEST_INFO="${TEST_INFO:-${BUILD_DIR}/coverage.test.info}"
TOTAL_INFO="${TOTAL_INFO:-${BUILD_DIR}/coverage.total.info}"
FILTERED_INFO="${FILTERED_INFO:-${BUILD_DIR}/coverage.info}"
LCOV_BIN="${LCOV_BIN:-lcov}"
GENHTML_BIN="${GENHTML_BIN:-genhtml}"
CMAKE_BIN="${CMAKE_BIN:-cmake}"
CTEST_BIN="${CTEST_BIN:-ctest}"
GCOV_TOOL="${GCOV_TOOL:-gcov}"
ENABLE_INITIAL_CAPTURE="${ENABLE_INITIAL_CAPTURE:-ON}"
ENABLE_BRANCH_COVERAGE="${ENABLE_BRANCH_COVERAGE:-ON}"
REMOVE_PATTERNS=(
"/usr/*"
"*/usr/include/*"
"*/tests/*"
"*/test/*"
"*/third_party/*"
"*/_deps/*"
"*/build-*/*"
)
# =========================
# Internal helpers
# =========================
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: command not found: $1" >&2
exit 1
fi
}
run_ctest() {
local args=(--test-dir "${BUILD_DIR}" --output-on-failure)
if [[ -n "${CTEST_LABEL}" ]]; then
args+=(-L "${CTEST_LABEL}")
fi
if [[ -n "${CTEST_PARALLEL}" ]]; then
args+=(-j "${CTEST_PARALLEL}")
fi
"${CTEST_BIN}" "${args[@]}"
}
lcov_branch_args=()
genhtml_branch_args=()
if [[ "${ENABLE_BRANCH_COVERAGE}" == "ON" ]]; then
lcov_branch_args+=(--rc lcov_branch_coverage=1)
genhtml_branch_args+=(--branch-coverage)
fi
# =========================
# Main flow
# =========================
require_command "${CMAKE_BIN}"
require_command "${CTEST_BIN}"
require_command "${LCOV_BIN}"
require_command "${GENHTML_BIN}"
echo "project root : ${PROJECT_ROOT}"
echo "build dir : ${BUILD_DIR}"
echo "report dir : ${REPORT_DIR}"
"${CMAKE_BIN}" -S "${PROJECT_ROOT}" -B "${BUILD_DIR}" \
-DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \
-D"${COVERAGE_OPTION_NAME}"="${COVERAGE_OPTION_VALUE}"
"${CMAKE_BIN}" --build "${BUILD_DIR}" --parallel
"${LCOV_BIN}" --directory "${BUILD_DIR}" --zerocounters \
--gcov-tool "${GCOV_TOOL}" \
"${lcov_branch_args[@]}"
if [[ "${ENABLE_INITIAL_CAPTURE}" == "ON" ]]; then
"${LCOV_BIN}" --capture --initial \
--directory "${BUILD_DIR}" \
--output-file "${BASE_INFO}" \
--gcov-tool "${GCOV_TOOL}" \
"${lcov_branch_args[@]}"
fi
run_ctest
"${LCOV_BIN}" --capture \
--directory "${BUILD_DIR}" \
--output-file "${TEST_INFO}" \
--gcov-tool "${GCOV_TOOL}" \
"${lcov_branch_args[@]}"
if [[ "${ENABLE_INITIAL_CAPTURE}" == "ON" ]]; then
"${LCOV_BIN}" \
--add-tracefile "${BASE_INFO}" \
--add-tracefile "${TEST_INFO}" \
--output-file "${TOTAL_INFO}" \
"${lcov_branch_args[@]}"
else
cp "${TEST_INFO}" "${TOTAL_INFO}"
fi
"${LCOV_BIN}" --remove "${TOTAL_INFO}" \
"${REMOVE_PATTERNS[@]}" \
--output-file "${FILTERED_INFO}" \
"${lcov_branch_args[@]}"
"${LCOV_BIN}" --list "${FILTERED_INFO}" "${lcov_branch_args[@]}"
rm -rf "${REPORT_DIR}"
"${GENHTML_BIN}" "${FILTERED_INFO}" \
--output-directory "${REPORT_DIR}" \
--title "Coverage Report" \
--legend \
--show-details \
--demangle-cpp \
"${genhtml_branch_args[@]}"
echo "coverage html report: ${REPORT_DIR}/index.html"
变量说明
| 变量 | 默认值 | 说明 |
|---|---|---|
PROJECT_ROOT |
当前目录 | 工程根目录,即顶层 CMakeLists.txt 所在目录 |
BUILD_DIR |
${PROJECT_ROOT}/build-coverage |
覆盖率构建目录 |
REPORT_DIR |
${PROJECT_ROOT}/coverage-html |
HTML 报告输出目录 |
CMAKE_BUILD_TYPE |
Debug |
CMake 构建类型 |
COVERAGE_OPTION_NAME |
ENABLE_COVERAGE |
工程中控制覆盖率编译的 CMake 选项名 |
COVERAGE_OPTION_VALUE |
ON |
覆盖率选项值 |
CTEST_LABEL |
空 | 只运行指定标签测试,空表示运行全部测试 |
CTEST_PARALLEL |
空 | CTest 并行数,空表示不显式指定 |
ENABLE_INITIAL_CAPTURE |
ON |
是否捕获初始覆盖率,用于显示未执行文件 |
ENABLE_BRANCH_COVERAGE |
ON |
是否启用分支覆盖率 |
GCOV_TOOL |
gcov |
lcov 调用的 gcov 工具,应与编译器工具链匹配 |
REMOVE_PATTERNS |
数组 | 从报告中排除系统头、测试代码、第三方依赖等路径 |
使用方式
保存为 coverage_lcov.sh 后执行:
chmod +x coverage_lcov.sh
./coverage_lcov.sh
指定工程路径和构建目录:
PROJECT_ROOT=/path/to/project \
BUILD_DIR=/path/to/project/build-coverage \
REPORT_DIR=/path/to/project/coverage-html \
./coverage_lcov.sh
只运行 CTest 中带 unit 标签的测试:
CTEST_LABEL=unit ./coverage_lcov.sh
关闭初始覆盖率捕获:
ENABLE_INITIAL_CAPTURE=OFF ./coverage_lcov.sh
指定 gcov 工具:
GCOV_TOOL=gcc-13-gcov ./coverage_lcov.sh
GCOV_TOOL=/opt/toolchain/bin/arm-none-eabi-gcov ./coverage_lcov.sh
GCOV_TOOL="llvm-cov gcov" ./coverage_lcov.sh
GCOV_TOOL 应尽量与生成 .gcno、.gcda 的编译器来自同一套工具链。例如 GCC 13 编译的覆盖率数据,优先使用 GCC 13 对应的 gcov;交叉编译工程应使用交叉工具链中的 *-gcov。
执行流程说明
- 检查
cmake、ctest、lcov、genhtml是否存在。 - 使用覆盖率选项配置 CMake 构建目录。
- 编译测试目标。
- 使用
lcov --zerocounters清零旧计数,避免历史数据污染。 - 可选执行
lcov --initial,让未执行文件也能进入报告。 - 运行 CTest,从而执行 GoogleTest 测试程序。
- 使用
lcov --capture收集测试后的覆盖率数据。 - 合并初始覆盖率和测试覆盖率。
- 使用
lcov --remove排除系统、第三方、测试目录等无关路径。 - 使用
genhtml生成 HTML 报告。
路径过滤建议
REMOVE_PATTERNS 应根据项目结构调整。常见过滤策略如下:
| 需要排除的内容 | 推荐模式 |
|---|---|
| 系统头文件 | /usr/*、*/usr/include/* |
| GoogleTest 和第三方依赖 | */third_party/*、*/_deps/* |
| 测试代码 | */tests/*、*/test/* |
| 构建生成目录 | */build-*/* |
如果希望只保留产品代码,也可以在 lcov --remove 后增加 lcov --extract,例如:
lcov --extract coverage.info "*/src/*" "*/include/*" --output-file coverage.product.info
常见问题
| 问题 | 可能原因 | 处理 |
|---|---|---|
| 报告为空 | 测试未运行或未生成 .gcda |
检查 CTest 是否执行、程序是否正常退出 |
| 没有未执行文件 | 未进行初始捕获 | 保持 ENABLE_INITIAL_CAPTURE=ON |
| 系统头文件进入报告 | 过滤规则不足 | 增加 REMOVE_PATTERNS |
| 分支覆盖率不显示 | 未启用分支参数 | 保持 ENABLE_BRANCH_COVERAGE=ON |
| 行号不准确 | 编译优化影响 | 使用 Debug、-O0、-g |
| gcov 版本不匹配 | gcov 与编译器不是同一工具链 |
设置 GCOV_TOOL |