在疫情背景下各大公司都有所异动,toB 的团队企业内卷也越来越明显。此时此刻如果团队中的产品又出现各种低级问题无疑是雪上加霜。本文围绕团队在产品质量攻坚工作中做的一些质量检查手段,介绍如何让你团队的代码质量可以量化,并保留最珍贵、可维护、可持续、可传承的工程化代码。
整合目标
本文除了让大家了解这些工具如何使用以外,还会重点描述如何组织这些代码质量的周边工具链使其达到工程化程度,讨论什么该做,什么不该做,为什么这么做。我对所谓 工程化 有以下几个明确的定义:
- 可维护:贴近 GitOps,尽量将所有可变配置放到代码仓库,而不是分散维护,Everything is code.
- 可持续:不是应付一次检查或攻坚,而是形成常态
- 可传承:新人只关注代码,不关注质量工具的配置细节,通过类似于 MRs 的结果反馈不断改进自身代码质量
一些涉及到权限控制的位置(如代码质量阈设置)是需要有管理员把控的,要实施这些步骤首先团队的技术管理负责人要有这些认知并且能在关键点拍板。如果仅认为这些东西有了就够了恐怕是很难实施下去的! 本文示例均已一个简单的 ne-phoenix 基础库代码作为示例,介绍围绕该工程展开的质量突击。
准备工具
- clang-tidy、infer 用于静态代码检查
- lcov 用于统计单元测试代码覆盖率
- gcovr 用于生成覆盖率报告及转为 SonarQube 支持的报告格式
- sonar-scanner 用于传送以上工具的报告结果到 SonarQube 平台(平台搭建请参考官方文档)
- pre-commit 可选,用于提交时本地执行静态代码检查
这些工具在 macOS 中均可通过 brew 来进行安装,比较特殊的是 clang-tidy,它在 LLVM 工具链中,您需要在 brew install llvm 后再通过 brew link llvm 按提示将可执行文件添加到环境变量中,使脚本可以直接访问到 clang-tidy 可执行程序。
Code coverage
单元测试、API 测试、集成测试,只听这些概念就足够让我们晕头转向,但无论如何,我一直很认同一句话:没有覆盖率统计的测试就是耍流氓。即便你提供了所谓每天的自动化测试报告,貌似可以量化,但真正的作用谁有知道呢?虽然覆盖率统计并不能代表代码就是 100% 可靠的。但它可以通过量化的数据告诉我们代码的哪些分支、哪些逻辑我们还没有覆盖,至少能让你知道,你的测试是不是在做一些无意义的事情。 在 ne-phoenix 基础库中,我们以 CMake + Conan 驱动整个工程的编译,单元测试的框架使用了 Google Test。要统计执行测试程序后对代码的覆盖情况,我们要做以下几点工作:
- 增加编译选项为 coverage 做准备
- lcov 初始化一次基础扫描
- 编译并运行测试可执行程序
- lcov 扫描执行测试程序后的结果捕获覆盖到的代码情况
- lcov 与基础报告对比生成结果
这个步骤比较繁琐,我们找到了一个开源的 CMake 插件 CodeCoverage.cmake,有了这个插件,您只需要在您的工程中添加几行 CMake 代码即可实现覆盖率统计能力:
1 | if (APPLE) |
首先在脚本开始判断平台并引入 CodeCoverage.cmake 插件,调用 append_coverage_compiler_flags()
接口全局添加统计代码覆盖率所需的编译选项。随后调用 setup_target_for_coverage_lcov()
添加一个自定义 CMake 目标用来执行并输出覆盖率统计报告,它的参数分别如下:
NAME
在 CMake 中生成的自定义目标名称SONARQUBE
是否生成 SonarQube 兼容的覆盖率统计报告BASE_DIRECTORY
要统计覆盖率源码的起始目录EXECUTABLE
执行测试的程序,这里使用接入了 Google Test 的可执行程序EXECUTABLE_ARGS
执行测试程序是的命令行参数,用于生成 GTest 结果报告为 xml 上报给 GitLabEXCLUDE
在报告中排除一些不需要的目录
添加完成后只需要如下两条命令,就可以自动在 CMake 缓存目录生成覆盖率统计报告了:
1 | # 初始化工程为 Debug |
生成 coverage 目标完成后,该目标会自动化执行测试程序并统计出报告:
1 | Writing directory view page. |
我们打开 build/coverage/index.html 就可以看到完整的覆盖率情况了: 点击某个文件进入可查看当前测试程序覆盖到了哪些条件判断,其中红色的表示你的测试程序没有覆盖到该位置的代码: 除了可视化的 html 查看覆盖率报告外,还输出了 SonarQube 兼容的 xml 格式报告 build/coverage_sonarqube.xml 文件,稍后我们介绍如何将该文件上传到 SonarQube 平台用以统计。 同时测试程序的成功、失败情况也输出在了 build/result.xml 中,稍后我们介绍如何将该文件上传到 GitLab 展示。
Code static analyzer
无论颗粒度是怎样的测试不仅能帮助我们发现业务流程中的问题,也能让我们尽快发现代码实现上的问题。但代码质量、可读性、可扩展性这些都是无法得知的,这些可以通过静态代码检查来实现。 Google 团队在 Chromium 项目中很早就应用了诸多静态代码检查工具,有的是依赖编译的,有的是通过正则模式分析的,各有优劣。仰仗于各个大厂和开源社区的努力,周边工具链越来越给力,类 clang-tidy、infer 的工具,不仅能实现完整的静态代码检查,还可以完全替代以前的正则类扫描工具如 cpplint 等。本文以 clang-tidy 分析 C++ 代码举例,让我们一起了解如何从头分析一个完整的工程。 如果你是 CMake 工程,做到这件事情非常简单,只需要在 CMake 初始化工程时增加参数:-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
,该参数的增加会告知 CMake 将所有源文件的编译选项写入到一个名为 compile_commands.json 的 json 文件中。后续所有的静态代码检查都是基于该文件进行的。 以 ne-phoenix 工程举例,首先使用如下命令生成工程配置:
1 | cmake -Bbuild -DCMAKE_BUILD_TYPE=Debug \ |
compile_commands.json
会生成到 CMake 的缓存目录 build 下,然后调用 clang-tidy 命令,对你关注的文件进行分析:
1 | clang-tidy -p=build main.cpp |
clang-tidy 有默认的规则是启用所有,如果你希望控制规则细节,可在工程目录放置一个 .clang-tidy 配置文件,内容类似如下:
1 | Checks: > |
该规则参考了 Google Cloud 开源项目配置,所有配置项可参考 LLVM 官网:Extra Clang Tools 上面单独调用 clang-tidy 只能分析单个文件,如果要分析多个文件,我们要把所有文件依次传给 clang-tidy。这样命令写起来比较繁琐且不易阅读。更重要的是无法实现多个实例同时对多个文件进行检查,效率极低。LLVM 工具链中早就想好了这些问题,他们提供了 run-clang-tidy.py 提供我们进行批量分析,见:LLVM run-clang-tidy.py。有了这个脚本,我们就可以批量进行分析了:
1 | python3 .build/run-clang-tidy.py -p=build -j 8 > build/clang-tidy-output.txt |
-p
表示指定 compile_commands.json 文件所在目录-j 8
表示最多可以同时执行 8 路分析不同的文件,加快分析速度
分析时间视工程目录源码文件多少而定,如果代码中使用的模板较多,分析的时间会较长。分析完成后会在 build 目录下生成名为 clang-tidy-output.txt
的分析结果,手动打开该文件你就可以可以看到一分析的错误信息了。接下来就是将这个报告上传到 SonarQube 平台。
SonarQube 集成
由于考虑篇幅问题,这里不详细介绍 SonarQube 的部署及多分支插件的安装,这部分资料官网和 StackOverflow 资料非常多,大家可参考搭建部署。在上传报告前参考文章开头,先安装好 sonar-scanner 上传工具。将项目的配置信息保存到名为 sonar-project.properties
的配置文件中并存放到项目根目录下,内容类似:
1 | # must be unique in a given SonarQube instance |
各个参数用途如下:
- sonar.host.url SonarQube 服务器地址
- sonar.projectKey SonarQube 平台上创建的工程名
- sonar.login SonarQube 上传 token
- sonar.qualitygate.wait 表示等待 SonarQube 返回扫描结果
- sonar.source 表示源码是当前根目录
- sonar.sourceEncodin 表示以 UTF-8 格式分析报告
- sonar.cxx.file.suffixes 表示要分析的 C++ 文件后缀
- sonar.lang.patterns.objc 表示要分析的 Objective-C 代码的文件后缀
- sonar.cxx.clangtidy.reportPaths 表示要上报的 clang-tidy 分析报告
- sonar.exclusions 表示要排除的目录,包括测试覆盖率、静态分析结果
上传时只需要调用 sonar-scanner 即可将当前分支信息上报到 SonarQube 平台,如果一切顺利,将会返回如下内容:
1 | INFO: ------------------------------------------------------------------------ |
此时再次打开 SonarQube 平台,就可以看到分析结果了(一定要安装 cxx-community 插件并应用 clang-tidy 规则): SonarQube 支持设置每个工程的质量阈,如果您的团队短时间内无法对新代码实现高的覆盖率,可适当调整质量阈,以管理员身份登陆 SonarQube,点击上方菜单的 Quility Gates: 内置有默认的质量阈,代码覆盖率的要求达到了 80%,您可以自己手动新建一个质量与并在单独的工程设置中选择你自己创建的质量阈。
CI 集成
GitLab 测试报告集成
GitLab 和 SonarQube 都支持展示测试覆盖率统计结果,GitLab 还可以把测试的所有子项内容展示在 Pipeline 结果页中: GitLab 展示测试覆盖率: 要显示这些内容在 GitLab 上非常简单,你只需在 gitlab-ci.yml 中将 GTest 测试结果的 result.xml 当作 Artifacts 上传到 GitLab 即可:
1 | stages: |
命令跟我们本地测试一样,先初始化 CMake 工程,然后编译 coverage 项目,编译时会自动执行代码覆盖率检查并输出 result.xml,最后调用 lcov 在终端输出了覆盖率报告内容,类似如下效果:
1 | $ lcov --list build/coverage.info |
最后的 Total:80.6%
就是总的覆盖率情况,这一步很重要,我们要在 GitLab 中添加一段正则代码,匹配最终的结果,GitLab 会在 Job 执行完成后从输出内容中正则匹配到对应内容并显示到 GitLab Job 结果页面,打开 Project->Settings->CI/CD
页面,展开 General pipelines
选项卡,在最下方的 Test coverage parsing
中输入如下正则,即可匹配到覆盖率统计数据:
1 | Total:\(\d+\.?\d+\%) |
如下图所示: 添加后 Save 保存,下一次触发 Pipeline 就会自动上报覆盖率百分比结果。
SonarQube 测试覆盖率集成
要上传测试覆盖率到 SonarQube 只需要在 sonar-project.properties 的配置文件中添加一行上报之前生成的 sonarqube_coverage.xml 即可:
1 | # must be unique in a given SonarQube instance |
主意倒数第二行。通过次方式上传报告后,在 SonarQube 平台就可以展示测试覆盖率百分比的情况了: SonarQube 平台支持设置统一的质量阈,当你的代码发现 Major 级别以上的错误又或者覆盖率达不到一定百分比,则 SonarQube 会回报给 GitLab 告诉他本次 MR 不通过并创建一个临时的错误 Job: 而如果一切正常,也会创建一个反馈入口: 点击即可直达本次 Merge request 的分析详情。
Merge request 增量代码静态检查
如果你实操过代码静态检查,你会发现在你庞大的工程中做一次代码静态分析的时间成本是非常昂贵的,我们不可能也不允许在每次 CI 阶段都要等待这么长的时间,符合逻辑的场景应该是只检查本次变更,如果能细化到代码行就更好了。可以实现吗?当然! LLVM 工具链提供了一个脚本 clang-tidy-diff.py,它可以实现细化到代码行。用于帮助我们在 CI 集成时对增量数据进行检查。下载该脚本保存到项目工程的 .build 目录下。 同样的在使用 clang-tidy-diff.py 脚本进行增量分析时,也是需要生成整个项目的 compile_commands.json
配置文件。该脚本只是将我们变更的文件列表通过参数的方式传递给 clang-tidy 可执行文件。然后到 compile_commands.json
中查找这些文件的编译指令来进行静态代码检查。生成请参考上方全量分析命令。 在发起一个 Merge request 或者 Pull request 时,一些 CI 集成工具都会帮我们收集要合并的分支已经合并的目标分支信息。通过这两个分支我们就可以确定下来修改的文件有多少。以下 git 命令可以展示从开启新的功能分支后所有的提交及文件变更信息:
1 | git diff -U0 feature/new-feature origin/develop^ |
以 GitLab CI 举例,假如 feature/new-feature 是基于 develop 开出的新功能分支,当发起 Merge request 时,会有两个变量分别描述这两个分支的信息。
- ${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} 用来描述新的 feature 分支
- ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME} 用来描述目标合并分支
连带上面的已经实践过的 gitlab-ci.yml 代码,在 GitLab .gitlab-ci.yml 中我们可以这样编写脚本:
1 | coverage: |
注意第 10,11 行,我们通过 git diff 将变更信息传递给 clang-tidy-diff.py 脚本,该脚本会自己分析 git diff 结果对变更文件进行静态代码检查。同时在使用 sonar-scanner 进行扫描时我们也传递对应的源分支、目标分支信息,SonarQube 会创建一个 Merge Request 的分支分析信息提供您查看。 需要注意的是给 git diff 传递 base 时需要指定 origin 使用远端分支,通常情况下 GitLab CI 只会 checkout 你要编译的分支,本地可能不存在 base 分支的代码,无法进行比较。如 origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}^ 这样在上报到 SonarQube 平台后我们就可以按分支查看分析报告了(提要安装好多分支插件 branch-plugin): 查看某个 MR 或者某个分支的数据是单独显示的。
Merge request 反馈
数据上报到 SonarQube 平台后,我们每次都要人工去这个平台查看反馈报告,这样非常不方便,幸运的是 SonarQube 提供了 SCM 平台反馈能力,以管理员身份登录 SonarQube 平台,设置 GitLab 的配置如下(前提要安装好多分支插件 branch-plugin): 确保配置没有问题后,选择一个你的项目,进入项目设置页面,输入项目 ID 并选择刚才配置好的 GitLab API 保存: 确认连接无问题后保存,再次触发某个 Pipeline 并上报结果到 SonarQube 后,SonarQube 平台会调用 GitLab 提供的 API 将问题数据回报给每个 MR,并且在你有问题的代码中添加评论,效果如下:
Pre-commit 集成
如果你的团队启用了 pre-commit-hooks,您可以添加如下脚本,在每次提交时就检查一次变更的文件,这样在没有上传代码到 GitLab 时就可以及时的发现问题:
1 | repos: |
最后两行描述了如何在提交时进行 clang-tidy 的检查。pre-commit 相关信息请见文章最后扩展阅读。
总结
日常开发中一些 IDE 的辅助工具可以帮助我们随写随发现问题,如 VSCode 的 clang-tidy 检查、CLion 自带 clang-tidy 检查、VS IDE clang-tidy 检查等。通过 IDE 自带的测试工具如 VS Code TestMate、VS IDE 的 Test Explorer 都可以帮助我们本地执行单元测试、API 测试代码。这些可个根据不同开发者不同的开发环境需求而定,并不是强制要求。我们只要在 GitLab 做收口即可。 至此,代码质量相关工具工程化基本结束,我们从代码提交到 CI 再到 SonarQube 报告最后到反馈全流程均通过仓库代码配置文件的方式实现,符合我们预期的想法。在未来维护和扩展中给后来者提供了非常详尽的历史,将最有价值的数据留给他们。这也是写这篇文章的初衷。
扩展阅读
sonarqube branch plugin:https://blog.csdn.net/ewferferr/article/details/120432746 sonarqube C++ plugin:https://blog.csdn.net/qq\_15559817/article/details/100736498 clang-tidy:https://hokein.github.io/clang-tools-tutorial/clang-tidy.html pre-commit:https://pre-commit.com/