雲端 Mac CI 錄製 iOS UI 測試失敗現場

CI/CD 實踐 ·約 7 分鐘閱讀

雲端 Mac CI 錄製 iOS UI 測試失敗現場

當 UI 測試在雲端 Mac 上失敗時,只看斷言與截圖往往不夠。彈窗可能出現後立刻消失、頁面轉場可能停在半途,鍵盤也可能遮住按鈕;只有連續畫面才能釐清這些事件發生的先後順序。可靠的做法並非永久保留所有錄影,而是在測試開始前錄製指定的模擬器,測試成功就刪除影片,只有失敗時才將影片、日誌與結果套件一併封存。

固定測試裝置與取證範圍

不要直接將 booted 傳給錄影指令。並行工作、上一次異常結束後遺留的裝置,或人工偵錯,都可能使多部模擬器同時處於啟動狀態,導致錄到錯誤的裝置。應先建立或找出本次工作專用的裝置,取得其 UDID,再將它傳給啟動、測試、錄影與清理步驟。

DEVICE_NAME="CI-iPhone"
RUNTIME="com.apple.CoreSimulator.SimRuntime.iOS-18-0"
DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16"

UDID="$(xcrun simctl list devices available -j |
  plutil -extract devices json -o - - |
  jq -r --arg name "$DEVICE_NAME" '
    .. | objects | select(.name? == $name) | .udid
  ' | head -n 1)"

if [ -z "$UDID" ]; then
  UDID="$(xcrun simctl create "$DEVICE_NAME" "$DEVICE_TYPE" "$RUNTIME")"
fi

xcrun simctl boot "$UDID" 2>/dev/null || true
xcrun simctl bootstatus "$UDID" -b

執行階段識別碼會隨已安裝的 Xcode 版本而變化。流水線應在執行前透過 xcrun simctl list runtimes 驗證,而不是假設每部機器都具備相同版本。多個工作並行執行時,裝置名稱也應包含工作編號,以免共用狀態。

錄影的目的是補足失敗時間線,而不是取代日誌。影片回答「介面先發生了什麼」,日誌與 xcresult 則回答「測試框架記錄了什麼」。

讓錄影程序可靠收尾

simctl io recordVideo 會持續執行,因此指令稿必須保存其 PID。測試結束後不要立即傳送強制終止訊號;應先傳送 INT,讓工具完成影片容器索引的寫入,再等待程序結束。

ARTIFACTS="${PWD}/artifacts"
VIDEO="${ARTIFACTS}/ui-test.mp4"
RESULT="${ARTIFACTS}/UITests.xcresult"
LOG="${ARTIFACTS}/xcodebuild.log"

mkdir -p "$ARTIFACTS"
rm -rf "$RESULT"
rm -f "$VIDEO" "$LOG"

xcrun simctl io "$UDID" recordVideo \
  --codec=h264 \
  --force "$VIDEO" &
RECORDER_PID=$!

finish_recording() {
  if kill -0 "$RECORDER_PID" 2>/dev/null; then
    kill -INT "$RECORDER_PID"
    wait "$RECORDER_PID" || true
  fi
}

trap finish_recording EXIT INT TERM

H.264 通常方便在瀏覽器及工單附件中預覽。若團隊之後需要逐格分析,可在確認產物系統相容後改用其他編碼;但不要在同一條流水線中混用格式,否則檢視工具與保存策略將變得難以預測。

保留真實的測試結束碼

錄影是旁路程序,不能掩蓋 xcodebuild 的結果。最穩妥的方式是暫時關閉遇錯即退出,另外保存測試結束碼,再正常結束錄影。若還要透過 tee 顯示日誌,必須讀取管線中 xcodebuild 的結束狀態,不能誤用 tee 回傳的成功狀態。

set +e
xcodebuild test \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination "platform=iOS Simulator,id=${UDID}" \
  -resultBundlePath "$RESULT" \
  >"$LOG" 2>&1
TEST_STATUS=$?
set -e

finish_recording
trap - EXIT INT TERM

if [ "$TEST_STATUS" -eq 0 ]; then
  rm -f "$VIDEO"
  exit 0
fi

exit "$TEST_STATUS"

這裡刻意不在測試失敗後立即退出,因為錄影仍需完成收尾。resultBundlePath 應使用每次工作各自獨立的路徑,並在執行前刪除舊目錄,否則先前的結果可能與本次失敗混在一起。

驗證影片,而不只是確認檔案存在

零位元組檔案、只有容器標頭的短檔案,以及遭到強制中斷的錄影,都沒有診斷價值。封存前至少應檢查檔案大小,並使用系統媒體工具讀取片長。測試剛開始就失敗時,影片很短是合理情況,因此片長門檻不宜設得過高。

檢查項目 建議動作 失敗時的處理方式
檔案存在 test -f 保留日誌並標記錄影缺失
檔案非空 test -s 檢查錄影 PID 與磁碟空間
可讀取片長 查詢媒體中繼資料 視為損毀檔案,不上傳空殼
裝置一致 比對記錄的 UDID 終止工作並清理錯誤裝置
測試結果套件 檢查 xcresult 目錄 保留原始日誌作為備援

可以將 UDID、測試開始時間與錄影路徑寫入純文字清單,並與影片一同封存。不要將存取憑證、完整的環境變數集合,或未經遮蔽的命令列寫入清單。影片本身也可能出現通知、測試帳號名稱或業務資料,因此測試夾具應使用專用且不敏感的樣本。

處理並行、逾時與殘留程序

每個工作都應擁有獨立的模擬器、結果目錄與錄影檔名。共用 booted、固定的 artifacts/ui-test.mp4,或公用的 DerivedData 目錄,都會讓並行工作彼此覆寫。工作層級的目錄可使用流水線提供的唯一編號產生,並在退出時刪除成功工作的暫存檔案。

外層逾時器終止測試時,仍必須讓清理函式有機會執行。先向測試程序傳送可處理的終止訊號,預留數秒讓 trap 完成錄影收尾,最後才進行強制清理。下一次工作開始前,可以檢查是否存在父程序已結束的 simctl io 錄影程序,但不要使用範圍過大的 killall,否則可能誤傷同一部機器上的其他工作。

穩定的封存集合通常只有三項:失敗影片、xcresult 結果套件與原始建置日誌。三者應使用同一個工作編號命名,保存週期也應一致。如此一來,從失敗清單進入某次工作後,可以先觀看影片確認介面狀態,再從結果套件定位斷言與附件,最後使用日誌核對指令、目標裝置與結束碼,形成完整但不過量的取證鏈。

常見問題

為什麼不應直接對 booted 模擬器錄影?

平行工作可能同時啟動多部模擬器,booted 無法保證指向本次測試裝置。應取得明確 UDID,並讓測試、錄影與清理共用它。

為什麼錄影結束後的影片可能無法播放?

強制終止程序可能使影片容器來不及寫入索引。應傳送 INT 訊號並等待錄影程序結束,再驗證檔案。

測試成功時也要保留錄影嗎?

通常不需要。成功時刪除影片,失敗時才封存影片、日誌與 xcresult,可降低儲存與上傳成本。

Debug M4

在獨享 Apple Silicon 實體節點上執行下一次建置

選擇租用期限與節點區域,確認 Mac Mini M4、16GB RAM、256GB SSD 及應付 USD 金額後提交訂單。

立即租用雲端 Mac