TDengine 的 C++ 测试打桩利器:cppstub Conan 包使用与集成指南
【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine
本文以 conan/cppstub/README.md 为核心,结合 conan/cppstub/conanfile.py 打包配方、cmake/conan.cmake 构建集成与仓库内大量单元测试用例,系统讲解 TDengine 中 cppstub 头文件式打桩库的 Conan 打包原理、创建安装、项目接入与底层实现。读完本文,你将能够独立完成 cppstub 包的本地构建与验证,并在自己的 C++ 测试工程中通过 Conan + CMake 直接使用
stub.h与addr_any.h进行函数级打桩,同时理解 TDengine 各模块单元测试对它的实际依赖方式。
一、为什么 TDengine 需要 cppstub
TDengine 是面向工业物联网(IIoT)场景的高性能时序数据库,其源码中大量模块(如 catalog、executor、parser、qworker、scheduler、scalar、monitor、new-stream、vnode、mnode 等)的单元测试都需要隔离外部依赖——例如替换系统调用、mock 底层存储或调度接口。cppstub 正是一个专为 C++ 单元测试设计的轻量级函数打桩(stub)/模拟(mock)库,它允许测试代码在运行期把目标函数临时替换为自定义实现,从而做到"不依赖真实环境也能验证业务逻辑"。
从仓库中可以看到,Stub类被广泛使用于测试代码,例如:
- source/common/test/tmsgTest.cpp 中通过
Stub stub_对消息处理相关函数打桩; - source/libs/catalog/test/catalogTests.cpp 中以
static Stub stub;的方式隔离目录服务; - source/libs/executor/test/joinTests.cpp、source/libs/executor/test/queryPlanTests.cpp 在查询执行器测试中打桩;
- source/dnode/mnode/impl/test/stream/stream.cpp、source/libs/monitor/test/monTest.cpp 等也直接
#include "stub.h"。
这些测试文件同时还会#include <addr_any.h>,说明 cppstub 的两个头文件在 TDengine 测试体系中是成对使用的基础设施。
二、包信息与核心特性
根据 conan/cppstub/README.md,cppstub Conan 包的基本信息如下:
| 属性 | 值 |
|---|---|
| 包名 | cppstub |
| 版本 | 1.0.0 |
| 许可证 | MIT |
| 类型 | 头文件库(Header-only) |
| 主要头文件 | stub.h(打桩核心功能)、addr_any.h(平台相关的地址操作) |
| 上游 | cpp-stub 项目(对应提交 3137465194014d66a8402941e80d2bccc6346f51,LICENSE 见 conan/cppstub/cppstub/LICENSE) |
其核心特性包括:
- 纯头文件库:无需编译、无需链接,消费方只需把头文件目录加入 include 路径;
- 跨平台支持:Linux、macOS、Windows 均有对应的平台适配头;
- 简单易用的打桩 API:通过
Stub类的set()/reset()/clear()三个方法即可完成打桩、还原与批量清理; - 平台相关实现内置:
addr_any.h随操作系统自动选择。
由于是 header-only 库,包 ID 被清空(平台无关),一份二进制包可以在不同配置下复用,极大简化了 CI 场景下的缓存命中。
三、仓库内的源码与目录结构
在本仓库 conan/cppstub 目录下,完整的文件布局如下:
conan/cppstub/ ├── conanfile.py # Conan 打包配方 ├── README.md # 包文档(本文核心) ├── SUMMARY.md # 打包与集成迁移摘要 ├── cppstub/ # 上游源码快照 │ ├── LICENSE # MIT 许可证 │ ├── src/ │ │ └── stub.h # 打桩主头文件 │ ├── src_linux/ │ │ └── addr_any.h # Linux 平台地址操作实现 │ ├── src_darwin/ │ │ └── addr_any.h # macOS 平台地址操作实现 │ └── src_win/ │ └── addr_any.h # Windows 平台地址操作实现 └── test_package/ # 校验包可用性的测试工程 ├── conanfile.py ├── CMakeLists.txt └── test_package.cpp其中 conan/cppstub/SUMMARY.md 记录了一次完整的打包迁移过程:包创建成功、测试包构建运行通过、头文件可访问、包已装入本地 Conan 缓存,并给出了将 cppstub 接入 TDinternal 构建体系的后续步骤。
四、打包原理:conanfile.py 逐段解析
conan/cppstub/conanfile.py 是一个典型的 header-only 库打包配方,各关键部分作用如下:
4.1 元信息与设置
class CppStubConan(ConanFile): name = "cppstub" version = "1.0.0" license = "MIT" description = "A simple and easy-to-use C++ stub library for unit testing" topics = ("cpp", "stub", "testing", "mock") settings = "os", "compiler", "build_type", "arch" no_copy_source = Truesettings声明了 os/compiler/build_type/arch 四项,但由于后文package_id()会清空信息,实际生成的包仍是平台无关的;no_copy_source = True告诉 Conan 不要把源码复制到构建目录,直接在 export 目录工作,效率更高(SUMMARY.md 中也特别强调了这个优化点)。
4.2 源码导出与打包
def export_sources(self): copy(self, "*", src=os.path.join(self.recipe_folder, "cppstub"), dst=os.path.join(self.export_sources_folder, "cppstub")) def source(self): pass # 依赖 export_sources 提供的本地源码 def build(self): pass # header-only,无需构建 def package_id(self): self.info.clear() # header-only,包 ID 与配置无关export_sources将仓库内 conan/cppstub/cppstub 目录整体导出为源码;build()为空体现"无需编译";package_id()调用self.info.clear()使同一版本对所有配置共享一个包。
4.3 按操作系统选择平台头文件
package()阶段是打包的关键:除许可证文件外,它先把stub.h放入include/,然后依据self.settings.os选择addr_any.h:
| 操作系统(settings.os) | 来源目录 | 打包目标 |
|---|---|---|
| Linux | src_linux/ | include/addr_any.h |
| Macos | src_darwin/ | include/addr_any.h |
| Windows | src_win/ | include/addr_any.h |
| 其他(默认) | src_linux/ | include/addr_any.h |
对应代码片段:
if self.settings.os == "Linux": platform_dir = "src_linux" elif self.settings.os == "Macos": platform_dir = "src_darwin" elif self.settings.os == "Windows": platform_dir = "src_win" else: platform_dir = "src_linux" # Default to Linux4.4 消费方元信息
def package_info(self): self.cpp_info.bindirs = [] self.cpp_info.libdirs = [] self.cpp_info.includedirs = ["include"]bindirs与libdirs置空表示没有任何二进制需要链接,只暴露include目录——这正是 header-only 库的标准做法,也是 CMake 中cppstub::cppstub目标无需链接任何库文件的底层原因。
五、创建与验证包
在包含 conan/cppstub/conanfile.py 的目录下执行:
# 创建包到本地 Conan 缓存(缺失依赖时自动构建) conan create . --build=missing # 验证安装结果 conan list "cppstub/*"SUMMARY.md 中记录的预期输出为:
Local Cache cppstub cppstub/1.0.0conan create会依次执行 export、package、build(空实现)与 test_package 校验。test_package 工程由三部分组成:
- conan/cppstub/test_package/conanfile.py:通过
self.requires(self.tested_reference_str)依赖被测包,并用 CMakeDeps/CMakeToolchain 生成器驱动 CMake 构建,can_run(self)判断本地可运行后执行test_cppstub; - conan/cppstub/test_package/CMakeLists.txt:
find_package(cppstub REQUIRED CONFIG)后链接cppstub::cppstub; - conan/cppstub/test_package/test_package.cpp:验证
stub.h、addr_any.h可包含且add(2, 3)正常返回 5。
实际测试运行输出(来源 SUMMARY.md):
Testing cppstub package... Normal add(2, 3) = 5 stub.h header is available addr_any.h header is available All tests passed!需要注意的是,测试包只验证头文件可用性,并未完整验证打桩功能(打桩需要更复杂的运行环境设置),这一点在 test 源码的注释中有明确说明。
六、在项目中使用 cppstub
6.1 声明依赖
在conanfile.txt中声明:
[requires] cppstub/1.0.0 [generators] CMakeDeps CMakeToolchain或在conanfile.py中声明:
def requirements(self): self.requires("cppstub/1.0.0")针对 TDengine 的构建场景,SUMMARY.md 还给出了"仅构建测试时引入"的推荐写法,避免生产构建携带测试依赖:
def requirements(self): if self.options.get_safe("build_tests"): self.requires("cppstub/1.0.0")6.2 CMake 集成
cmake_minimum_required(VERSION 3.15) project(MyProject CXX) find_package(cppstub REQUIRED CONFIG) add_executable(mytest test.cpp) target_link_libraries(mytest cppstub::cppstub)find_package(cppstub REQUIRED CONFIG)由CMakeDeps生成器自动生成配置文件;cppstub::cppstub目标只携带 include 路径,不携带任何链接库。
6.3 代码中使用
#include <stub.h> #include <addr_any.h>stub.h提供打桩功能,addr_any.h提供平台相关的地址操作实现,两者需配套包含。一个真实的打桩用法(参考 test_package/test_package.cpp 的函数结构)如下:
#include <stub.h> #include <addr_any.h> // 待打桩的函数 int add(int a, int b) { return a + b; } // 打桩替换函数 int stub_add(int a, int b) { return 100; } int main() { Stub stub; stub.set(add, stub_add); // 将 add 临时替换为 stub_add int r = add(2, 3); // 返回 100 stub.reset(add); // 还原 add return 0; }七、stub.h 的底层实现原理(源码级)
打桩的本质是"运行时改写函数入口处的机器码,使其跳转到替身函数"。从 conan/cppstub/cppstub/src/stub.h 的源码结构可以清晰看到这一机制:
宏
ADDR(CLASS_NAME, MEMBER_NAME):用于取得成员函数地址,配合Stub::set()可对类成员函数打桩。架构相关的替换宏
REPLACE_FAR/REPLACE_NEAR:按编译期宏选择指令序列,覆盖 x86_64(13 字节movabs r11, imm64; jmp r11与 5 字节jmp rel32两种形态)、AArch64、ARM/Thumb、MIPS64、RISC-V(64/32 位)、LoongArch64、PowerPC64、Alpha、SPARC64、SW64、s390x 等平台,每种架构都有对应的CODESIZE(替换指令占用的字节数)。指令缓存刷新宏
CACHEFLUSH:POSIX 平台使用__builtin___clear_cache,Windows 使用FlushInstructionCache;若定义了__VALGRIND__还会调用VALGRIND_DISCARD_TRANSLATIONS,保证改写后的指令在 Valgrind 下也能生效。Stub类的三阶段流程(对应set()方法):- 用
mprotect(POSIX)或VirtualProtect(Windows)把目标函数所在页临时改为可读可写可执行(PROT_READ | PROT_WRITE | PROT_EXEC); - 先保存原始指令到
code_buf,再依据目标与替身地址距离选择REPLACE_FAR或REPLACE_NEAR写入跳转指令; - 恢复页权限为可读可执行(
PROT_READ | PROT_EXEC),并将函数指针登记到内部std::map。 reset(addr)用保存的原始指令还原函数;析构函数和clear()会遍历 map 一次性还原全部打桩,避免测试间相互污染。
- 用
页大小在构造函数中通过sysconf(_SC_PAGE_SIZE)(Windows 为GetSystemInfo)获取,异常时兜底为 4096。这些细节解释了为什么 cppstub 在 Linux/macOS/Windows 上都需要各自独立的addr_any.h——不同平台获取函数地址、处理页权限的方式差异巨大。
八、与 TDengine 构建系统的集成
8.1 cmake/conan.cmake 中的接入
TDengine 主构建脚本 cmake/conan.cmake 中已内置 cppstub 的集成逻辑:
- 第 36–38 行:
find_package(cppstub QUIET),未找到时回退到 ExternalProject 方案,保证两种依赖获取路径都能工作; - 第 455–471 行:定义了
DEP_ext_cppstub/DEP_ext_cppstub_INC/DEP_ext_cppstub_LIB三个宏,核心逻辑是:
macro(DEP_ext_cppstub tgt) if(TARGET cppstub::cppstub) target_link_libraries(${tgt} PUBLIC cppstub::cppstub) endif() endmacro()各测试目标只需调用DEP_ext_cppstub(${target})即可获得 include 路径。仓库中 source/util/test/CMakeLists.txt、source/libs/catalog/test/CMakeLists.txt、source/libs/executor/test/CMakeLists.txt、source/libs/scheduler/test/CMakeLists.txt、source/libs/qworker/test/CMakeLists.txt、source/libs/scalar/test/filter/CMakeLists.txt、source/libs/parser/test/CMakeLists.txt、source/dnode/vnode/test/CMakeLists.txt、source/libs/monitor/test/CMakeLists.txt 等测试工程均与此宏配合使用。
8.2 测试代码中的使用形态
从源码搜索看,TDengine 各模块测试对 cppstub 的使用高度一致:#include "stub.h"(部分同时#include <addr_any.h>),然后在测试函数或文件作用域声明Stub对象。典型的成员函数打桩场景可参考 source/common/test/tmsgTest.cpp 与 source/dnode/vnode/test/metaTxnAlterStbTest.cpp(其中Stub stub;局部对象在作用域结束即自动还原)。
九、使用注意事项
- 纯头文件,无需链接:
cppstub::cppstub目标只提供 include 路径;不要为它配置任何库搜索路径。 - 平台头自动选择:打包时已按
settings.os固化addr_any.h,消费方无需关心平台差异;但代码中两个头文件通常要一起包含。 - 包 ID 平台无关:
package_id()清空了配置信息,同一版本包可在不同编译器/架构配置间复用,利于 CI 缓存。 - 测试包范围有限:官方 test_package 仅验证头文件可达与基本编译运行,完整的打桩行为需在真实测试工程中验证。
- 打桩的底层代价:由于涉及改写可执行代码段的机器码并切换页权限,仅建议在单元测试中使用,且需注意目标函数所在页的写保护处理(stub.h 已通过
mprotect/VirtualProtect处理,但要求代码段页可临时写)。
十、总结
cppstub Conan 包是 TDengine 测试基础设施的重要组成部分:它以 header-only 的形式提供了跨 Linux/macOS/Windows 的函数打桩能力,通过 conan/cppstub/conanfile.py 完成平台头文件的选择与打包,通过 test_package 保证交付质量,并已通过 cmake/conan.cmake 中的DEP_ext_cppstub宏无缝接入数十个单元测试工程。对希望为 TDengine 贡献测试代码或复用其测试模式的开发者而言,掌握本文的打包、接入与底层原理,即可快速上手基于 cppstub 的 C++ 打桩测试开发。
【免费下载链接】TDengineHigh-performance, scalable time-series database designed for Industrial IoT (IIoT) scenarios项目地址: https://gitcode.com/GitHub_Trending/tde/TDengine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考