开发前期没有硬件,所有代码都在模拟模式下跑通:模拟设备产生数据,经协议编码, 写进一个内存回环通道,再读出来解码。几百个测试全绿。
回环通道与串口实现同一个接口(connect / send / receive),按理说换成串口上层不必改。
但接口只约定了方法,没有约定行为。回环通道有三个串口没有的性质:
| 性质 | 回环通道 | 真实串口 |
|---|---|---|
| 消息边界 | 一次 send() 对应一次 receive() |
没有边界:几帧可能一次读出,一帧可能分两次读出 |
| 应答时机 | 写进去立即能读回 | 设备要过一段时间才回应 |
| 失败方式 | 不失败 | 丢字节、插入干扰字节、比特翻转 |
上层代码在模拟下正确,只是因为无意中依赖了这些性质。
第一次接上真实字节流(当时用 WSL + socat 造的一对虚拟串口,另一端是用 Python 写的虚拟设备):
receive() 的返回值整个交给解码器,
隐含假设”一次读取恰好一帧”。真实字节流里常常一次读出多帧,解码器报长度错误,
上层的容错逻辑把它计入错误数后继续——界面表现为”订阅成功、一直没数据”,现象离根因很远模拟能验证功能逻辑,不能验证对传输介质的假设。 凡是与介质性质相关的代码路径—— 帧边界、时序、延迟、失败方式——必须在真实字节流上验证过,模拟通过不构成证据。
落到代码上:
application/frame_stream.py),上行接收与命令应答等待共用一份,
不让任何调用方自己假设帧边界communication/pipe.py:管道一端的 receive() 返回已到达的全部字节,
不管分几次写入。--mode virtual 用它把虚拟设备接到与硬件模式完全相同的接收器上src/ 零改动接通真板子tests/application/test_link_monitor.py)模拟模式没有被废弃:界面开发、业务逻辑、大部分测试仍然在它上面跑,因为它快、确定、不依赖任何东西。 变化在于不再把它当作介质相关代码的证据。
同一类问题后来又出现过一次,形式不同:硬件模式的装配代码绕过了统一注册入口, 入口里顺带做的事(订阅报警、设置连接状态)因此丢失,只在硬件模式下表现出来。 根因同样是”两条路径以为自己等价、其实不等价”。处理方式是补一组同时对两条装配路径断言的测试。