ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pelco-D云台模拟器:用pytest与虚拟串口做三层集成测试

Pelco-D云台模拟器:用pytest与虚拟串口做三层集成测试 做Pelco KBD300A模拟器做到第19个版本我最大的感受是单体单测写得再漂亮只要serial、protocol、macro这三层没有在同一套测试里真正跑通过一次你就不敢说自己做好了“集成”。这一版我干脆把测试体系切到pytest用虚拟串口把三层链路串起来做集成测试。这样做的直接收益是以前只能在真机上手工验证的“按键宏回放→Pelco-D帧编码→串口字节发送”整条链路现在一条命令就能在CI里反复跑。这篇内容适合正在做串口工具、PTZ云台控制、模拟键盘、以及任何需要把“协议编解码”和“真实字节流”绑在一起测试的朋友参考。你会看到我是怎么搭虚拟串口、怎么做Pelco-D参数化、怎么让宏播放可控以及最后三层合测时踩过的一堆坑。如果你现在还在用unittest硬撑强烈建议看看pytest在这类场景下的组织方式。1. 为什么这个阶段才真正需要三层集成测试1.1 模拟器三层结构是怎么长出来的这个模拟器刚起步时其实很简单打开串口、读写字节能做回显就算成功。但随着键盘功能越来越接近真实KBD300A逻辑自然分成了三层。第一层是serial负责真实串口的打开、关闭、读写、超时控制对上层只暴露最朴素的“字节进、字节出”。第二层是protocol专门处理Pelco-D的编码和解码把“云台向左转、速度0x20”这类动作变成7字节帧再把收到的帧解析回动作。第三层是macro它是键盘业务的体现录制一串操作、定时回放让摄像机按预设路径巡检。三层拆分清楚之后每一层都可以独立测试。serial层测试读写protocol层测试编解码macro层测试动作序列。问题在于真实联调时bug往往出在三层之间的接缝上宏引擎编出的动作经协议层编码后发到串口对端收到的帧到底是不是我们想要的这种问题单测永远覆盖不到。1.2 单体测试全绿串起来仍然会翻车的三类典型问题我这里踩过的坑大致能归成三类。第一类是编码器与串口write之间的不匹配。protocol层单测里send往往是一个假的回调函数测试只关心编码后的字节内容对不对。可一旦换成真实串口写就会遇到缓冲区、写入时序、甚至丢字节的问题。明明编码结果是正确的串口另一端就是收不齐一帧。第二类是宏层与协议层的停止动作映射不一致。宏引擎认为“结束一个动作”就是发一个cmd2为0x00的停止帧但protocol层的动作表里如果对停止的定义不统一就可能在宏回放结束时漏发或错发停止指令。单测里宏层用的是mock发送器protocol层只认编码结果两边各自都对合在一起就错。第三类是协议解码器对粘包半包的处理。单测里经常直接喂一个完整7字节帧给解码器而真实串口是字节流哪有什么帧边界。一次读操作可能只读到半个帧也可能一次读到两三个帧粘在一起。解析逻辑稍不小心宏回放过程中稍微有点回码整个解析就乱了。这三类问题指向同一个结论必须把serial、protocol、macro放进同一个pytest进程用同一个虚拟串口对跑真正的端到端链路。1.3 pytest在这里赢在哪最早我也用过unittest但在这个项目里pytest的几个特性确实更顺手。fixture很适合管理虚拟串口的生命周期。每个测试函数用vserial_pair这个fixture测试前自动创建伪终端对测试后自动关闭fd比unittest里手写setUp和tearDown干净得多还能按module、session粒度控制创建和销毁。parametrize非常契合Pelco-D这种“字段多、枚举值多、边界要覆盖全”的协议。一个动作组合一个测试用例的写法会让文件爆炸用参数化直接把有效地址、非法地址、速度边界全部铺开。断言失败时pytest能同时打印参数和实际字节排查效率高很多。再加上pytest-timeout这类插件可以对整个集成测试设一个总超时防止宏线程或者串口读阻塞导致测试进程永远卡死。对串口类测试来说超时兜底不是可选项是必选项。2. 串口测试前置先把虚拟串口对搭稳2.1 为什么不能用真实串口直接测如果测试直接往真实串口写首先你得有硬件其次你得有对端设备。更麻烦的是像我这种控制云台的模拟器一个集成测试跑起来很可能真的把摄像机转走位了。测试本身应该是可重复、无副作用的真实串口没法满足这些要求。所以集成测试要用虚拟串口。在Linux和macOS上最方便的手段是pty伪终端对。一个终端端被模拟器当串口打开另一个终端端由测试进程持有模拟器写进来的数据从另一端原样读出完全不经过物理线路。提示如果只想在开发环境临时看效果也可以用socat快速拉一对pseudo终端比如socat -d -d pty,raw,echo0,link/tmp/ttyV0 pty,raw,echo0,link/tmp/ttyV1。但放到pytest里直接用pty模块动态创建更干净不依赖外部命令。2.2 用pty模块封装一个vserial_pair fixture我通常把虚拟串口对写在conftest.py里。原因很简单多个测试模块要共用而且它非常需要稳定的创建和销毁流程。import os import pty import tty import pytest from pelco_kbd.serial_io import SerialIO pytest.fixture def vserial_pair(): master_fd, slave_fd pty.openpty() tty.setraw(master_fd) slave_name os.ttyname(slave_fd) serial_io SerialIO(portslave_name, baudrate9600, timeout0.1) serial_io.open() try: yield serial_io, master_fd finally: serial_io.close() try: os.close(master_fd) except OSError: pass try: os.close(slave_fd) except OSError: pass几个细节值得说一下。tty.setraw(master_fd)是必须的。伪终端默认带行规则会对输入做回显、换行转换之类的处理这跟真实串口的裸字节流不一样。不关掉raw模式你写进去的字节很可能被加工过测试结果完全没意义。master_fd和slave_fd必须都关闭。测试里经常出现跑完一轮之后fd耗尽、下一次用例打不开串口的问题绝大多数是上一个测试没清理干净。用yield加finally的写法可以保证即使断言失败文件描述符也会被释放。SerialIO的timeout建议设小一点比如0.1秒。这样如果对端没有数据可读read会在100毫秒左右超时返回不会把整个测试卡死。真实模拟器里这个值可以大一些但测试环境里越小越稳。2.3 serial基础用例写入、读取与超时行为fixture搭好之后第一个集成用例一定是“写入内容能被另一端原样读到”。这个用例看起来简单但它能一次性验证串口打开、写入、pty连通性、读超时这些基础链路。def test_serial_write_to_virtual_port(vserial_pair): serial_io, master_fd vserial_pair frame bytes([0xFF, 0x01, 0x00, 0x04, 0x20, 0x00, 0x24]) written serial_io.write(frame) assert written len(frame) readable, _, _ select.select([master_fd], [], [], 0.5) assert readable, 对端没有读到数据 data os.read(master_fd, 4096) assert data frameread这步要注意os.read在fd上没有数据时会阻塞。这里用select先等0.5秒确保了读操作不会让pytest挂住。0.5秒也不是随便选的后面会专门讲这个时间怎么估算。注意虚拟串口的另一端读到的数据务必用os.read或os.fdopen去读不要试图用serial.Serial同时打开master端。伪终端对里的master端和slave端语义并不完全等同于两台物理串口设备互联。我自己调试时就在这个问题上浪费过不少时间。3. 协议层参数化验证Pelco-D编解码不能靠“手测几个包”3.1 Pelco-D帧结构与编解码接口约定Pelco-D本身是安防行业非常常见的云台控制协议帧结构不复杂但字段位置一旦记错设备根本不会有反应。我用的帧结构是7字节|
返回列表