解決方案
隨著團隊規模擴大、測試數量超過 5,000 個(多為功能/整合型測試),Ramp 的 Python 單體式(monolith)測試套件耗時逐漸逼近 20 分鐘。基礎架構團隊沒有重寫測試套件,而是直接從執行階段著手:提高 CI 並行度,修復一個讓平行 worker 數量被限制在四個的資料庫設定瓶頸;停止在本機執行之間將測試資料庫整個拆除再重建;並打造一個增量測試執行器,只執行與特定程式碼變更相關的測試。
增量執行器的第一版使用 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 直接暴露呼叫框架(call frame),因此檢查 `self`/`cls` 便能還原「這是哪個類別」,與行層級執行發生在何處無關。
從該類別沿著 `__bases__` 往上遍歷,能把單一呼叫事件轉換成完整的祖先清單,因此基底類別的變更會被歸因於繼承鏈中的每一個檔案。
讓增量執行器與完整測試套件並行執行,而非取而代之,意味著那約 10% 的偵測缺口從未成為正式環境的風險。
可套用的經驗
以覆蓋率為基礎的依賴捷徑可能漏掉真正的依賴關係,例如繼承,因為覆蓋率工具不會把它歸因於變更的程式碼——在信任這條快速路徑之前,先檢查這個盲點。
後續發展
Ramp 判斷增量執行器到此為止不值得進一步投資——要補上剩餘約 10% 的偵測缺口,需要圍繞 Bazel 之類的工具進行重構,而這在當時並非最高優先事項。光是並行度修正(在移除每個 worker 多餘的資料庫重置後,將機器規模加倍)就讓 CI 時間減半,而在本機略過完整的資料庫重建,每次執行可省下 10-30 秒。團隊將以 sys.settrace 為基礎的繼承遍歷發布出來,作為 pytest-testmon 等開源選項的替代方案,因為後者缺乏 Ramp 所需的平行化支援。
追溯依據
參考來源
- Faster Python Tests Ramp Engineering (Builders Blog) · builders.ramp.com ·