Skip to content

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。

执行流程说明

  1. 检查 cmake、ctest、lcov、genhtml 是否存在。
  2. 使用覆盖率选项配置 CMake 构建目录。
  3. 编译测试目标。
  4. 使用 lcov --zerocounters 清零旧计数,避免历史数据污染。
  5. 可选执行 lcov --initial,让未执行文件也能进入报告。
  6. 运行 CTest,从而执行 GoogleTest 测试程序。
  7. 使用 lcov --capture 收集测试后的覆盖率数据。
  8. 合并初始覆盖率和测试覆盖率。
  9. 使用 lcov --remove 排除系统、第三方、测试目录等无关路径。
  10. 使用 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