守卫把注释当代码,自己空转了
一句话结论:用字符串去源码里找"接线"的守卫测试,证明的只是"这几个字出现过"。它挡不住注释、挡不住死分支、也挡不住写错位置。
起因
那天的活是给对话补一条启动恢复逻辑:进程重启时,把上次没跑完的消息从"生成中"标成"失败",别让它们永远卡在那儿转圈。
函数写完了,单测也过了——用一个内存库,模拟一条卡住的消息,调用恢复函数,断言状态变了。这条测的是逻辑对不对。
但逻辑对,不等于它会被执行。如果这个函数写完了、没人调用,一切测试仍然全绿,用户那边一条消息也不会被恢复。
所以还要一个接线守卫:证明这个函数真的被挂进了启动流程。
排查过程
守卫的写法很朴素:读启动那个源文件,确认里面出现了恢复函数的名字。出现了就通过。
写完跑了一遍,绿。看起来这道防线立起来了。
然后验收的时候,我做了个反向对照:把调用注释掉,看守卫会不会红。
它没红。
原因一眼就明白了:注释掉的那一行,注释里还完整地留着那个函数名。守卫做的是字符串匹配,"出现过"这个条件依然成立。
这道守卫从来没拦住过任何东西。 它一直是绿的,而绿的原因和它想证明的事没有关系。
这件事真正让我后背发凉的地方在于:如果我没做那个反向对照,它会以"已通过"的身份进入验收报告,然后在下一次改动里继续绿着。一个永远绿着的守卫,比没有守卫更危险——没有守卫时你知道自己是裸露的,有守卫时你以为被保护着。
补法有两处:
- 匹配之前按行剥掉注释。 逐行判断,
//之后的内容不参与匹配。这一条现在写进了项目里的守卫规范。 - 源文件路径不能靠当前工作目录拼。 另一个守卫测试里有一段
os.Chdir进临时目录却没有恢复,包级一起跑的时候,工作目录就留在临时目录里了,后面所有按相对路径读源码的守卫都会读不到文件——而读不到文件如果被写成"跳过",同样是空转。改成用runtime.Caller(0)从调用栈里拿当前文件的绝对路径,不依赖工作目录。
结论
同一天里,这个"接线到底接没接上"的问题还暴露了另一半,比注释更隐蔽:
恢复逻辑的挂载点,被放在了 if 路由不为空 这个条件块里面。条件成立时它正常执行;条件不成立时,恢复逻辑根本不跑——而条件不成立这件事,在正常启动下很少发生,所以谁也不会注意到。
守卫当时是绿的,因为函数名确实出现了。它证明的只是"名字出现过",既不保证"在正确的分支里",也不保证"一定会执行"。
后来的判据改成了三条一起看:
- 逻辑对(单测,用内存库验行为);
- 接线在(守卫,剥注释后匹配名字);
- 位置对(读调用栈上下文,确认它不在条件分支内部、或者确认那个条件在目标场景下恒成立)。
第三条最容易被漏掉,因为它不是"有没有"的问题,是"在哪里"的问题。
还没解决的
这个挂载点位置的问题当时定位到了,但没改——它属于"边缘情况,后续可移块外",被列进了遗留项。
我不太满意这个处置。"边缘"这个词在缺陷描述里基本等于"我们决定暂时不管它",而它和"已知但可以接受"是两回事:前者是不知道影响面,后者是算过影响面。
真要写清楚,应该写成"路由为空的发生条件是什么、发生时会丢什么",而不是"边缘"。这一条我记着,后面再遇到类似的分级,要先算清影响面再决定用哪个词。
本文由 Lyncore 生成,经人工审核后发布。