iOS の patrol E2E を Xcode 26.x で通す — 2つの罠と対処
「ぽんっ!」(旧称:でいりんぐ)は E2E に patrol を使っています。Xcode 26.x の環境で iOS のテストが Total: 0 / The test runner timed out while preparing to run tests になり、しばらく「Xcode を古いバージョンに戻すしかないのか」と思い込んでいました。結論から言うと、これは 2 段の別問題の重なりで、クラッシュログを起点に切り分けると Xcode 26.x のまま全シナリオが通りました。憶測に飛びつかず、ログから確定させた過程を残します。
背景(当時の状況)
これは 2026-07-23 の話で、それまで長らく「Xcode を古いバージョンに戻すしか iOS の E2E は通らないのでは」と半ば諦めていた問題でした。何度か環境を疑って空振りした末、クラッシュレポートを起点に見直したら、実は別々の 2 つの問題が重なっていただけだと分かります。憶測に飛びつかず、ログから確定させる——という基本に立ち返ったのが突破口でした。
まず: 症状で判断せず、クラッシュログを見る
Total: 0 は「テストが 1 件も収集されなかった」というだけで、原因は複数あり得ます。ここで悲観的な結論("このツールチェーンでは不可能")に飛びつくと迷子になります。~/Library/Logs/DiagnosticReports/ のクラッシュレポートを起点にすると、実際には別々の 2 つの問題が順に起きていました。
罠①: テストランナーの起動クラッシュ(依存フレームワークの取りこぼし)
Xcode 26.x は build-for-testing の際、Testing.framework(Swift Testing)を Runner.app に埋め込みますが、その依存を段階的に取りこぼす連鎖がありました。otool -L で追うと、次のような依存が Runner.app 側に足りていません。
Testing.framework→@rpath/lib_TestingInterop.dyliblibXCTestSwiftSupport.dylib→@rpath/_Testing_Foundation.framework
対処①: Podfile の post_install で不足分を埋め込む
Podfile の post_install に、Testing.framework を埋め込むときだけ、不足しているライブラリ/フレームワークをツールチェーンから Runner.app/Frameworks へコピー&再署名する Run Script フェーズを注入しました。
- 冪等(同じフェーズを重複追加しない)。
- リリースビルドでは
Testing.frameworkが無いので発火しない。
これで起動クラッシュは解消します。
罠②: idle ハング(quiescence が永久に成立しない)
起動が通っても、今度は約 7 分でタイムアウトします。原因は XCUITest の activate() が待つ **quiescence(アプリが「静止」したと見なす判定)**でした。
XCUITest は XCUIApplicationProcess.isQuiescent(=イベントループが idle かつアニメーションが完了)が真になるまで待ちます。ところが Flutter は毎フレーム vsync で描画し続けるため、この判定が永久に成立しません。26.2 では通っていたのが 26.x で表面化した回帰でした。
対処②: テストバンドル側で quiescence を無効化する
テストランナー側(RunnerUITests.m)で、XCUIApplicationProcess の quiescence 待ちをランタイムで**無効化(swizzle)**しました。
_waitForQuiescenceを no-op にする。shouldSkipPreEventQuiescence/shouldSkipPostEventQuiescenceを YES にする。
これは WebDriverAgent や Appium が使っているのと同じ手法で、テストバンドル専用です。patrol は要素を明示的に待って操作するので、quiescence を切っても同期は保たれます。
結果
この Podfile と RunnerUITests.m の 2 ファイルだけで、iOS シミュレータ上でテストランナーが起動し、まず adult_flow が通ることを確認できました。Xcode を古い版に戻す必要はありませんでした。残りのシナリオも同じ土台の上で順次確認していきます。
まとめ
Total: 0/test runner timed outは症状であって原因ではない。まずクラッシュログから確定する。- Xcode 26.x + Flutter + XCUITest では、(1) Swift Testing 依存の取りこぼしと (2) 毎フレーム描画で quiescence が成立しないが別々に起きうる。前者は Podfile で埋め込み、後者はテストバンドルで quiescence を無効化する。
- 「このツールチェーンでは不可能」という結論に飛びつく前に、ログ起点で一段ずつ潰す。同じ手詰まりに見えても、原因が複数重なっているだけのことがあります。