VLink 2.0.0
A high-performance communication middleware
Loading...
Searching...
No Matches
  1. 测试与覆盖率

本文档介绍 VLink 的单元测试体系与代码覆盖率实践。

**相关文档**:构建系统 01-build.md;基础库 11-base-library.md;传输后端 07-transport.md


1. 测试框架:doctest

VLink 单元测试采用 doctest——头文件形式的 C++ 测试框架,与 Catch2 语法接近。

  • 头文件形式(thirdparty::doctest),无需额外链接动态库
  • 支持 TEST_CASESUBCASECHECKREQUIRECHECK_THROWS_AS 等断言宏
  • 支持按用例名过滤(--test-case=-tce=

1.1 基本用法

#include <doctest/doctest.h>
TEST_CASE("module-feature") {
int x = 1 + 1;
CHECK(x == 2);
REQUIRE(x > 0); // REQUIRE 失败会中止当前 TEST_CASE
}
TEST_CASE("module-subcase") {
int value = 0;
SUBCASE("increment") {
value += 1;
CHECK(value == 1);
}
SUBCASE("decrement") {
value -= 1;
CHECK(value == -1);
}
}

1.2 常用断言宏

本项目实际使用的 doctest 断言宏如下:

含义
CHECK(expr) 检查表达式为真,失败继续执行
REQUIRE(expr) 检查表达式为真,失败中止当前用例
CHECK_FALSE(expr) 检查表达式为假
CHECK_THROWS_AS(e, T) 检查表达式抛出类型 T 的异常
CHECK_NOTHROW(expr) 检查表达式不抛出异常
SUBCASE("name") 在同一 TEST_CASE 中定义独立子场景

doctest 还提供 CHECK_EQ / CHECK_NE / CHECK_LT / CHECK_GE 等等价/比较形式的断言宏,当前本项目测试代码中并未直接使用,统一以 CHECK(a == b) 等形式书写;如有需要可参见 doctest 官方文档。

1.3 测试入口

test/main.cc 当前使用自定义 main() 包装 doctest::Context

#define DOCTEST_CONFIG_IMPLEMENT
#include <doctest/doctest.h>
int main(int argc, char** argv) {
doctest::Context ctx;
ctx.setOption("no-introductory-string", true);
ctx.setOption("exit", true);
ctx.applyCommandLine(argc, argv);
return ctx.run();
}

这样可以在保留 doctest 入口的同时统一设置默认运行选项。其余测试文件只需包含 <doctest/doctest.h>,无需再定义 main()


2. 测试目录结构

test/
├── main.cc # 自定义 doctest 入口
├── CMakeLists.txt # 测试构建配置
├── common_test.h # 公共头文件(vlink 全量 include + 常用类型)
├── idl/ # IDL 定义文件(.proto / .fbs / .idl)
├── base/ # base 库测试
├── extension/ # bag / discovery / qos / schema / security 等扩展测试
├── impl/ # NodeImpl / URL / SSL / AckManager 等实现层测试
├── modules/ # 各 transport Conf 解析测试
├── zerocopy/ # CameraFrame / PointCloud / ProxyData 等测试
├── serializer_test.cc # Serializer 单元测试
├── intra_test.cc # intra:// 传输后端
├── shm_test.cc # shm:// 传输后端(iceoryx)
├── shm2_test.cc # shm2:// 传输后端(iceoryx2)
├── dds_test.cc # dds:// 传输后端(FastDDS)
├── ddsc_test.cc # ddsc:// 传输后端(CycloneDDS)
├── ddsr_test.cc # ddsr:// 传输后端
├── ddst_test.cc # ddst:// 传输后端
├── zenoh_test.cc # zenoh:// 传输后端
├── someip_test.cc # someip:// 传输后端
├── mqtt_test.cc # mqtt:// 传输后端
├── fdbus_test.cc # fdbus:// 传输后端
└── qnx_test.cc # qnx:// 传输后端(QNX 平台)

2.1 测试分层

测试分层结构

3. 编译与运行测试

CMake 构建详情见 01-build.md。本节仅覆盖与测试相关的部分。

3.1 test/CMakeLists.txt 事实

  • 目标:vlink-test(doctest 可执行文件)
  • 链接:vlink::vlink + vlink::all(所有已启用的传输模块)+ thirdparty::doctest
  • CTest 只注册一个测试:add_test(NAME vlink-test COMMAND vlink-test)
  • 条件宏:
    • VLINK_TEST_SUPPORT_SECURITY——顶层 ENABLE_SECURITY 开启
    • VLINK_TEST_SUPPORT_PROTOBUF——find_package(Protobuf) 成功
    • VLINK_TEST_SUPPORT_FLATBUFFERS——find_package(Flatbuffers) 成功
    • VLINK_TEST_SUPPORT_FASTDDS_GEN——ENABLE_FASTDDS_GEN_TEST 开启且找到 fastddsgen;默认 OFF
  • 运行时 LD_LIBRARY_PATH(Linux)/ DYLD_LIBRARY_PATH(macOS)指向 CMAKE_LIBRARY_OUTPUT_DIRECTORY

3.2 编译选项与开关

控制 vlink-test 行为的主要顶层选项:

CMake 选项 默认 对测试的影响
ENABLE_TEST ON 控制是否添加 test/ 子目录
ENABLE_SECURITY ON 激活 Security 相关断言和 SecurityPublisher/Subscriber 用例
ENABLE_C_API ON 不影响 vlink-test;C API 用例在独立可执行 vlink-c-test
ENABLE_PROXY ON 不影响 vlink-test;proxy 相关测试在 vlink-test 内的 extension 目录下由运行时条件控制
ENABLE_SQLITE ON 决定 bag 测试中 .vdb 路径可用性
ENABLE_ZSTD ON 决定 bag 测试中 Zstd 分支
ENABLE_TEST_WARN OFF 开启后对核心库目标追加严格警告标志(GCC/Clang:-Wall -Wpedantic -Wextra -Werror;MSVC:/W4 /WX
ENABLE_TEST_SANITIZE OFF 对核心库目标追加 -fsanitize=address;MSVC/QNX 不支持
ENABLE_TEST_COVERAGE OFF 通过 gcovrlcov 采集覆盖率(见第 12 节)

ENABLE_TEST_WARN/SANITIZE/COVERAGE 实际作用的函数是 cmake/functions/common.cmake 中的 vlink_test_warn / vlink_test_sanitize / vlink_test_coverage,它们作用在核心库 vlink 目标上。

3.3 编译并运行

# 配置(启用所有常用功能)
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DENABLE_SECURITY=ON \
-DENABLE_C_API=ON
# 编译测试目标
cmake --build build --target vlink-test -j$(nproc)
# 运行全部测试
ctest --test-dir build --output-on-failure

**注意**:test/CMakeLists.txt 中只通过 add_test(NAME vlink-test ...) 注册了 一个 CTest 用例,即主 doctest 可执行文件 vlink-testc_api/test/ 下的 vlink-c-testpython_api/test_vlink.py / test_vlink_full.py 均为独立 测试,不会被 ctest 自动调度;它们需要单独编译和运行。

3.3.1 使用 AddressSanitizer 运行

# 配置 ASAN 构建
cmake -B build -DCMAKE_BUILD_TYPE=Debug \
-DENABLE_TEST_SANITIZE=ON
cmake --build build --target vlink-test -j$(nproc)
# 运行时需设置 LD_LIBRARY_PATH 指向构建输出的 lib 目录
LD_LIBRARY_PATH=build/output/lib ./build/output/bin/vlink-test
# SOMEIP 测试在无 vsomeip daemon 时会挂起,需排除
LD_LIBRARY_PATH=build/output/lib ./build/output/bin/vlink-test -tce="someip-*"
# 如有 ASAN 抑制文件,可通过以下方式指定
# export ASAN_OPTIONS=suppressions=path/to/asan_suppressions.txt

**注意**:-tce 是 doctest 的 test case exclude 标志(不是 -e,后者为 exit 标志)。

3.4 doctest 命令行参数

# 只运行名称包含 "base" 的测试用例
./build/output/bin/vlink-test --test-case="base*"
# 只运行名称包含 "Logger" 的测试
./build/output/bin/vlink-test --test-case="*Logger*"
# 排除指定测试用例(-tce = test case exclude)
./build/output/bin/vlink-test -tce="someip-*"
# 排除多个测试用例
./build/output/bin/vlink-test -tce="someip-*" -tce="shm-*"
# 列出所有测试用例名称(不运行)
./build/output/bin/vlink-test --list-test-cases
# 输出更详细的信息
./build/output/bin/vlink-test --success
# 静默模式(仅打印失败)
./build/output/bin/vlink-test --minimal

**注意**:doctest 的 -tce 标志用于排除测试用例(test case exclude),不要与 -e 标志混淆——-e 是退出标志(exit),不是排除标志。

3.5 通过 ctest 运行

cd build
# 运行所有测试
ctest
# 并行运行(4 个进程)
ctest -j4
# 显示详细输出
ctest --verbose
# 只运行名称匹配的测试
ctest -R "vlink-test"
# 失败时打印输出
ctest --output-on-failure

4. 各测试文件/模块说明

4.1 Base 库测试

测试文件 测试用例名 测试内容
base/logger_test.cc base-Logger 日志级别、格式化输出、handler 回调、backtrace
base/bytes_test.cc base-Bytes 空 Bytes、浅拷贝/深拷贝、移动语义、用户输入解析
base/timer_test.cc base-Timer 定时触发、循环次数、restart、call_once
base/wheel_timer_test.cc base-WheelTimer 定时触发、重复定时、暂停/恢复、删除定时器
base/elapsed_timer_test.cc base-ElapsedTimer 计时启动、restart、get 精度
base/deadline_timer_test.cc base-DeadlineTimer 截止时间检测
base/message_loop_test.cc base-MessageLoop 优先级任务队列、async_run、wait_for_quit
base/thread_pool_test.cc base-ThreadPool 任务提交、shutdown、任务计数
base/multi_loop_test.cc base-MultiLoop 多事件循环并发调度
base/graph_task_test.cc base-GraphTask DAG 任务图构建、条件节点、执行顺序
base/mpmc_queue_test.cc base-MpmcQueue 空队列、push/pop、满队列、多线程生产消费
base/spin_lock_test.cc base-SpinLock 单线程加锁/解锁、多线程互斥、压力测试
base/semaphore_test.cc base-Semaphore 信号量 wait/post、超时
base/sys_semaphore_test.cc base-SysSemaphore 系统级信号量操作
base/sys_sharemem_test.cc base-SysSharemem 共享内存创建/读写/释放
base/process_test.cc base-Process 进程信息查询
base/utils_test.cc base-Utils 工具函数(路径、字符串等)
base/cached_timestamp_test.cc base-CachedTimestamp 缓存时间戳性能优化
base/condition_variable_test.cc base-ConditionVariable 条件变量封装
base/cpu_profiler_test.cc base-CpuProfiler CPU 性能分析器
base/cpu_profiler_guard_test.cc base-CpuProfilerGuard CPU Profiler RAII 守卫
base/exception_test.cc base-Exception 异常处理工具
base/fast_stream_test.cc base-FastStream 高性能流式输出
base/format_test.cc base-Format 字符串格式化工具
base/helpers_test.cc base-Helpers 辅助工具函数(类型转换等)
base/macros_test.cc base-Macros 宏定义测试
base/name_detector_test.cc base-NameDetector 编译期类型名提取
base/object_pool_test.cc base-ObjectPool 对象池(预分配复用)
base/plugin_test.cc base-Plugin 动态插件加载器
base/schedule_test.cc base-Schedule 调度器
base/traits_test.cc base-Traits 类型特征检测工具
base/uint128_test.cc base-Uint128 128 位无符号整数

4.2 传输后端测试

测试文件 前置宏 典型测试用例
intra_test.cc VLINK_SUPPORT_INTRA intra-init, intra-method, intra-event, intra-dynamic, intra-serialize
shm_test.cc VLINK_SUPPORT_SHM shm-init, shm-method, shm-event, shm-field, shm-dynamic, shm-serialize
shm2_test.cc VLINK_SUPPORT_SHM2 shm2-init, shm2-method, shm2-event, shm2-field
dds_test.cc VLINK_SUPPORT_DDS dds-init, dds-method, dds-event, dds-field, dds-dynamic, dds-serialize
ddsc_test.cc VLINK_SUPPORT_DDSC ddsc-init, ddsc-method, ddsc-event, ddsc-field
ddsr_test.cc VLINK_SUPPORT_DDSR ddsr-init, ddsr-method, ddsr-event
ddst_test.cc VLINK_SUPPORT_DDST ddst-init, ddst-method, ddst-event
zenoh_test.cc VLINK_SUPPORT_ZENOH zenoh-init, zenoh-method, zenoh-event, zenoh-field
someip_test.cc VLINK_SUPPORT_SOMEIP someip-init, someip-method, someip-event
mqtt_test.cc VLINK_SUPPORT_MQTT mqtt-init, mqtt-method, mqtt-event
fdbus_test.cc VLINK_SUPPORT_FDBUS fdbus-init, fdbus-method, fdbus-event
qnx_test.cc VLINK_SUPPORT_QNX qnx-init, qnx-method, qnx-event(仅 QNX 平台)

5. Base 库测试详细说明

5.1 Logger 测试

logger_test.cc 验证以下行为:

  • 设置控制台/文件日志级别(Logger::set_console_levelLogger::set_file_level
  • 格式化日志输出(MLOG_I、带占位符)
  • 注册自定义 handler(register_console_handlerregister_file_handler
  • 各级别日志回调(Trace/Debug/Info/Warn/Error):验证回调触发次数为 10(5 个级别 x 2 个 handler)
  • VLOG_F 抛出 std::runtime_error(Fatal 级别)
  • backtrace 功能:enable_backtrace、循环记录、dump_backtracedisable_backtrace
  • C 风格格式化日志宏(CLOG_*)和流式日志(SLOG_W
// 验证 Fatal 级别抛异常
auto fatal_func = [] { VLOG_F("test fatal log"); };
CHECK_THROWS_AS(fatal_func(), std::runtime_error&);
#define VLOG_F(...)
Definition logger.h:856

5.2 Bytes 测试

bytes_test.cc 验证:

  • 空 Bytes 的 empty()size() 为 0
  • Bytes::shallow_copy 浅拷贝与数据比较
  • deep_copy_self 深拷贝后数据仍相等
  • std::vector<uint8_t> 赋值
  • 移动语义(std::move
  • Bytes::from_user_input 解析十六进制字符串(含空格、前导零)

5.3 Timer 测试

timer_test.cc 验证:

  • 定时器在指定间隔(50ms)触发,误差 < 30ms
  • 循环次数控制(3 次后停止)
  • restart() 重新计时
  • Timer::call_once 一次性定时器
  • 多个 call_once 按时序触发(先 10ms 后 20ms)
  • 需要绑定 MessageLoop 运行事件循环

5.4 WheelTimer 测试

wheel_timer_test.cc 验证:

  • WheelTimer(resolution_ms, wheel_size) 构造
  • is_running()start()stop()
  • add(delay_ms, callback) 单次触发,回调 key 一致
  • add(delay_ms, callback, repeat_ms) 重复触发
  • pause() / resume() 暂停期间 get_remaining_time 不变
  • remove(key) 移除后不再触发
  • 移除不存在 key 返回 false

5.5 MessageLoop 测试

message_loop_test.cc 验证优先级队列:

  • kPriorityType 模式按优先级(数值越大越先执行)调度
  • async_run() 异步启动后台线程
  • invoke_task_with_priority 阻塞等待结果
  • post_task_with_priority 投递任务,优先级 50 > 30 > 20
  • wait_for_quit() 等待 quit() 被调用

5.6 ThreadPool 测试

thread_pool_test.cc 验证:

  • 4 线程池提交 10 个累加任务
  • shutdown() 等待所有任务完成
  • 任务总和为 55(1+2+...+10)
  • get_task_count() == 0 表示空队列
  • is_in_work_thread() == false 在主线程中检查

5.7 GraphTask 测试

graph_task_test.cc 验证 DAG 调度:

  • 创建 7 个任务节点(a/b/c/d/e/f/g),使用 --> 运算符连接依赖
  • d 是条件节点(返回 3),分支到 e(1)、f(2)、g(3)
  • 按条件结果选择分支(返回 3 时执行 g)
  • 执行顺序验证:test_str == "abcdg"
  • export_to_dot() 导出 Graphviz 格式

5.8 MpmcQueue 测试

mpmc_queue_test.cc 验证:

  • 空队列初始状态
  • 单元素 push/pop
  • 满队列 try_push 返回 false
  • 空队列 try_pop 返回 false
  • emplace / try_emplace 就地构造
  • 多线程(4 生产者 + 4 消费者):100 元素/线程,共 400 项,无丢失无重复

5.9 SpinLock 测试

spin_lock_test.cc 验证:

  • 单线程 lock/unlock 不抛异常
  • 持锁期间 try_lock 返回 false,解锁后恢复 true
  • 8 线程 x 100 次累加:无竞态,结果精确
  • try_lock 多线程:成功次数在 [1, 8x100] 范围内
  • 16 线程 x 1000 次压力测试:结果精确

6. 传输后端测试详细说明

每个传输后端测试文件采用统一的测试结构,均包含以下子用例:

6.1 init 测试

验证 URL 解析和 Conf 解析的正确性:

TEST_CASE("dds-init") {
Url url("dds://hello_topic");
DdsConf conf("hello_topic", 0);
// 非法 ImplType 应抛出异常
CHECK_THROWS_AS(url.parse(kUnknownImplType), std::runtime_error&);
// 合法解析
CHECK(url.parse(kPublisher));
CHECK(conf.parse(kPublisher));
// Url 解析出的 Conf 与手动构造的 Conf 相等
CHECK(*static_cast<DdsConf*>(const_cast<Conf*>(url.get_target())) == conf);
// 错误 transport 应抛出异常
auto error1 = [] { Publisher<int> pub("dds1://hello_topic"); };
CHECK_THROWS_AS(error1(), std::runtime_error&);
}

6.2 method 测试(Client/Server)

验证 RPC 模型:

  • Server<Req> + Client<Req>:fire-and-forget(send)
  • Server<Req, Resp> + Client<Req, Resp>:invoke(带返回值)
  • wait_for_connected() 等待连接建立
  • async_invoke 返回 std::future,验证 wait_for 超时机制
  • get_abstract_node()->get_native_handle().has_value() 验证原生句柄有效

6.3 event 测试(Publisher/Subscriber)

验证 Pub-Sub 模型:

  • Publisher<std::string> + Subscriber<std::string>
  • wait_for_subscribers() 等待订阅者就绪
  • publish(msg) 返回成功发布数量
  • 多订阅者场景(1 发 2 收)
  • std::atomic<int> count 验证收到消息数

6.4 field 测试(Setter/Getter)

验证状态同步模型:

  • Setter<T> 设置最新值
  • 等待一段时间后,Getter<T> 可以读取到最新值
  • get().value() 验证值正确性

6.5 dynamic 测试

验证 DynamicData 动态类型传输:

DynamicData data;
pub1.publish(data.load("type1", std::string("hello")));
pub1.publish(data.load("type2", 2));
pub1.publish(data.load("type3", Bytes{1, 2, 3, 4, 5, 6, 7, 8, 9}));
sub1.listen([&count](const DynamicData& msg) {
if (msg.get_type() == "type1") {
if (msg.as<std::string>() == "hello") { ++count; }
}
// ...
});

6.6 serialize 测试

条件编译,验证与 Protobuf 或 FlatBuffers 序列化的集成:

  • VLINK_TEST_SUPPORT_PROTOBUF:使用 pb::Request/pb::Response/pb::Message
  • VLINK_TEST_SUPPORT_FLATBUFFERS:使用 fbs::RequestT/fbs::ResponseT/fbs::MessageT
  • VLINK_TEST_SUPPORT_SECURITY:使用 SecurityPublisher/SecuritySubscriber/SecurityClient/SecurityServer

7. 编写新测试的规范与示例

7.1 命名规范

元素 规范 示例
TEST_CASE "模块名-功能名",全小写,连字符分隔 "base-Timer"
SUBCASE 描述场景的简短短语 "Single-threaded lock"
测试文件 模块名_test.cc,下划线命名 wheel_timer_test.cc
原子计数器 使用 std::atomic<int> 而非普通 int std::atomic<int> count{0}

7.2 新测试文件模板

#include "./base/my_module.h" // 被测模块头文件
#include <doctest/doctest.h>
#include <chrono>
#include <thread>
#include "./base/logger.h"
using namespace std::chrono_literals;
using namespace vlink;
TEST_CASE("base-MyModule") {
// 1. 构造被测对象
MyModule obj;
// 2. happy path
SUBCASE("normal operation") {
auto result = obj.do_something(42);
CHECK(result == 42);
}
// 3. edge case
SUBCASE("empty input") {
auto result = obj.do_something(0);
CHECK(result == 0);
}
// 4. error case
SUBCASE("invalid input throws") {
CHECK_THROWS_AS(obj.do_something(-1), std::invalid_argument&);
}
}
Global singleton logger with three output styles and pluggable backends.

7.3 传输后端新测试模板

#include "./common_test.h"
#if defined(VLINK_SUPPORT_MYBACKEND)
TEST_CASE("mybackend-init") {
Url url("mybackend://test_topic");
// 验证 URL 解析...
}
TEST_CASE("mybackend-event") {
std::atomic<int> count{0};
Publisher<std::string> pub("mybackend://test_topic");
Subscriber<std::string> sub("mybackend://test_topic");
sub.listen([&count](const std::string& msg) {
if (msg == "hello") {
++count;
}
});
CHECK(pub.wait_for_subscribers());
pub.publish("hello");
std::this_thread::sleep_for(100ms);
CHECK(count.load() == 1);
}
#endif

8. 打桩(Mock/Stub)技巧

doctest 本身不提供 mock 框架,VLink 测试中采用以下打桩方式:

8.1 回调 Lambda 捕获计数

最常用的手段,通过 std::atomic<int> 验证回调触发次数和条件:

std::atomic<int> count{0};
std::atomic<bool> got_expected{false};
sub.listen([&count, &got_expected](const std::string& msg) {
++count;
if (msg == "expected") {
got_expected.store(true);
}
});
// 发布消息后等待处理
std::this_thread::sleep_for(50ms);
CHECK(count.load() == 1);
CHECK(got_expected.load());

8.2 自定义 Handler 打桩

Logger 测试中将回调替换为捕获 lambda,验证参数:

Logger::register_console_handler([&count](Logger::Level level, std::string_view log) {
++count;
if (level == Logger::kInfo) {
CHECK(log == "expected message");
}
});
// 测试完成后清除 handler

8.3 条件编译隔离外部依赖

依赖外部库(Protobuf/FlatBuffers)的测试使用条件编译,确保在未安装依赖时仍能编译:

TEST_CASE("transport-serialize") {
#if defined(VLINK_TEST_SUPPORT_PROTOBUF)
// Protobuf 相关测试
#endif
#if defined(VLINK_TEST_SUPPORT_FLATBUFFERS)
// FlatBuffers 相关测试
#endif
}

8.4 利用 std::future 超时

避免测试无限阻塞,使用 async_invoke + wait_for 设置超时:

auto ret = client.async_invoke("request");
REQUIRE(ret.wait_for(5s) == std::future_status::ready);
CHECK(ret.get() == "response");

9. 测试用例模式

9.1 Happy Path(正常路径)

验证在正常输入下功能按预期工作:

SUBCASE("normal push and pop") {
q.push(42);
int v = 0;
q.pop(v);
CHECK(v == 42);
}

9.2 Edge Cases(边界用例)

验证边界条件:

SUBCASE("queue full") {
CHECK(q.try_push(1) == true);
CHECK(q.try_push(2) == true);
CHECK(q.try_push(3) == false); // 满队列返回 false
}
SUBCASE("queue empty") {
int v = 0;
CHECK(q.try_pop(v) == false); // 空队列返回 false
}

9.3 Error Cases(错误用例)

验证错误处理路径,使用 CHECK_THROWS_AS

SUBCASE("invalid transport throws") {
auto error = [] { Publisher<int> pub("invalid_transport://topic"); };
CHECK_THROWS_AS(error(), std::runtime_error&);
}

9.4 Concurrency Cases(并发用例)

对共享资源和并发操作的测试:

TEST_CASE("base-SpinLock-stress") {
SpinLock spinlock;
std::atomic<int> shared = 0;
constexpr int kThreads = 16;
constexpr int kIters = 1000;
std::vector<std::thread> threads;
for (int i = 0; i < kThreads; ++i) {
threads.emplace_back([&spinlock, &shared]() {
for (int j = 0; j < kIters; ++j) {
spinlock.lock();
shared++;
spinlock.unlock();
}
});
}
for (auto& t : threads) { t.join(); }
CHECK(shared == kThreads * kIters);
}

10. 并发测试注意事项

  1. **使用 std::atomic 而非普通变量**:多线程读写计数器必须用 std::atomic<int>,避免数据竞争。
  2. **等待时间要有余量**:发布消息后的 sleep_for 至少留 50ms 余量,避免偶发性失败(flaky test)。传输后端(如 shm)初始化较慢,可能需要 500ms 甚至 2000ms。
  3. **使用 wait_for_subscribers / wait_for_connected**:不要假设订阅者/连接立刻就绪,务必等待就绪后再发送。
  4. **std::future::wait_for 设合理超时**:RPC 测试中超时设为 5s,保证在慢机器上不误判超时。
  5. **SUBCASE 内变量状态独立**:doctest 的 SUBCASE 会重新执行 TEST_CASE 体,父级代码每个 SUBCASE 都执行一次。避免在 SUBCASE 之间共享副作用。
  6. **压力测试线程数量**:生产/消费线程建议不超过 CPU 核心数的 2 倍,避免调度延迟引起误判。
  7. **避免测试之间的状态污染**:使用局部变量而非全局变量,每个 TEST_CASE 应独立,不依赖其他测试用例的执行顺序。

11. 测试数据准备

11.1 IDL 文件

测试依赖的 IDL 文件位于 test/idl/

  • *.proto:Protobuf 消息定义(pb::Requestpb::Responsepb::Message
  • *.fbs:FlatBuffers 消息定义(fbs::RequestTfbs::ResponseTfbs::MessageT
  • *.idl:DDS IDL 定义(用于 FastDDS-gen 代码生成,默认关闭)

CMake 配置会自动生成对应的 C++ 代码:

vlink_generate_cpp(TARGET sample_gen_protobuf PROTO ${IDL_SRCS})
vlink_generate_cpp(TARGET sample_gen_flatbuffers FBS ${IDL_SRCS})

12. 代码覆盖率

12.1 覆盖率类型

类型 说明 工具支持
行覆盖率 被执行的代码行数 / 总代码行数 gcov, lcov
分支覆盖率 被执行的分支(if/else/switch)数 / 总分支数 gcov -b, lcov -b
函数覆盖率 被调用的函数数 / 总函数数 gcov, lcov
语句覆盖率 被执行的语句数 / 总语句数 gcov

12.2 默认排除路径

顶层 CMakeLists.txtENABLE_TEST_COVERAGE=ON 时传入以下排除:

*/thirdparty/* */test/* */private/* */extension/* */examples/* */cli/* */builtin/*

13. 覆盖率编译配置

13.1 GCC/Clang 覆盖率编译选项

覆盖率插桩需要在编译和链接时同时加入覆盖率标志:

# GCC 方式(--coverage 等价于 -fprofile-arcs -ftest-coverage)
--coverage
# 完整等价写法
-fprofile-arcs -ftest-coverage

编译后,编译器会为每个 .cc 文件生成:

  • *.gcno:记录代码分支结构(编译时生成)
  • *.gcda:记录实际执行路径和次数(运行测试后生成)

13.2 优化级别注意事项

覆盖率编译必须关闭内联优化,否则结果不准确:

# 覆盖率专用构建类型(推荐)
-O0 -g --coverage
# 不要使用 -O2/-O3,否则内联函数无法被正确统计

13.3 项目内置的覆盖率集成

本仓库的覆盖率由 cmake/functions/common.cmake 里的 vlink_test_coverage(target ...) 辅助函数提供;顶层 CMakeLists.txtENABLE_TEST_COVERAGE=ON 时调用它,并排除 thirdparty/ test/ private/ extension/ examples/ cli/ builtin/ 路径。

  • 优先使用 gcovr(若已安装);否则回退到 lcov + genhtml
  • 生成两个 CMake 自定义目标:
    • coverage(运行 ctest 并采集)
    • coverage-report(生成 HTML 报告)

因此一般**不需要**自己再定义 CMAKE_CXX_FLAGS_COVERAGE 或新增 add_custom_target,直接:

cmake -B build-coverage -DCMAKE_BUILD_TYPE=Debug -DENABLE_TEST_COVERAGE=ON
cmake --build build-coverage --target coverage -j

14. 手动 lcov 流水线(不依赖 vlink_test_coverage

如果需要直接用 lcov,而非项目内置的 coverage 目标:

步骤一:编译含覆盖率插桩的测试

mkdir -p build-coverage && cd build-coverage
cmake .. \
-DCMAKE_BUILD_TYPE=Coverage \
-DCMAKE_CXX_FLAGS="--coverage -O0 -g" \
-DCMAKE_EXE_LINKER_FLAGS="--coverage" \
-DENABLE_SECURITY=ON
cmake --build . --target vlink-test -j$(nproc)

步骤二:清零旧的计数数据

lcov --directory . --zerocounters

步骤三:运行测试

ctest --output-on-failure

步骤四:收集覆盖率数据

lcov \
--directory . \
--capture \
--output-file coverage.info \
--ignore-errors mismatch

--ignore-errors mismatch 用于处理头文件多次包含时的轻微不匹配问题。

步骤五:过滤不需要统计的文件

lcov \
--remove coverage.info \
'*/thirdparty/*' \
'*/internal/*' \
'*/test/*' \
'/usr/*' \
'/opt/*' \
'*/doctest/*' \
--output-file coverage_filtered.info \
--ignore-errors unused

过滤规则说明:

过滤路径 原因
*/thirdparty/* 第三方库,不属于 VLink 代码
*/internal/* 内部实现细节,不要求覆盖
*/test/* 测试代码本身不需要统计
/usr/* 系统头文件
/opt/* 外部安装库(如 FastDDS)
*/doctest/* 测试框架头文件

步骤六:生成 HTML 报告

genhtml \
coverage_filtered.info \
--output-directory coverage_html \
--title "VLink Code Coverage" \
--legend \
--show-details \
--num-spaces 4
参数 作用
--output-directory HTML 报告输出目录
--title 报告页面标题
--legend 显示颜色图例
--show-details 展示每个文件的详细覆盖数据
--num-spaces tab 展开空格数

步骤七:查看报告

xdg-open coverage_html/index.html
# 或查看文本摘要
lcov --summary coverage_filtered.info

输出示例:

Reading tracefile coverage_filtered.info
Summary coverage rate:
lines......: 91.3% (4521 of 4952 lines)
functions..: 88.7% (312 of 352 functions)
branches...: 74.2% (1823 of 2456 branches)

15. 使用 gcov 查看单文件覆盖率

gcov 可以为单个文件生成逐行覆盖率注解:

cd build-coverage
# 为特定文件生成覆盖率
gcov -r src/CMakeFiles/vlink.dir/base/bytes.cc.gcno
# 查看每行执行次数
cat bytes.cc.gcov

gcov 输出格式:

-: 0:Source:src/base/bytes.cc
1: 42:void Bytes::deep_copy_self() {
1: 43: if (data_ == nullptr) { return; }
#####: 44: // 未执行行(0 次)
1: 45: ...
  • 数字 = 执行次数
  • ##### = 未覆盖行
  • - = 非代码行(注释/空行)

16. 排除不需要覆盖的代码

16.1 使用 lcov 排除注解

在源码中可以用特殊注解通知 lcov 跳过特定代码块:

// 排除整个函数
// LCOV_EXCL_START
void debug_only_function() {
// 这段代码不统计覆盖率
}
// LCOV_EXCL_STOP
// 排除单行
int x = unreachable_code(); // LCOV_EXCL_LINE
// 排除分支
if (unlikely_condition) { // LCOV_EXCL_BR_LINE
handle_rare_case();
}

16.2 典型排除场景

// 1. 断言/panic 路径
VLINK_ASSERT(ptr != nullptr); // LCOV_EXCL_LINE
// 2. 平台相关代码(非目标平台不执行)
#if defined(__QNX__)
// QNX 特定代码
#endif
// 3. 无法通过正常测试触发的错误处理
} catch (const std::bad_alloc& e) { // LCOV_EXCL_LINE
VLOG_F("Out of memory"); // LCOV_EXCL_LINE
} // LCOV_EXCL_LINE

17. 提高覆盖率的建议

查看 HTML 报告中红色(未覆盖)行时,通常的补测方向:

  • 错误处理分支:catch、错误返回路径
  • 边界条件:空指针检查、空容器
  • 条件编译块:需要特定选项才能激活的代码
  • 虚函数基类默认实现:如 NodeImpl::suspend() / NodeImpl::resume() 的默认返回 false 分支
  • 模板实例化:对常用 MsgTint / std::string / Bytes)都跑一轮

常用命令:

lcov --list coverage_filtered.info # 每文件覆盖率
lcov --diff base.info new.info --output-file diff.info # diff 覆盖率

18. Clang 覆盖率工具链(llvm-cov 替代方案)

如使用 Clang 编译器,应改用 llvm-covllvm-profdata

# 编译(Clang 方式)
-fprofile-instr-generate -fcoverage-mapping
# 合并数据
llvm-profdata merge -sparse default.profraw -o default.profdata
# 生成报告
llvm-cov show ./vlink-test -instr-profile=default.profdata \
--format=html --output-dir=coverage_html
# 导出为 lcov 格式(供 genhtml 使用)
llvm-cov export ./vlink-test -instr-profile=default.profdata \
--format=lcov > coverage.info

19. CI/CD 集成参考

当前仓库**未附带** CI 配置文件(既无 .github/workflows/,也无 .gitlab-ci.yml)。下述最小流水线仅作模板,采用时需按实际需求调整。

19.1 GitHub Actions 最小模板

name: Test
on:
push: { branches: [master] }
pull_request: { branches: [master] }
jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install -y cmake ninja-build
- run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
- run: cmake --build build --target vlink-test -j
- run: ctest --test-dir build --output-on-failure

覆盖率采集可在上面基础上追加 -DENABLE_TEST_COVERAGE=ON,并在构建后执行 cmake --build build --target coverage(由 vlink_test_coverage 辅助函数定义)。


20. 常见问题排查

Q1: genhtml 报告中有大量系统头文件

原因:lcov 收集时包含了系统头文件。

解决:在 --remove 步骤中添加 /usr/*/opt/* 过滤。

Q2: 覆盖率数据为空(0)

可能原因:

  1. 忘记在链接阶段加 --coverageCMAKE_EXE_LINKER_FLAGS
  2. 测试二进制未运行,<tt>.gcda

文件未生成

  1. lcov --directory 指向了错误的构建目录

排查命令:

# 检查 .gcda 文件是否生成
find build-coverage -name "*.gcda" | head -20
# 检查编译标志
cmake -B build-coverage --log-level=VERBOSE 2>&1 | grep "coverage"

Q3: lcov 报 "mismatch" 错误

原因:源文件修改后未重新编译,<tt>.gcno 与 .gcda 版本不一致。

解决:

# 清除构建目录重新编译
rm -rf build-coverage && cmake -B build-coverage ...
# 或使用 --ignore-errors mismatch 忽略
lcov ... --ignore-errors mismatch

Q4: 模板函数覆盖率显示异常

原因:模板在头文件中实例化,gcov 可能将同一行计数多次或显示在不同文件。

解决:将 include/vlink/internal/ 也加入排除列表。