開発Tips

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.dylib
  • libXCTestSwiftSupport.dylib@rpath/_Testing_Foundation.framework

対処①: Podfile の post_install で不足分を埋め込む

Podfilepost_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 を無効化する。
  • 「このツールチェーンでは不可能」という結論に飛びつく前に、ログ起点で一段ずつ潰す。同じ手詰まりに見えても、原因が複数重なっているだけのことがあります。