随着团队规模扩大、测试数量超过5000个(大多为功能/集成风格),Ramp的Python单体测试套件耗时正逐渐逼近20分钟。基础设施团队没有重写测试套件,而是直接针对运行时下手:通过修复一个将并行工作进程限制在四个的数据库设置瓶颈来提高CI并发度,停止在本地运行之间从头拆除并重建测试数据库,并构建一个只执行与特定代码变更相关测试的增量测试运行器。

增量运行器的第一版使用coverage.py来为每个测试映射其执行了哪些代码行,然后在新提交时将变更文件与该映射进行差异比对。这一方案有效,但存在一个特定缺口:Ramp的代码库使用Flask-SQLAlchemy模型类,而覆盖率测量几乎将所有执行都归因于SQLAlchemy的内部实现,而非模型类声明本身。在纯行覆盖率下,向模型添加一列不会被记录为“该类已变更”,因此增量运行器可能跳过本应捕获模式变更缺陷的测试。

为解决这一问题,团队用Python内置的sys.settrace替换了coverage.py,检查`call`事件中的`self`或`cls`参数,将该对象解析为其类,并沿`__bases__`进行广度优先遍历,将每个祖先类映射到其定义所在的文件。这使得快照不仅记录测试执行了哪些行,还记录其完整继承链中依赖了哪些类——因此对基类声明的变更会正确地使所有子类的测试失效,即使没有直接执行任何子类代码路径。

结合并发修复和跳过完整数据库重建,增量运行器在典型提交上覆盖了测试套件的30-40%,并捕获了完整运行所能捕获的约90%的失败,使工程师能在几分钟内而非完整的10-20分钟内获得可操作的信号。

在运行时追踪类继承关系,使得未被执行的基类编辑仍能触发其测试

Coverage.py记录的是已执行的行,而非这些行所属的类,因此SQLAlchemy ORM内部实现中的执行不会被归因到用户的模型类。

sys.settrace直接暴露调用帧,因此检查`self`/`cls`可以恢复“这是哪个类”的信息,与行级执行发生的位置无关。

从该类沿`__bases__`遍历,将单个调用事件转化为完整的祖先列表,因此基类的变更会被归因到链中的每个文件。

将增量运行器与完整套件并行运行(而非替代它),意味着约10%的检测缺口从未成为生产风险。

基于覆盖率的依赖捷径可能遗漏真实依赖(如继承),因为覆盖率工具不会将其归因到变更的代码——在信任快速路径之前,请检查这一盲点。

Ramp判断增量运行器在此之后不值得进一步投入——弥合约10%的剩余检测缺口需要围绕类似Bazel的方案进行重构,而当时这并非最高优先级。仅并发修复(在移除每个工作进程冗余的数据库重置后将机器规模翻倍)就将CI时间减半,而在本地跳过完整数据库重建每次运行可节省10-30秒。团队将基于sys.settrace的继承遍历作为开源方案(如pytest-testmon,其缺乏Ramp的并行化支持)的替代方案公开发布。

从这里,追溯依据

参考来源

  1. Faster Python Tests Ramp Engineering (Builders Blog) · builders.ramp.com ·