ELL3N HSU
← 文章

XAT 并发执行:方法隔离、fixture 作用域与超时边界

从同一文件的共享 fixture 出发,比较方法级与文件级子进程,并解释发现隔离、检查点与父进程终止。

假设一个测试文件里的三个方法共用 setUpClass 创建的客户端。现在想让它们同时运行,是把整个文件交给一个进程,还是让每个方法各用一个进程?这会直接改变客户端创建几次、哪些状态还能共享,以及一个方法卡住后其他方法能不能继续。

XAT 默认让每个方法进入独立子进程,也保留整文件执行的模式。下面沿着这个共享客户端的例子,看两种模式如何准备资源,再追到父进程如何处理方法超时。讨论限于当前本机运行器,尚未做两种模式的吞吐对比。

调度单位决定哪些状态可以共享

假设一个测试文件含 A、B、C 三个方法,setUpClass 创建一个共享客户端。文件模式把整个文件放进一个子进程,三个方法串行使用这个类 fixture。方法模式则分别启动三个解释器,每个解释器只执行选中的方法及其 fixture,因此类级准备会发生三次。

观察点方法模式文件模式
调度单位一个方法一个文件
同文件方法可以并发串行
类/模块 fixture随各方法重复执行在同一文件 suite 中共享
进程内模块变量不跨方法共享可能被先前方法修改
某个方法挂住终止对应 worker,其他任务可以继续终止文件 worker,同文件后续方法未执行

方法模式适合每个方法可以独立准备和清理的用例。依赖共享 fixture 的旧用例可以选择文件模式。不能只根据 CPU 核数决定并发数:测试环境的账号、服务连接、数据库记录也可能限制同时执行的任务数。

进程隔离解决的是解释器内状态边界。两个方法若同时写同一条数据库记录,仍然会发生业务冲突;即使每个方法都用了独立 Session,也没有改变这一事实。

为什么发现用例也放在子进程

Python 发现用例需要导入模块,模块顶层代码会执行。若在调度父进程里完成导入,业务模块可能修改全局状态,甚至直接结束父进程。

XAT 在独立发现进程中读取方法清单,核对文件、schema 与重复方法身份后冻结任务,再进入执行。某个文件导入或发现失败时,错误单独记录;不会把已经发现的一部分方法当作完整文件继续跑。

隔离发现进程并不会消除模块顶层的外部副作用。发现一次、执行再导入一次,可能重复创建业务数据。因此,发现阶段应只建立可执行清单,测试数据准备放进 fixture。工作目录与配置读取也应能接受“多次导入”这个前提。

线程池调度,业务在子进程执行

任务清单确定以后,父进程需要启动 worker,并在它结束时接收结果。这部分等待和日志处理交给线程池;测试方法本身仍在子进程中运行。-c 限制的是同时运行的 worker 进程数。

xatrun tests -c 4 --case-timeout 600
xatrun tests -c 4 --parallel-scope file

调度器维护未提交队列与已提交任务的映射。一个 worker 结束后,父进程核对结果、输出所属日志,再补充一个任务。这样并发时日志按任务分组;代价是某个任务运行中的完整输出通常要等它结束才集中展示。并发数为 1 时,执行日志可以持续读取输出。

这种实现仍然承担新解释器启动、导入和重复 fixture 的开销。一个复用进程池可能减少部分开销,但需要处理跨任务残留状态;线程内执行则共享更多状态,也难以单独强制停止卡住的 Python 线程。这里是在解释不同执行模型的代价,不代表项目已对所有方案做过实验选型。

方法时限从哪里开始计时

默认时限为 300 秒,--case-timeout 可以修改。它不是一次 HTTP 请求的 timeout,也不是整条命令从启动到退出的总时限。

方法模式在进入 suite 前记录活跃方法,覆盖类/模块 fixture、方法 setup、测试、teardown、cleanup 及 fixture 收尾;排队、发现、模块导入和解释器启动不在这段计时内。文件模式在 unittest 开始一个方法时记录进度,所以共享类/模块 fixture 不在同一方法计时范围。两种模式不能套用完全相同的超时覆盖说明。

worker 写入包含任务身份、活跃方法和开始时间的检查点。检查点先写临时文件,再替换目标文件,减少父进程读到写入一半内容的机会;其中报告被标为未完成,不能用进度快照冒充最终结果。

父进程发现超时后还会再读一次。如果文件模式刚结束 A、开始 B,而父进程还拿着 A 的旧开始时间,直接终止就可能误杀 B。复读用于确认当前活跃方法没有变化。worker 正常退出后也会复核实际耗时,覆盖两次轮询之间刚好结束的边界。

终止一个 worker 后,还剩哪些事实

父进程记录 TIMEOUT_ERROR,保留已完成结果,并将整批标为未完成。文件模式还记录同文件后续方法没有执行;不会自动将它们换个进程重跑,因为共享 fixture 和前序操作的外部副作用可能已经发生。

实际回归包含一个持续循环且捕获异常的测试方法,验证父进程能够终止它;同时检查后续方法能否执行、进程是否退出,以及登记的 cleanup 没有被调用。这与“抛一个 TimeoutError”不同:后者仍是业务代码主动返回的异常,不能验证父进程强制终止的能力。

当前时限仍有覆盖范围之外的等待,例如发现或导入阶段卡住。未来若增加全局 watchdog,需要另行定义发现、启动和整个批次的预算,不能直接把这些等待算成某个方法的业务失败。XAT 项目介绍讨论报告核对如何与这些执行边界配合。