Skip to content

ci: wait for the iOS simulator to be ready before running integration tests - #78

Open
vahidlazio wants to merge 2 commits into
mainfrom
vahidt/deflake-integration-test-ci
Open

ci: wait for the iOS simulator to be ready before running integration tests#78
vahidlazio wants to merge 2 commits into
mainfrom
vahidt/deflake-integration-test-ci

Conversation

@vahidlazio

@vahidlazio vahidlazio commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Test-iOS intermittently burned its full 30-minute timeout without running a single test. This makes it recover instead.

The symptom

Xcode build done.                    104.9s
   (~24 minutes of no output)
##[error]The action 'Run iOS integration tests' has timed out after 30 minutes

The app builds, installs and launches, but the test harness never attaches, so the job sits silent until it is killed:

No tests ran.
Error waiting for a debug connection: The log reader failed unexpectedly

What this is not

My first theory was a boot race — simctl boot returns as soon as boot is initiated, not when the device is usable, so flutter test could race a still-booting simulator. That is real and measurable: on a CI runner bootstatus takes 92-94s to reach ready, while the bare boot it replaced returned in ~2s. That left a ~90 second window.

But it is not the cause. The first run of this branch had the readiness gate in place, the gate succeeded in 94s with the device fully booted, and flutter test stalled anyway and hit the 30-minute timeout. So the failure is flutter test failing to attach to the Dart VM, which simulator readiness does not affect.

The gate is kept regardless: it closes a genuine race, and a boot failure now fails its own step (bootstatus exits non-zero on an invalid device — observed exit 148) rather than surfacing later as an unexplained test timeout.

What actually fixes it

The stall is intermittent, not deterministic — the same commit passes on some runs and stalls on others. A second attempt, with the app already built and the simulator warm, succeeds quickly.

This run demonstrates the whole cycle:

Attempt 1  16:18:26
  Error waiting for a debug connection: The log reader failed unexpectedly
  Attempt 1 failed. Reason: Timeout of 780000ms hit          16:31:39
Attempt 2  16:31:39
  Command completed after 2 attempt(s).                      16:33:06

Attempt 1 stalled with exactly the diagnosed error and hit its bound; attempt 2 passed in 87 seconds. Total step time 14m51s, comfortably inside the job's 45-minute budget — whereas the previous single 30-minute attempt consumed almost the entire budget before failing, leaving no room to recover.

Changes

  • Boot the simulator in its own step, gated on readiness with simctl bootstatus -b, so the wait is visible in the timeline and a boot failure is attributed correctly.
  • Run the test through nick-fields/retry@v3 with 2 bounded 13-minute attempts (26 min total, vs the 45 min job budget).
  • flutter config --no-enable-swift-package-manager moved to its own step so it is not repeated per attempt.

Honest caveat

A retry hides the flake, it does not cure it. The underlying flutter test attach failure on cold runners is still there, and if it ever becomes deterministic both attempts will stall and the job will fail — more slowly than before, though still inside budget. The real fix is upstream in the Flutter tooling or in how the harness attaches.

Not changed: Test-Android

Test-Android delegates emulator lifecycle to reactivecircus/android-emulator-runner, so there is no un-gated boot call of ours to fix. Its observed failure (Unable to connect to adb daemon on port: 5037) happened inside that action's own setup, which cannot be gated from this workflow. Separately, that job has no timeout-minutes, so a hang runs to GitHub's 360-minute default — worth setting, but out of scope here.

Known follow-up: the integration tests hit the live backend

example/test/widget_test.dart asserts on real flag resolution against the Confidence backend, so both integration jobs are network- and credential-dependent. One android-test failure showed flag resolution returning "" alongside Failed to upload events. http code 400. That coupling makes these jobs flaky independent of anything here, and is worth decoupling separately.

🤖 Generated with Claude Code

vahidlazio and others added 2 commits September 7, 2026 17:38
… tests

`xcrun simctl boot` returns as soon as boot is initiated, not when the
device is usable. Booting inline in the test step therefore let
`flutter test` race the Dart VM debug connection against a still-booting
simulator. When it lost that race the app built, installed and launched
but the harness never attached, so the job sat silent until the 30 minute
timeout with "No tests ran." and "Error waiting for a debug connection:
The log reader failed unexpectedly".

Measured locally: `simctl boot` returned after 2s while the device needed
3s more when warm and 17s more when cold, most of that "Waiting on Data
Migration" running migrator plugins. Since every CI job starts cold, the
outcome was effectively a coin flip per run.

`bootstatus -b` boots the device if needed and blocks until it reports
Finished, and exits non-zero for an invalid device, so a boot failure now
fails its own step rather than surfacing as a test timeout later.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The simulator readiness gate did not stop the stall. On the first run of
this branch the gate succeeded in 94s and the device was fully booted, and
`flutter test` still hung after "Xcode build done." until the 30 minute
timeout. That rules out the boot race: the failure is `flutter test`
failing to attach to the Dart VM, which readiness does not affect.

It is intermittent rather than deterministic — the same commit passes on
some runs and stalls on others — and a second attempt with the app already
built and the simulator warm has consistently succeeded.

Two bounded 13 minute attempts fit inside the job's 45 minute budget. The
previous single 30 minute attempt consumed almost the entire budget before
failing, leaving no room to recover.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant