工程指南

在雲端 Mac 用 git bisect 自動找出 Xcode 回歸

在雲端 Mac 用 git bisect 自動找出 Xcode 回歸

某個 UI 測試從上週開始穩定失敗,但近期已合併數十個提交。逐一切換提交、開啟 Xcode 再執行測試,不但耗時,也很容易忽略環境差異。更有效率的做法,是把「這個提交是否引入目標故障」寫成可重複執行的命令,再交給 git bisect 進行二分搜尋。若有 64 個候選提交,理論上只需要約 6 輪判定。真正的難點不在二分演算法,而在於如何確保每一輪的結果都可信。

先把回歸定義成機器可判斷的結果

開始前至少要準備兩個邊界:一個已確認正常的提交,以及一個已確認異常的提交。不要只用「頁面有問題」這類模糊描述,而應把回歸收斂成單一可觀察項目,例如某個 XCTest 斷言失敗、指定 Scheme 無法封存,或固定輸入產生錯誤輸出。

判定條件應同時固定以下變數:

  • Scheme、Configuration 與測試目標
  • Xcode 選取路徑與相依套件鎖定檔
  • 模擬器型號、系統版本、語言及時區
  • 測試資料、網路相依項目與執行帳號
  • 判斷正常與異常時採用的唯一證據

git bisect 會如實放大判定器的誤差。若把一次偶發失敗誤判為異常提交,即使後續搜尋順利完成,結論仍可能完全錯誤。

先在正常與異常提交上,分別手動執行兩次相同命令。若結果無法穩定重現,應先改善測試隔離,而不是立刻開始二分搜尋。

為每個候選提交隔離執行環境

在 G-Mini 的雲端 Mac 上執行時,建議使用專用工作副本,不要在開發者正在編輯的目錄中操作。二分過程會頻繁切換提交,未追蹤檔案、自動產生的設定與共用快取都可能污染判定結果。

隔離項目 建議做法 原因
Git 工作區 使用獨立 clone 或 worktree 避免覆寫日常修改
DerivedData 依提交雜湊分目錄 防止舊產物跨提交重複使用
模擬器 固定 UDID 避免 destination 自動選取發生漂移
結果套件 每輪分別儲存 方便複核第一個異常提交
相依套件 保留鎖定檔並停用隱式更新 避免版本解析結果改變

開始前執行 git status --porcelain,結果必須為空。模擬器應預先建立並啟動,再把 UDID 寫入環境變數。不要讓判定指令碼在執行時依名稱臨時選取「任何可用裝置」;同名裝置與系統升級都可能改變實際使用的目標。

編寫三態 Xcode 判定指令碼

git bisect run 不只接受成功與失敗兩種結果。結束碼 0 代表正常,1127 代表異常,而 125 則表示目前提交無法判斷,應予以跳過。對歷史較久的專案而言,第三種狀態尤其重要:舊提交可能無法搭配目前的相依套件完成編譯,但不代表它一定包含正在調查的回歸。

以下指令碼會先確認專案能否建置,再只執行目標測試。基礎建置失敗會被標記為無法判斷;只有測試明確失敗時,才會視為異常。

#!/bin/zsh
set -u

: "${DESTINATION_ID:?Set DESTINATION_ID first}"

sha="$(git rev-parse --short HEAD)"
root="${TMPDIR:-/tmp}/xcode-bisect"
derived="$root/derived-$sha"
result="$root/result-$sha.xcresult"
log="$root/test-$sha.log"

mkdir -p "$root"
rm -rf "$result"

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -destination "platform=iOS Simulator,id=$DESTINATION_ID" \
  -derivedDataPath "$derived" \
  build >"$root/build-$sha.log" 2>&1

if [[ $? -ne 0 ]]; then
  exit 125
fi

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -destination "platform=iOS Simulator,id=$DESTINATION_ID" \
  -derivedDataPath "$derived" \
  -resultBundlePath "$result" \
  -only-testing:AppTests/CheckoutReducerTests/testExpiredCart \
  test >"$log" 2>&1

status=$?

if [[ $status -eq 0 ]]; then
  exit 0
fi

if grep -q "TEST FAILED" "$log"; then
  exit 1
fi

exit 125

依故障類型調整分類方式

如果調查的是編譯回歸,建置失敗就應回傳 1,而不是 125。如果調查的是行為回歸,則相依套件下載失敗、模擬器服務異常、磁碟寫入失敗等基礎環境問題都必須跳過。不要把所有 xcodebuild 的非零結束碼一律解讀成目標回歸。

啟動二分搜尋並複核第一個異常提交

準備好指令碼後,先記錄目前分支與工作區狀態,再執行:

chmod +x ./scripts/bisect-xcode.sh
export DESTINATION_ID="固定的模拟器UDID"

git bisect start
git bisect bad BAD_COMMIT
git bisect good GOOD_COMMIT
git bisect run ./scripts/bisect-xcode.sh

搜尋結束後,Git 會指出第一個異常提交。此時不要立刻關閉問題,應分別在該提交及其父提交上手動執行指令碼,並檢查已儲存的建置記錄與 .xcresult。同時也要閱讀提交差異,確認變更內容確實能解釋觀察到的行為,而不是剛好觸發另一個失敗。

完成後執行 git bisect reset 回到原本的分支。如果二分期間產生大量 DerivedData,可在複核完成後統一清理。調查階段不要太早刪除結果套件,否則會失去判定過程的證據。

處理偶發測試與歷史斷層

偶發測試採用多數判定

如果單次執行不穩定,可讓判定器連續執行三次,只有同一個目標失敗至少出現兩次時才回傳 1。如果三次結果彼此矛盾,則回傳 125。這會增加執行時間,但仍比在錯誤提交上繼續調查更划算。每次重複執行前仍須重設測試資料,不能讓上一輪的狀態影響下一輪。

跳過過多提交時縮小邊界

大量 125 可能使 Git 無法找出唯一提交。常見原因包括專案格式、相依套件管理方式或測試名稱曾在歷史版本中變更。此時應將搜尋範圍移到遷移完成之後,或為舊目錄結構加入相容分支,但不要讓指令碼自動修改受測原始碼。

最終應保留正常提交、異常提交、第一個異常提交、判定指令碼版本、模擬器 UDID、Xcode 版本及結果套件路徑。如此一來,另一位工程師才能重新執行並驗證定位結果,也能在修正提交完成後,直接使用相同設定進行回歸驗證。

常見問題

git bisect 判定腳本應回傳哪些結束碼?

正常提交回傳 0,確認出現目標回歸時回傳 1,無法可靠判定時回傳 125。依賴下載或模擬器故障不應直接被標成異常提交。

偶發失敗的 Xcode 測試能直接用 git bisect 嗎?

應先固定模擬器、語系、時區與測試資料,並讓每個提交重複執行多次。只有失敗次數達到既定門檻時才判為異常,結果不明時回傳 125。

專屬實體節點

為下一個建置佇列準備雲端 Mac

比較兩種 M4 配置與五個節點,依實際工作週期選擇日租、週租、月租或季租。

選擇雲端 Mac 方案