那几天,桌面上不断弹出“意外退出”的窗口。乍看像是后台模式不稳定,甚至像是某个真实故障又回来了。可把崩溃记录按时间翻出来,才发现其中有一批记录来自故意崩溃的自测入口:它们不是业务运行中偶然发生的崩溃,而是为了验证崩溃收集功能,程序主动把自己撞停。
这件事的麻烦不在于“程序能不能崩”。测试当然应该能够制造崩溃,问题在于测试制造出来的信号,是否还要沿着生产故障的路径交给操作系统。一个故意发生的测试事件,如果被系统当成用户遭遇的真实事故,就会把验证噪声伪装成产品故障。
弹窗看起来像故障,真正的问题在边界
当时的场景很具体:后台模式要在 Mac 和 Windows 上持续运行,压测覆盖了 12 台演示机;同时还要反复验证 5 种故意崩溃,确认应用自己的记录、系统报告和后续诊断包都能工作。后台运行本身并没有出问题,Mac 和 Windows 各跑了 10 分钟,正常退出,没有新崩溃。真正让桌面变得吵闹的,是自测入口把崩溃信号重新抛给了系统。
从系统视角看,这个行为很合理:进程收到了异常信号,崩溃处理器完成一部分工作,然后继续把信号交还给操作系统,于是系统生成报告,还可能弹出提示。从测试视角看,它却把两件事混到了一起:应用想验证“我能不能留下诊断记录”,系统却以为“用户刚刚遇到了一个未处理的崩溃”。
如果不处理,后面会导致两个直接后果。第一个是测试人员看到一堆弹窗和新报告,无法快速判断到底有没有新的真实故障;第二个是压测环境越来越像故障现场,历史样本和本轮测试样本混在一起,聚类结果也会被污染。最后,团队可能为了让测试通过,反过来关闭系统崩溃报告。那样表面安静了,真实故障的证据也一起消失了。
我一开始看到的不是同一个问题
这次排查让我重新意识到,崩溃问题不能只按“有没有崩”来分。至少要分成3层:应用有没有写下自己的记录,操作系统有没有生成系统级报告,平台事件日志有没有新增相关事件。它们对应的是不同的问题,不能用其中一个结果替代另外两个。
比如,应用记录存在,只能说明崩溃处理器至少走到了写记录这一步;它不能证明系统不会弹窗。Mac 上没有新增诊断报告,也不能直接证明应用没有执行自测,还要去看 crashes 目录里是否留下了预期的5份记录。Windows 上退出码是70,也不能只看进程结束,还要确认事件日志没有新增与自测对应的错误。
诊断链路一旦被混用,排查者看到的就不再是故障本身,而是测试方式留下的回声。
这里最容易误判的是,把“系统弹了崩溃窗口”当成“真实故障发生了”。我没有直接去改系统设置,是因为那只能改变系统如何报告,不能解决应用把测试信号送错路径的问题。更重要的是,关闭全局崩溃报告会影响生产环境,等于为了清理测试现场,把真正需要保留的报警器拆掉。
把自测和真实故障拆成两条路
我选择先给测试入口明确的模式标记,再让崩溃处理器根据模式做不同收尾,是因为这能把变化限制在应用自己的边界内。测试路径的目标不是模拟操作系统的全部行为,而是验证应用能不能识别异常、写下必要信息,并以稳定的结果告诉调用者“这次自测结束了”。
因此,故意崩溃自测入口进入测试模式后,处理器写完应用自己的崩溃记录,就直接以退出码70结束,不再把信号重新抛给系统。生产路径则保持原来的处理:真实崩溃仍然交给系统,由系统生成诊断报告。两条路径都能留下证据,但证据的归属不同。
这个选择还有一个实际好处:退出码70比一个弹窗更适合自动化。脚本可以明确判断进程是否按预期结束,压测也不需要等待人工关闭窗口。对于测试来说,稳定的非零码不是“把错误藏起来”,而是把测试失败变成调用方可以读取的结果。
当然,这里有一个边界不能省略。测试模式必须由明确入口开启,不能因为“最近正在压测”就让所有崩溃都走测试路径。否则生产环境中的真实崩溃可能被误判为自测,系统报告就会被静默丢掉。模式标记不是一个方便的开关,而是诊断证据的一部分。
验证不能只看应用有没有留下文件
改完以后,我没有只看 crashes 目录里多了多少文件,而是把验证拆成4个观察面。第一面看应用记录:5种自测都应该留下对应记录。第二面看 Mac 的系统诊断报告:自测前后数量应该不变。第三面看 Windows 的退出码和事件日志:5种自测退出码都应该是70,相关系统事件不能新增。第四面再看后台压测,确认正常运行和退出没有被新的处理逻辑影响。
Mac 侧先用1次故意崩溃做小范围验证,确认诊断报告数量前后都是11份,再继续跑另外4种。结果是5种自测都留下了应用记录,但没有新增系统崩溃报告。Windows 侧的5种自测退出码全部是70,事件日志没有新增相关事件。这里的顺序很重要:先做低成本、容易定位的单次验证,再扩大到完整自测和压测。
已有的崩溃样本也没有删除。它们虽然不应该继续制造弹窗,却仍然有价值,因为后续要按栈签名做聚类,验证同一次崩溃留下的多份痕迹能不能合并。清理现场并不等于修复现场;如果把旧样本一并删掉,新的聚类验证就失去了参照。
真正可靠的验证,不是证明“今天没有弹窗”,而是同时证明应用记录还在、系统报告没有被误伤、平台日志没有新增噪声。
错误捷径是让系统闭嘴
最容易想到的错误捷径,是直接关闭 Mac 的崩溃报告,或者修改全局设置,让 Windows 不再记录相关事件。它的确可能让桌面马上安静下来,却没有回答一个关键问题:真实故障发生时,我们还看不看得到?
另一种捷径是让所有崩溃都不再交给系统。这样测试很干净,但生产问题会变成应用目录里一份普通文本,失去平台级报告携带的上下文。还有一种相反的做法,是让自测完全模拟真实崩溃,认为只有弹窗和系统报告都出现才算测试完整。它验证了系统链路,却把每一轮自测都变成了用户体验上的事故。
这几种做法的共同问题,是把“测试是否逼真”放在了“证据是否属于正确的场景”之前。测试的目的不是制造越多系统痕迹越好,而是让我们能区分痕迹来自哪里。只要这个归属关系清楚,自测可以用应用记录和退出码完成验证,生产故障则继续保留系统级证据。
下次我会怎么做
下次遇到类似问题,我会先问3个问题:这次是在验证业务运行,还是在验证故障收集?测试产生的信号应该由应用记录,还是应该交给操作系统?生产故障的报告边界有没有被改变?这3个问题能先把“测试失败”和“产品故障”分开,避免一上来就修改系统设置。
具体流程也可以固定下来。先给测试入口加明确模式标记;再让测试路径写应用记录并返回稳定的非零码;保留真实故障的原处理;最后分别检查应用记录、系统报告、平台事件日志和压测结果。任何一个观察面不符合预期,都先回到路径边界排查,而不是用关闭报告的方式把现象抹掉。
这件事让我重新理解了“安静的测试环境”。安静不是没有错误,也不是系统什么都不报告,而是每种错误都被送到了应该去的地方。一个流程成熟的标志,不是它能让所有测试看起来像成功,而是它能让自测有自己的证据、真故障有自己的报警。
这条规则不只适用于崩溃。任何会主动制造异常的自测,都应该先想清楚:我们是在验证应用的记录能力,还是在模拟用户真正遇到的事故。把这条边界提前写进流程,下一次排查就不必从一堆弹窗里猜测哪一个才是真的问题。