news 2026/9/26 3:14:07

P0-P3 落地实录(三):从测试隔离到并发竞态,1109 个测试全绿以后我又发现了什么?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
P0-P3 落地实录(三):从测试隔离到并发竞态,1109 个测试全绿以后我又发现了什么?

AutoPilot Test V1.1 工程化治理系列

前两篇分别写了数据库迁移问题和执行状态问题。

到了这一篇,我开始问一个更绕的问题:

如果 AutoPilot 已经有 1109 个通过的测试,为什么我还是不敢直接说“它已经没问题了”?

答案其实很简单:

测试体系本身,也可能存在 Bug。

测试可以通过,但测试之间可能互相污染;Mock 可以全绿,但真实浏览器未必真的能跑;并发场景如果没有专门验证,生产代码一样可能存在竞态。

这一篇,就是我在 V1.1 收口阶段做的最后一轮测试工程化排查。


一、1109 passed,为什么我没有立刻庆祝?

AutoPilot 的后端测试按几个层次组织:

unit services routers integration

项目里的测试大量使用 SQLite,并通过 Mock 隔离 LLM、Playwright、Appium、文件系统等外部依赖。

在这一轮 V1.1 收口的完整回归记录里,结果是:

1109 passed 2 skipped

同时还有:

compileall:OK Real Chromium + Mock LLM 黄金路径:通过

数字确实很好看。

但我没有把它理解成:

“1109 个测试通过,所以系统没有 Bug。”

我更愿意把它理解成:

1109 个已验证的断言通过。

这两句话差别很大。


二、第一个问题:为什么测试单独跑 PASS,组合跑却可能 FAIL?

这是测试工程里很危险的一类问题。

理想状态应该是:

Test A ↓ 不会改变 ↓ Test B 看到的环境

但在 AutoPilot 的测试体系里,我碰到了一个很典型的场景。

部分模型测试会执行:

PRAGMA foreign_keys=ON

而测试数据库使用共享连接模型。

测试结束以后虽然做了:

rollback

但PRAGMA属于连接级状态,并不会因为事务回滚自动恢复。

于是可能出现:

Test A ↓ 打开 foreign_keys ↓ rollback ↓ 连接级 PRAGMA 还在 ↓ Test B ↓ 外键行为发生变化 ↓ FAIL

这里最容易出现的误区是:

“都 rollback 了,怎么还会污染?”

答案就是:

事务状态和连接级状态不是一回事。


三、最终怎么处理?让公共 Fixture 对环境负责

我没有让每个测试自己记得写:

PRAGMA foreign_keys=OFF

而是把清理动作放进公共db_sessionfixture 的 teardown。

最终逻辑是:

测试开始 ↓ 创建连接 / Session ↓ 执行测试 ↓ rollback ↓ 恢复连接级状态 ↓ 关闭连接

测试 Fixture 中最终有类似:

try:connection.execute(text("PRAGMA foreign_keys = OFF"))exceptException:pass

这个改动看起来很小,但它带来的工程原则很重要:

测试可以修改共享环境,但必须负责把环境还原。


四、第二个测试隔离问题:数据库迁移居然还能影响日志环境

这次排查里我特别喜欢的一点,是它让我意识到:

测试隔离绝对不只是数据库回滚。

项目的 Alembic 环境会使用:

fileConfig(...)

而 migration 在 pytest 进程里运行时,可能修改当前进程里的 logging 状态。

于是可能出现:

Test A ↓ 执行 Alembic ↓ logger 配置被修改 ↓ Test B ↓ pytest 的日志捕获行为发生变化

本来只是一个数据库迁移测试,最后却影响了后面所有测试看到的日志环境。


五、这个问题最后怎么处理?

我用了两层防线。

第一层,在 Alembic 环境里显式设置:

fileConfig(config.config_file_name,disable_existing_loggers=False,)

它的意义是避免 migration 配置把既有 logger 简单粗暴地全部禁用。

第二层,是在相关测试里做 logging 状态快照和恢复,包括:

root logger logger level handlers disabled propagate

这样即使测试过程改变了 logging 状态,测试结束以后也会恢复现场。

这个问题最后给我的结论非常简单:

只要测试改了环境,就应该由测试负责恢复环境。


六、第三个问题:Mock 测试全部正常,真实 Chromium 却炸了

前一轮我已经遇到过一次这个问题,所以这次把它正式纳入“真实黄金路径”验证。

AutoPilot 的SafePlaywright会限制 AI 生成代码访问底层 Playwright 对象。

设计目标是:

AI 代码 ↓ 白名单 API ↓ 不能直接拿到底层 page ↓ 不能通过私有属性轻易绕过限制

问题在于,Playwright 自己在真实 API 调用时,也会进行调用栈分析。

实际内部会读取:

frame.f_locals["self"].__class__.__name__

而原来的SafePlaywright为了安全,把__class__也拦截了。

于是:

Mock ↓ 全部通过 Real Chromium ↓ Playwright 真实内部逻辑 ↓ 访问 __class__ ↓ 被 SafePlaywright 拦截 ↓ 真实执行失败

这就是为什么我越来越不愿意把 Mock 全绿直接当成真实环境全绿。


七、最终怎么修?安全边界不能以破坏底层框架为代价

这次没有直接把:

__class__

全部放开。

而是提供了一个极小能力的 class proxy:

_SAFE_CLASS_PROXY=type("SafePlaywright",(),{"__name__":"SafePlaywright"},)

当运行时请求__class__时,只返回这个受限对象。

也就是说:

Playwright ↓ 能拿到自己需要的 __name__

但:

AI 代码 ↓ safe.__class__ ↓ 仍然被 AST Validator 拦截

最后形成:

运行时兼容性 + AI 代码静态限制

两层防线。


八、第四个问题:报告生成还有并发竞态

前面的测试更多关注“正确性”和“隔离”。

继续往下看,我又发现报告生成其实也可能发生竞态。

AutoPilot 的 Report 既可能被编排器后台流程触发,也可能被人工接口触发。

如果两个调用同时对同一个execution_id执行:

查询报告是否存在 ↓ 发现不存在 ↓ 生成 HTML ↓ INSERT

两个线程都可能判断:

不存在

然后一起 INSERT。

最终就可能撞上数据库唯一约束。

这种 Bug 单线程测试很难稳定复现。

所以在这轮治理里增加了进程内互斥:

_REPORT_LOCK=threading.Lock()

生成入口使用:

with_REPORT_LOCK:returnself._generate(execution_id)

并补了并发测试,用来确认同一个进程内不会同时进入这段关键区。


九、但这里还有一个重要边界:进程内锁不是分布式锁

threading.Lock()解决的是:

一个 Python 进程 + 多个线程

它不能天然解决:

Worker 1 Worker 2

或者:

多个进程 多个容器 多副本部署

之间的竞态。

所以当前方案更准确的表达应该是:

V1.1 通过进程内互斥控制当前进程内的并发,同时依赖数据库唯一约束作为最终边界。

如果未来扩展到多副本,再考虑:

幂等写入 + 数据库唯一约束 + execution 粒度锁 / 分布式锁

这个边界必须说清楚。


十、第五个问题:ORM 和数据库 Schema 也可能“各说各话”

还有一种问题特别容易被忽略:

代码里的 Model、初始化 Schema 和未来 Migration,不一定天然一致。

如果:

schema.sql ≠ ORM Model ≠ Alembic 预期结构

那么短期业务可能完全不报错。

但未来一旦开始做 schema diff,就会不断出现:

“这个索引为什么又想创建一次?”

所以这轮也把已经存在于数据库结构中的部分索引补回 ORM 定义,并用 autogenerate 检查结构差异。

这类问题看起来不性感,但对长期维护非常重要。

因为:

数据库结构最怕的不是复杂,而是多个地方各自维护一份“差不多”的真相。


十一、到了最后,我才真正开始做“风险登记”

做到后面,我开始意识到一个比 Bug 更危险的问题:

我很容易忘记,到底哪些东西真的验证过,哪些东西只是“理论上应该可以”。

所以最后不再追求一句:

“所有问题都解决了。”

而是明确区分:

已修复 已覆盖 环境缺失 / 未验证 已接受 / 设计边界

例如真实环境类项目,当前仍应该明确区分:

Docker 实机构建与联调 Real LLM 全链路 MySQL 实机迁移 前端生产 build 多副本并发

能验证的就验证。

没有环境的就明确写“未验证”。

已经知道但当前版本暂时不解决的,就记成“设计边界”。

这比一句:

“生产级。”

更有价值。


十二、1109 个测试全绿,到底证明了什么?

这一轮 V1.1 收口时记录的完整回归结果是:

1109 passed 2 skipped

同时:

Real Chromium + Mock LLM 黄金路径:通过 compileall:OK

这可以证明:

当前验证范围内的大量断言通过 真实 Chromium 黄金路径跑通

但它不能直接推出:

系统已经没有 Bug

更不能直接推出:

已经全面生产级

因为仍然存在明确的验证边界。

所以我更愿意把测试结果理解成:

一组证据。

而不是:

一个结论。


十三、这轮收口以后,我对“测试开发”的理解又变了一点

以前我更容易关注:

功能有没有通过? 覆盖率多少? 测试数量多少?

现在我会继续往后追:

测试会不会污染测试? Mock 通过以后真实环境呢? 并发情况下还成立吗? 状态是不是只有一个解释? 数据库模型和实际 schema 一致吗? 失败以后能不能恢复? 没有验证的风险有没有被记录?

到了这里,我觉得测试开发真正做的已经不只是“写测试”。

而是在做:

建立一个能够被验证、被追溯、被解释的工程系统。


十四、结尾:1109 个测试全绿,只是一个起点

如果要把 AutoPilot V1.1 这一轮工程治理浓缩成一句话,我现在会这样说:

不是把 Bug 藏起来,而是把问题、边界和证据都摆到台面上。

一个真正可靠的系统,不应该只是:

“我觉得它应该没问题。”

而应该能够回答:

这里已经验证 这里已经修复 这里有自动化测试 这里有真实环境验证 这里因为环境限制暂时没验证 这里是当前版本接受的设计边界

当一个系统开始能清楚回答这些问题时,我觉得它才真正开始具备工程系统的样子。

而 AutoPilot 的 V1.1,只是这条路上的一个阶段。


系列文章

上一篇:《P0-P3 落地实录(二):进度 100% 却还在自愈,AI 测试平台的数据为什么会“自己打自己脸”?》

本文:《P0-P3 落地实录(三):从测试隔离到并发竞态,1109 个测试全绿以后我又发现了什么?》

下一篇:《我把一个 AI 编程 Prompt 推翻了很多轮,最后才明白:Prompt 不是让 AI 写代码,而是防止它绕过架构》

项目地址

GitHub:https://github.com/ZipUp-dot/AutoPilot-Test

Gitee:https://gitee.com/Mr-6Lawrence/auto-pilot-test

关于作者

我是 EthanPeng,正在做 AutoPilot。

我一直比较相信一件事:

工具应该适应人,而不是人适应工具。

AutoPilot 想做的事情也很简单:让测试人员不需要把大量时间花在重复编写、维护自动化脚本上,而是把更多精力放到测试设计、风险识别、质量保障和 AI 代码解释上。

而这次测试工程化的过程也让我越来越确定:

AI 可以帮你生成代码,但真正决定一个工具能不能进入真实工程环境的,永远是代码之外的那一整套工程能力。

欢迎交流。


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 3:13:09

面向内容安全的多标签文本分类实战解析 antiporn infopoisk

这道 Kaggle 竞赛聚焦文本内容安全,目标是识别色情相关信息,本质上属于多标签文本分类在审核场景中的一次小型实战演练。题目规模不大,但任务链路完整,能够把数据读取、标签理解、特征构造、模型训练和指标优化串成一条清晰的工程路径。 更重要的价值在于,这类题目与真实…

作者头像 李华
网站建设 2026/9/26 3:11:55

Clone-Git Add-Git Commit

找了一个使用 Python 编写的车辆动力学仿真项目,可以根据油门、制动、转向等输入,计算车辆动力系统、轮胎以及车身状态。用它来:找到一个现有项目中的问题,分析原因,修改代码,验证结果,然后使用…

作者头像 李华
网站建设 2026/9/26 3:10:44

生产级智能体平台实战:任务编排、工具管理与运行监控

1. 从单机脚本到生产级智能体平台:为什么“能跑”和“敢用”之间隔着一整个工程体系很多人第一次接触 Agent,都是从一段几十行的脚本开始的:调一个模型接口,塞几个工具函数,跑通一个“查天气发邮件”的流程&#xff0c…

作者头像 李华
网站建设 2026/9/26 3:09:20

Linux系统:IPC进程间的通信--共享内存

一 共享内存本质上是内存预留的一块空间,就是一块物理空间,进程间的复制是虚拟的空间。内核物理内存,映射到多个不同进程的虚拟地址空间。多个进程可以直接读写这块内存,实现进程间的通信。两个进程通过唯一的key对应一个IPC对象/…

作者头像 李华