背景
在自定义 TestStep 时,很多人会把初始化写在 PrePlanRun(),把清理写在 PostPlanRun()。但线上最容易困惑的一点是:如果某个步骤在 PrePlanRun 里抛错导致计划启动失败,为什么日志里仍然能看到一批 PostPlanRun 执行记录?这不是“异常吞掉继续跑”,而是 OpenTAP 故意做的收尾策略。
框架分析
核心逻辑在 Engine/TestPlanExecution.cs。执行阶段先走 ExecTestPlan(),内部按树形顺序递归 RunPrePlanRunMethods():父步骤先于子步骤执行,并把已经“登记过预运行阶段”的步骤放进 planRun.StepsWithPrePlanRun。一旦任意步骤预运行失败,函数直接返回 StartFail,主执行(DoRun)不会继续。
重点在收尾:finishTestPlanRun() 不看你是否完整跑完主流程,而是统一遍历 StepsWithPrePlanRun,并且从尾到头逆序调用 PostPlanRun()。这使它的行为接近“栈回退”:先初始化的后清理,尽量保证资源、上下文、状态回滚顺序正确。
实现过程
可复现命令(直接定位关键分支):
1 | cd /home/ops/clawd/repos/opentap |
若你想在插件里验证顺序,可写一个父子步骤:父、子都实现 PrePlanRun/PostPlanRun,然后让子步骤在 PrePlanRun 抛异常。观察日志会是“父Pre → 子Pre(失败) → 子Post/父Post(逆序收尾)”。
注意事项
PrePlanRunUsed优化会跳过未重写预/后处理方法的步骤;不要误以为“没进列表就是没参与执行树”。PostPlanRun是“尽力执行”,异常只记警告,不再二次中断整个收尾流程。- 自定义步骤里不要假设主
Run()一定发生;凡是在PrePlanRun申请的外部资源,都应能在PostPlanRun独立释放。
小结
OpenTAP 在计划启动阶段采用“前序初始化 + 失败短路 + 逆序清理”的组合,目标不是“继续执行”,而是“即使失败也尽量有序回收”。理解 StepsWithPrePlanRun 的登记时机和倒序遍历策略后,很多“为什么失败后还在跑 PostPlanRun”的疑问就能解释清楚。
关键源码路径:
Engine/TestPlanExecution.cs(RunPrePlanRunMethods、ExecTestPlan、finishTestPlanRun)Engine/TestStep.cs(PrePostPlanRunUsed与PrePlanRun/PostPlanRun默认行为)Engine/TestPlanRun.cs(StepsWithPrePlanRun记录容器)