|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
代码覆盖率分析是软件开发生命周期中至关重要的一环,它帮助开发团队评估测试用例的有效性,识别未被测试的代码区域,从而提高软件质量和可靠性。CMake作为一个跨平台的构建系统,提供了强大的功能来支持代码覆盖率分析,使开发人员能够轻松地将覆盖率检查集成到他们的构建和测试流程中。
本文将详细介绍如何使用CMake进行代码覆盖率分析,从基础配置到高级技巧,帮助您全面掌握项目测试质量评估方法。无论您是CMake的新手还是有经验的用户,本指南都将为您提供有价值的见解和实用的技巧。
代码覆盖率分析基础
在深入CMake配置之前,让我们先了解代码覆盖率分析的基本概念。
什么是代码覆盖率?
代码覆盖率是衡量测试用例执行过程中覆盖了多少源代码的指标。它通常以百分比表示,表示被测试执行的代码量占总代码量的比例。高代码覆盖率通常意味着测试更全面,但需要注意的是,100%的覆盖率并不一定意味着没有bug,它只是表示所有代码都已被执行过。
覆盖率类型
常见的代码覆盖率类型包括:
1. 行覆盖率(Line Coverage):测量有多少行代码被执行。
2. 分支覆盖率(Branch Coverage):测量代码中的每个分支(如if语句的true和false分支)是否都被执行。
3. 函数覆盖率(Function Coverage):测量有多少函数被调用。
4. 语句覆盖率(Statement Coverage):类似于行覆盖率,但更精确地计算每个语句的执行情况。
5. 路径覆盖率(Path Coverage):测量代码中可能的执行路径有多少被测试覆盖。
为什么代码覆盖率很重要?
• 识别未测试代码:帮助开发人员找到未被测试覆盖的代码区域。
• 提高代码质量:通过增加测试覆盖率,可以及早发现潜在的bug。
• 指导测试开发:为测试团队提供明确的测试目标。
• 评估测试效果:量化测试用例的有效性。
• 满足合规要求:某些行业和项目有特定的覆盖率要求。
CMake基础配置
CMake提供了多种方式来配置代码覆盖率分析。在本节中,我们将介绍如何设置CMake项目以支持基本的代码覆盖率分析。
创建支持覆盖率的基本CMake项目
首先,让我们创建一个简单的CMake项目结构:
- coverage_example/
- ├── CMakeLists.txt
- ├── src/
- │ ├── CMakeLists.txt
- │ └── math_functions.cpp
- └── tests/
- ├── CMakeLists.txt
- └── test_math_functions.cpp
复制代码- cmake_minimum_required(VERSION 3.10)
- project(coverage_example VERSION 1.0 LANGUAGES CXX)
- # 设置C++标准
- set(CMAKE_CXX_STANDARD 11)
- set(CMAKE_CXX_STANDARD_REQUIRED ON)
- # 添加编译选项,如果启用了覆盖率
- option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
- if(ENABLE_COVERAGE)
- # 添加覆盖率编译选项
- if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
- add_compile_options(-fprofile-arcs -ftest-coverage)
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
- elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
- add_compile_options(/profile)
- endif()
- endif()
- # 添加子目录
- add_subdirectory(src)
- add_subdirectory(tests)
复制代码- # 创建库
- add_library(math_functions math_functions.cpp)
- # 包含目录
- target_include_directories(math_functions
- PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..
- )
复制代码- #include "math_functions.h"
- int add(int a, int b) {
- return a + b;
- }
- int subtract(int a, int b) {
- return a - b;
- }
- int multiply(int a, int b) {
- return a * b;
- }
- double divide(int a, int b) {
- if (b == 0) {
- return 0; // 简化处理,实际应用中可能需要抛出异常
- }
- return static_cast<double>(a) / b;
- }
- bool is_prime(int n) {
- if (n <= 1) return false;
- if (n == 2) return true;
- if (n % 2 == 0) return false;
-
- for (int i = 3; i * i <= n; i += 2) {
- if (n % i == 0) {
- return false;
- }
- }
- return true;
- }
复制代码- # 查找并配置Google Test
- find_package(GTest REQUIRED)
- include_directories(${GTEST_INCLUDE_DIRS})
- # 创建测试可执行文件
- add_executable(test_math_functions test_math_functions.cpp)
- # 链接库
- target_link_libraries(test_math_functions
- math_functions
- ${GTEST_LIBRARIES}
- pthread
- )
- # 包含目录
- target_include_directories(test_math_functions
- PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..
- )
- # 启用测试
- enable_testing()
- # 添加测试
- add_test(NAME math_functions_test COMMAND test_math_functions)
复制代码- #include <gtest/gtest.h>
- #include "math_functions.h"
- TEST(MathFunctionsTest, Add) {
- EXPECT_EQ(add(2, 3), 5);
- EXPECT_EQ(add(-1, 1), 0);
- EXPECT_EQ(add(0, 0), 0);
- }
- TEST(MathFunctionsTest, Subtract) {
- EXPECT_EQ(subtract(5, 3), 2);
- EXPECT_EQ(subtract(1, 1), 0);
- EXPECT_EQ(subtract(0, 5), -5);
- }
- TEST(MathFunctionsTest, Multiply) {
- EXPECT_EQ(multiply(2, 3), 6);
- EXPECT_EQ(multiply(-1, 5), -5);
- EXPECT_EQ(multiply(0, 5), 0);
- }
- TEST(MathFunctionsTest, Divide) {
- EXPECT_DOUBLE_EQ(divide(10, 2), 5.0);
- EXPECT_DOUBLE_EQ(divide(5, 2), 2.5);
- EXPECT_DOUBLE_EQ(divide(0, 5), 0.0);
- // 注意:我们没有测试除以零的情况
- }
- TEST(MathFunctionsTest, IsPrime) {
- EXPECT_FALSE(is_prime(0));
- EXPECT_FALSE(is_prime(1));
- EXPECT_TRUE(is_prime(2));
- EXPECT_TRUE(is_prime(3));
- EXPECT_FALSE(is_prime(4));
- EXPECT_TRUE(is_prime(5));
- EXPECT_FALSE(is_prime(9));
- EXPECT_TRUE(is_prime(17));
- }
- int main(int argc, char **argv) {
- ::testing::InitGoogleTest(&argc, argv);
- return RUN_ALL_TESTS();
- }
复制代码
构建和运行测试
现在我们已经设置了基本的项目结构,可以构建并运行测试:
- # 创建构建目录
- mkdir build && cd build
- # 配置项目,启用覆盖率
- cmake -DENABLE_COVERAGE=ON ..
- # 构建项目
- make
- # 运行测试
- ./tests/test_math_functions
复制代码
集成覆盖率工具
在CMake中启用覆盖率编译选项后,我们需要使用特定的工具来收集覆盖率数据并生成报告。常用的覆盖率工具包括gcov、lcov和llvm-cov等。
使用gcov和lcov
gcov是GCC自带的覆盖率分析工具,而lcov是gcov的前端工具,可以生成更友好的HTML报告。
在Ubuntu/Debian系统上:
- sudo apt-get install gcovr lcov
复制代码
在CentOS/RHEL系统上:
在macOS系统上(使用Homebrew):
让我们修改主CMakeLists.txt文件,添加对lcov的支持:
- cmake_minimum_required(VERSION 3.10)
- project(coverage_example VERSION 1.0 LANGUAGES CXX)
- # 设置C++标准
- set(CMAKE_CXX_STANDARD 11)
- set(CMAKE_CXX_STANDARD_REQUIRED ON)
- # 添加编译选项,如果启用了覆盖率
- option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
- if(ENABLE_COVERAGE)
- # 添加覆盖率编译选项
- if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
- add_compile_options(-fprofile-arcs -ftest-coverage)
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
-
- # 添加lcov目标
- find_program(LCOV_EXECUTABLE lcov)
- find_program(GENHTML_EXECUTABLE genhtml)
-
- if(LCOV_EXECUTABLE AND GENHTML_EXECUTABLE)
- # 定义覆盖率目录
- set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage")
-
- # 创建覆盖率目标
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- COMMAND ${LCOV_EXECUTABLE} --directory . --capture --output-file ${COVERAGE_DIR}/coverage.info
- COMMAND ${LCOV_EXECUTABLE} --remove ${COVERAGE_DIR}/coverage.info '/usr/*' '${CMAKE_BINARY_DIR}/*' --output-file ${COVERAGE_DIR}/coverage.info.cleaned
- COMMAND ${GENHTML_EXECUTABLE} ${COVERAGE_DIR}/coverage.info.cleaned --output-directory ${COVERAGE_DIR}
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report"
- )
- endif()
- elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
- add_compile_options(/profile)
- endif()
- endif()
- # 添加子目录
- add_subdirectory(src)
- add_subdirectory(tests)
复制代码
现在,我们可以使用以下命令生成覆盖率报告:
- # 构建项目并运行测试
- mkdir build && cd build
- cmake -DENABLE_COVERAGE=ON ..
- make
- # 运行测试以生成覆盖率数据
- ./tests/test_math_functions
- # 生成覆盖率报告
- make coverage
复制代码
生成的HTML报告将位于build/coverage/index.html,您可以在浏览器中打开它查看详细的覆盖率信息。
使用gcovr
gcovr是另一个流行的覆盖率工具,它可以生成XML和HTML报告,并且与Jenkins等CI工具集成良好。
在Ubuntu/Debian系统上:
- sudo apt-get install gcovr
复制代码
在CentOS/RHEL系统上:
或者使用pip安装:
- cmake_minimum_required(VERSION 3.10)
- project(coverage_example VERSION 1.0 LANGUAGES CXX)
- # 设置C++标准
- set(CMAKE_CXX_STANDARD 11)
- set(CMAKE_CXX_STANDARD_REQUIRED ON)
- # 添加编译选项,如果启用了覆盖率
- option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
- if(ENABLE_COVERAGE)
- # 添加覆盖率编译选项
- if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
- add_compile_options(-fprofile-arcs -ftest-coverage)
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
-
- # 添加gcovr目标
- find_program(GCOVR_EXECUTABLE gcovr)
-
- if(GCOVR_EXECUTABLE)
- # 定义覆盖率目录
- set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage")
-
- # 创建覆盖率目标
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- COMMAND ${GCOVR_EXECUTABLE} --root ${CMAKE_SOURCE_DIR} --exclude ${CMAKE_BINARY_DIR} --html --html-details -o ${COVERAGE_DIR}/coverage.html
- COMMAND ${GCOVR_EXECUTABLE} --root ${CMAKE_SOURCE_DIR} --exclude ${CMAKE_BINARY_DIR} --xml -o ${COVERAGE_DIR}/coverage.xml
- COMMAND ${GCOVR_EXECUTABLE} --root ${CMAKE_SOURCE_DIR} --exclude ${CMAKE_BINARY_DIR} --sonarqube -o ${COVERAGE_DIR}/sonarqube.xml
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report with gcovr"
- )
- endif()
- elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
- add_compile_options(/profile)
- endif()
- endif()
- # 添加子目录
- add_subdirectory(src)
- add_subdirectory(tests)
复制代码
使用gcovr生成覆盖率报告的步骤与lcov类似:
- # 构建项目并运行测试
- mkdir build && cd build
- cmake -DENABLE_COVERAGE=ON ..
- make
- # 运行测试以生成覆盖率数据
- ./tests/test_math_functions
- # 生成覆盖率报告
- make coverage
复制代码
生成的HTML报告将位于build/coverage/coverage.html,XML报告位于build/coverage/coverage.xml,SonarQube兼容的报告位于build/coverage/sonarqube.xml。
使用llvm-cov
如果您使用的是Clang编译器,可以使用llvm-cov进行覆盖率分析。
- cmake_minimum_required(VERSION 3.10)
- project(coverage_example VERSION 1.0 LANGUAGES CXX)
- # 设置C++标准
- set(CMAKE_CXX_STANDARD 11)
- set(CMAKE_CXX_STANDARD_REQUIRED ON)
- # 添加编译选项,如果启用了覆盖率
- option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
- if(ENABLE_COVERAGE)
- # 添加覆盖率编译选项
- if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
- add_compile_options(-fprofile-arcs -ftest-coverage)
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
-
- # 添加lcov目标
- find_program(LCOV_EXECUTABLE lcov)
- find_program(GENHTML_EXECUTABLE genhtml)
-
- if(LCOV_EXECUTABLE AND GENHTML_EXECUTABLE)
- # 定义覆盖率目录
- set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage")
-
- # 创建覆盖率目标
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- COMMAND ${LCOV_EXECUTABLE} --directory . --capture --output-file ${COVERAGE_DIR}/coverage.info
- COMMAND ${LCOV_EXECUTABLE} --remove ${COVERAGE_DIR}/coverage.info '/usr/*' '${CMAKE_BINARY_DIR}/*' --output-file ${COVERAGE_DIR}/coverage.info.cleaned
- COMMAND ${GENHTML_EXECUTABLE} ${COVERAGE_DIR}/coverage.info.cleaned --output-directory ${COVERAGE_DIR}
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report"
- )
- endif()
- elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
- # 使用Clang的覆盖率标志
- add_compile_options(-fprofile-instr-generate -fcoverage-mapping)
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fprofile-instr-generate")
-
- # 添加llvm-cov目标
- find_program(LLVM_COV_EXECUTABLE llvm-cov)
- find_program(LLVM_PROFDATA_EXECUTABLE llvm-profdata)
-
- if(LLVM_COV_EXECUTABLE AND LLVM_PROFDATA_EXECUTABLE)
- # 定义覆盖率目录
- set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage")
-
- # 创建覆盖率目标
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- # 合并原始配置文件
- COMMAND ${LLVM_PROFDATA_EXECUTABLE} merge -sparse default.profraw -o ${COVERAGE_DIR}/coverage.profdata
- # 生成HTML报告
- COMMAND ${LLVM_COV_EXECUTABLE} show ./tests/test_math_functions -instr-profile=${COVERAGE_DIR}/coverage.profdata -format=html -output-dir=${COVERAGE_DIR} -show-line-counts-or-regions
- # 生成文本报告
- COMMAND ${LLVM_COV_EXECUTABLE} report ./tests/test_math_functions -instr-profile=${COVERAGE_DIR}/coverage.profdata > ${COVERAGE_DIR}/coverage.txt
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report with llvm-cov"
- )
- endif()
- elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
- add_compile_options(/profile)
- endif()
- endif()
- # 添加子目录
- add_subdirectory(src)
- add_subdirectory(tests)
复制代码
使用llvm-cov生成覆盖率报告的步骤:
- # 构建项目并运行测试
- mkdir build && cd build
- cmake -DENABLE_COVERAGE=ON ..
- make
- # 运行测试以生成原始配置文件
- LLVM_PROFILE_FILE="default.profraw" ./tests/test_math_functions
- # 生成覆盖率报告
- make coverage
复制代码
生成的HTML报告将位于build/coverage/index.html,文本报告位于build/coverage/coverage.txt。
生成覆盖率报告
在上一节中,我们已经集成了各种覆盖率工具并生成了基本的覆盖率报告。本节将更详细地介绍如何生成和解读覆盖率报告。
lcov报告详解
lcov生成的HTML报告非常直观,它提供了以下信息:
1. 总体覆盖率:显示行覆盖率、函数覆盖率和分支覆盖率的总体百分比。
2. 目录视图:按目录组织显示覆盖率信息。
3. 文件视图:显示每个源文件的详细覆盖率信息。
4. 源代码视图:在源代码中标记已执行和未执行的行。
• 绿色行:表示已执行的代码行。
• 红色行:表示未执行的代码行。
• 黄色标记:表示分支覆盖情况。
在我们的示例中,您可能会注意到divide函数中的除以零情况没有被测试覆盖,这显示为红色行。这提示我们需要添加相应的测试用例。
gcovr报告详解
gcovr生成的报告与lcov类似,但提供了一些额外的功能:
1. 多种输出格式:支持HTML、XML、文本和SonarQube格式。
2. 分支覆盖信息:详细显示每个分支的覆盖情况。
3. 汇总信息:提供项目整体覆盖率的汇总。
gcovr的HTML报告提供了与lcov类似的视图,但布局略有不同。它还提供了XML报告,可以与CI工具集成。
llvm-cov报告详解
llvm-cov生成的报告提供了以下信息:
1. 区域覆盖率:比行覆盖率更细粒度的覆盖率指标。
2. 函数覆盖率:显示每个函数的覆盖情况。
3. 行覆盖率:显示每行代码的执行次数。
llvm-cov的HTML报告非常详细,它显示了每行代码的执行次数,并用颜色区分覆盖情况。它还提供了函数级别的覆盖率信息。
高级技巧
现在我们已经掌握了基本的CMake覆盖率分析配置,让我们探索一些高级技巧,以进一步提高我们的覆盖率分析能力。
条件编译和覆盖率
有时,我们可能希望只在特定条件下启用覆盖率分析,例如在CI环境中或特定的构建类型中。
- # 在CMakeLists.txt中添加以下内容
- # 检查是否是CI环境
- if(DEFINED ENV{CI})
- set(ENABLE_COVERAGE ON CACHE BOOL "Enable coverage reporting in CI environment")
- endif()
- # 或者只在Debug构建中启用覆盖率
- if(CMAKE_BUILD_TYPE STREQUAL "Debug")
- set(ENABLE_COVERAGE ON CACHE BOOL "Enable coverage reporting in Debug builds")
- endif()
复制代码
排除特定文件或目录
在实际项目中,我们可能希望排除某些文件或目录的覆盖率分析,例如第三方代码或自动生成的文件。
- # 修改CMakeLists.txt中的coverage目标
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- # 捕获覆盖率数据
- COMMAND ${LCOV_EXECUTABLE} --directory . --capture --output-file ${COVERAGE_DIR}/coverage.info
- # 排除特定文件和目录
- COMMAND ${LCOV_EXECUTABLE} --remove ${COVERAGE_DIR}/coverage.info
- '/usr/*'
- '${CMAKE_BINARY_DIR}/*'
- '*/third_party/*'
- '*/tests/*'
- '*/mocks/*'
- --output-file ${COVERAGE_DIR}/coverage.info.cleaned
- # 生成HTML报告
- COMMAND ${GENHTML_EXECUTABLE} ${COVERAGE_DIR}/coverage.info.cleaned --output-directory ${COVERAGE_DIR}
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report"
- )
复制代码- # 修改CMakeLists.txt中的coverage目标
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- # 生成HTML报告,排除特定文件和目录
- COMMAND ${GCOVR_EXECUTABLE}
- --root ${CMAKE_SOURCE_DIR}
- --exclude ${CMAKE_BINARY_DIR}
- --exclude '*/third_party/*'
- --exclude '*/tests/*'
- --exclude '*/mocks/*'
- --html --html-details
- -o ${COVERAGE_DIR}/coverage.html
- # 生成XML报告
- COMMAND ${GCOVR_EXECUTABLE}
- --root ${CMAKE_SOURCE_DIR}
- --exclude ${CMAKE_BINARY_DIR}
- --exclude '*/third_party/*'
- --exclude '*/tests/*'
- --exclude '*/mocks/*'
- --xml
- -o ${COVERAGE_DIR}/coverage.xml
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report with gcovr"
- )
复制代码
设置覆盖率阈值
为了确保代码质量,我们可以设置覆盖率阈值,如果覆盖率低于阈值,则构建失败。
- # 修改CMakeLists.txt中的coverage目标
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- # 生成报告并检查覆盖率阈值
- COMMAND ${GCOVR_EXECUTABLE}
- --root ${CMAKE_SOURCE_DIR}
- --exclude ${CMAKE_BINARY_DIR}
- --html --html-details
- -o ${COVERAGE_DIR}/coverage.html
- --fail-under-line 80 # 行覆盖率至少80%
- --fail-under-branch 70 # 分支覆盖率至少70%
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report with gcovr"
- )
复制代码
合并多个测试运行的覆盖率数据
在大型项目中,我们可能有多个测试可执行文件,需要合并它们的覆盖率数据。
- # 假设我们有多个测试可执行文件
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
-
- # 为每个测试运行并收集数据
- COMMAND ${CMAKE_COMMAND} -E env LLVM_PROFILE_FILE="test1.profraw" ./tests/test_math_functions1
- COMMAND ${CMAKE_COMMAND} -E env LLVM_PROFILE_FILE="test2.profraw" ./tests/test_math_functions2
-
- # 合并原始配置文件
- COMMAND ${LLVM_PROFDATA_EXECUTABLE} merge -sparse test1.profraw test2.profraw -o ${COVERAGE_DIR}/coverage.profdata
-
- # 生成HTML报告
- COMMAND ${LLVM_COV_EXECUTABLE} show ./tests/test_math_functions1 -instr-profile=${COVERAGE_DIR}/coverage.profdata -format=html -output-dir=${COVERAGE_DIR} -show-line-counts-or-regions
-
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report"
- )
复制代码
使用自定义函数简化CMake配置
为了在多个项目中重用覆盖率配置,我们可以创建自定义CMake函数。
- # 在CMakeLists.txt中添加以下函数
- function(enable_code_coverage target_name)
- if(NOT ENABLE_COVERAGE)
- return()
- endif()
-
- if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
- target_compile_options(${target_name} PRIVATE -fprofile-arcs -ftest-coverage)
- set_target_properties(${target_name} PROPERTIES LINK_FLAGS "-fprofile-arcs -ftest-coverage")
- elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
- target_compile_options(${target_name} PRIVATE /profile)
- endif()
- endfunction()
- function(add_coverage_target)
- if(NOT ENABLE_COVERAGE)
- return()
- endif()
-
- # 定义覆盖率目录
- set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage")
-
- # 根据编译器选择适当的工具
- if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
- find_program(LCOV_EXECUTABLE lcov)
- find_program(GENHTML_EXECUTABLE genhtml)
-
- if(LCOV_EXECUTABLE AND GENHTML_EXECUTABLE)
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- COMMAND ${LCOV_EXECUTABLE} --directory . --capture --output-file ${COVERAGE_DIR}/coverage.info
- COMMAND ${LCOV_EXECUTABLE} --remove ${COVERAGE_DIR}/coverage.info '/usr/*' '${CMAKE_BINARY_DIR}/*' --output-file ${COVERAGE_DIR}/coverage.info.cleaned
- COMMAND ${GENHTML_EXECUTABLE} ${COVERAGE_DIR}/coverage.info.cleaned --output-directory ${COVERAGE_DIR}
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report"
- )
- endif()
- elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
- find_program(LLVM_COV_EXECUTABLE llvm-cov)
- find_program(LLVM_PROFDATA_EXECUTABLE llvm-profdata)
-
- if(LLVM_COV_EXECUTABLE AND LLVM_PROFDATA_EXECUTABLE)
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- COMMAND ${LLVM_PROFDATA_EXECUTABLE} merge -sparse default.profraw -o ${COVERAGE_DIR}/coverage.profdata
- COMMAND ${LLVM_COV_EXECUTABLE} show ./tests/test_math_functions -instr-profile=${COVERAGE_DIR}/coverage.profdata -format=html -output-dir=${COVERAGE_DIR} -show-line-counts-or-regions
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating coverage report with llvm-cov"
- )
- endif()
- endif()
- endfunction()
复制代码
然后,在项目中使用这些函数:
- # 为库启用覆盖率
- add_library(math_functions math_functions.cpp)
- enable_code_coverage(math_functions)
- # 为测试启用覆盖率
- add_executable(test_math_functions test_math_functions.cpp)
- enable_code_coverage(test_math_functions)
- # 添加覆盖率目标
- add_coverage_target()
复制代码
使用CMake的CTest模块集成覆盖率测试
CMake的CTest模块提供了测试管理功能,我们可以利用它来集成覆盖率测试。
- # 在CMakeLists.txt中添加以下内容
- # 启用CTest
- include(CTest)
- if(ENABLE_COVERAGE)
- # 添加覆盖率测试
- add_test(NAME coverage_test
- COMMAND ${CMAKE_MAKE_PROGRAM} coverage
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- )
-
- # 设置测试属性
- set_tests_properties(coverage_test PROPERTIES
- LABELS "coverage"
- REQUIRED_FILES "${CMAKE_BINARY_DIR}/coverage/index.html"
- )
- endif()
复制代码
然后,可以使用CTest运行所有测试,包括覆盖率测试:
- # 运行所有测试
- ctest
- # 只运行覆盖率测试
- ctest -R coverage
复制代码
最佳实践
在本节中,我们将介绍一些使用CMake进行代码覆盖率分析的最佳实践,帮助您在项目中有效地实施覆盖率分析。
在CI/CD中集成代码覆盖率分析
持续集成/持续部署(CI/CD)是现代软件开发的核心实践,将代码覆盖率分析集成到CI/CD流程中可以确保代码质量。
在Jenkins中,您可以使用以下步骤集成代码覆盖率分析:
1. 安装必要的插件,如”Cobertura Plugin”或”Coverage Plugin”。
2. 在Jenkinsfile中添加覆盖率分析步骤:
- pipeline {
- agent any
- stages {
- stage('Build and Test') {
- steps {
- sh 'mkdir -p build && cd build'
- sh 'cmake -DENABLE_COVERAGE=ON ..'
- sh 'make'
- sh 'ctest --output-on-failure'
- }
- }
- stage('Generate Coverage') {
- steps {
- sh 'cd build && make coverage'
- }
- post {
- always {
- // 发布Cobertura格式的覆盖率报告
- publishCoverage adapters: [coberturaAdapter('build/coverage/coverage.xml')], sourceFileResolver: sourceFiles('STORE_LAST_BUILD')
-
- // 存储HTML报告
- publishHTML([
- allowMissing: false,
- alwaysLinkToLastBuild: true,
- keepAll: true,
- reportDir: 'build/coverage',
- reportFiles: 'index.html',
- reportName: 'Coverage Report'
- ])
- }
- }
- }
- }
- }
复制代码
在GitHub Actions中,您可以创建一个工作流文件来运行覆盖率分析:
- name: CI with Coverage
- on: [push, pull_request]
- jobs:
- build-and-test:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
-
- - name: Install dependencies
- run: |
- sudo apt-get update
- sudo apt-get install -y lcov gcovr
-
- - name: Configure and build
- run: |
- mkdir -p build && cd build
- cmake -DENABLE_COVERAGE=ON ..
- make
-
- - name: Run tests
- run: |
- cd build
- ctest --output-on-failure
-
- - name: Generate coverage report
- run: |
- cd build
- make coverage
-
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v1
- with:
- file: ./build/coverage/coverage.xml
- flags: unittests
- name: codecov-umbrella
- fail_ci_if_error: true
复制代码
在GitLab CI中,您可以创建一个.gitlab-ci.yml文件:
- stages:
- - build
- - test
- - coverage
- build:
- stage: build
- script:
- - mkdir -p build
- - cd build
- - cmake -DENABLE_COVERAGE=ON ..
- - make
- test:
- stage: test
- script:
- - cd build
- - ctest --output-on-failure
- artifacts:
- paths:
- - build/
- coverage:
- stage: coverage
- script:
- - cd build
- - make coverage
- artifacts:
- reports:
- cobertura: build/coverage/coverage.xml
- paths:
- - build/coverage/
复制代码
设置覆盖率目标
为项目设置合理的覆盖率目标是很重要的。这些目标应该基于项目的重要性、复杂性和团队的能力。
不要试图一次性达到100%的覆盖率。相反,设置渐进式的目标:
1. 初始目标:达到60%的行覆盖率。
2. 中期目标:达到80%的行覆盖率和70%的分支覆盖率。
3. 长期目标:达到90%的行覆盖率和85%的分支覆盖率。
核心业务逻辑应该有更高的覆盖率要求,而辅助代码(如日志记录、UI代码)可以有较低的覆盖率要求。
- # 在CMakeLists.txt中设置不同模块的不同覆盖率阈值
- add_custom_target(core_coverage
- COMMAND ${GCOVR_EXECUTABLE}
- --root ${CMAKE_SOURCE_DIR}
- --filter '*/core/*'
- --fail-under-line 90
- --fail-under-branch 85
- --html --html-details
- -o ${COVERAGE_DIR}/core_coverage.html
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating core coverage report"
- )
- add_custom_target(utils_coverage
- COMMAND ${GCOVR_EXECUTABLE}
- --root ${CMAKE_SOURCE_DIR}
- --filter '*/utils/*'
- --fail-under-line 70
- --fail-under-branch 60
- --html --html-details
- -o ${COVERAGE_DIR}/utils_coverage.html
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Generating utils coverage report"
- )
复制代码
定期审查覆盖率报告
仅仅生成覆盖率报告是不够的,定期审查这些报告并采取行动才是关键。
1. 每周审查:每周安排一次会议,审查覆盖率报告,识别未覆盖的代码区域。
2. 任务分配:为团队成员分配任务,为未覆盖的代码编写测试。
3. 跟踪进度:使用图表跟踪覆盖率随时间的变化,确保持续改进。
结合其他质量指标
代码覆盖率只是代码质量的一个方面,结合其他质量指标可以获得更全面的视图:
1. 静态代码分析:使用工具如Cppcheck、Clang-Tidy等。
2. 动态分析:使用工具如Valgrind、AddressSanitizer等。
3. 代码复杂度:监控圈复杂度等指标。
- # 在CMakeLists.txt中集成多种质量检查工具
- if(ENABLE_CODE_QUALITY)
- # 添加静态分析
- find_program(CPPCHECK_EXECUTABLE cppcheck)
- if(CPPCHECK_EXECUTABLE)
- add_custom_target(cppcheck
- COMMAND ${CPPCHECK_EXECUTABLE}
- --enable=all
- --inconclusive
- --xml
- --xml-version=2
- ${CMAKE_SOURCE_DIR}/src 2> cppcheck.xml
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- COMMENT "Running static analysis with cppcheck"
- )
- endif()
-
- # 添加动态分析
- if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
- add_compile_options(-fsanitize=address -fsanitize=undefined)
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address -fsanitize=undefined")
- endif()
-
- # 添加质量检查目标
- add_custom_target(quality_check
- DEPENDS coverage cppcheck
- COMMENT "Running all quality checks"
- )
- endif()
复制代码
常见问题和解决方案
在使用CMake进行代码覆盖率分析时,您可能会遇到一些常见问题。本节将介绍这些问题及其解决方案。
问题1:覆盖率数据不准确
症状:生成的覆盖率报告显示的覆盖率与预期不符,某些已执行的代码被标记为未执行。
可能原因:
• 编译优化导致代码被优化掉或重新排序。
• 覆盖率数据文件(.gcda或.profraw)没有正确生成或合并。
• 源代码路径与构建路径不匹配。
解决方案:
1. 禁用编译优化:
- if(ENABLE_COVERAGE)
- if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
- add_compile_options(-O0 -fprofile-arcs -ftest-coverage)
- set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
- endif()
- endif()
复制代码
1. 确保在运行测试前删除旧的覆盖率数据:
- add_custom_target(coverage
- COMMAND ${CMAKE_COMMAND} -E remove_directory ${COVERAGE_DIR}
- COMMAND mkdir -p ${COVERAGE_DIR}
- # ... 其余命令 ...
- )
复制代码
1. 使用绝对路径或确保路径一致性:
- # 使用gcovr时指定根目录
- add_custom_target(coverage
- COMMAND ${GCOVR_EXECUTABLE}
- --root ${CMAKE_SOURCE_DIR}
- --html --html-details
- -o ${COVERAGE_DIR}/coverage.html
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- )
复制代码
问题2:多目录项目中的覆盖率问题
症状:在多目录项目中,某些目录的代码没有被正确包含在覆盖率报告中。
可能原因:
• 覆盖率工具没有正确递归到所有子目录。
• 某些目标没有启用覆盖率编译选项。
解决方案:
1. 确保所有目标都启用了覆盖率:
- # 创建一个函数来为所有目标启用覆盖率
- function(enable_coverage_for_all_targets)
- if(ENABLE_COVERAGE)
- get_all_targets(all_targets ${CMAKE_CURRENT_SOURCE_DIR})
- foreach(target ${all_targets})
- if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
- target_compile_options(${target} PRIVATE -fprofile-arcs -ftest-coverage)
- set_target_properties(${target} PROPERTIES LINK_FLAGS "-fprofile-arcs -ftest-coverage")
- endif()
- endforeach()
- endif()
- endfunction()
- # 获取所有目标的辅助函数
- function(get_all_targets var dir)
- get_property(subdirectories DIRECTORY ${dir} PROPERTY SUBDIRECTORIES)
- foreach(subdir ${subdirectories})
- get_all_targets(${var} ${subdir})
- endforeach()
- get_property(current_targets DIRECTORY ${dir} PROPERTY BUILDSYSTEM_TARGETS)
- set(${var} ${current_targets} PARENT_SCOPE)
- endfunction()
复制代码
1. 在覆盖率分析命令中包含所有相关目录:
- add_custom_target(coverage
- COMMAND ${GCOVR_EXECUTABLE}
- --root ${CMAKE_SOURCE_DIR}
- --html --html-details
- -o ${COVERAGE_DIR}/coverage.html
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- )
复制代码
问题3:CI环境中的覆盖率问题
症状:在本地环境中覆盖率工作正常,但在CI环境中失败或产生不准确的结果。
可能原因:
• CI环境中缺少必要的工具或依赖项。
• CI环境的构建路径与本地不同。
• CI环境中的并行执行导致覆盖率数据冲突。
解决方案:
1. 在CI脚本中安装必要的工具:
- # GitHub Actions示例
- - name: Install dependencies
- run: |
- sudo apt-get update
- sudo apt-get install -y lcov gcovr
复制代码
1. 使用相对路径或环境变量来处理路径差异:
- # 使用CMAKE_CURRENT_SOURCE_DIR和CMAKE_BINARY_DIR
- add_custom_target(coverage
- COMMAND ${GCOVR_EXECUTABLE}
- --root ${CMAKE_SOURCE_DIR}
- --exclude ${CMAKE_BINARY_DIR}
- --html --html-details
- -o ${CMAKE_BINARY_DIR}/coverage/coverage.html
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- )
复制代码
1. 为每个测试使用唯一的覆盖率数据文件:
- # 为每个测试创建唯一的覆盖率数据文件
- foreach(test ${TESTS})
- add_test(NAME ${test}
- COMMAND ${CMAKE_COMMAND} -E env LLVM_PROFILE_FILE="${test}.profraw" $<TARGET_FILE:${test}>
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- )
- endforeach()
- # 合并所有覆盖率数据文件
- add_custom_target(coverage
- COMMAND mkdir -p ${COVERAGE_DIR}
- # 合并所有.profraw文件
- COMMAND ${LLVM_PROFDATA_EXECUTABLE} merge -sparse *.profraw -o ${COVERAGE_DIR}/coverage.profdata
- # 生成报告
- COMMAND ${LLVM_COV_EXECUTABLE} show $<TARGET_FILE:${TESTS}> -instr-profile=${COVERAGE_DIR}/coverage.profdata -format=html -output-dir=${COVERAGE_DIR}
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- )
复制代码
问题4:第三方库的覆盖率问题
症状:覆盖率报告包含大量第三方库的代码,稀释了项目本身的覆盖率数据。
可能原因:
• 覆盖率工具没有排除第三方库的代码。
解决方案:
1. 明确排除第三方库:
- add_custom_target(coverage
- COMMAND ${GCOVR_EXECUTABLE}
- --root ${CMAKE_SOURCE_DIR}
- --exclude '*/third_party/*'
- --exclude '*/external/*'
- --html --html-details
- -o ${COVERAGE_DIR}/coverage.html
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- )
复制代码
1. 使用模式匹配排除特定目录:
- add_custom_target(coverage
- COMMAND ${LCOV_EXECUTABLE} --directory . --capture --output-file ${COVERAGE_DIR}/coverage.info
- COMMAND ${LCOV_EXECUTABLE} --remove ${COVERAGE_DIR}/coverage.info
- '/usr/*'
- '${CMAKE_BINARY_DIR}/*'
- '*/third_party/*'
- '*/external/*'
- '*/boost/*'
- '*/gtest/*'
- --output-file ${COVERAGE_DIR}/coverage.info.cleaned
- COMMAND ${GENHTML_EXECUTABLE} ${COVERAGE_DIR}/coverage.info.cleaned --output-directory ${COVERAGE_DIR}
- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
- )
复制代码
问题5:模板代码的覆盖率问题
症状:模板代码的覆盖率不准确或不完整。
可能原因:
• 模板代码的实例化可能导致覆盖率数据不准确。
• 编译器可能为模板代码生成额外的代码,这些代码在源文件中不可见。
解决方案:
1. 确保模板代码的所有实例化都被测试覆盖:
- // 在测试文件中显式实例化模板
- template class MyTemplateClass<int>;
- template class MyTemplateClass<double>;
复制代码
1. 使用编译器特定的选项来改善模板代码的覆盖率:
- if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
- add_compile_options(-fno-inline-small-functions -fno-inline-functions-called-once)
- elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
- add_compile_options(-fno-inline)
- endif()
复制代码
结论
代码覆盖率分析是确保软件质量的重要工具,而CMake提供了强大的功能来支持这一过程。通过本文,我们详细介绍了如何使用CMake进行代码覆盖率分析,从基础配置到高级技巧。
我们首先了解了代码覆盖率的基础知识,包括不同类型的覆盖率指标及其重要性。然后,我们介绍了如何设置CMake项目以支持代码覆盖率分析,包括如何集成常用的覆盖率工具如gcov、lcov和llvm-cov。
我们还探讨了如何生成和解读覆盖率报告,以及如何使用高级技巧来提高覆盖率分析的效率和准确性。这些技巧包括条件编译、排除特定文件或目录、设置覆盖率阈值、合并多个测试运行的覆盖率数据等。
此外,我们还介绍了在CI/CD环境中集成代码覆盖率分析的最佳实践,以及如何设置合理的覆盖率目标。最后,我们讨论了一些常见问题及其解决方案,帮助您在使用CMake进行代码覆盖率分析时避免陷阱。
通过正确实施代码覆盖率分析,您可以:
1. 提高代码质量:通过识别未测试的代码区域,可以及早发现潜在的bug。
2. 指导测试开发:为测试团队提供明确的测试目标。
3. 评估测试效果:量化测试用例的有效性。
4. 满足合规要求:满足特定行业或项目的覆盖率要求。
记住,代码覆盖率只是衡量测试质量的指标之一,它应该与其他质量指标结合使用,如静态代码分析、动态分析和代码复杂度分析等。通过综合使用这些工具和技术,您可以建立一个全面的软件质量保证体系。
希望本文能帮助您掌握使用CMake进行代码覆盖率分析的方法,并在您的项目中有效地实施这些技术。 |
|