DEBUG.WIKI
PanstarCloud:部署应用,接入 AI,云服务器与 AI 网关
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →

pytest 日志捕获报 dictionary changed size,检查线程注册 logger

pytest 9.1.1 捕获日志时遍历实时 logger 注册表,后台线程新增 logger 可能使字典迭代失败。先收敛日志初始化时机;临时关闭 logging 插件会同时失去 caplog。

测试还没进入业务断言,就在 _pytest/logging.py 的 catching_logs.__enter__() 报:

RuntimeError: dictionary changed size during iteration

如果栈里紧接着是遍历 root_logger.manager.loggerDict.values(),同时程序有线程持续创建新的 logger,要检查 pytest 的日志捕获建立过程。pytest 9.1.1 正式代码会遍历这份实时注册表,寻找不会向根 logger 传播的 logger 并挂上捕获处理器;另一个线程此时调用 logging.getLogger(),就可能改变字典大小。

这里并不是 caplog 的日志级别过滤错误。改成捕获 DEBUG、关闭终端实时日志,或者降低业务日志数量,都不保证停止向注册表添加新的名字。

将 logger 创建与捕获阶段分开

应用可以控制日志初始化时,尽量在后台线程开始工作前建立固定名称的 logger,工作循环复用已有对象。例如模块级保存 logger = logging.getLogger(__name__),而不是每次任务、每个请求都用新的名称创建一个 logger。

这需要覆盖真实的第三方组件与线程入口。只在测试模块预先创建一个同名 logger,无法阻止另一个库创建全新名称;如果线程一直在捕获建立和拆除期间存活,也不能靠调整某一个 fixture 的作用域保证没有竞争。应按原来的资源所有权停止并等待自己创建的线程,不要让前一个测试留下的线程继续注册日志对象。

不能为了躲开迭代错误就清空 loggerDict。它是全进程日志注册状态,清空后已有 logger 引用与新查找结果可能分离,其他库的配置也会受到影响。直接调用 logging 的私有锁来包住整个 pytest 会话同样不合适,后台线程可能因此无法完成工作。

只为定位时,可临时禁用内置 logging 插件

确实需要确认故障是否来自这条捕获路径时,可以在独立运行中禁用该插件:

python -m pytest -p no:logging

这会失去 pytest 内置的日志捕获、相关日志配置以及 caplog fixture。依赖 caplog 的测试会出现新的 fixture 错误,其他测试也可能不再执行原有的日志断言,所以这只能是范围明确的诊断或临时取舍,不能把少跑了日志检查的绿色结果当成完整验收。

该问题有针对注册表快照的修补建议,但 pytest 9.1.1 的正式代码仍使用实时视图。不要把后续开发分支上的建议写成正式版本已经修复,也不建议在测试配置里大范围复制和替换私有捕获类。

恢复正常运行后,应使用原来的 logging 插件与原线程流程复查,确认捕获建立、测试执行和清理都不再因新 logger 名称竞争失败,同时日志内容与 caplog 断言仍然生效。若完整栈指向业务自己的字典迭代,或根本没有经过这处捕获代码,需要继续按那个字典的写入者排查。