ros_testing 源码详细分析
工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros2/ros_testing
版本:0.4.0(Humble),子包 2 个,许可证 Apache 2.0。
ros_testing 仓库体量很小(源码文件约 16 个),本身几乎不包含测试框架逻辑。它的定位是 ROS 2 集成测试的 统一入口(metapackage):把 launch_testing 生态、ros2 test CLI 与 CMake add_ros_test() 串成一条链路,供涉及 Node / DDS / launch 的包做 colcon test 集成测试。
重要区分:真正的 launch 集成测试框架在
launch与launch_ros仓库的launch_testing/launch_testing_ros包中。ros_testing是对 ROS 场景的薄封装与依赖聚合。
1. 总体认识
1.1 核心职责
| 能力 | 实现位置 | 说明 |
|---|---|---|
| CMake 注册 launch 测试 | ros_testing/cmake/add_ros_test.cmake |
包装 ament_add_test,命令改为 ros2 test |
| 依赖聚合 | ros_testing/package.xml |
导出 launch_testing* + ros2test,下游 test_depend 一处搞定 |
| CLI 入口 | ros2test/command/test.py |
ros2 test 子命令,带 ROS 域隔离 |
| ROS 专用 runner | 委托 launch_testing_ros.LaunchTestRunner |
与 launch_test 相同,预留 ROS 扩展点 |
1.2 在 ROS 2 测试栈中的位置
| 对比项 | ros_testing |
launch_testing_ament_cmake |
launch_testing |
|---|---|---|---|
| 角色 | ROS 集成测试 入口包 | 通用 CMake 集成 | Python 测试 引擎 |
| CMake 函数 | add_ros_test() |
add_launch_test() |
— |
| 运行命令 | ros2 test |
python -m launch_testing.launch_test |
被上述两者调用 |
| 域隔离 | 有(domain_coordinator) |
无 | 无 |
| 适用场景 | 涉及 ROS Node 的包 | 任意 launch 测试(含非 ROS) | 框架本体 |
1.3 与 ROS 1 rostest 的关系
README 仍写 “rostest”,CMake 宏里保留 rostest__strip_prefix 命名——这是从 ROS 1 rostest 移植 CMake 辅助逻辑时的历史痕迹。ROS 2 中:
- 不再有
rostest包与test_rostest宏; - 等价能力 =
launch_testing+launch_testing_ros+ros_testing; - 测试文件是 Python launch test(
*_launch_test.py或test_*.py),不是.testXML。
2. 仓库结构
1 | ros_testing/ |
| 包 | 构建类型 | 源码规模 | 职责 |
|---|---|---|---|
ros_testing |
ament_cmake(project(... NONE)) |
~82 行 CMake | 导出 add_ros_test、传递依赖 |
ros2test |
ament_python |
~51 行业务逻辑 | ros2 test 命令 |
3. 包 ros_testing:CMake 入口
3.1 CMakeLists.txt
1 | project(ros_testing NONE) |
特点:project(... NONE) 表示 不编译任何目标,纯配置/安装包。安装后下游通过 find_package(ros_testing REQUIRED) 获得 add_ros_test()。
3.2 ros_testing-extras.cmake
1 | find_package(launch_testing_ament_cmake REQUIRED) |
依赖链:ros_testing → launch_testing_ament_cmake(复用其 parse_launch_test_arguments 宏)。
3.3 add_ros_test() — 核心逻辑
1 | function(add_ros_test filename) |
与 add_launch_test() 的 唯一实质差异 是 COMMAND:
| 函数 | 执行命令 |
|---|---|
add_launch_test |
${PYTHON_EXECUTABLE} -m launch_testing.launch_test ... |
add_ros_test |
ros2 test ... |
其余(TARGET 推导、TIMEOUT 默认 60s、xUnit 路径、ARGS 传递)均来自共享宏 parse_launch_test_arguments。
parse_launch_test_arguments 行为摘要
(定义于 launch_testing_ament_cmake/cmake/add_launch_test.cmake)
| 参数 | 默认 | 说明 |
|---|---|---|
TIMEOUT |
60 | ctest 超时秒数 |
TARGET |
由文件路径生成 | 去掉 PROJECT_SOURCE_DIR 前缀,/ → _ |
ARGS |
— | 传给 launch 的 name:=value 参数 |
LABELS |
launch_test |
ctest 标签(add_ros_test 未显式设置,继承 ament_add_test 默认) |
RESULT_FILE |
$AMENT_TEST_RESULTS_DIR/$PROJECT_NAME/$TARGET.xunit.xml |
JUnit 报告 |
3.4 package.xml 依赖设计
1 | <buildtool_export_depend>launch_testing_ament_cmake</buildtool_export_depend> |
下游包只需:
1 | <test_depend>ros_testing</test_depend> |
1 | find_package(ros_testing REQUIRED) |
即可在 colcon test 时拉起完整 ROS launch 测试链。不必单独声明 launch_testing_ament_cmake / ros2test(但直接依赖 launch_testing_ament_cmake 也常见,见下文)。
4. 包 ros2test:ros2 test CLI
4.1 注册方式
setup.py:
1 | entry_points={ |
安装后可用:
1 | ros2 test /path/to/test_lifecycle.py |
4.2 TestCommand 完整流程
1 | class TestCommand(CommandExtension): |
两步增强(相对 python -m launch_testing.launch_test):
ROS_DOMAIN_ID 自动隔离
- 若环境未设置
ROS_DOMAIN_ID且未--disable-isolation,通过domain_coordinator.domain_id()上下文管理器选取 未被占用 的 domain id(基于端口协调,避免并行colcon test互相发现)。 - 若用户已设置
ROS_DOMAIN_ID,则 尊重 用户值。 --disable-isolation关闭自动选取(与 CI 固定 domain 场景兼容)。
- 若环境未设置
指定
LaunchTestRunner- Humble 上
launch_testing_ros.LaunchTestRunner继承基类且 无额外 override——占位以便将来注入 ROS 专用行为。 - 实际 ROS 辅助工具(
WaitForTopics等)在测试文件中 显式 import 使用。
- Humble 上
4.3 依赖
| 依赖 | 用途 |
|---|---|
ros2cli |
CommandExtension 基类 |
launch_testing |
add_arguments / run |
launch_testing_ros |
LaunchTestRunner |
launch / launch_ros |
间接(launch 文件常用) |
domain_coordinator |
并行测试 domain 协调 |
4.4 历史变更(CHANGELOG)
- 0.1.0:引入
ros_testingmetapackage +ros2test --disable-isolation从launch_testing迁至ros2test(避免非 ROS 场景误用)- domain_coordinator API 改为 context manager(
with domain_coordinator.domain_id())
5. 端到端执行流程
5.1 从 colcon test 到断言
1 | colcon test --packages-select lifecycle |
5.2 Launch 测试文件契约
测试文件必须提供:
| 元素 | 要求 |
|---|---|
generate_test_description() |
返回 LaunchDescription 或 (LaunchDescription, dict) |
launch_testing.actions.ReadyToTest() |
必须出现在 LD 中,通知框架可开始 active 测试 |
unittest.TestCase 子类 |
pre-shutdown 测试(与 launch 并发) |
@launch_testing.post_shutdown_test() |
可选,shutdown 后测试 |
可选第二返回值(context dict)将 action 对象注入测试方法参数,例如:
1 | return launch.LaunchDescription([...]), {'talker_node': talker_node} |
5.3 测试阶段划分
注入对象(launch_testing 自动绑定):
proc_info— 进程 exit code、运行状态proc_output— stdout/stderr 捕获launch_service— 运行中可动态发 Event(仅 pre-shutdown)test_args— CLI 传入的key:=valuelaunch 参数
6. 下游使用示例(lifecycle demo)
demos/lifecycle/CMakeLists.txt:
1 | find_package(ros_testing REQUIRED) |
test/test_lifecycle.py 要点:
generate_test_description()- 启动
LifecycleNode+ 普通Node - 用
RegisterEventHandler+OnStateTransition驱动 lifecycle 状态机 - 末尾
ReadyToTest()
- 启动
TestLifecyclePubSub(pre-shutdown)proc_output.assertWaitFor('on_configure() is called', ...)- 正则匹配 listener 收到的消息
TestLifecyclePubSubAfterShutdown(@post_shutdown_test)launch_testing.asserts.assertExitCodes(proc_info, process=talker_node)
手动运行等价命令:
1 | ros2 test src/ros2/demos/lifecycle/test/test_lifecycle.py |
7. 关联包详解(框架本体,非本仓库)
理解 ros_testing 必须熟悉以下包:
7.1 launch_testing(launch 仓库)
| 模块 | 职责 |
|---|---|
launch_test.py |
CLI:run() 加载模块、写 JUnit |
test_runner.py |
LaunchTestRunner:线程模型、LaunchService 生命周期 |
loader.py |
从 Python 模块加载 TestRun |
asserts.py |
assertExitCodes、assertInStdout 等 |
proc_info_handler.py / io_handler.py |
进程信息与 I/O 捕获 |
线程约束:LaunchService.run() 必须在主线程;pre-shutdown 测试在 后台线程 与 launch 并发(见 launch/issues/126)。
7.2 launch_testing_ros(launch_ros 仓库)
| 工具 | 用途 |
|---|---|
WaitForTopics |
阻塞直到指定 topic 收到消息 |
DataRepublisher |
订阅→修改→再发布(fuzz 测试) |
MessagePump |
异步消息泵 |
launch_testing_ros/tools |
输出解析等 |
pytest/hooks.py |
@pytest.mark.rostest 与 pytest 集成 |
Pytest 集成:launch_testing_ros_pytest_entrypoint.py 注册 rostest marker,使 pytest 收集 launch test 模块。
7.3 launch_testing_ament_cmake
提供 add_launch_test() 与 parse_launch_test_arguments。大量系统测试包 直接依赖 此包而非 ros_testing,例如:
system_tests/test_communicationdemos/demo_nodes_cpprcl/rcl/test
选择建议:
| 场景 | 推荐 |
|---|---|
| 测试涉及 ROS Node、需并行 domain 隔离 | test_depend ros_testing + add_ros_test |
| 纯 launch/process 测试、CI 固定 domain | launch_testing_ament_cmake + add_launch_test |
| 本地快速调试 | ros2 test 或 launch_test |
7.4 domain_coordinator(ament_cmake_ros 仓库)
1 | with domain_coordinator.domain_id() as domain_id: |
通过端口锁协调,保证多个测试进程不共用同一 domain。仅 ros2 test 默认启用。
8. add_ros_test vs add_launch_test 对照
1 | # ros_testing — ROS 场景推荐 |
| 维度 | add_ros_test |
add_launch_test |
|---|---|---|
| 命令 | ros2 test |
python -m launch_testing.launch_test |
| Domain 隔离 | 默认有 | 无 |
| 输出目录 | build/ros_test/ |
build/launch_test/ |
| xUnit | 相同路径规则 | 相同 |
| 依赖包 | ros_testing(更重) |
launch_testing_ament_cmake(更轻) |
9. 常见断言与 ROS 交互模式
9.1 仅看进程输出(无 rclpy)
1 | proc_output.assertWaitFor('expected string', process=node_action, timeout=5) |
9.2 测试中启动 rclpy 节点
1 | class TestFoo(unittest.TestCase): |
见 launch_testing_ros/test/examples/talker_listener_launch_test.py。
9.3 WaitForTopics
1 | from launch_testing_ros import WaitForTopics |
10. 与 ros2cli 的循环依赖说明
CHANGELOG 记录:ros2topic 等包曾依赖 ros_testing,后改为 直接依赖 launch 包 以避免与 ros2cli ↔ ros_testing ↔ ros2cli 的循环。
结论:
ros2test依赖ros2cli,但ros2cli各插件不应 test_dependros_testing;- 需要 launch 测试的 库/节点包 才
test_depend ros_testing。
11. 限制与注意事项
| topic | 说明 |
|---|---|
| LaunchService 线程 | pre-shutdown 测试中勿阻塞主线程式调用 LaunchService.run |
| Domain | 并行测试务必用 ros2 test 或自行协调 ROS_DOMAIN_ID |
| 超时 | 默认 60s,复杂 lifecycle/导航测试需显式 TIMEOUT |
| Keyed topic | discovery 等内部 topic 与 RMW 能力相关;测试文件需自洽 |
| Windows | parse_launch_test_arguments 含 Debug Python 可执行文件分支 |
| 框架代码位置 | 调试断言/ runner 问题应查 launch_testing,不是 ros_testing |
12. 调试建议
单测文件手动跑:
ros2 test path/to/test.py --verbose查看 launch 参数:
ros2 test --show-args path/to/test.py隔离 domain 问题:
对比ros2 test与launch_test(后者不自动分配 domain)看 stdout 日志:
build/ros_test/<target>.txt(add_ros_test)或build/launch_test/(add_launch_test)JUnit:
build/test_results/<package>/<target>.xunit.xml并行 ctest 失败且表现为 discovery 串扰:
确认未--disable-isolation且未多个测试强制同一ROS_DOMAIN_ID
13. 推荐阅读顺序
- 本仓库
add_ros_test.cmake— 理解入口仅一行命令差异 ros2test/command/test.py— domain 隔离 + runner 选择launch_testing/README.md— launch test 文件契约与断言 APIlaunch_testing/launch_test.py—run()加载与 JUnitlaunch_testing/test_runner.py— 线程与 LaunchService 生命周期demos/lifecycle/test/test_lifecycle.py— 真实 ROS 集成测试样例launch_testing_ros/test/examples/— talker/listener、WaitForTopicslaunch_testing_ament_cmake/cmake/add_launch_test.cmake— CMake 宏细节
14. 小结
ros_testing 仓库是 ROS 2 集成测试的薄入口层,价值在于 聚合依赖 与 统一 CLI:
ros_testing包:导出add_ros_test(),把 ctest 接到ros2 test;ros2test包:在launch_testing.launch_test.run外包一层 ROS_DOMAIN_ID 隔离;- 测试框架本体在
launch_testing/launch_testing_ros,Launch 文件 + unittest 两阶段(running / post-shutdown)模型与 ROS 1 rostest 哲学相似,但实现完全基于 launch 系统。
编写涉及 Node 的包测试时,推荐 test_depend ros_testing + add_ros_test();若仅需 launch 进程测试或要避免引入 ros2cli 链,可直接使用 launch_testing_ament_cmake 的 add_launch_test()。两种方式的 测试 Python 文件格式相同,差异几乎只在 谁执行命令 与 是否自动隔离 DDS domain。
正在加载留言…