在雲端 Mac 診斷 XCUITest 啟動失敗
在同一台雲端 Mac 上,透過 SSH 執行 xcodebuild build 一切正常,切換到 test 後卻可能卡在「啟動測試執行器」之前:沒有任何測試案例開始執行、模擬器視窗也未出現,日誌中只留下應用程式啟動或工作階段連線失敗的訊息。此時先不要清除 DerivedData。既然建置流程已經通過,真正需要確認的是任務是否位於可用的 macOS 圖形工作階段中。
先區分建置環境與圖形工作階段
XCUITest 不只是編譯一個測試套件。它還必須啟動目標應用程式、測試宿主與模擬器服務,並透過目前使用者的圖形工作階段協調各個行程。SSH 登入成功只能證明遠端 Shell 可用,無法保證該 Shell 與桌面登入使用者位於同一個 launchd GUI 網域。
先記錄以下四項狀態,不要一開始就重新啟動所有行程:
| 檢查項目 | 可接受結果 | 異常線索 |
|---|---|---|
| 主控台使用者 | 實際執行自動化的使用者 | root 或 loginwindow |
| GUI 啟動網域 | 可查詢 gui/<uid> |
網域不存在或沒有權限 |
| WindowServer | 行程存在 | 登入工作階段尚未建立 |
| 目標模擬器 | 狀態為 Booted |
持續停留在 Booting 或 Shutdown |
WindowServer 存在只是必要條件之一。若測試任務由其他使用者或系統層級的常駐程式啟動,仍可能無法存取目標使用者的圖形工作階段。
使用預檢腳本保留現場狀態
以下腳本不會修改系統狀態,適合放在測試任務的最前端。它會將目前執行使用者、主控台使用者、GUI 網域與可用裝置寫入日誌;任何一項檢查失敗,都會立即終止任務。
#!/bin/zsh
set -euo pipefail
RUNNER_USER="$(id -un)"
RUNNER_UID="$(id -u)"
CONSOLE_USER="$(stat -f '%Su' /dev/console)"
echo "runner_user=${RUNNER_USER}"
echo "runner_uid=${RUNNER_UID}"
echo "console_user=${CONSOLE_USER}"
if [[ "${CONSOLE_USER}" == "root" || "${CONSOLE_USER}" == "loginwindow" ]]; then
echo "No interactive console user"
exit 21
fi
CONSOLE_UID="$(id -u "${CONSOLE_USER}")"
if [[ "${RUNNER_UID}" != "${CONSOLE_UID}" ]]; then
echo "Runner and console user differ"
exit 22
fi
if ! launchctl print "gui/${CONSOLE_UID}" >/dev/null 2>&1; then
echo "GUI launch domain is unavailable"
exit 23
fi
if ! pgrep -x WindowServer >/dev/null; then
echo "WindowServer is unavailable"
exit 24
fi
xcrun simctl list devices available
不要將使用者不一致視為一般警告後繼續執行。後續失敗通常只會產生更模糊的 XCTest 錯誤,反而會失去最有價值的根本原因資訊。
固定模擬器,不要依賴模糊名稱
確認 GUI 工作階段正常後,再處理裝置選擇。只指定 name=iPhone,在安裝多個執行階段時很容易產生歧義。請先建立或選定一台專用裝置,儲存其 UDID,然後等待裝置完成啟動:
DEVICE_UDID="${IOS_TEST_DEVICE_UDID:?missing device udid}"
xcrun simctl boot "${DEVICE_UDID}" 2>/dev/null || true
xcrun simctl bootstatus "${DEVICE_UDID}" -b
xcodebuild test \
-workspace App.xcworkspace \
-scheme AppUITests \
-destination "platform=iOS Simulator,id=${DEVICE_UDID}" \
-resultBundlePath "$PWD/artifacts/ui-tests.xcresult"
simctl boot 返回不代表系統已經可以執行測試,真正的同步點是 bootstatus -b。平行任務也應使用不同的裝置 UDID,避免兩個任務爭用同一個模擬器的資料目錄。任務結束後可以關閉裝置,但不要在其他作業仍在執行時進行全域裝置清理。
保留三層證據
進行疑難排解時,至少應保留三層資料:預檢腳本輸出、xcodebuild 原始日誌,以及 .xcresult。如果測試宿主沒有啟動,記錄以下行程也會有所幫助:
ps -axo user,pid,ppid,command | \
grep -E 'XCTest|Simulator|CoreSimulator' | \
grep -v grep > artifacts/ui-processes.txt
這些資訊能回答「測試是否曾進入模擬器」,而不只是告訴你命令最後執行失敗。
讓任務從使用者 GUI 網域執行
穩定的做法不是讓每個 SSH 工作階段直接啟動 UI 測試,而是由已登入使用者網域中的 LaunchAgent 接收任務。SSH 只負責寫入任務描述或觸發佇列,LaunchAgent 則在固定使用者、固定工作目錄與固定環境變數下執行腳本。
LaunchAgent 應明確設定 WorkingDirectory,並將標準輸出與標準錯誤寫入任務目錄。工具路徑不應依賴互動式 Shell 的啟動檔案;請在腳本中明確設定 PATH,並使用 xcode-select -p 記錄目前的開發工具目錄。如此即可將「SSH 能夠執行」與「GUI 自動化能夠執行」拆分為兩個獨立契約。
不建議透過系統層級的 LaunchDaemon 直接啟動 XCUITest,也不要將 nohup 視為進入 GUI 工作階段的方法。這些方式可以讓行程持續存在,卻不會自動變更行程所屬的啟動網域。
建立接收任務前的最低驗收標準
一台節點在接收 UI 測試前,可以依照以下順序進行准入檢查:
- 主控台使用者與任務使用者一致。
- 目前任務可以查詢
gui/<uid>。 - WindowServer 存在。
xcode-select -p指向預期的工具目錄。- 目標模擬器 UDID 存在,且可完成
bootstatus。 - 產出目錄可寫入,並為本次任務使用獨立路徑。
- 執行一項只啟動應用程式並檢查首個畫面元素的輕量測試。
如果第七步失敗,請先封存現場資料,再決定是否重建該專用模擬器。不要將刪除所有裝置、清除全部快取視為預設復原方式;這不僅會掩蓋啟動網域問題,也可能影響同一台主機上的其他任務。
將圖形工作階段預檢放到流水線入口後,XCUITest 的「無法啟動」就不再是籠統的故障。任務會在主控台使用者、GUI 網域、WindowServer 或模擬器階段指出明確的失敗點,後續疑難排解也能依據同一組證據進行。
常見問題
為什麼 SSH 中可以建置,XCUITest 卻無法啟動?
建置主要依賴命令列工具,但 UI 測試還需要已登入的主控台使用者、launchd GUI 網域、WindowServer,以及完成啟動的模擬器。
只確認 WindowServer 程序存在就足夠嗎?
不足夠。還要確認主控台使用者不是 loginwindow、gui/UID 可查詢、測試由同一使用者執行,且目標模擬器已完成啟動。
自動化工作應如何進入正確的 GUI 工作階段?
由已登入使用者網域中的 LaunchAgent 接收並執行工作。不要直接從系統層級常駐程序或臨時 SSH Shell 啟動 UI 測試。
為下一項開發任務設定雲端 Mac
選擇 Oak M4、Oak M4 Plus 或 Oak M4 Pro,並依團隊所在地從五個實體節點中設定租用方案。