Launch 冒烟测试:合并前抓住图断裂
launch_testing 断言进程、端点与 lifecycle;不替代算法测,但阻止集成回退。

1. 「我这边能起」挡不住合并后的图断裂
重构 launch 把 scan remap 成 lidar/scan、漏加 robot_state_publisher 依赖、lifecycle 节点卡在 inactive——开发者本机手动 ros2 launch 一次通过,CI 合并后仿真 CI 与 nightly 全红。根因不是算法,是没有自动化断言图完整性。Launch 冒烟要回答:进程在、关键 topic/service 在、规定时间内 lifecycle 进 active。它不替代导航成功率或控制精度测试,但能抓住大部分集成回退。
冒烟在 CI 里应跑在无头环境:设 headless=true 或 mock 掉 RViz,避免 DISPLAY 依赖。Docker 镜像预装与现场一致的 underlay,否则「CI 绿、现场红」来自依赖版本差而非图本身。
2. launch_testing 最小骨架
launch_testing 把 launch 描述当 fixture,在 pytest 里跑图并断言:
import launch_testing
import pytest
from launch import LaunchDescription
from launch_ros.actions import Node
@pytest.mark.launch_test
def generate_test_description():
return LaunchDescription([
Node(package='my_pkg', executable='bringup'),
launch_testing.actions.ReadyToTest(),
])
class TestSmoke(launch_testing.test_tools.LaunchTest):
def test_nodes_up(self, proc_output):
proc_output.assertWaitFor('robot_state_publisher', timeout=30)
def test_scan_topic(self):
import subprocess
out = subprocess.check_output(
['ros2', 'topic', 'list'], text=True, timeout=15)
assert '/scan' in out断言要少而硬:节点日志出现、/tf 有发布者、控制器 state==active。避免在冒烟里断言「导航到达目标点」——那是系统测试,慢且 flaky。
3. 写什么、不写什么
| 适合冒烟 | 不适合冒烟 |
|---|---|
| 节点进程存在 | 完整 Nav2 导航成功 |
| 关键 topic 有 publisher | SLAM 建图精度 |
| lifecycle 进 active | 10 分钟 stress |
| 启动 60s 内无 crash | 多机发现时序 |
超时要稳:CI 机器慢时用宽松但有限的 bound(如 90s),失败时 pytest 附件保留 ros2 node list、ros2 topic list 与 stderr 快照——审 MR 的人不需要复现就能看见图缺什么。
4. 稳定性工程:睡眠换轮询
time.sleep(5) 后断言 topic 存在,在负载高的 runner 上必 flaky。改用 launch_testing.tools.wait_for_conditions 或自写轮询:每 500ms 查一次,总预算 30s,失败打印最后一次图状态。有限重试(最多 2 次)仅用于已知 DDS 发现抖动,且每次重试留日志——默许「再跑一遍就绿」会摧毁门禁信誉,人学会 --no-verify 或跳过 CI。
pytest 失败时把 launch 子进程 stderr 全文 attach,比只存 assert 消息更能定位 pluginlib 或 shared library 缺失。超时 bound 写进测试常量并注释依据(如「CI runner 冷启动 40s」),避免 magic number 被误删。
5. 与 launch 入口绑定
冒烟文件路径与 launch 入口一一对应:launch/bringup.launch.py ↔ test/test_bringup_launch.py。入口改名或拆文件,测试必须同 MR 修改。把冒烟注册为 required check;没有冒烟的 launch 重构,审查只能靠信仰。
COLCON_IGNORE 或 tag 可以把超慢测试排除在 PR 门禁外,但 nightly 必须跑全量。setup.py 里用 pytest marker 区分 smoke 与 system:colcon test --pytest-args -m smoke 给 PR,-m "not smoke" 留给周末长跑。测试命名带 launch basename,搜 test_bringup 就能找到对应入口。
6. lifecycle 与延迟激活
带 lifecycle 的栈,冒烟要显式等 active,而不是假设 autostart 永远成功:
def test_controller_active(self):
import subprocess, time
deadline = time.time() + 45
while time.time() < deadline:
out = subprocess.check_output(
['ros2', 'lifecycle', 'get', '/controller_manager'],
text=True, stderr=subprocess.DEVNULL)
if 'active [3]' in out:
return
time.sleep(1)
raise AssertionError(f'controller not active: {out}')inactive 与 unconfigured 失败原因不同——断言应区分,日志才指向 configure 参数错还是 activate 服务超时。
7. 验收
- 故意删掉 launch 里一个 Node:冒烟失败,快照显示缺节点。
- 故意 remap 错 topic 名:断言
/scan失败,列表里可见实际名。 - 连续 20 次 CI 同 commit:零 flaky。
- MR 改 launch 未改测试:review bot 或 CODEOWNERS 拦截。
8. 案例:依赖漏加三周才被发现
某 PR 把 joint_state_broadcaster 从 launch 移到 optional 配置,开发者本机已有手动启动的 broadcaster,冒烟不存在,三周后客户仿真 TF 全断。补一条 ros2 topic echo /joint_states --once 级断言后,同类回归 MR 当天被抓。
9. service 与 action 冒烟
除 topic 外,关键 service 是否 advertise 也应断言:ros2 service list | grep /controller_manager/configure_controller。action server(NavigateToPose)可只测 server 存在,不在冒烟里等结果——与导航行为测试边界相同。service 名同样受 namespace 影响,断言用解析后全名。
9. 与录包复盘
事故袋若只有算法日志没有启动图快照,很难证明「当时 /scan 是否存在」。冒烟失败时 pytest 附件应自动保存 ros2 node list、ros2 topic list -t、ros2 service list 三件套——与录包并列归档,分得出是集成断还是算法错。门禁绿了再谈行为指标,顺序不能反。
冒烟保图完整;系统测试保行为。两者不要互相冒充——但图都不完整时,谈行为测试是空中楼阁。
相关
也可以看看
- ·15 分钟阅读
ContentFilteredTopic:把过滤下推到中间件还是应用层
对比订阅端丢弃、应用层条件判断与 DDS 内容过滤的 CPU/带宽边界,说明表达式能力与发现时序限制;用高频率噪声话题与延迟尖峰验收过滤位置选择。
- ·16 分钟阅读
ROS 2 Action 取消语义:收到 cancel 不等于执行器已经停下
梳理 goal handle 状态机、取消回调与硬件停止之间的异步边界;用可中断执行循环、截止时间和终态发布构建契约并以注入验收。
- ·9 分钟阅读
ROS 2 大消息内存路径:intra-process、loaned message 与 shared memory 不是一回事
以 4K 图像和点云管线为例,逐段核对 rclcpp、RMW 与进程边界上的所有权、分配和真实拷贝。
johan's blog