源码拆解|PrePlanRun 失败后为何仍会执行逆序 PostPlanRun

背景

在自定义 TestStep 时,很多人会把初始化写在 PrePlanRun(),把清理写在 PostPlanRun()。但线上最容易困惑的一点是:如果某个步骤在 PrePlanRun 里抛错导致计划启动失败,为什么日志里仍然能看到一批 PostPlanRun 执行记录?这不是“异常吞掉继续跑”,而是 OpenTAP 故意做的收尾策略。

框架分析

核心逻辑在 Engine/TestPlanExecution.cs。执行阶段先走 ExecTestPlan(),内部按树形顺序递归 RunPrePlanRunMethods():父步骤先于子步骤执行,并把已经“登记过预运行阶段”的步骤放进 planRun.StepsWithPrePlanRun。一旦任意步骤预运行失败,函数直接返回 StartFail,主执行(DoRun)不会继续。

重点在收尾:finishTestPlanRun() 不看你是否完整跑完主流程,而是统一遍历 StepsWithPrePlanRun,并且从尾到头逆序调用 PostPlanRun()。这使它的行为接近“栈回退”:先初始化的后清理,尽量保证资源、上下文、状态回滚顺序正确。

实现过程

可复现命令(直接定位关键分支):

1
2
3
4
5
6
cd /home/ops/clawd/repos/opentap
# 1) 看 PrePlanRun 的递归执行与失败短路
grep -n "RunPrePlanRunMethods\|PrePlanRun of\|Aborting TestPlan" Engine/TestPlanExecution.cs

# 2) 看收尾阶段如何逆序 PostPlanRun
grep -n "StepsWithPrePlanRun\|PostPlanRun\|for (int i = run.StepsWithPrePlanRun.Count - 1" Engine/TestPlanExecution.cs

若你想在插件里验证顺序,可写一个父子步骤:父、子都实现 PrePlanRun/PostPlanRun,然后让子步骤在 PrePlanRun 抛异常。观察日志会是“父Pre → 子Pre(失败) → 子Post/父Post(逆序收尾)”。

注意事项

  1. PrePlanRunUsed 优化会跳过未重写预/后处理方法的步骤;不要误以为“没进列表就是没参与执行树”。
  2. PostPlanRun 是“尽力执行”,异常只记警告,不再二次中断整个收尾流程。
  3. 自定义步骤里不要假设主 Run() 一定发生;凡是在 PrePlanRun 申请的外部资源,都应能在 PostPlanRun 独立释放。

小结

OpenTAP 在计划启动阶段采用“前序初始化 + 失败短路 + 逆序清理”的组合,目标不是“继续执行”,而是“即使失败也尽量有序回收”。理解 StepsWithPrePlanRun 的登记时机和倒序遍历策略后,很多“为什么失败后还在跑 PostPlanRun”的疑问就能解释清楚。

关键源码路径:

  • Engine/TestPlanExecution.csRunPrePlanRunMethodsExecTestPlanfinishTestPlanRun
  • Engine/TestStep.csPrePostPlanRunUsedPrePlanRun/PostPlanRun 默认行为)
  • Engine/TestPlanRun.csStepsWithPrePlanRun 记录容器)