ELL3N HSU
← 文章

XAT 结果报告:为什么失败、缺失和退出码要分开

沿断言失败、cleanup 异常和 worker 崩溃的例子,解释多错误保留、complete、任务身份核对与命令退出码。

一批测试中,A 已通过,B 的断言失败,C 所在进程突然退出。汇总应怎样显示?如果只统计收到的正常报告,C 会消失;如果整批只显示失败,又无法判断应该修业务断言、脚本还是执行器。

这三条用例需要分别留下记录:A 已完成,B 有具体断言失败,C 没能返回正常结果。整批还要说明信息是否收齐,并给命令行调用方一个退出码。下面用同一组例子看 XAT 如何组织这三层信息。

方法状态由执行事实决定

unittest 已有断言失败与异常回调。XAT 的收集器在这些回调上记录结构化原因,再加上父进程发现的系统错误与方法时限。

状态什么情况下产生不能据此推断什么
SUCCESS方法级执行成功;类/模块 fixture 错误另行记录HTTP 一定为 200;负向断言也可能成功
ASSERT_ERROR断言不满足,或声明 expectedFailure 却意外成功一定是产品缺陷;断言本身也可能写错
SCRIPT_ERROR未处理的脚本或生命周期异常仅凭异常名称确定根因;请求超时也可能在这里
SYSTEM_ERRORworker、结果协议或框架执行发生问题业务断言已经执行并失败
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退出码
方法均正常返回并通过true0
断言或脚本失败,但报告完整返回true1
worker 崩溃、报告缺失或不可信false1
方法超时,被终止或事后判超时false1
用户中断false130

前面的三方法例子因此是 complete=false、退出码 1。若移除 C,B 仍然失败,整批却可以是 complete=true。

还有两个容易混淆的边界。全部跳过时命令可以退出 0,但成功方法数为 0;空集合退出码为 3,输入非法为 2。所以调用方若要求“至少执行并通过一个方法”,需要读取方法结果,不能只判断退出码 0。

父进程需要核对哪一份报告属于哪个任务

worker 正常退出并生成 JSON,只说明存在一份数据。父进程还要检查它与提交任务之间的关系:文件路径一致,方法身份一致,报告数量合法,schema 支持,报告退出码与进程退出码对应。

方法模式下,发现后方法消失、报告返回另一方法、重复或缺失的记录,都不能直接合并。已知身份的任务会形成对应系统错误;若发现阶段就失败,还没有方法清单,只能记录文件或发现阶段错误。类/模块 fixture 阻止方法启动时,也可能只有 fixture 错误,不能伪造一个已运行方法。

检查点与最终报告用途不同。检查点帮助保留进度和识别被中断的方法;最终报告才承载完整返回。读取不到可信的检查点时,父进程仍可按已提交任务标记系统错误,但不能猜测测试执行到了哪一条业务语句。

验证要故意破坏结果通道

仅运行成功与断言失败用例,覆盖不到报告损坏这条路径。当前回归还在 worker 结束时修改 schema,或制造方法身份变化,检查父进程是否拒绝错误报告;中断测试检查已收到结果能否保留。

另一个测试让最终 JSON 文件写入失败。方法本身已经成功,命令仍要以非零退出并记录 report_write_error;写报告失败不应把已执行方法改成断言失败。这进一步说明方法状态与命令状态需要分开。

这些检查验证的是执行结果的表示和传递。对支付等有副作用的测试,worker 失联仍无法说明服务端动作有没有发生,重新运行前需要业务层确认状态。结果模型能明确暴露这份不确定性,无法替业务接口提供幂等保证。