當 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,可降低儲存與上傳成本。
在獨享 Apple Silicon 實體節點上執行下一次建置
選擇租用期限與節點區域,確認 Mac Mini M4、16GB RAM、256GB SSD 及應付 USD 金額後提交訂單。