XAT 结果报告:为什么失败、缺失和退出码要分开
沿断言失败、cleanup 异常和 worker 崩溃的例子,解释多错误保留、complete、任务身份核对与命令退出码。
一批测试中,A 已通过,B 的断言失败,C 所在进程突然退出。汇总应怎样显示?如果只统计收到的正常报告,C 会消失;如果整批只显示失败,又无法判断应该修业务断言、脚本还是执行器。
这三条用例需要分别留下记录:A 已完成,B 有具体断言失败,C 没能返回正常结果。整批还要说明信息是否收齐,并给命令行调用方一个退出码。下面用同一组例子看 XAT 如何组织这三层信息。
方法状态由执行事实决定
unittest 已有断言失败与异常回调。XAT 的收集器在这些回调上记录结构化原因,再加上父进程发现的系统错误与方法时限。
| 状态 | 什么情况下产生 | 不能据此推断什么 |
|---|---|---|
SUCCESS | 方法级执行成功;类/模块 fixture 错误另行记录 | HTTP 一定为 200;负向断言也可能成功 |
ASSERT_ERROR | 断言不满足,或声明 expectedFailure 却意外成功 | 一定是产品缺陷;断言本身也可能写错 |
SCRIPT_ERROR | 未处理的脚本或生命周期异常 | 仅凭异常名称确定根因;请求超时也可能在这里 |
SYSTEM_ERROR | worker、结果协议或框架执行发生问题 | 业务断言已经执行并失败 |
TIMEOUT_ERROR | 执行器判定方法超过时限 | 等同于 HTTP 客户端抛出的 TimeoutError |
被测试代码抛出异常,如果用例通过 assertRaises 正确验证了它,方法仍可成功。skip 和 expectedFailure 则使用独立执行备注,不作为第六种失败状态,也不计为成功方法。
一个方法可以有多个失败原因
测试先断言失败,cleanup 又抛出除零异常时,一条方法记录会包含两个 issues。主状态按 SYSTEM_ERROR > TIMEOUT_ERROR > SCRIPT_ERROR > ASSERT_ERROR 选取,但详细原因全部保留。
因此,汇总表可以把这个方法归为脚本错误,展开详情时仍能看到先发生的断言失败。统计也按同样的关系处理:这是一个失败方法、两个错误事件,不能把两条 issues 当成执行了两个方法。
下面的故障用例可以在当前 XAT 版本中运行。它只创建本地进程,不访问业务系统;os._exit 用于模拟 worker 来不及返回报告的情况。
# 保存为 test_report_example.py
import os
from xat import BaseCase
class ReportExample(BaseCase):
def test_a_ok(self):
self.assertEqual(2 + 2, 4)
def test_b_two_failures(self):
self.addCleanup(lambda: 1 / 0)
self.assertEqual(1, 2, "business assertion")
def test_c_worker_exit(self):
os._exit(7)
xatrun test_report_example.py -c 2 --json-report result.json
在核对版本 d8a6f672 的方法模式中,这个例子得到三个方法记录:A 为 SUCCESS,B 为 SCRIPT_ERROR 且保留 assertion_failed 与 script_error,C 为 SYSTEM_ERROR 并标记 interrupted。进程结束顺序可以改变显示顺序,按方法身份读取的结论不变。C 的退出原因保存在批次级 issues,并通过 target 关联方法,所以读取器还需要检查批次错误,不能只遍历方法内部的 issues。
complete 与退出码怎样配合
对 B 来说,断言虽然失败,执行器却拿到了完整的失败信息。对 C 来说,父进程只能记录它被中断,补一条错误占位并不能找回它原本应返回的报告。complete 区分的正是这两种情况,因此不能用“通过数等于总数”来计算。
| 情况 | complete | 退出码 |
|---|---|---|
| 方法均正常返回并通过 | true | 0 |
| 断言或脚本失败,但报告完整返回 | true | 1 |
| worker 崩溃、报告缺失或不可信 | false | 1 |
| 方法超时,被终止或事后判超时 | false | 1 |
| 用户中断 | false | 130 |
前面的三方法例子因此是 complete=false、退出码 1。若移除 C,B 仍然失败,整批却可以是 complete=true。
还有两个容易混淆的边界。全部跳过时命令可以退出 0,但成功方法数为 0;空集合退出码为 3,输入非法为 2。所以调用方若要求“至少执行并通过一个方法”,需要读取方法结果,不能只判断退出码 0。
父进程需要核对哪一份报告属于哪个任务
worker 正常退出并生成 JSON,只说明存在一份数据。父进程还要检查它与提交任务之间的关系:文件路径一致,方法身份一致,报告数量合法,schema 支持,报告退出码与进程退出码对应。
方法模式下,发现后方法消失、报告返回另一方法、重复或缺失的记录,都不能直接合并。已知身份的任务会形成对应系统错误;若发现阶段就失败,还没有方法清单,只能记录文件或发现阶段错误。类/模块 fixture 阻止方法启动时,也可能只有 fixture 错误,不能伪造一个已运行方法。
检查点与最终报告用途不同。检查点帮助保留进度和识别被中断的方法;最终报告才承载完整返回。读取不到可信的检查点时,父进程仍可按已提交任务标记系统错误,但不能猜测测试执行到了哪一条业务语句。
验证要故意破坏结果通道
仅运行成功与断言失败用例,覆盖不到报告损坏这条路径。当前回归还在 worker 结束时修改 schema,或制造方法身份变化,检查父进程是否拒绝错误报告;中断测试检查已收到结果能否保留。
另一个测试让最终 JSON 文件写入失败。方法本身已经成功,命令仍要以非零退出并记录 report_write_error;写报告失败不应把已执行方法改成断言失败。这进一步说明方法状态与命令状态需要分开。
这些检查验证的是执行结果的表示和传递。对支付等有副作用的测试,worker 失联仍无法说明服务端动作有没有发生,重新运行前需要业务层确认状态。结果模型能明确暴露这份不确定性,无法替业务接口提供幂等保证。