返回博客列表
最佳实践2026-09-1311 分钟阅读

测试的测试:孤儿测试三连败与截帧铁证方法论

验证手段自身的 Bug · shutdown vs close · 真实节奏测试 · 视觉铁证

测试的测试:孤儿测试三连败与截帧铁证方法论

WeClaw 技术博客 · 第 106 篇 - 模块九第 6 篇 · 验证手段自身的 Bug · shutdown vs close · 真实节奏测试 · 视觉铁证


📚 专栏信息

《从零到一构建跨平台 AI 助手:WeClaw 实战指南》专栏 · 模块九【稳定性实战:故障链解剖与自愈工程】(共 8 篇)

本文是模块九第 6 篇,也是桌面侧五篇的收官。前面几篇里每个"实测✅"都有个 没说出口的代价——验证产品之前,测试脚本自己先在坑里躺了三轮。 这是一篇"元测试"文章。

上一篇:《WeClaw_105 故障降级的三级自愈链》 | 下一篇:《WeClaw_107 KeyError 的五级放大故障链》


👨‍💻 作者与项目

作者简介:翁勇刚 WENG YONGGANG 新概念龙虾-WeClaw 开发团队负责人,一群专注于跨平台 AI 应用的实践者 理念:"再复杂的技术,也能用代码讲清楚"


📝 摘要

本文结构概览: 以"验证孤儿自退出"这一条测试的三连败开场,逐坑拆解 close 与阻塞 readline 的死锁、tasklist 的 GBK 输出崩掉 UTF-8 脚本、嵌套作用域 global 不穿透;随后给出三条正面方法论:复刻真实使用节奏的跨超时窗口测试、坏源注入与杀进程断言、截帧/抓屏的视觉铁证法。

背景: v11.16.0 收口前必须验证四件事:换台跨窗口可靠、首播失败必报错、子进程死亡自愈、父进程骤死无孤儿。产品代码已就绪,验证脚本连吃三败。

核心问题: 当"测量仪器"本身有 Bug 时,产品到底修没修好?

解决方案: 模拟对端骤死用 shutdown(SHUT_RDWR);跨进程输出一律字节级判断;测试节奏以真实用户行为为基准;一切"能播"的断言最终落在像素上。

关键成果:

  • 孤儿自退出验证通过(撕断后 6 秒 tasklist 无残留)
  • 换台间隔 10~18s(≫ 旧 1s 死亡窗口)全部起播
  • 911KB CCTV-1 全高清截帧 + 桌面抓屏双视觉铁证

适合读者:写集成测试/端到端测试的开发者、被"测试挂了还是产品挂了"困扰的人、桌面应用测试同学

阅读时长:约 11 分钟

关键词:测试死锁、shutdown、GBK编码、真实节奏、截帧验证、坏源注入


一、开场:一条测试的三连败

产品侧的孤儿自退出逻辑(第 104/105 篇)已经上线: 父进程暴死 → 子进程读线程拿到 EOF → 自行退出。 验证它需要一个"扮演父进程暴死"的脚本。就是这么一个 40 行的脚本,连摔三次。

1.1 第一败:close 引发的死锁(v1 卡死 120 秒)

最初的想当然写法:

# ❌ v1:模拟"父进程死了"
conn_f.close()        # 关掉读端的文件对象,总该触发对面 EOF 了吧?
# ……脚本从此卡死,120 秒超时被外部杀掉

死锁机理:sock.makefile("r") 返回的是 BufferedReader 包装。 子进程方向的读线程此刻正阻塞在同一个文件对象的 readline 上, 持有 BufferedReader 的内部锁;主线程调 close() 需要先拿到同一把锁 才能安全冲刷——两把锁互等,谁也不让谁。 (而 close socket 对象本身也不行:makefile 持有 socket 引用时, socket 的引用计数没归零,连接根本不会断。)

正确姿势——问"进程真死时 OS 干了什么":强制回收该进程全部 fd, TCP 栈立即发出 FIN/RST。等价物是:

# ✅ v2:立即撕断 TCP,唤醒一切阻塞读
sock.shutdown(socket.SHUT_RDWR)
sock.close()

shutdown 不抢任何用户态锁,直接作用于协议栈:本端所有阻塞在 read/write 上的线程立即返回(EOF 或错误),对端连接确定性地断。 这是 OS 级"骤死"的最小 faithful 模拟。

沉淀规则:模拟"对端骤死"永远用 shutdown,不用 close。 close 是"礼貌地用完了",shutdown 才是"被人拔了网线"。

1.2 第二败:tasklist 崩掉 UTF-8 脚本

子进程退出与否要用 tasklist 独立确认(不能只信 poll()——测试要外部证人对吧)。 然后脚本炸了:

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xce ...

原因:Windows tasklist 的输出编码跟随系统 ANSI 代码页(中文机器 = GBK), 而 subprocess.run(..., text=True) 默认用 UTF-8 解码。 修复不需要解码——PID 是纯 ASCII,直接字节级比对:

# ✅ 字节级判断,绕开一切编码坑
out = subprocess.run(["tasklist", "/FI", f"PID eq {pid}"],
                     capture_output=True).stdout        # 不解码
alive = str(pid).encode() in out

沉淀规则:跨进程取文本,要么显式指定对侧真实编码,要么根本不解码。 Windows 工具链(tasklist/dir/netstat)默认 GBK,PowerShell 管道默认 UTF-8, 两套默认值相撞是常态。

1.3 第三败:global 不穿透嵌套作用域

脚本最后一处低级失误:

old_pid = 0
def scenario():
    def hard_drop():          # 嵌套函数里想改全局
        old_pid = proc.pid    # ❌ 这是局部赋值!
    ...
print(old_pid)                # 永远打印 0,断言莫名挂掉

global 声明不随作用域链穿透——每个需要改全局的嵌套函数内部 都得重新声明:

def hard_drop():
    global old_pid            # ✅ 在这个函数里再声明一次
    old_pid = proc.pid

三连败的共同点:没有一败是产品的错,但每一败都可能让人误判 "产品没修好"(v1 卡死像产品挂死、1.3 的 0 像"根本没 spawn 成功")。 这就是本文标题——测试也需要被测试。


二、正面方法论一:测试节奏 = 真实使用节奏

2.1 soak 快速连切为什么立过反功

修复 TimeoutError 之前,soak 脚本用毫秒级连续换台压测——六次换台全部 "看起来有响应"。修复后的复盘揭穿它:读线程启动后 1 秒内是活的, 快速连切的所有命令恰好都打在 1 秒窗口内,这个测试天然测不到空闲死亡。 换句话说,它不仅能漏测,还主动生产过"基本能用"的假阳性证据。

2.2 跨超时窗口测试

真正的用户是这样看电视的:点开 CCTV-1 看一段,想一想,10 秒后切 CCTV-2。 于是终版测试的换台间隔设定为 10~18 秒(≫ 旧的 1 秒死亡窗口), 每一跳断言 started=True:

场景旧 bug 下修复后实测
启动后 0.5s 点播✅ 能播(症状"偶尔能放出中央1台"的来源)✅
间隔 10~18s 逐台切换❌ 全部石沉大海✅ 每跳均起播
关闭对话框 30s 后再开再点❌ 无反应✅

沉淀规则:为"超时/空闲死亡"类缺陷设计测试时,测试的等待时间必须 显著大于缺陷的触发窗口。 压测的"快"在这里是反方向的价值。


三、正面方法论二:故障注入要有"必然失败"的靶子

"首播失败必须报错"(第 105 篇 had_stream 修复)怎么验证? 不能靠碰运气找一个恰好死掉的直播源——需要确定性坏源:

# 127.0.0.1:9 (discard 端口) 上没有任何服务 → 连接必然被拒
widget.play("http://127.0.0.1:9/dead.m3u8")
# 断言:恰好收到一次 playback_error,且 reason 非空
assert errors == 1

同类确定性靶子还有:

  • 杀进程:proc.kill() ×2 验证自愈链两分支(首死无感重生 / 再死恰好 1 次回退信号);
  • 撕连接:shutdown(SHUT_RDWR) 验证孤儿自退(§1.1);
  • 属性观察:轮询 hwdec_current=='d3d11va' and video_fps==25.0 验证硬解真实激活 ——而不是"看起来没卡"。

四、正面方法论三:像素才是终审法官

团队有一条铁律:要验证,不要推断。对播放功能而言,一切 started/ack/fps 数字都只是中间证据,终点证据是画面。本次收口的两枚铁证:

证据内容说明
temp/_fix_frame.png911KB,CCTV-1 全高清片尾职员表 + 台标 + 中国IPTV水印由 request_screenshot 走 IPC 让子进程 screenshot-to-file 生成,跨 18 秒窗口换台之后截的——证明"命令送达 + 解码正常 + 画面干净"三件事同时成立
temp/_desktop.png桌面级抓屏:视频精确铺满播放区证明 SetParent 嵌入方案在真实窗体里无偏移、无黑边、无 Z 序问题

截帧链路本身就是个小架构(也是 103 篇行协议里 ack shot <0|1> 的用途):

# 父进程
widget.request_screenshot("temp/_fix_frame.png")
# 50ms 泵内消费回执:
elif head == "ack" and parts[1] == "shot":
    self._shot_ok = parts[2] == "1"
# 子进程
m.command("screenshot-to-file", arg, "video")
_out("ack", "shot", "1" if os.path.exists(arg) else "0")

为什么"911KB"也重要:mpv 截图若落在解码失败/黑帧上,文件会显著变小 (纯黑帧 PNG 高度可压缩)。体积是画面的第一道哨兵,肉眼看图是终审。

沉淀规则:UI 渲染类功能的成功断言必须包含"看得见的像素"。 日志说播了、信号说好了、属性说 25fps——都不如你亲眼看见片尾字幕在跑。


五、把四套方法论装进一个 pytest 骨架

桌面侧的自动化回归最终沉淀为 55 个用例(tests/test_watch_tv.py + tests/test_watch_tv_iptv_mpv.py),关键设计:

# conftest.py:会话级闸门——CI/offline 环境不碰真实 libmpv
os.environ["WECLAW_TV_MPV"] = "0"   # is_available() 直接 False,
                                    # 播放内核用例降级为协议/状态机 mock 测试
  • 分层:协议解析/状态机/had_stream 逻辑用纯 mock 进 pytest(毫秒级、可 CI); 真渲染/真进程/真像素验证走手工脚本(本文 §一~§四),产出 PNG 证据归档 temp/;
  • 纪律:手工脚本放 test_scripts/,不污染 tests/ 的 pytest 根目录;
  • 口径:负向指标(错误日志增量=0)与正向断言同权重—— _mpv_runner.err 在修复前后各采一次样,11 → 11 才算闭环。

六、总结与展望

6.1 核心要点回顾

  1. 三连败三规则:骤死用 shutdown 不用 close;跨进程输出字节级判断; 嵌套 global 逐函数声明;
  2. 节奏即正义:测超时缺陷的等待时间必须显著大于触发窗口, soak 快切会系统性漏测空闲死亡;
  3. 确定性靶子:坏源/杀进程/撕连接——故障要注入,不能守株待兔;
  4. 像素终审:started 信号、fps 属性、ack 回执都是中间证据, 一张人眼可验的截帧才是终点证据。

1 个核心公式:

可信的修复 = 产品代码 × (真实节奏测试 + 确定性故障注入 + 视觉铁证)

6.2 下一步学习方向

  • 📖 下一篇:《WeClaw_107 KeyError 的五级放大故障链》——换个战场,方法论原封不动: 误导性报错、逐层取证、防线分层
  • 🔜 再下一篇:《WeClaw_108 别猜你的模型预算》

下期预告:《KeyError 的五级放大故障链》

  • 两个长期稳定的定时任务突然全挂,报错却与真相毫无关系
  • 五级放大链:大参数 → 动态压缩 → 截断 → 静默吞异常 → 裸下标
  • "刻意不自动修复":一个反直觉但正确的工程决定

敬请期待!


附录 A:本篇涉及的代码/证据清单

文件路径作用
src/ui/watch_tv/mpv_player.py request_screenshot / _drain截帧请求与回执消费
src/ui/watch_tv/mpv_runner.py screenshot 分支子进程侧 screenshot-to-file
temp/_fix_frame.png、temp/_desktop.png两枚视觉铁证(已归档)
tests/test_watch_tv.py + tests/test_watch_tv_iptv_mpv.py55 用例回归基线
conftest.py WECLAW_TV_MPV 闸门CI 与真实渲染分层

附录 B:参考资料

  1. 上一篇:《WeClaw_105 故障降级的三级自愈链》
  2. socket.shutdown 文档:https://docs.python.org/3/library/socket.html#socket.socket.shutdown
  3. BufferedReader 线程安全语义:https://docs.python.org/3/library/io.html#io.BufferedReader
  4. WeClaw_12《日志分析实战》、WeClaw_83《Mock Fixture 录制回放》(测试家族姊妹篇)

版权声明:本文为CSDN 博主「翁勇刚」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/yweng18