测试、静态分析与 Sanitizer 门禁 / Testing, Analysis, and Sanitizer Gates
1. 质量门禁要覆盖不同风险层
很多 C++ 项目把“能编译”和“跑了几个单测”当作质量结论。工程上更可靠的做法是把不同工具放到不同风险层:未运行路径靠静态分析,已运行路径靠 sanitizer,输入空间靠 fuzz,性能退化靠 benchmark。
2. 最简单心智模型
质量门禁不是一个工具,而是一组互补证据。 每个门禁都要回答它能发现什么、发现不了什么、失败后谁处理。 Release 构建也必须验证,因为优化、NDEBUG、内联和运行库选择会改变行为。
compile warnings/static analysis -> pattern risks before running
unit/integration tests -> expected behavior
sanitizers -> runtime memory/UB/thread errors
fuzz -> unexpected input and state space
benchmark/profile -> performance budget evidence3. 核心术语
单元测试:验证一个小范围逻辑是否满足预期,适合覆盖纯函数、边界条件和容易回归的业务规则。
集成测试:让多个模块或外部依赖一起运行,检查接口连接、资源生命周期、配置和真实 I/O 路径。
静态分析:不运行程序就检查源码或编译中间信息,适合发现未初始化、悬空引用、可疑分支和编码规范问题。
ASan/UBSan/TSan:运行期插桩工具,分别关注内存错误、未定义行为和数据竞争。它们依赖测试把代码路径跑起来。
fuzz:用大量自动生成或变异输入持续冲击解析器、协议处理和状态机,寻找人手写用例不容易覆盖的崩溃路径。
benchmark:可重复的性能测量。它更关心趋势、预算和回归阈值,而不是某一次运行的漂亮数字。
4. 机制展开
先定义门禁含义
门禁不是为了让 CI 看起来严格,而是为了阻止风险进入主干、包和发布渠道。
一个门禁应有清晰失败消息、负责人、允许豁免的规则和回归测试要求。
Sanitizer 的边界
ASan 关注越界和 use-after-free,UBSan 关注未定义行为,TSan 关注数据竞争。
它们只覆盖被执行路径;没有跑到的路径不会自动安全。
Sanitizer 构建常常改变内存布局和性能,因此不能把 sanitizer 性能当成 release 性能。
benchmark 不是跑得快截图
性能证据要固定输入、机器条件、构建类型、统计方法和噪声阈值。
一次运行变快可能只是缓存或调度偶然;基线和趋势比单次数字更重要。
5. 最小示例
下面的示例不是完整项目,而是为了把本篇概念落到可审查文本。阅读代码时关注它暴露了哪些边界,而不是照抄每个名字。
add_library(project_sanitizers INTERFACE)
target_compile_options(project_sanitizers INTERFACE
$<$<CXX_COMPILER_ID:GNU,Clang>:-fsanitize=address,undefined;-fno-omit-frame-pointer>)
target_link_options(project_sanitizers INTERFACE
$<$<CXX_COMPILER_ID:GNU,Clang>:-fsanitize=address,undefined>)
add_executable(mesh_tests tests/mesh_tests.cpp)
target_link_libraries(mesh_tests PRIVATE Mesh::Core project_sanitizers)6. 工程取舍
| 选择 | 收益 | 风险 | 需要的证据 |
|---|---|---|---|
| 只在本机验证 | 反馈最快 | 环境不可复现 | 仅适合草稿,不适合作为发布结论 |
| CI 快速门禁 | 阻止常见错误 | 覆盖不完整 | 明确矩阵、日志和失败归属 |
| 完整发布门禁 | 证据最强 | 成本更高 | 制品、符号、SBOM、ABI 和回滚记录 |
| 最小消费者测试 | 最接近用户 | 需要维护样例 | 独立目录、安装包、运行输出 |
7. 常见错误
- 把“当前源码树能构建”误认为“安装包能被别人消费”。
- 把 PRIVATE 实现细节通过头文件、导出符号或包配置泄漏出去。
- 只保存最终二进制,不保存构建输入、依赖锁、调试符号和版本信息。
- 只在 Debug 测试,不在 Release 或发布配置里测试。
- 把 CI 缓存当成依赖管理,把绿色状态当成发布结论。
- 遇到链接或 ABI 问题时只改 flags,不留下能够复盘的符号证据。
8. 调试与验证方法
定义公开承诺
写清楚本库承诺源码兼容、二进制兼容、协议兼容还是仅内部使用。承诺越模糊,升级事故越难判责。
observe 集成测试
-> isolate 静态分析
-> run minimal proof
-> store evidence建立干净环境
用全新 build directory 和最小 consumer project 验证,不依赖源码树中的 include 路径和本机缓存。
observe 单元测试
-> isolate 集成测试
-> run minimal proof
-> store evidence固定工具版本
记录编译器、CMake、Ninja、包管理器、系统镜像和关键 flags,避免把偶然环境当成规则。
observe benchmark
-> isolate 单元测试
-> run minimal proof
-> store evidence保存失败证据
失败日志、符号报告、测试输出和构建命令要能被别人复现,而不是只留一句“CI failed”。
observe fuzz
-> isolate benchmark
-> run minimal proof
-> store evidence区分私有与公开
私有实现可以快速迭代;公开头、导出符号、包元数据和 ABI policy 必须谨慎变更。
observe ASan/UBSan/TSan
-> isolate fuzz
-> run minimal proof
-> store evidence审查跨平台差异
Windows 的 DLL/import lib/PDB 与 Linux 的 so/SONAME/RPATH/debug file 不同,不能用一个平台的经验套全部。
observe 静态分析
-> isolate ASan/UBSan/TSan
-> run minimal proof
-> store evidence限制全局状态
全局 include、全局 flags、隐式环境变量和手工 PATH 修改都会让构建不可解释。
observe 集成测试
-> isolate 静态分析
-> run minimal proof
-> store evidence做升级测试
old consumer + new library、new consumer + package install 是比单纯编译源码更接近用户现场的测试。
observe 单元测试
-> isolate 集成测试
-> run minimal proof
-> store evidence设置回滚入口
发布前准备上一稳定制品、数据库或文件格式迁移策略、插件兼容说明和符号包。
observe benchmark
-> isolate 单元测试
-> run minimal proof
-> store evidence写入 CI
能自动检查的规则不要只写在文章里;把它变成 preset、脚本、workflow 或 release checklist。
observe fuzz
-> isolate benchmark
-> run minimal proof
-> store evidence管理例外
允许临时豁免时必须记录原因、过期时间和补偿测试,否则门禁会慢慢失去意义。
observe ASan/UBSan/TSan
-> isolate fuzz
-> run minimal proof
-> store evidence保持证据总账
把构建输入、依赖锁、测试、ABI、性能、SBOM、签名和制品 hash 放在同一发布记录中。
observe 静态分析
-> isolate ASan/UBSan/TSan
-> run minimal proof
-> store evidence9. 本篇专属检查表
- 失败日志是否定位到文件行。
- 门禁是否覆盖 Debug 和 Release。
- TSan 是否单独 job。
- fuzz corpus 是否保存。
- 性能阈值是否有噪声区间。
| 检查点 | 通过标准 | 失败时优先看哪里 |
|---|---|---|
| 单元测试 | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| 集成测试 | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| 静态分析 | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| ASan/UBSan/TSan | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| fuzz | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
| benchmark | 有可重复命令、报告或 consumer 证明 | target 属性、CI 日志、符号报告、包元数据 |
10. 练习任务
- 建立一个只有一个库和一个 consumer 的最小工程,证明安装树可用。
- 故意破坏一个 PUBLIC/PRIVATE 作用域,观察消费者构建失败方式。
- 保存一次链接或 ABI 报告,并写下它能证明什么、不能证明什么。
- 把一个手工验证命令改成 CI 步骤,并让失败消息能指向具体文件或配置。
- 写一份 release evidence 小清单,列出制品、符号、依赖、测试和回滚入口。
12. 总结
《测试、静态分析与 Sanitizer 门禁 / Testing, Analysis, and Sanitizer Gates》的核心不是多记几个命令,而是把 单元测试、集成测试、静态分析、ASan/UBSan/TSan、fuzz、benchmark 变成可审查、可复现、可回滚的工程证据。
深化问题 1:集成测试 怎样变成证据
围绕 集成测试 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:用全新 build directory 和最小 consumer project 验证,不依赖源码树中的 include 路径和本机缓存。
rule: 集成测试
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 2:静态分析 怎样变成证据
围绕 静态分析 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:记录编译器、CMake、Ninja、包管理器、系统镜像和关键 flags,避免把偶然环境当成规则。
rule: 静态分析
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 3:ASan/UBSan/TSan 怎样变成证据
围绕 ASan/UBSan/TSan 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:失败日志、符号报告、测试输出和构建命令要能被别人复现,而不是只留一句“CI failed”。
rule: ASan/UBSan/TSan
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 4:fuzz 怎样变成证据
围绕 fuzz 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:私有实现可以快速迭代;公开头、导出符号、包元数据和 ABI policy 必须谨慎变更。
rule: fuzz
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 5:benchmark 怎样变成证据
围绕 benchmark 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:Windows 的 DLL/import lib/PDB 与 Linux 的 so/SONAME/RPATH/debug file 不同,不能用一个平台的经验套全部。
rule: benchmark
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 6:单元测试 怎样变成证据
围绕 单元测试 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:全局 include、全局 flags、隐式环境变量和手工 PATH 修改都会让构建不可解释。
rule: 单元测试
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 7:集成测试 怎样变成证据
围绕 集成测试 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:old consumer + new library、new consumer + package install 是比单纯编译源码更接近用户现场的测试。
rule: 集成测试
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 8:静态分析 怎样变成证据
围绕 静态分析 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:发布前准备上一稳定制品、数据库或文件格式迁移策略、插件兼容说明和符号包。
rule: 静态分析
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 9:ASan/UBSan/TSan 怎样变成证据
围绕 ASan/UBSan/TSan 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:能自动检查的规则不要只写在文章里;把它变成 preset、脚本、workflow 或 release checklist。
rule: ASan/UBSan/TSan
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 10:fuzz 怎样变成证据
围绕 fuzz 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:允许临时豁免时必须记录原因、过期时间和补偿测试,否则门禁会慢慢失去意义。
rule: fuzz
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 11:benchmark 怎样变成证据
围绕 benchmark 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:把构建输入、依赖锁、测试、ABI、性能、SBOM、签名和制品 hash 放在同一发布记录中。
rule: benchmark
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 12:单元测试 怎样变成证据
围绕 单元测试 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:写清楚本库承诺源码兼容、二进制兼容、协议兼容还是仅内部使用。承诺越模糊,升级事故越难判责。
rule: 单元测试
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 13:集成测试 怎样变成证据
围绕 集成测试 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:用全新 build directory 和最小 consumer project 验证,不依赖源码树中的 include 路径和本机缓存。
rule: 集成测试
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 14:静态分析 怎样变成证据
围绕 静态分析 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:记录编译器、CMake、Ninja、包管理器、系统镜像和关键 flags,避免把偶然环境当成规则。
rule: 静态分析
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 15:ASan/UBSan/TSan 怎样变成证据
围绕 ASan/UBSan/TSan 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:失败日志、符号报告、测试输出和构建命令要能被别人复现,而不是只留一句“CI failed”。
rule: ASan/UBSan/TSan
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 16:fuzz 怎样变成证据
围绕 fuzz 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:私有实现可以快速迭代;公开头、导出符号、包元数据和 ABI policy 必须谨慎变更。
rule: fuzz
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 17:benchmark 怎样变成证据
围绕 benchmark 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:Windows 的 DLL/import lib/PDB 与 Linux 的 so/SONAME/RPATH/debug file 不同,不能用一个平台的经验套全部。
rule: benchmark
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 18:单元测试 怎样变成证据
围绕 单元测试 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:全局 include、全局 flags、隐式环境变量和手工 PATH 修改都会让构建不可解释。
rule: 单元测试
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 19:集成测试 怎样变成证据
围绕 集成测试 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:old consumer + new library、new consumer + package install 是比单纯编译源码更接近用户现场的测试。
rule: 集成测试
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 20:静态分析 怎样变成证据
围绕 静态分析 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:发布前准备上一稳定制品、数据库或文件格式迁移策略、插件兼容说明和符号包。
rule: 静态分析
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception深化问题 21:ASan/UBSan/TSan 怎样变成证据
围绕 ASan/UBSan/TSan 写一个项目规则:谁可以修改,修改后跑什么命令,失败时保存哪些输出。
对应的工程动作是:能自动检查的规则不要只写在文章里;把它变成 preset、脚本、workflow 或 release checklist。
rule: ASan/UBSan/TSan
owner: module maintainer
proof: command + report + artifact
failure: block merge or require documented exception