Skip to content

守卫把注释当代码,自己空转了 ​

一句话结论:用字符串去源码里找"接线"的守卫测试,证明的只是"这几个字出现过"。它挡不住注释、挡不住死分支、也挡不住写错位置。

起因 ​

那天的活是给对话补一条启动恢复逻辑:进程重启时,把上次没跑完的消息从"生成中"标成"失败",别让它们永远卡在那儿转圈。

函数写完了,单测也过了——用一个内存库,模拟一条卡住的消息,调用恢复函数,断言状态变了。这条测的是逻辑对不对。

但逻辑对,不等于它会被执行。如果这个函数写完了、没人调用,一切测试仍然全绿,用户那边一条消息也不会被恢复。

所以还要一个接线守卫:证明这个函数真的被挂进了启动流程。

排查过程 ​

守卫的写法很朴素:读启动那个源文件,确认里面出现了恢复函数的名字。出现了就通过。

写完跑了一遍,绿。看起来这道防线立起来了。

然后验收的时候,我做了个反向对照:把调用注释掉,看守卫会不会红。

它没红。

原因一眼就明白了:注释掉的那一行,注释里还完整地留着那个函数名。守卫做的是字符串匹配,"出现过"这个条件依然成立。

这道守卫从来没拦住过任何东西。 它一直是绿的,而绿的原因和它想证明的事没有关系。

这件事真正让我后背发凉的地方在于:如果我没做那个反向对照,它会以"已通过"的身份进入验收报告,然后在下一次改动里继续绿着。一个永远绿着的守卫,比没有守卫更危险——没有守卫时你知道自己是裸露的,有守卫时你以为被保护着。

补法有两处:

  1. 匹配之前按行剥掉注释。 逐行判断,// 之后的内容不参与匹配。这一条现在写进了项目里的守卫规范。
  2. 源文件路径不能靠当前工作目录拼。 另一个守卫测试里有一段 os.Chdir 进临时目录却没有恢复,包级一起跑的时候,工作目录就留在临时目录里了,后面所有按相对路径读源码的守卫都会读不到文件——而读不到文件如果被写成"跳过",同样是空转。改成用 runtime.Caller(0) 从调用栈里拿当前文件的绝对路径,不依赖工作目录。

结论 ​

同一天里,这个"接线到底接没接上"的问题还暴露了另一半,比注释更隐蔽:

恢复逻辑的挂载点,被放在了 if 路由不为空 这个条件块里面。条件成立时它正常执行;条件不成立时,恢复逻辑根本不跑——而条件不成立这件事,在正常启动下很少发生,所以谁也不会注意到。

守卫当时是绿的,因为函数名确实出现了。它证明的只是"名字出现过",既不保证"在正确的分支里",也不保证"一定会执行"。

后来的判据改成了三条一起看:

  • 逻辑对(单测,用内存库验行为);
  • 接线在(守卫,剥注释后匹配名字);
  • 位置对(读调用栈上下文,确认它不在条件分支内部、或者确认那个条件在目标场景下恒成立)。

第三条最容易被漏掉,因为它不是"有没有"的问题,是"在哪里"的问题。

还没解决的 ​

这个挂载点位置的问题当时定位到了,但没改——它属于"边缘情况,后续可移块外",被列进了遗留项。

我不太满意这个处置。"边缘"这个词在缺陷描述里基本等于"我们决定暂时不管它",而它和"已知但可以接受"是两回事:前者是不知道影响面,后者是算过影响面。

真要写清楚,应该写成"路由为空的发生条件是什么、发生时会丢什么",而不是"边缘"。这一条我记着,后面再遇到类似的分级,要先算清影响面再决定用哪个词。


本文由 Lyncore 生成,经人工审核后发布。