大多数由Agent生成的补丁里,那些飘忽不定的测试并不神秘。它们在调用链的某个地方读了一次时钟,然后断言比较了两次从未在同一瞬间取得的读数。
把测试冻结,套件会变绿,同时删掉了你手上唯一的证据。所以,在冻结登记表多出一行之前,先审计时间源。顺序很重要:依赖时间的测试没法靠重跑来判断,而冻结后的测试根本没法再判断。
为什么时钟要排在冻结前面
重跑只回答一个问题:这个失败在当前物理环境下会复现吗?时钟审计回答的是更好的问题:是否存在一个测试从未控制过的输入?
Agent补丁特别容易踩这个坑。它们会加TTL检查、重试退避、缓存过期、"创建于最近N天内"的过滤条件、幂等窗口。每一项都需要时钟,而Agent会从环境里直接读,因为周围的代码本来就是这么干的。
下面这套审计有四道闸门。每道闸门要么产出一个失败的测试——这正是你想要的结果——要么产出一个能在评审里站得住脚的具名缺口。
闸门一:扫描新增行里的墙钟读取
扫描范围限定在diff,不是整个仓库。既有的时钟读取是别人的问题,你的补丁引入的时钟读取才是你的。
脚本用git diff拿到补丁新增的行,再用一组正则匹配常见的墙钟调用:JS的Date.now、new Date()、performance.now,Python的datetime.now、time.time,Go的time.Now,JVM的Instant.now、System.currentTimeMillis,以及setTimeout、sleep这类延迟调用。输出按类型统计总数,逐条列出文件、行号、类型和代码文本,有命中就以非零码退出。
读输出有两条规则。命中出现在生产代码里,说明这是一个你还没注入的依赖。命中出现在测试代码里,说明这是一个你还没搭好的夹具。延迟类型要单独看:测试里的sleep是一次忘了对自己诚实的时钟读取。
静态扫描会漏掉动态的时钟使用——数据库的now()、HTTP的Date头、文件系统的mtime。扫描干净只能当作"没有问题的证据",绝不能当作"没有问题"。
闸门二:把性质重述为时钟的函数
找到读取点之后,性质就不能再依赖它什么时候运行。把时间变成参数,然后扫它。
做法是造一个可推进的假时钟,把发放时间从时钟取,把过期判断也写成时钟的函数。然后写两条性质:一条验证TTL之前不会提前过期,一条钉住边界——推进到TTL减1毫秒仍未过期,再推进1毫秒必须过期。
两条性质对应两种不同的失败。第一条抓的是比较里的差一错误,第二条钉住边界,让后续重构没法悄悄翻转它。
闸门三与闸门四:矩阵与冻结登记
闸门三把时区乘以重复次数跑矩阵,这部分工作会和开发机上的其他任务抢资源。闸门四处理冻结:如果确实要冻结,登记表里必须有原因、时钟来源、负责人、开启日期、过期日期和复入条件。
用一小段脚本在CI里校验:必填键齐全,过期日期不超过开启后21天,过期日期不在过去,复入条件引用一个性质名,原因里提到flake时时钟字段不能为空。价值不在schema本身,而在于冻结从此有了负责人和日期,它会在diff评审里过期,而不是在某个人的记忆里过期。
决策表里六行有五行的结论是"不"。这个比例就是重点:冻结登记表是有纸面记录的最后手段,不是面对红灯的默认反应。
这套流程的边界
静态扫描是启发式的。它会漏掉藏在辅助函数后面的时钟读取,也会误报纯粹用于日志的performance.now。要为误报留预算。
注入时钟会改变定时器语义。如果你的代码依赖真实退避或真实调度,虚拟时钟可能让测试通过却从未走到定时器路径——要么单独测定时器接线,要么接受这条性质只覆盖算术部分。
如果你的补丁不碰时间,如果套件没有可挂扫描器的diff级CI,如果你说不出冻结的负责人是谁,那就整个跳过。最后一种情况下,登记表会变成测试被遗忘的地方,那比一个诚实的红色构建更糟。
这周只做一件事的话:拿扫描器跑一遍你上一个Agent补丁,数一数新增行里读墙钟的有多少。这个数就是你的时钟债。在冻结之前还,别在冻结之后还。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.