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 项目介绍讨论报告核对如何与这些执行边界配合。