原本十多分鐘就能完成的 XCTest 工作,在雲端 Mac 上執行四十分鐘後仍未結束。日誌最後一行只顯示某個測試案例開始執行,CPU 使用率不高,結果套件也尚未完成。此時若直接停止工作,只會得到「逾時」結果,無法判斷究竟是主執行緒互鎖、非同步回呼未返回、模擬器服務失聯,還是測試程序仍在等待一個永遠不會成立的條件。
處理這類問題的關鍵不是增加重試次數,而是在終止前保全現場。程序關係、連續的執行緒取樣、模擬器狀態、原始日誌與未完成的 xcresult,都應歸入同一次執行記錄。
先區分緩慢測試與真正卡死
只設定一個總逾時上限很容易誤判。首次編譯、模擬器啟動與測試執行的耗時來源各不相同,因此應先拆開處理:
- 使用
build-for-testing完成編譯。 - 使用
test-without-building執行測試。 - 分別記錄建置時間、測試時間,以及日誌最後一次變更的時間。
- 測試超過歷史上限時先採集現場,而不是立即強制結束。
全域上限可防止工作無限執行,靜默上限則用來發現「程序仍在,但沒有進度」的狀態。靜默不能直接等同於標準輸出為空:有些長時間測試原本就很少輸出內容。更可靠的判斷方式,是同時觀察日誌檔案的修改時間、測試程序是否仍存活,以及 xcresult 目錄是否持續變化。
逾時是一種保護機制,不是根本原因。只有在逾時發生時留下足夠證據,它才具有診斷價值。
固定可重現的執行入口
先固定 scheme、測試計畫、目標裝置與 DerivedData 路徑。不要讓診斷工作順便更換 Xcode、模擬器執行階段或相依套件版本,否則兩次現場將無法比較。
set -euo pipefail
WORKSPACE="Demo.xcworkspace"
SCHEME="Demo"
PLAN="Regression"
DERIVED="$PWD/.build/DerivedData"
RESULT="$PWD/.artifacts/TestRun.xcresult"
rm -rf "$RESULT"
mkdir -p "$PWD/.artifacts"
xcodebuild build-for-testing \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-testPlan "$PLAN" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-derivedDataPath "$DERIVED"
xcodebuild test-without-building \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-testPlan "$PLAN" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-derivedDataPath "$DERIVED" \
-resultBundlePath "$RESULT"
如果問題只出現在單一測試類別,應先使用 -only-testing:TargetName/TestClass 縮小範圍。不要一開始就平行重跑整個測試計畫,否則會增加程序與模擬器造成的雜訊。
為測試程序加入可保全證據的監控程序
macOS 預設沒有一套能在所有環境統一使用的 GNU timeout 指令,CI 指令稿可以改用背景監控程序實作。正確順序是:啟動測試、等待時間上限、採集現場、傳送 TERM、保留清理時間,最後才傳送 KILL。
set -euo pipefail
OUT="$PWD/.artifacts/hang-$(date +%Y%m%d-%H%M%S)"
LIMIT="${TEST_TIMEOUT_SECONDS:-1200}"
mkdir -p "$OUT"
xcodebuild test-without-building \
-workspace Demo.xcworkspace \
-scheme Demo \
-testPlan Regression \
-destination "platform=iOS Simulator,name=iPhone 16" \
-derivedDataPath "$PWD/.build/DerivedData" \
-resultBundlePath "$OUT/TestRun.xcresult" \
>"$OUT/xcodebuild.log" 2>&1 &
test_pid=$!
(
sleep "$LIMIT"
if kill -0 "$test_pid" 2>/dev/null; then
date -u >"$OUT/timeout-at.txt"
ps -axo pid,ppid,state,%cpu,%mem,etime,command >"$OUT/host-processes.txt"
xcrun simctl list devices >"$OUT/simulators.txt"
xcrun simctl spawn booted ps -axo pid,ppid,state,etime,command \
>"$OUT/simulator-processes.txt" 2>&1 || true
for round in 1 2 3; do
xcrun simctl spawn booted sample DemoTests-Runner 5 1 \
>"$OUT/test-runner-sample-$round.txt" 2>&1 || true
sleep 3
done
kill -TERM "$test_pid" 2>/dev/null || true
sleep 10
kill -KILL "$test_pid" 2>/dev/null || true
fi
) &
watchdog_pid=$!
set +e
wait "$test_pid"
status=$?
set -e
kill "$watchdog_pid" 2>/dev/null || true
wait "$watchdog_pid" 2>/dev/null || true
exit "$status"
測試 Runner 名稱必須依專案產物調整。若名稱不固定,可從模擬器程序清單中依測試套件路徑篩選 PID,再交給 sample。相較於一次長時間取樣,三次短時間取樣更容易判斷呼叫堆疊是否真的停止推進。
從執行緒堆疊判斷等待類型
先檢查三份取樣中的主執行緒與測試執行緒,是否都停留在相同位置。常見模式包括:
主執行緒等待測試執行緒
主執行緒停在號誌、條件變數或同步派送,而測試執行緒又在等待必須由主執行緒執行的回呼,通常表示同步等待非同步 API 導致互鎖。修正方向是改用 XCTest expectation,並確保等待動作不會占住負責執行回呼的佇列。
測試執行緒等待外部條件
如果呼叫堆疊持續停在 socket、檔案協調、資料庫鎖定或程序間通訊上,應進一步核對相依服務、暫存目錄權限,以及先前測試留下的鎖定。不要把所有等待都歸因於網路;應先利用程序與檔案狀態證明實際等待的物件是什麼。
呼叫堆疊仍在變化
如果三次取樣的熱點持續變化,工作可能只是執行緩慢。此時應記錄單一測試案例的耗時、縮小資料規模,或為測試本身設定合理的 expectation 逾時,而不是無限制提高全域上限。
將 xcresult 與系統狀態一併封存
即使結果套件未正常收尾,也應原樣保留,不要在失敗分支中先行刪除。完整結果可使用以下指令匯出為便於搜尋的 JSON:
xcrun xcresulttool get \
--legacy \
--path ".artifacts/TestRun.xcresult" \
--format json \
>".artifacts/xcresult.json"
每次逾時的目錄至少應包含:
- 完整的
xcodebuild輸出與結束碼; - scheme、測試計畫、destination 與 Xcode 版本;
- 主機與模擬器的程序清單;
- 間隔採集的三份執行緒樣本;
- 模擬器裝置狀態;
- 原始 xcresult 與匯出的結構化結果;
- 本次提交識別碼與測試入口參數。
在 PingMini 的雲端 Mac 上執行時,也應讓每次工作只使用一個明確指定的模擬器裝置集,避免其他平行工作改變裝置狀態。至於配置與節點選擇,應在控制台確認目前可選項目,不要把機器差異混入同一輪故障重現。
將修正轉化為迴歸檢查
找出根本原因後,不要只是移除監控程序。應保留逾時採集機制,但將門檻改為根據歷史分位數設定的時間預算;同時為已修正的情境加入最小迴歸測試。例如,互鎖問題應驗證回呼能在限定時間內完成;資源鎖定問題則應驗證測試結束後,鎖定檔案與子程序都已清理。
最後檢查三件事:正常工作不會觸發取樣;異常工作能在有限時間內結束;封存內容足以回答「哪個程序正在等待什麼」。做到這一步,偶發的 XCTest 卡死就不再只是一筆只能手動重跑的紅色記錄,而會成為能持續收斂的工程問題。
常見問題
XCTest 多久沒有輸出才應判定為卡死?
不要只依總時間判斷。應同時設定依歷史正常時間推算的全域上限,以及較短的無進度上限;程序仍存活且日誌與測試狀態都沒有推進時才開始取樣。
為什麼逾時後不能立刻強制終止?
立刻強制終止會遺失執行緒狀態,也可能破壞結果檔收尾。應先保存程序與堆疊,再送出 TERM,等待短暫清理期後才以 KILL 作最後手段。
程序取樣中應優先查看哪些位置?
先檢查主執行緒、測試執行緒、資料庫佇列與自訂序列佇列,尋找鎖定循環、IPC 等待與同步呼叫;多次取樣都停在相同堆疊時最具判斷價值。
準備下一次建置所需的獨享物理 Mac
從三種固定配置中選擇適合開發、持續整合或本地模型測試的雲端 Mac,並依照任務週期完成設定。