文件最后提交记录最后更新时间
2 个月前
4 个月前
3 个月前
5 个月前
2 个月前
1 个月前
1 个月前
22 天前
3 个月前
4 个月前
1 个月前
4 个月前
5 个月前
README

Devmon 测试模块总结

概述

tests/ 目录包含了 Devmon 设备管理系统的完整测试套件,覆盖了设备发现、设备管理、拓扑管理和工具函数等核心功能。测试使用 Google Test 框架编写,通过 meson 构建系统运行。

测试模块结构

测试套件按功能模块组织,分为以下几个子目录:

1. discovery/ - 设备发现测试模块

测试设备发现和 CSR(配置状态寄存器)解析相关的核心功能,包括应用模式(App Schema)、CSR 解析器、CSR 工具函数等。 详细说明请参考:discovery/README.md

2. device/ - 设备管理测试模块

测试设备管理相关的核心功能,包括设备管理器、CSR 处理和设备驱动等。 详细说明请参考:device/README.md

3. topology/ - 拓扑管理测试模块

测试设备拓扑管理相关的核心功能,包括拓扑对象的创建、设备关系的建立、Host Bus 继承等。 详细说明请参考:topology/README.md

4. plugins/ - 应用层 Lua 插件测试(独立目标)

C++ 宿主加载生产 hwproxy.plugins.* 快照(power_mgmt / general_hardware / smbus / pcie_device / sml)。五个独立 executable,不并入下面的 646 个 devmon_test。运行时需要 /opt/bmc/luaclib/mc.so、/opt/bmc/lualib/mc/init.lua 与 /opt/bmc/mcskynet(lua_engine 硬编码)。解释器与 mc.so 都用 Conan LuaJIT(与主 builddir / 板上同 ABI);smart_build.sh / Fixture 会把 builddir 产物拷到 /opt/bmc,并把断链的 lualib 改指 /usr/local/opt/bmc/lualib。

hwproxy.plugins.* 是 Lua require 名,不是 C++ libs/hwproxy。这些用例测的是 Chip so 里的 lua_engine + init.lua,不迁入 libs/hwproxy/tests/(那边测 hwproxy_service 启停/dump;且 if unidev 才编译,并会链 LuaJIT)。理由见 plugins/README.md「为何不迁入 libs/hwproxy/tests」。

总览:plugins/README.md。分插件:power_mgmt、general_hardware、mctpd/smbus、pcie_device、storage/sml。

5. libs/device_runtime/tests/ - 硬件访问调度器

独立目标 devmon_device_runtime_test(suite hwproxy_plugin,37 用例),测 access_domain_scheduler 的域准入、限流、合并读与 logical_executor 分离。不并入 devmon_test。

详细说明请参考:device_runtime 测试

6. 根目录测试文件

根目录包含以下测试文件:

test_devmon.cpp - Devmon 应用测试

测试 Devmon 应用的核心功能,包括服务初始化、清理机制、根对象管理、信息导出等。

测试用例数量:15 个

服务初始化测试:

  • init_with_app_paths: 测试服务初始化(带应用路径配置)
  • init_populates_app_schema: 测试初始化时加载应用模式(Schema)

清理机制测试:

  • cleanup: 测试清理方法(基本功能)
  • cleanup_after_start: 测试服务启动后调用清理方法
  • cleanup_multiple_calls: 测试多次调用清理方法(幂等性)

根对象管理测试:

  • root_objects_exist_after_start: 验证服务启动后根对象已注册(Devmon、DevTopology、ObjectGroupRoot)
  • root_object_duplicate_creation_throws: 测试重复创建根对象抛出异常
  • object_group_root_duplicate_creation_throws: 测试重复创建对象组根对象抛出异常

服务生命周期测试:

  • stop_without_topology_instance_returns_true: 测试未启动拓扑发现时停止服务
  • stop_handles_topology_cleanup_exception: 测试停止服务时的拓扑清理异常处理

命令行选项测试:

  • register_options: 测试命令行选项注册(boost::program_options 集成)

信息导出测试(Dump 功能):

  • dump_scanners_info: 测试导出扫描器信息到 CSV 文件(snapshot.csv)
  • dump_drivers_load_info: 测试导出驱动加载信息到 CSV 文件(drivers_load_info.csv)
  • dump_chips_access_info: 测试导出芯片访问统计信息到 CSV 文件(chips_access_statistic.csv)
  • dump_accessors_info: 测试导出访问器信息到 CSV 文件(snapshot.csv,包含访问器标题行)

主要功能:

  • 服务初始化和配置(应用路径、Schema 加载)
  • 清理机制和资源管理(幂等性、异常安全)
  • 根对象生命周期管理(创建、验证、重复创建检测)
  • 服务启动和停止流程(拓扑发现集成、异常处理)
  • 命令行选项注册和配置
  • 信息导出功能(扫描器、驱动、芯片访问、访问器信息的 CSV 导出)

test_utils.cpp - 工具函数测试

测试工具函数的正确性,包括时间获取、连接器位置解析、Unit 信息提取等。

测试用例数量:49 个

get_sys_ms 测试(2 个用例):

  • test_get_sys_ms_basic: 基本功能测试(时间获取、时间递增验证)
  • test_get_sys_ms_multiple_calls: 多次调用测试(时间单调递增)

get_connector_position 测试(23 个用例):

  • 正常情况:
    • 整数类型(int8_t、int16_t、int32_t、int64_t、uint8_t、uint16_t)
    • 字符串类型(正常字符串、小写字符串)
    • 零值处理
    • 大值处理(0xFF、0x1234、0xABCD、0xEF01、0x3DEF)
    • 三位十六进制数补零(0xABC → "0ABC")
  • 边界情况:
    • 空字符串
    • 字段缺失
    • 其他类型(布尔值、null、double、字典)
    • 奇数长度补齐(0x0A、0x01、0x05、0x0B、0x0F)

get_connector_unit 测试(24 个用例):

  • 正常情况:
    • 成功获取 Unit 信息(基本功能)
    • 多个兼容驱动
    • 空兼容列表
    • 兼容列表包含数字(自动转换为字符串)
    • 包含额外字段(应被忽略)
  • 异常情况:
    • Unit 字段缺失
    • Unit 字段为空字典
    • Unit 字段类型错误(字符串、整数、数组、布尔值、null、double、嵌套字典)
    • 空类型(m_type 为空)
    • 缺少 Name 字段
    • 缺少 Compatible 字段
  • 类型转换错误:
    • Type 字段类型不匹配(数组、整数、字典)
    • Name 字段类型不匹配(字典)
    • Compatible 字段类型不匹配(字符串、字典)
    • 复杂嵌套结构处理

主要功能:

  • 系统时间获取(单调递增、多次调用)
  • 连接器位置解析(多种整数类型、字符串、边界情况、类型容错)
  • Unit 信息提取(反射解析、类型验证、异常处理、字段容错)

动态驱动加载资源回归

本节跟踪 PR !416 / 任务 #144 的动态加载问题。生产代码位于 device_driver.cpp,回归用例位于 test_driver.cpp,真实插件夹具为 memory_throwing_driver.cpp 和 memory_nonstandard_throwing_driver.cpp。范围限于注册失败清理和重复加载保护,不代表全库内存安全验收。

问题与清理顺序

原 device_driver::load_file() 在 dlopen 成功、注册失败后仍正常返回,工厂随后可能缓存失败驱动;再次加载还可能覆盖旧句柄,丢失对应的 dlclose 入口。当前实现先拒绝已有句柄的重复加载,并在注册失败时清理后向调用者报告失败。

以下是同一函数的关键片段;省略日志和耗时统计,不是完整函数。

// 修复前:捕获注册异常后继续正常返回。
try {
    register_driver();
    info.success      = true;
    info.device_count = m_devices.size();
} catch (const std::exception& e) {
    info.error_msg = e.what();
}
// 当前:先复制插件错误,离开 catch 后再卸载动态库。
bool        registration_failed = false;
std::string registration_error;
try {
    register_driver();
    info.success      = true;
    info.device_count = m_devices.size();
} catch (const std::exception& e) {
    registration_error  = e.what();
    registration_failed = true;
} catch (...) {
    registration_error  = "unknown driver registration error";
    registration_failed = true;
}

try 内的函数抛出异常后,控制流进入匹配的 catch,不会继续执行成功赋值。const std::exception& e 是对异常对象的只读引用,可类比 C 中借用对象的指针;它不会复制异常对象。e.what() 返回的错误文本也不应跨越异常对象的寿命使用,赋给 std::string 才得到独立拥有的文本副本,类似 C 中另行分配并复制错误字符串。catch (...) 还接住 throw 42 这类不继承 std::exception 的异常。

当前失败路径的执行顺序为:复制错误文本 → 离开 catch 并销毁本路径的插件异常对象 → unload() 清空设备项并 dlclose → 记录失败信息 → 抛出 mc::bad_function_call_exception。C++ 异常对象销毁时会执行其析构函数;若异常类型由插件定义,这段代码可能位于 .so,所以必须先结束异常处理再卸载库。复制后的错误文本仍保留到日志记录和框架异常构造完成。mc::bad_function_call_exception 是项目的错误类型,并非 C++ 标准库提供的资源清理机制。

device_driver.h 明确约定:同一实例的加载、卸载和设备访问由调用者串行化。工厂为每次候选加载创建独立实例,在缓存锁内发布成功结果;竞争失败的独立实例会卸载自身句柄。unload() 的普通指针赋值与 C 的 void *handle = obj->handle; obj->handle = NULL; 一样,并非原子交换,也没有互斥作用。get_device() / get_devices() 返回借用引用,设备中的函数指针仍指向插件;调用方必须在卸载前结束使用,不能把 std::shared_ptr 持有的设备结构体误认为动态库也被自动保活。

回归用例与运行入口

以下用例均属于 DeviceDriverTest:

用例 场景及关键断言
LoadLibraryWithoutRegistrationRejectsAndClosesHandle 库缺注册入口:抛出错误、句柄置空、设备集合为空,并记录失败
RepeatedLoadKeepsOriginalHandle 已有句柄时再次加载:拒绝操作,保留原句柄
NonstandardRegistrationExceptionClosesHandle 真实插件抛出非标准异常:清理后报错,RTLD_NOLOAD 检查插件不再驻留
PluginDefinedRegistrationExceptionClosesHandle 真实插件抛出自定义 std::exception:异常对象先于库卸载销毁,检查句柄及驻留状态

从仓库根目录使用正式测试入口:

bingo test -ut -jit
bingo test -ut -jit -as

这是 CI 环境的完整复现入口。本轮本机 Bingo 缺少 test 子命令,因此本地使用复用依赖的原生 Meson 构建,不能将其表述为 Bingo 全流程通过。当前 Lua 包装入口不会将 Bingo 的 -f 参数转交给 GTest,不能用 -f 'DeviceDriverTest.*' 证明这些 C++ 用例已运行;应检查日志中的实际 RUN / OK / FAILED 记录。执行前核对实际构建源码、测试程序和依赖版本,同时保留实际可执行文件路径。新增用例依赖 libdl.so.2、/proc/self/exe 和 RTLD_NOLOAD,目前验证范围为 Linux/glibc。

插件构建、安装与用例隔离

common 测试构建配置 将 test_driver.cpp 同时编入 common_test 可执行文件及 common_test_lib 静态库;devmon 测试构建配置 链接该静态库。test_csr.cpp 引用同一对象文件中的 test_driver::create(),因此插件回归用例也会进入 devmon_test。这与 C 链接静态库时为满足一个函数引用而取出整个 .o 的情形相同。

原配置仅构建插件,未安装;测试又只在自身可执行文件目录找库,导致 CI 的 temp/bin/devmon_test 缺少夹具。现将两个插件安装到与测试程序相同的 bindir,并在所有引用这些用例的 Meson 测试入口声明插件构建依赖:

入口 插件定位方式
Meson common_test / devmon_test / unidev 的 hwdiscovery_test Meson 设置 DEVMON_TEST_DRIVER_DIR,指向构建目录的 common 测试夹具目录
安装或移动后的测试程序(包括 CI temp/bin/devmon_test) 未设置该变量时,在 /proc/self/exe 所在目录找插件

scripts/run_tests_loop.sh 从 Meson 测试定义读取并传递插件目录;缺少定义时使用构建目录的 libs/common/tests。直接运行构建目录内的 devmon_test 时也应设置 DEVMON_TEST_DRIVER_DIR,或使用 Meson 测试入口。部署验收必须取消该变量,防止意外从构建树借到插件而掩盖漏打包。测试先检查夹具存在,再断言注册失败和插件卸载;不能把缺库导致的 mc::system_exception 当作注册失败的 mc::bad_function_call_exception。

两个插件分别固定抛出整数异常和插件定义的 std::exception,不再通过 DEVMON_TEST_STD_EXCEPTION 切换行为。这里抛整数用于覆盖 catch (...);另一个插件的异常对象包含定义在 .so 中的虚函数实现,测试确保捕获结束后才卸载库。路径变量只配置测试资产所在目录,不改变插件行为。夹具只在启用测试的构建中安装,不进入关闭测试的产品构建。

路径修复与 C++ 写法

原测试直接使用可执行文件目录,无法覆盖插件位于 common 目录、程序位于 devmon 目录的构建布局:

// 修改前,来自原测试。
auto library_path = std::filesystem::read_symlink("/proc/self/exe").parent_path() / "libmemory_throwing_driver.so";

现在两个用例各自选固定插件,并提前诊断缺失文件。以下摘自整数异常用例:

const auto library_path = throwing_driver_path("libmemory_nonstandard_throwing_driver.so");
ASSERT_TRUE(std::filesystem::is_regular_file(library_path)) << "Missing test driver: " << library_path;

auto 让编译器从右侧推导类型,此处得到 std::filesystem::path;const 表示这个局部路径不再修改。path 自己保存路径内容,可类比一个管理字符串的 C 结构体;路径之间的 / 是 C++ 重载的拼接操作,不是整数除法。throwing_driver_path() 先通过与 C 相同的 getenv() 借用目录字符串,再构造自己的路径对象;未设置目录时回到可执行文件目录。ASSERT_TRUE 是 GTest 的断言宏,失败即结束当前用例;后面的 << 为诊断输出追加实际路径。它先排除“测试资产缺失”,后面的异常类型、错误文本、加载记录和 RTLD_NOLOAD 断言才检验“插件已执行但注册失败并卸载”。

flowchart LR
    A[原安装配置:只安装测试程序] --> B[temp/bin 缺少插件]
    B --> C[dlopen 失败,未执行注册函数]
    D[修复:两个固定插件随测试程序安装] --> E[同目录找到插件]
    E --> F[调用注册函数并捕获异常]
    F --> G[复制错误文本,异常对象销毁]
    G --> H[清空设备并 dlclose]
    H --> I[断言错误文本及插件不再驻留]

验证记录与限制

  • 本轮合入主线 9eea32c 后,以 GCC 9、原生 Meson、本机既有依赖完成构建;三个已配置测试入口全部通过:调度测试 59 项、common_test 415 项、devmon_test 1024 项通过,后者另有 14 项 CSR 引用对象用例按现有条件跳过。不是完整 Bingo/产品依赖构建结果。
  • 构建目录中两个主要入口的 DeviceDriverTest.* 各 17 项通过。执行实际 meson install 后,取消插件目录变量,以安装目录中的库运行两个测试程序,各 17 项随机顺序重复 10 轮通过;临时移走两个已安装夹具时,目标两用例明确因缺文件失败,恢复夹具后通过。
  • unidev=true 单独完成 Meson 配置,核对 common_test、devmon_test、hwdiscovery_test 均具有插件目录和两个插件依赖;本轮没有编译或运行完整 unidev 套件。循环脚本的实际 Meson 参数解析、空库路径字段及解析失败回退已验证。
  • 历史本地 57e786db 的 ASAN 驱动测试 17/17、common_test 408/408 属于旧提交与旧构建,不能计入本轮验证。本轮未重新执行 ASAN 或目标板测试。
  • 用户提供的 2026-10-13 CI 日志对应源提交 5aa8a6a 的合成提交 42a06e874afcb094d0e5cb2dfce139ffa0b7d6b7:编译了插件,包内 .so 列表却不含插件,实际运行 temp/bin/devmon_test,最终列出两条插件异常用例失败。附件仅包含失败摘要,没有原始断言全文。
  • tests/main.cpp 使用 std::quick_exit;仅设置 detect_leaks=1 不能证明 LSAN 正常退出扫描已完成。远端 CI 必须按推送后的实际提交单独核对,不能复用上述历史失败或本地结果替代。

测试统计

以下总数、覆盖率和连续运行统计为既有文档记录,未在本轮重新测量;不能替代上节按提交和入口区分的验证结果。

总体统计

该测试套件共包含 646 个测试用例:

模块 测试文件 测试用例数 说明
discovery/ 4 个文件 133 个 设备发现和 CSR 解析
device/ 3 个文件 175 个 设备管理
topology/ 2 个文件 274 个 拓扑管理
根目录 2 个文件 64 个 应用和工具函数测试(test_devmon.cpp: 15 个,test_utils.cpp: 49 个)
plugins/ 5 个文件 48 个 真实 Lua 插件(独立目标,不计入上行 646)
总计 11 个文件 646 个 devmon_test / common_test 主套件

代码覆盖率

当前测试覆盖情况如下:

总体覆盖率

指标 覆盖率 覆盖数量 总数
代码行(Lines) 93.1% 2,071 行 2,225 行
函数(Functions) 72.0% 376 个 522 个
分支(Branches) 47.6% 3,812 个 8,016 个

覆盖率分析

代码行覆盖率(93.1%)

  • 覆盖情况良好:超过 93% 的代码行已被测试覆盖
  • 未覆盖部分:主要是错误处理路径、边界情况和一些特殊场景
  • 改进方向:可以增加边界情况和错误处理的测试用例

函数覆盖率(72.0%)

  • 覆盖情况良好:超过 72% 的函数已被测试覆盖
  • 未覆盖部分:主要集中在反射相关函数、Lambda 函数、错误处理函数和辅助函数
  • 改进方向:可以增加对辅助函数和工具函数的测试覆盖

分支覆盖率(47.6%)

  • 覆盖情况仍偏低:这是需要重点改进的方面
  • 未覆盖部分:条件分支、错误处理分支和特殊逻辑分支仍然是薄弱环节
  • 改进方向:需要增加更多的边界情况测试和条件分支测试

覆盖率改进建议

  1. 增加边界情况测试:覆盖更多的边界条件和错误处理路径
  2. 增加条件分支测试:提高分支覆盖率,特别是复杂条件判断的分支
  3. 增加错误处理测试:测试异常情况和错误恢复机制
  4. 增加并发测试:测试多线程场景和并发安全性
  5. 增加集成测试:测试模块间的交互和集成场景
  6. 间接测试反射函数:通过实际使用场景间接测试反射相关函数
  7. 触发 Lambda 执行:通过触发相应条件来覆盖 Lambda 中的代码路径

测试框架和基础设施

测试框架

  • Google Test:所有测试用例使用 Google Test 框架编写
  • meson 构建系统:通过 meson 构建和运行测试
  • TestBase 继承:所有测试类继承自 mc::test::TestWithEngine,提供统一的测试环境

测试基础设施

测试驱动和模拟

  • test_driver:提供模拟设备驱动,支持创建 PCIeNicCard、NetworkPort、OpticalModule 等设备类型
  • mock_topology_drivers:提供模拟的拓扑设备驱动(I2c、Chip、Pca9545)

D-Bus 集成

  • 测试中集成了 D-Bus 通信,验证设备在系统中的注册和属性访问
  • 支持重试机制(最多 5 次重试,每次间隔 100-200ms)

测试隔离机制

  • 对象表清理:每个测试用例后自动清理对象表中的对象(保留根对象)
  • 多次清理:多次清理确保对象被完全删除(最多 3 次重试,处理依赖关系)
  • 智能删除顺序:按路径长度降序删除,先删除子对象(路径更长的)
  • 缓存清理:清空 func_collection 缓存,避免测试用例间相互影响
  • 驱动清理:移除测试驱动,确保驱动状态干净
  • 异常安全:支持异常安全的清理操作

测试最佳实践

1. 测试隔离

  • 每个测试用例应该是独立的,不依赖其他测试用例的执行顺序
  • 使用 SetUp() 和 TearDown() 确保测试环境的一致性和清理

2. 测试清理

  • 确保测试后清理所有创建的资源(对象、驱动、缓存等)
  • 使用多次清理机制处理依赖关系
  • 按正确顺序删除对象(先删除子对象)

3. 错误处理

  • 测试异常情况时使用 EXPECT_THROW 或 ASSERT_THROW
  • 测试正常情况时使用 EXPECT_NO_THROW
  • 验证错误消息和异常类型

4. D-Bus 测试

  • 使用重试机制处理 D-Bus 调用的异步性
  • 验证 D-Bus 消息的有效性和类型
  • 处理路径冲突等边界情况

5. 覆盖率考虑

  • 编写测试时考虑覆盖率目标
  • 优先覆盖关键路径和核心功能
  • 逐步增加边界情况和错误处理的测试

持续改进

当前状态

  • ✅ 测试用例数量充足(646 个)
  • ✅ 代码行覆盖率良好(93.1%)
  • ✅ 函数覆盖率良好(72.0%)
  • ⚠️ 分支覆盖率较低(47.6%,仍需改进)
  • 历史稳定性记录为 100 次连续运行通过;动态加载回归仍有上文列出的 CI 失败,不能据此判断当前完整测试通过。

改进方向

  1. 提高分支覆盖率:增加条件分支和边界情况的测试
  2. 完善函数覆盖:增加对辅助函数和工具函数的测试
  3. 增加集成测试:测试模块间的交互和复杂场景
  4. 增加并发测试:测试多线程场景和并发安全性
  5. 增加性能测试:测试大规模数据和复杂场景下的性能

相关文档