開発タスクメモ

クラウドMacでXCUITestの起動失敗を診断する

クラウドMacでXCUITestの起動失敗を診断する

同じクラウドMacでも、SSH経由の xcodebuild build は正常に完了する一方、test に切り替えるとテストランナーの起動前に停止することがあります。テストケースは開始されず、Simulatorのウィンドウも表示されず、ログにはアプリケーションの起動失敗やセッション接続エラーだけが残ります。この段階でDerivedDataを削除してはいけません。ビルド経路はすでに正常であり、本当に確認すべきなのは、ジョブが利用可能なmacOSのグラフィカルセッション内で実行されているかどうかです。

ビルド環境とグラフィカルセッションを切り分ける

XCUITestは、単にテストバンドルをコンパイルするだけではありません。対象アプリケーション、テストホスト、Simulatorサービスを起動し、現在のユーザーのグラフィカルセッションを介して各プロセスを連携させる必要があります。SSHログインの成功が示すのは、リモートシェルを利用できるということだけです。そのシェルが、デスクトップにログインしているユーザーと同じlaunchd GUIドメインに属している保証はありません。

最初からすべてのプロセスを再起動するのではなく、まず次の4項目を記録します。

確認項目 正常と判断できる結果 異常の手掛かり
コンソールユーザー 実際に自動化を実行するユーザー root または loginwindow
GUI起動ドメイン gui/<uid> を照会できる ドメインが存在しない、または権限がない
WindowServer プロセスが存在する ログインセッションがまだ確立されていない
対象Simulator 状態が 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エラーしか出ないことが多く、原因を特定するうえで最も重要な情報が失われてしまいます。

曖昧な名前ではなくSimulatorを固定する

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を使用し、複数のジョブが同じSimulatorのデータディレクトリを取り合わないようにします。ジョブの終了後にデバイスをシャットダウンしても構いませんが、ほかのジョブが実行中の状態でデバイス全体を一括消去してはいけません。

3層の証拠を残す

トラブルシューティングでは、少なくとも事前確認スクリプトの出力、xcodebuild の生ログ、.xcresult の3層を保存します。テストホストが起動しない場合は、次のプロセス情報も役立ちます。

ps -axo user,pid,ppid,command | \
  grep -E 'XCTest|Simulator|CoreSimulator' | \
  grep -v grep > artifacts/ui-processes.txt

これらの情報があれば、コマンドが最終的に失敗したという結果だけでなく、テストがSimulatorまで到達したかどうかも判断できます。

ユーザーのGUIドメインからジョブを実行する

安定した方法は、SSHセッションごとにUIテストを直接起動することではありません。ログイン済みユーザーのドメインにあるLaunchAgentでジョブを受け取ります。SSHはジョブ定義の書き込みやキューの起動だけを担当し、LaunchAgentが固定ユーザー、固定の作業ディレクトリ、固定の環境変数でスクリプトを実行します。

LaunchAgentでは WorkingDirectory を明示的に設定し、標準出力と標準エラー出力をジョブのディレクトリへ書き込みます。ツールのパスを対話型シェルの起動ファイルに依存させてはいけません。スクリプト内で PATH を明示的に設定し、xcode-select -p で現在の開発ツールディレクトリを記録します。これにより、「SSHでコマンドを実行できること」と「GUI自動化を実行できること」を、独立した2つの条件として分離できます。

システムレベルのLaunchDaemonからXCUITestを直接起動する方法は推奨できません。また、nohup をGUIセッションへ入るための手段として使ってはいけません。これらはプロセスを存続させることはできますが、そのプロセスが所属する起動ドメインを自動的に変更するものではありません。

ジョブ受け入れ前の最小確認項目を定める

ノードがUIテストを受け入れる前に、次の順序で受け入れ条件を確認できます。

  1. コンソールユーザーとジョブの実行ユーザーが一致している。
  2. 現在のジョブから gui/<uid> を照会できる。
  3. WindowServerが存在する。
  4. xcode-select -p が想定したツールディレクトリを指している。
  5. 対象SimulatorのUDIDが存在し、bootstatus を完了できる。
  6. 成果物ディレクトリが書き込み可能で、このジョブ専用のパスを使用している。
  7. アプリケーションの起動と初期画面の要素確認だけを行う軽量テストを実行する。

7番目の手順に失敗した場合は、まず現場の情報をアーカイブしてから、専用Simulatorを再作成するか判断します。すべてのデバイスの削除や全キャッシュの消去を、標準の復旧手順にしてはいけません。起動ドメインの問題が隠れるだけでなく、同じマシン上で実行中のほかのジョブにも影響する可能性があります。

グラフィカルセッションの事前確認をパイプラインの入口に配置すれば、XCUITestの「起動できない」という問題を曖昧な障害として扱う必要はなくなります。ジョブはコンソールユーザー、GUIドメイン、WindowServer、Simulatorのどの段階で失敗したかを明確に示し、その後の調査も同じ証拠に基づいて進められます。

よくある質問

SSHではビルドできるのにXCUITestが起動しないのはなぜですか?

ビルドはコマンドラインだけでも可能ですが、UIテストにはログイン済みユーザー、launchdのGUIドメイン、WindowServer、起動済みシミュレータが必要です。

WindowServerプロセスがあれば実行可能と判断できますか?

できません。コンソールユーザーがloginwindowではないこと、gui/UIDを参照できること、テストの実行ユーザーが一致すること、対象端末の起動完了も確認します。

自動化ジョブを正しいGUIセッションで動かす方法はありますか?

ログインユーザーのLaunchAgentにジョブを渡して実行します。システムデーモンや一時的なSSHシェルからUIテストを直接起動する構成は避けます。

専用開発環境

次の開発タスク用にクラウドMacを構成

Oak M4、Oak M4 Plus、Oak M4 Proから選び、チームの所在地に合わせて5つの物理ノードからレンタル構成を設定できます。

クラウドMacを構成