一个大输入触发了 bug。几十行、几百行,甚至更大。工程师开始手工删:删一段,跑一次;删错了,bug 没了;删对了,线索才露一点。

Laurence Tratt 写的 test-case reducer,盯的就是这个老问题。它让机器替人删输入,把一个大而乱的失败样本,压成尽可能小的可复现样本。原文提到,95%-99% 的缩减并不少见。

反常点在这里:这种工具通常不靠聪明。它不理解程序,不理解 bug,也不懂业务语义。它只问一个硬问题:删完以后,目标问题还在不在?

reducer 到底在做什么

test-case reducer 需要三样东西。

要素作用写错的后果
程序被调试或被验证的对象没有稳定目标,压缩无从谈起
输入触发问题的大样本样本不复现,结果会失真
interestingness test判断缩小后的输入是否仍触发目标问题条件太松,会过度缩减;条件太严,压不下去

这里的 interesting,不是“有意思”。它的意思是:这个输入仍然触发你关心的失败。比如仍然崩溃,仍然输出错误结果,仍然让两个配置跑出不同结果。

reducer 会不断尝试删掉输入的一部分。删完后,它把候选输入交给 interestingness test。测试通过,就保留更小版本;不通过,就换一种删法。

Shrink Ray 的例子说明了这个过程的力度:有的 C 输入先压掉 60%;有的真实缩减过程先找到关键删除点,压掉 90%,最后到 99%。bug 原本埋在噪声里,压完以后,剩下的才像线索。

这对经常处理复杂 bug 的工程师很直接:别先急着读完整大样本。先问自己能不能写一个脚本,稳定判断“问题还在”。只要能写出来,手工删输入这件事就应该交给机器。

它强在不懂,也败在测试写得烂

很多人会误会 reducer,以为它靠语义理解。原文强调的恰好相反:它的强大来自“不理解语义”。

它不需要知道 C 语言,不需要知道业务规则,也不需要知道为什么错。只要 interestingness test 能判断目标问题还在,它就能继续往下削。

这有点像“庖丁解牛”,但刀并不懂牛。刀只认一个反馈:这一刀下去,目标还在不在。落回调试现场,这比很多“智能分析”更实用,因为它把判断标准固定住了。

风险也在同一个地方。

interestingness test 写得太松,reducer 会 over-reduction。最糟糕时,它可能接受空输入。原文提到,Shrink Ray 还会专门检查 interestingness test 是否把空输入也当成 interesting,因为这种错误并不少见。

那个 FAST=0 和 FAST=1 输出不同的例子,也不能只检查“两边不同”。还要确认慢版本输出的是预期值。否则 reducer 可能保留一个无关差异,把你带到另一条岔路。

速度是另一个硬约束。reducer 可能每秒运行数百次测试,中等规模输入也可能产生数十万次尝试。interestingness test 慢一点,总耗时就会被放大很多倍。

非确定性错误、偶发失败、超时设置不当,也会污染结果。reducer 不是调试神药。它只是把战场缩小,前提是你的判定条件足够稳、足够快、足够准。

真正被低估的是调试自动化

我更在意的不是某个 reducer 工具,而是它暴露出的工程习惯。

很多团队谈研发效率,喜欢上更大的 IDE、更智能的 AI、更复杂的观测平台。可一到具体调试,还是人肉删输入,靠直觉猜路径,靠资深工程师熬夜看日志。

问题不一定是工具少,而是复现条件没有机器化。

对工程师来说,下一步动作很具体:遇到大输入 bug,先写一个最小判定脚本。它可以很朴素,只要能稳定回答“目标问题是否还在”。然后再考虑把 reducer 接进流程。

对技术负责人来说,重点也不是立刻采购一套新平台。更该检查团队有没有把复杂 bug 的复现条件沉淀成脚本、测试或 CI 任务。没有这个动作,调试自动化就只是口号。

这里有一个现实对比:

做法短期成本长期收益主要限制
手工删输入依赖个人经验,难复用慢,容易漏,难交接
写 interestingness test 后用 reducer可反复运行,可压缩样本,可复盘测试必须稳定、快速、严格

我不太买账的是那种“工具会自动理解问题”的期待。至少在这类场景里,更可靠的路线反而更朴素:人把判断条件写清楚,机器负责重复劳动。

这也是接下来最该观察的变量:团队是否愿意把 bug 的复现条件变成可运行资产。不是文档里一句“复现步骤”,而是一段能跑、能判定、能被 reducer 反复调用的测试。

大输入是迷雾。好的 interestingness test,是灯。灯不负责破案,但它能让你别在雾里乱砍。