返回专辑
·Johan·4 分钟阅读

Launch 冒烟测试:合并前抓住图断裂

launch_testing 断言进程、端点与 lifecycle;不替代算法测,但阻止集成回退。

Launch 冒烟测试:合并前抓住图断裂

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 里跑图并断言:

python
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 有 publisherSLAM 建图精度
lifecycle 进 active10 分钟 stress
启动 60s 内无 crash多机发现时序

超时要稳:CI 机器慢时用宽松但有限的 bound(如 90s),失败时 pytest 附件保留 ros2 node listros2 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 消息更能定位 pluginlibshared library 缺失。超时 bound 写进测试常量并注释依据(如「CI runner 冷启动 40s」),避免 magic number 被误删。

5. 与 launch 入口绑定

冒烟文件路径与 launch 入口一一对应:launch/bringup.launch.pytest/test_bringup_launch.py。入口改名或拆文件,测试必须同 MR 修改。把冒烟注册为 required check;没有冒烟的 launch 重构,审查只能靠信仰。

COLCON_IGNORE 或 tag 可以把超慢测试排除在 PR 门禁外,但 nightly 必须跑全量。setup.py 里用 pytest marker 区分 smokesystemcolcon test --pytest-args -m smoke 给 PR,-m "not smoke" 留给周末长跑。测试命名带 launch basename,搜 test_bringup 就能找到对应入口。

6. lifecycle 与延迟激活

带 lifecycle 的栈,冒烟要显式等 active,而不是假设 autostart 永远成功:

python
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}')

inactiveunconfigured 失败原因不同——断言应区分,日志才指向 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 listros2 topic list -tros2 service list 三件套——与录包并列归档,分得出是集成断还是算法错。门禁绿了再谈行为指标,顺序不能反。

冒烟保图完整;系统测试保行为。两者不要互相冒充——但图都不完整时,谈行为测试是空中楼阁。

相关

也可以看看

← 全部文章

johan's blog