机制图解|PrePlanRun/PostPlanRun 的反射快路径与逆序收尾

背景

很多人会在自定义 Step 里重写 PrePlanRun()PostPlanRun() 做连接预热、缓存准备、状态回收。但在大计划里,绝大多数步骤并不会重写这两个方法。如果框架每次都无脑调用一遍空实现,累计开销并不小。OpenTAP 在这里做了一个很实用的优化:先判断“这个类型是否真的重写过”,再决定要不要进入 Pre/Post 生命周期,同时保证收尾顺序正确。

框架分析

核心思路有两层:

  1. 类型级缓存TestStep 里用 Dictionary<Type, bool> 缓存“是否重写 Pre/Post”结果,避免反射重复计算。
  2. 实例级短路:每个步骤实例首次访问 PrePostPlanRunUsed 时再取缓存,后续直接走布尔值。

执行阶段上,RunPrePlanRunMethods() 会在递归遍历步骤树时决定是否调用 PrePlanRun();结束阶段 finishTestPlanRun() 再按 StepsWithPrePlanRun逆序回放 PostPlanRun()。这就天然形成“先初始化的后释放”,和栈式资源管理一致。

实现过程

可以在本地直接复现这条链路:

1
2
cd /home/ops/clawd/repos/opentap
grep -n "PrePostPlanRunUsed\|RunPrePlanRunMethods\|PostPlanRun" Engine/TestStep.cs Engine/TestPlanExecution.cs

读代码时建议关注三个点:

  • Engine/TestStep.cspreOrPostPlanRunOverridden(Type t) 通过方法句柄比对判断是否重写,并写入静态字典。
  • Engine/TestPlanExecution.cs:预执行阶段仅在 PrePostPlanRunUsed == true 时才真正调用 step.PrePlanRun()
  • 同文件收尾逻辑:for (int i = run.StepsWithPrePlanRun.Count - 1; i >= 0; i--) 逆序执行 step.PostPlanRun(),避免资源释放顺序错乱。

注意事项

  • 这个优化依赖“方法重写检测”,如果插件通过非常规方式注入行为(而非 override),可能不会触发预期路径。
  • StepsWithPrePlanRun 记录的是已进入预执行链路的步骤,排查收尾遗漏时先看这个列表是否入栈。
  • PostPlanRun() 异常不会让流程再次崩掉,只会记录告警并继续收尾;插件开发应自行保证幂等和可重入。

小结

这段实现的价值在于:既减少了空生命周期调用的常态成本,又通过逆序回放把资源释放语义做对了。对包含大量“模板步骤”的测试计划,这种“先判断再调用”的快路径能明显降低无效开销,同时让 Pre/Post 的行为更可预测。

关键源码路径:

  • Engine/TestStep.cs
  • Engine/TestPlanExecution.cs
  • Engine/TestPlanRun.cs