结论:panda safety 的测试链要分四层证据看——本机逻辑库、离线回放、板级验证、车辆验证,它们不能互相替代。源文档反复强调:默认启用的 ALLOW_DEBUG 测试库、test.sh 要求的 100% 行覆盖率、以及回放返回的布尔通过,都不等于目标固件、线束或实车安全认证。make_CANPacket() 得到的只是 DBC 构帧后的 CFFI CANPacket_t,不是 panda USB/SPI、FDCAN 或 ECU 真实发送回执。
一、本机库与通用断言
| 入口 | 作用与限制 |
|---|---|
_build_libsafety()、__getattr__() |
首次取 libsafety 时将测试 C 翻译单元编译为临时共享库;默认启用 ALLOW_DEBUG,release=True 才不加该宏。正常模式加 UBSan,覆盖率模式改用 gcov flags。 |
make_CANPacket()、CANPackerSafety.make_can_msg_safety() |
DBC 构帧后转成 CFFI CANPacket_t,保留地址、bus、长度码与数据;不是 panda USB/SPI、FDCAN 或 ECU 真实发送回执。 |
init_tests() |
测试库可人工改时钟、使能和测量窗口,并设置 safety_mode_cnt=2 等测试专用状态,并不模拟真实 MCU 启动。 |
test_spam_can_buses()、test_fwd_hook() |
对已列扫描地址、总线及测试用长度检查 TX/转发;不是 29 位地址和 0~64 字节载荷的全穷举。 |
test_safety_tick()、test_relay_malfunction() |
分别验证缺失关键 RX 时退出与继电器故障否决;仍是本机状态模拟。 |
库编译使用 -Wall -Wextra -Werror -nostdlib -fno-builtin;common.py 用继承式 mixin 组合地址扫描、踏板/巡航、转向扭矩、角度、曲率、纵向和转发断言。SCANNED_ADDRS 扫全 11 位地址,但仅取少量 29 位诊断区间,且基础 spam 测试固定构造 8 字节零载荷。test_tx_hook_on_wrong_safety_mode() 读取其他测试类声明的 TX 列表并有多组车型/地址重叠例外,它是跨模式回归检查,不是形式化证明。另一个关键点:独立 test_release_build.py 只确认两种测试库能编译,没有对 release 版本逐车型跑相同 TX/RX 行为矩阵。
还要注意测试库与真实固件的差异:libsafety/safety.c 内嵌仓库的 can.h、safety.h、ignition.h,以全局 timer_cnt 替代 MCU 微秒计时器,并暴露测试专用 getter/setter。模式切换仍走真实 safety 头文件,但总线驱动、线束继电器、电源/心跳和芯片时钟不在测试库内;init_tests() 在 set_safety_hooks() 之后另改测试状态,例如跳过初期继电器忽略窗口,故它是逻辑钩子验证而不是板级启动验证。
二、覆盖率与变异测试的解释边界
| 机制 | 给出的结论 | 不能被读成什么 |
|---|---|---|
test.sh + UBSan |
先跑 UBSan 单测,再在 SAFETY_COVERAGE=1 下重跑。 |
行覆盖率(排除 libsafety 包装器后 100%)不等于分支、状态序列、真实总线或发布固件覆盖。 |
MISRA(test_misra.sh) |
用 cppcheck 执行静态检查。 | 不能与动态 safety 断言合并为同一种证据。 |
enumerate_sites()、build_priority_tests()、eval_mutant() |
对预处理后的 safety C 生成变异点,按车型文件或共享核心测试选用例,统计杀死、存活或基础设施错误。 | modes/ 下默认只取同名 test_<mode>.py;共享模式头文件若无同名测试,所选列表可能为空,不能据「该文件变异全部存活」直接断言无测试能覆盖它。 |
变异统计里还有两个陷阱:worker 异常或进程池崩溃在结果汇总中被记为 killed,而另一路 Python 异常记为 infra_error——查看变异得分时必须把崩溃原因从有效测试断言失败中分离;脚本还有三条已知存活变异例外,不能把返回码零解释为零存活点。
三、回放的证据边界
replay_drive() 从日志 carParams.safetyConfigs[-1] 默认取最后一个模式,可覆盖参数;package_can_msg() 将 src % 4 映射为本地 bus。它按 logMonoTime 排序,给安全库设置软件时钟;在离片段首尾各超过 1 秒的区间,对每条 CAN 消息调用 safety_tick(),而板端该函数按 1 Hz 调用。回放成功条件是「控制允许时无 TX 被拒、无 RX 无效、没有区间内的 RX 健康失败」;控制未允许时被拒的 TX 仍计数但不使返回值失败。没有 can/sendcan 消息时会在首尾索引处失败,不是空路段的安全通过证明。
init_segment() 用首个识别到的转向 sendcan 帧人工设 controls_allowed、上轮目标及测量值,然后断言该帧 TX 可过。这样可降低片段开头状态缺失导致的误报,但也意味着回放不完整验证实际巡航使能沿和最初几帧的真实状态。测试前应记录路段是否从设备启动/控制开始,不能只看最终布尔结果。
四、自制硬件的验收证据链
- 本机逻辑:按目标安全模式分别运行 development/release 构建和关键 RX/TX 单测,保存精确源提交、编译宏、测试选择及失败列表。
- 离线时序:用目标车型路段回放,记录首帧预置、源 bus 映射、RX 健康与「控制未允许时拒帧」;回放不能替代无车故障注入。
- 板级验证:确认 MCU 固件签名/模式参数读回、实际 FDCAN 发送回执、继电器方向、心跳失联和异常报文抑制,不用 CFFI 测试库结果替代。
- 车辆验证:在合法合规的受控条件下单独确认 ECU 接受、原车 AEB/制动保留、驾驶员覆盖与恢复;插件系统不得把测试报告布尔值当作实时控制许可。
本轮未运行完整安全测试,也没有目标硬件/车辆证据;上述内容是测试框架能力与缺口的源码索引。
常见问题
默认测试通过能否当作发布固件权限证明?
不能。测试库默认启用 ALLOW_DEBUG,日常单测可覆盖仅调试注册的模式和纵向参数;test_release_build.py 只确认两种测试库能编译,没有对 release 版本逐车型跑相同 TX/RX 行为矩阵。
100% 行覆盖率说明什么?
只说明在 SAFETY_COVERAGE=1、排除 libsafety 包装器后行覆盖达到 100%,不等于分支、状态序列、真实总线或发布固件覆盖。MISRA 的 cppcheck 检查是另一种证据,不能与之合并。
回放成功是否等于通过了安全验证?
不等于。回放要求「控制允许时无 TX 被拒、无 RX 无效、无区间 RX 健康失败」,但控制未允许时被拒的 TX 仍计数而不使返回值失败;init_segment() 还人工预置首个转向帧状态,故不完整验证实际使能沿与最初几帧。
变异评分零存活就代表没有缺口吗?
不代表。共享模式头文件若无同名测试文件,所选用例列表可能为空;worker 异常或进程池崩溃会被汇总记为 killed,脚本还有三条已知存活变异例外,因此不能把返回码零解释为零存活点。
延伸阅读
- Volkswagen MEB panda safety:Gen2 CRC、双车速与停车保持 —— 一个具体安全模式如何被这些测试覆盖。
- panda 健康位与 ABI 版本映射 —— 承接安全库与主机侧接口的版本边界。
- panda 故障寄存器与预置契约 —— 对照继电器故障否决的板级语义。
- panda bootstub 签名与刷写边界 —— 说明固件签名与模式参数为何须板级读回。
- 从使能到车轮:执行链上的八道门,以及它们各自不保证什么 —— 解释测试布尔值为何不等于实时控制许可。
参考资料
- 项目 02 ——
openpilot-research/SAFETY_TEST_HARNESS_REPLAY_EVIDENCE_DEEP_DIVE.md(取用了本机库与通用断言、覆盖率与变异边界、回放证据边界、硬件验收证据链)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11