☰
QGroundControl 单元测试指南:从编译开关到命令行全量/单测运行
2026/10/5 6:37:09 网站建设 项目流程
  • 无人机
  • 智能硬件

【免费下载链接】qgroundcontrol

Cross-platform ground control station for drones (Android, iOS, Mac OS, Linux, Windows)

项目地址:https://gitcode.com/gh_mirrors/qg/qgroundcontrol
点击查看免费下载

QGroundControl(QGC)内置了一套覆盖通信链路、任务规划、地理围栏、遥测协议与车辆行为的单元测试体系,任何 Pull Request(PR)在合入前都必须通过这套测试。本文以开发文档docs/ko/qgc-dev-guide/contribute/unit_tests.md为骨架,结合仓库中test/目录与命令行解析源码,完整讲解如何构建带测试能力的 debug 版本、用--unittest运行全部或单个测试,并剖析测试框架的注册机制、标签体系与常用断言宏,帮助你掌握 QGC 单元测试的"构建—运行—编写"全流程。

单元测试在 QGC 开发流程中的位置

QGC 将单元测试视为代码合入的硬性门槛。开发文档明确说明:在 PR 被接受之前,QGC 内置的单元测试必须全部通过;当为 QGC 增加新的复杂子系统时,应当同时编写对应的新单元测试。

这一要求在 CI 侧同样被强制执行。贡献指南 中注明:所有 PR 都会进入 QGC 的 CI 构建系统,该系统会同时构建 release 与 debug 版本,编译器警告即可导致构建失败,并且单元测试会在受支持的 OS debug 构建上运行。也就是说,提交的代码不仅要"能编译",还要"测试能过"。

测试代码的组织结构:test/ 目录与测试清单

全部单元测试的源代码集中在仓库根目录的test/下,其构建入口是 test/CMakeLists.txt,该文件通过add_subdirectory依次引入 ADSB、AnalyzeView、AutoPilotPlugins、Camera、VideoManager、Comms、FactSystem、FlyView、FollowMe、GeoMap、Gimbal、GPS、Joystick、MAVLink、MissionManager、QtLocationPlugin、QmlControls、Settings、Terrain、Utilities、Viewer3D、Vehicle、QmlUITests 等模块的测试,并与UnitTestList.cc/UnitTestList.h一起编入主目标。

完整的测试注册清单位于 test/UnitTestList.cc,文档中也直接指向该文件作为"可运行单元测试的完整列表"。在 UnitTestList.cc 中,registeredTestNames()会遍历UnitTest::_testList()返回所有已注册测试的名字,因此该文件本质上是"测试注册表的运行时视图"。

第一步:构建带单元测试能力的 debug 版本

文档给出的构建要点是:以debug模式构建,并启用QGC_UNITTEST_BUILD编译定义。

从当前仓库的 CMake 源码看,这一步已经被自动化了:根 CMakeLists.txt 中,当QGC_BUILD_TESTING开关打开时,会通过add_compile_definitions(QGC_UNITTEST_BUILD)在目录范围内注入该宏,注释明确说明其作用是在预处理层面标记"测试能力构建",让生产代码可以按条件裁剪仅测试用的钩子(例如QGCCacheWorker::setUnitTestTileGenerator)。同一宏也用于test/Portable/CMakeLists.txt的便携测试目标,以及 test/UnitTestFramework/AllocationTracker.cc、test/Utilities/Platform/PlatformTest.cc 中的条件编译分支。

因此在实际操作中,你不需要手动添加宏,只需在配置阶段开启测试构建即可,例如:

# 以 Linux 为例,使用 CMake 预设并追加测试开关 cmake --preset Linux -DQGC_BUILD_TESTING=ON cmake --build --preset Linux --config Debug

几点值得注意:

  • QGC_UNITTEST_BUILD只会在QGC_BUILD_TESTING为真时定义;非测试构建中,命令行解析器不会注册任何--unittest*选项(见下文"命令行选项"一节)。
  • 测试目录只有在测试开关开启时才会被add_subdirectory引入(CMakeLists.txt),同时test/会被排除出翻译扫描。
  • 开启 AddressSanitizer / UBSan 等消毒器时,test/CMakeLists.txt 会自动放宽 CTest 的单测超时上限(如单测 180 秒、集成 360 秒、慢测 540 秒、默认 270 秒),避免消毒器带来的运行时开销造成误报超时。

第二步:运行单元测试——全量运行与单测运行

运行全部单元测试

构建完成后,按文档步骤把启动脚本放入 debug 目录,然后从命令行运行全部测试:

qgroundcontrol-start.sh --unittest

这里需要说明:文档撰写时依赖deploy/下的qgroundcontrol-start.sh脚本来设置运行环境(库路径等),而当前仓库的deploy/目录中已不再包含该脚本。如果你使用的是较新版本源码,直接执行构建产物二进制即可达到相同效果,例如:

# 直接运行构建出的可执行文件(路径按你的构建目录调整) ./build/Linux/debug/QGroundControl --unittest

从源码看,--unittest在无参数时意味着"运行所有已注册的测试":UnitTestList.cc 中,当unitTests列表为空时会取registeredTestNames()返回的全部测试;UnitTest.cc 中,全量运行时还会自动跳过standalone标记的测试(它们必须被显式点名才会执行)。整个流程会先打印测试数量,逐类执行,最终汇总"全部通过"或"失败数量"并以此作为进程退出码(失败时返回负数,见 UnitTestList.cc)。

运行单个单元测试

指定测试名即可只运行某一个测试,文档以RadioConfigTest为例:

qgroundcontrol-start.sh --unittest:RadioConfigTest

--unittest:Name中的冒号后面的值会被命令行解析器当作过滤器(filter)值。需要提醒的是:RadioConfigTest只是开发文档中的历史示例,当前仓库的test/中已不存在该测试类(在现有源码中搜不到RadioConfigTest符号)。实际可运行的测试名,请以你构建出的二进制输出的清单为准——见下文的--list-tests。一个典型的单测调用看起来像这样:

# 只运行 ADSB 模块测试 ./QGroundControl --unittest:ADSBTest

从源码看,单测名称匹配是精确的字符串比对(UnitTest.cc),因此必须与注册名完全一致,大小写敏感。

命令行测试选项全解

测试相关的命令行选项都定义在 src/Utilities/QGCCommandLineParser.cc,并且整体包裹在#ifdef QGC_UNITTEST_BUILD中——这意味着这些选项只在测试构建里存在。如果你在非测试构建中传入它们,解析器会直接报错:

--unittest/--unittest-stress/--unittest-output/--list-tests options are only available in unittest builds.

各选项说明如下:

选项说明附加参数
--unittest运行单元测试;不附带值则运行全部,附带值则只运行指定测试可选,测试名(filter)
--unittest-stress压力测试模式,将选中的测试连续重复运行可选,重复次数 count
--unittest-output将测试结果输出到文件(JUnit XML 格式)必填,文件路径
--unittest-label按标签过滤测试(如unit、integration、vehicle、missionmanager等)必填,逗号分隔的标签列表
--list-tests仅列出当前构建中可用的测试并退出无
--onscreen让测试窗口显示在屏幕上,而不是离屏运行无

开发指南 command_line_options.md 对其中两项的表述与源码一致:--unittest:name运行指定测试(省略:name即运行全部),--unittest-stress:name会把指定测试连续运行 20 次。在 UnitTestList.cc 中可以看到,压力测试的默认迭代次数由kStressIterations决定,也可以通过环境变量覆盖(UnitTest.h 中的stressIterations()支持QGC_TEST_STRESS_ITERATIONS)。

--unittest-output的 JUnit XML 输出在 UnitTest.cc 中实现:每个测试类会生成独立的 XML 文件(在文件名中插入测试类名),避免相互覆盖。--list-tests则会按注册顺序打印全部测试名,并额外打印可用标签列表(UnitTestList.cc),是排查"测试名到底怎么写"最可靠的依据。

这些选项在 src/main.cc 中汇入统一入口:当应用模式为ListTests或Test时,直接调用QGCUnitTest::handleTestOptions(args)执行测试并返回退出码,而不进入 GUI 事件循环。

框架内部原理:注册、标签与执行模型

静态注册机制

每个测试类通过UnitTestWrapper模板在静态初始化阶段自动注册(UnitTest.h):wrapper 构造时创建测试实例、设置对象名与标签,并调用UnitTest::_addTest()放入全局列表(UnitTest.cc)。测试实例由unique_ptr持有,静态析构时统一销毁,从而避免泄漏检测器的误报。

对应的三个注册宏(UnitTest.h):

// 常规测试:默认参与全量运行 UT_REGISTER_TEST(MyTest, TestLabel::Unit, TestLabel::MissionManager) // 独立测试:只有被显式点名时才运行 UT_REGISTER_TEST_STANDALONE(MyTestStandalone, TestLabel::Integration) // 轻量测试:可跑在裸 QCoreApplication 上的纯逻辑测试 UT_REGISTER_TEST_LIGHTWEIGHT(MyPureLogicTest, TestLabel::Unit, TestLabel::Utilities)

UT_REGISTER_TEST_STANDALONE注册的测试默认被全量运行跳过(UnitTest.cc),适合那些需要真实硬件或耗时很长的用例;UT_REGISTER_TEST_LIGHTWEIGHT注册的测试会在轻量入口下跳过昂贵的完整应用启动(QML 引擎、插件扫描、车辆实例),但依然可以安全地参与常规全量运行。

标签体系

测试标签在 UnitTest.h 中以位标志定义,目前包括:

标签含义
Unit快速、隔离的单元测试
Integration需要多组件协作的集成测试
Vehicle需要 MockLink 模拟车辆的测试
MissionManager任务规划相关测试
Comms通信/链路测试
Utilities工具类测试
Slow运行超过 5 秒的慢测试
Network需要网络访问的测试
Serial必须串行(不可并行)执行的测试
Joystick摇杆/控制器测试
AnalyzeView日志分析与地理标记测试
Terrain地形查询与瓦片测试

标签名与枚举的映射在 UnitTest.cc 中完成,大小写不敏感,--unittest-label接受逗号分隔的多个标签(例如--unittest-label=unit,comms),解析器会校验非法标签并给出可用标签列表(UnitTestList.cc)。

执行与生命周期

每个测试类的执行由 UnitTest::run 统一驱动:逐个执行测试类,先调init()(UnitTest.cc),运行测试函数后调cleanup()(UnitTest.cc)。框架对生命周期有严格断言:一旦init()被调用,cleanup()必须被调用,否则析构时触发Q_ASSERT(UnitTest.cc)。此外框架内置了严格日志模式——cleanup()阶段如果捕获到未被预期/忽略的日志消息,测试会直接失败(UnitTest.cc),这一机制强制测试对环境噪声进行显式管理。

编写新单元测试的实用要点

结合框架提供的工具,写一个新测试通常只需要三步:

  1. 继承UnitTest基类,实现init()/cleanup()(需要时)与测试函数;
  2. 用UT_REGISTER_TEST(或 standalone / lightweight 变体)注册,并按需打上标签;
  3. 在测试函数中使用 Qt Test 的断言宏,配合框架提供的异步等待宏处理信号与时序。

异步等待方面,框架在 UnitTest.h 提供了带超时诊断的宏,比裸QSignalSpy::wait的错误信息友好得多:

QVERIFY_SIGNAL_WAIT(spy, 5000); // 等待信号,超时自动带上下文输出 QVERIFY_NO_SIGNAL_WAIT(spy, 1000); // 验证超时窗口内没有多余信号 QVERIFY_SIGNAL_COUNT_WAIT(spy, 3, 5000); // 等待信号达到期望次数 QVERIFY_TRUE_WAIT(condition, 5000); // 轮询等待条件成立 QCOMPARE_TRUE_WAIT(actual, expected, 5000);

超时基准值封装在TestTimeout命名空间(UnitTest.h):shortDuration()/mediumDuration()/longDuration()会根据CI/GITHUB_ACTIONS环境变量自动切换本地与 CI 两套阈值(如本地 5 秒 / CI 10 秒),避免在慢速 CI 机器上误超时。需要跳过当前环境不满足的用例时,直接用QSKIP,例如 test/Comms/Bluetooth/BluetoothLiveAdapterTest.cc 在宿主机没有蓝牙适配器时跳过运行。

框架还提供了TEST_CONTEXT(msg)上下文辅助(失败时输出附加诊断信息)、TEST_DEBUG(msg)调试输出(仅在失败或 verbose 模式显示)等工具(UnitTest.h)。

仓库中现成的写法范例可以直接参考:test/ADSB/ADSBTest.cc、test/Comms/LinkManagerTest.cc、test/Comms/Serial/QGCSerialPortInfoTest.cc 等均展示了从注册、打标签到断言的完整模式。

常见问题与排错

  • 提示"选项仅存在于 unittest 构建":说明你的二进制没有QGC_UNITTEST_BUILD宏,多半是配置时未开启QGC_BUILD_TESTING=ON,重新配置并构建即可。
  • --unittest:Name找不到测试:先执行--list-tests查看当前构建实际注册的测试名,确认名称拼写(大小写敏感)且该测试未被 standalone 标记。
  • 测试因"意外的日志消息"失败:框架的严格日志模式会在cleanup()阶段把未被忽略的日志输出为失败详情。可参照 UnitTest.cc 中init()里用ignoreLogMessage(category, type, pattern)忽略已知无害噪声(如离屏 QPA 下QRhiGles2的告警)的做法。
  • CI 上偶发超时:检查是否启用了 ASan/UBSan——开启消毒器后 CTest 超时阈值会被自动放大;同时优先使用TestTimeout::mediumDuration()等 CI 感知的超时基准,而不是硬编码毫秒数。

总结

QGC 的单元测试体系与 PR 合入流程深度绑定:QGC_BUILD_TESTING开关自动注入QGC_UNITTEST_BUILD宏,test/下的每个模块测试在静态注册阶段进入统一清单,--unittest、--unittest:Name、--unittest-stress、--list-tests等选项则提供了从全量回归到单点调试的完整控制面。掌握了"开启测试构建 → 列出可用测试 → 全量/单测/压力运行 → 依据源码诊断失败"这条链路,你就能在提交 PR 前独立完成 QGC 的测试验证,也能为自己的新子系统补齐对应的单元测试。

  • 无人机
  • 智能硬件

【免费下载链接】qgroundcontrol

Cross-platform ground control station for drones (Android, iOS, Mac OS, Linux, Windows)

项目地址:https://gitcode.com/gh_mirrors/qg/qgroundcontrol
点击查看免费下载

相关推荐

上一篇:Windows-driver-samples智慧教育:智能教育成套解决方案驱动开发
下一篇:awesome-prometheus-alerts数据库性能基准监控规则

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询