From fc295274a3d92d26a4162efe4db9ded6274a8a6f Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Wed, 17 Jun 2026 18:24:48 -0500 Subject: [PATCH] ci(ios): add ensure-simulator-ready script for reliable boot/ready waits Port boot-status polling from react-native-firebase CI: wait for simctl bootstatus after simulator-action (or simctl boot) --- .github/workflows/e2e_tests_fdc.yaml | 12 +- .github/workflows/e2e_tests_pipeline.yaml | 6 + .github/workflows/ios.yaml | 7 +- .github/workflows/scripts/drive-example.sh | 9 +- .../scripts/ensure-simulator-ready.sh | 182 ++++++++++++++++++ 5 files changed, 206 insertions(+), 10 deletions(-) create mode 100755 .github/workflows/scripts/ensure-simulator-ready.sh diff --git a/.github/workflows/e2e_tests_fdc.yaml b/.github/workflows/e2e_tests_fdc.yaml index 979e715f88ca..d840b38f2695 100644 --- a/.github/workflows/e2e_tests_fdc.yaml +++ b/.github/workflows/e2e_tests_fdc.yaml @@ -218,7 +218,13 @@ jobs: - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 id: simulator with: + # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators model: "iPhone 16" + - name: Ensure Simulator Ready + env: + SIMULATOR: ${{ steps.simulator.outputs.udid }} + ENSURE_BOOT_IF_NEEDED: "0" + run: .github/workflows/scripts/ensure-simulator-ready.sh - name: 'E2E Tests' working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' env: @@ -226,13 +232,11 @@ jobs: run: | # Uncomment following line to have simulator logs printed out for debugging purposes. # xcrun simctl spawn booted log stream --predicate 'eventMessage contains "flutter"' & - # The iOS simulator sometimes fails to connect the VM Service. Keep a - # limit around the full test command and retry once with a simulator reboot. + # Retry once after VM Service / simulator flake (reboot + migration-aware wait). perl -e 'alarm 900; exec @ARGV' -- flutter test integration_test/e2e_test.dart -d "$SIMULATOR" --dart-define=CI=true --timeout 10x || { echo "First attempt failed or timed out. Rebooting simulator and retrying..." xcrun simctl shutdown "$SIMULATOR" || true - xcrun simctl boot "$SIMULATOR" - xcrun simctl bootstatus "$SIMULATOR" -b + "${GITHUB_WORKSPACE}/.github/workflows/scripts/ensure-simulator-ready.sh" flutter test integration_test/e2e_test.dart -d "$SIMULATOR" --dart-define=CI=true --timeout 10x } - name: Save Firestore Emulator Cache diff --git a/.github/workflows/e2e_tests_pipeline.yaml b/.github/workflows/e2e_tests_pipeline.yaml index 4b101184499d..c45b311ee3ed 100644 --- a/.github/workflows/e2e_tests_pipeline.yaml +++ b/.github/workflows/e2e_tests_pipeline.yaml @@ -204,11 +204,17 @@ jobs: - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 id: simulator with: + # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators model: "iPhone 16" - name: Build iOS (simulator) working-directory: packages/cloud_firestore/cloud_firestore/pipeline_example run: | flutter build ios --no-codesign --simulator --debug --target=./integration_test/pipeline/pipeline_live_test.dart --dart-define=CI=true + - name: Ensure Simulator Ready + env: + SIMULATOR: ${{ steps.simulator.outputs.udid }} + ENSURE_BOOT_IF_NEEDED: "0" + run: .github/workflows/scripts/ensure-simulator-ready.sh - name: Run pipeline E2E tests (iOS) working-directory: packages/cloud_firestore/cloud_firestore/pipeline_example env: diff --git a/.github/workflows/ios.yaml b/.github/workflows/ios.yaml index fa88c610490c..1a8d7841ff5c 100644 --- a/.github/workflows/ios.yaml +++ b/.github/workflows/ios.yaml @@ -125,8 +125,13 @@ jobs: - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 id: simulator with: - # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md#installed-simulators + # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md#installed-simulators model: "iPhone 16" + - name: Ensure Simulator Ready + env: + SIMULATOR: ${{ steps.simulator.outputs.udid }} + ENSURE_BOOT_IF_NEEDED: "0" + run: .github/workflows/scripts/ensure-simulator-ready.sh - name: 'E2E Tests' working-directory: ${{ matrix.working_directory }} env: diff --git a/.github/workflows/scripts/drive-example.sh b/.github/workflows/scripts/drive-example.sh index 2004a01b45ca..2c342e3f0fdd 100755 --- a/.github/workflows/scripts/drive-example.sh +++ b/.github/workflows/scripts/drive-example.sh @@ -13,12 +13,11 @@ fi if [ "$ACTION" == "ios" ] then - SIMULATOR="iPhone 14" - # Boot simulator and wait for System app to be ready. - xcrun simctl bootstatus "$SIMULATOR" -b + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + SIMULATOR="${SIMULATOR:-iPhone 16}" + export SIMULATOR + "${SCRIPT_DIR}/ensure-simulator-ready.sh" xcrun simctl logverbose "$SIMULATOR" enable - # Sleep to allow simulator to settle. - sleep 15 # Uncomment following line to have simulator logs printed out for debugging purposes. # xcrun simctl spawn booted log stream --predicate 'eventMessage contains "flutter"' & melos exec -c 1 --fail-fast --scope="$FLUTTERFIRE_PLUGIN_SCOPE_EXAMPLE" --dir-exists=integration_test -- \ diff --git a/.github/workflows/scripts/ensure-simulator-ready.sh b/.github/workflows/scripts/ensure-simulator-ready.sh new file mode 100755 index 000000000000..5a18a42f791a --- /dev/null +++ b/.github/workflows/scripts/ensure-simulator-ready.sh @@ -0,0 +1,182 @@ +#!/bin/bash + +# Wait until an iOS Simulator is fully ready for integration tests (including first-boot +# data migration). Intended to run after futureware-tech/simulator-action (or any step +# that has issued simctl boot) and before flutter test / flutter drive. +# +# Usage: +# SIMULATOR= ./ensure-simulator-ready.sh +# ./ensure-simulator-ready.sh +# +# Environment: +# SIMULATOR UDID or device name (required if not passed as arg) +# BOOT_POLL_INTERVAL_SECONDS Poll interval (default: 20) +# BOOT_PROBE_TIMEOUT_SECONDS Per-probe timeout (default: 12) +# BOOT_MAX_WAIT_SECONDS Max wait for full boot (default: 660) +# ENSURE_OPEN_SIMULATOR_APP Open Simulator.app when booting (default: 1) +# ENSURE_BOOT_IF_NEEDED simctl boot when not Booted yet (default: 1) +set -euo pipefail + +BOOT_POLL_INTERVAL_SECONDS="${BOOT_POLL_INTERVAL_SECONDS:-20}" +BOOT_PROBE_TIMEOUT_SECONDS="${BOOT_PROBE_TIMEOUT_SECONDS:-12}" +BOOT_MAX_WAIT_SECONDS="${BOOT_MAX_WAIT_SECONDS:-660}" +ENSURE_OPEN_SIMULATOR_APP="${ENSURE_OPEN_SIMULATOR_APP:-1}" +ENSURE_BOOT_IF_NEEDED="${ENSURE_BOOT_IF_NEEDED:-1}" + +DEVICE="${SIMULATOR:-${1:-}}" +if [[ -z "$DEVICE" ]]; then + echo "[boot-status] ERROR: set SIMULATOR or pass device UDID/name as first argument" >&2 + exit 1 +fi + +run_with_timeout() { + local max="$1" + shift + "$@" & + local cmd_pid=$! + local waited=0 + while kill -0 "$cmd_pid" 2>/dev/null && (( waited < max )); do + sleep 1 + waited=$((waited + 1)) + done + if kill -0 "$cmd_pid" 2>/dev/null; then + kill "$cmd_pid" 2>/dev/null + wait "$cmd_pid" 2>/dev/null || true + return 124 + fi + wait "$cmd_pid" +} + +log_boot_status() { + echo "[boot-status] $*" +} + +is_udid() { + [[ "$1" =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]] +} + +describe_booted_device() { + local device="$1" + if is_udid "$device"; then + xcrun simctl list devices booted 2>/dev/null \ + | grep -F "$device" \ + | grep -v 'unavailable' \ + | head -1 \ + || true + else + xcrun simctl list devices booted 2>/dev/null \ + | grep -i "${device} (" \ + | grep -v 'Phone:' \ + | grep -v 'unavailable' \ + | grep -v CoreSimulator \ + | head -1 \ + || true + fi +} + +is_device_booted() { + [[ -n "$(describe_booted_device "$1")" ]] +} + +log_migration_status() { + local device="$1" + local migration_output probe_rc + + log_boot_status "probing data migration (bootstatus -d, up to ${BOOT_PROBE_TIMEOUT_SECONDS}s)..." + set +e + migration_output="$(run_with_timeout "$BOOT_PROBE_TIMEOUT_SECONDS" xcrun simctl bootstatus "$device" -d 2>&1)" + probe_rc=$? + set -e + + if [[ "$probe_rc" -eq 124 ]]; then + log_boot_status " data migration / system bring-up still in progress" + return 1 + fi + + if [[ -n "$migration_output" ]]; then + while IFS= read -r line; do + [[ -z "$line" ]] && continue + log_boot_status " ${line}" + done <<<"$migration_output" + else + log_boot_status " no migration details reported" + fi + return 0 +} + +wait_for_simulator_ready() { + local device="$1" + local start=$SECONDS + + while (( SECONDS - start < BOOT_MAX_WAIT_SECONDS )); do + local elapsed=$(( SECONDS - start )) + local booted_line ready_rc + + log_boot_status "elapsed=${elapsed}s phase=wait_for_full_boot device=\"${device}\"" + + booted_line="$(describe_booted_device "$device")" + if [[ -z "$booted_line" ]]; then + log_boot_status " simctl list: not in Booted state yet" + else + log_boot_status " simctl list: ${booted_line}" + log_migration_status "$device" || true + fi + + set +e + run_with_timeout "$BOOT_PROBE_TIMEOUT_SECONDS" xcrun simctl bootstatus "$device" >/dev/null 2>&1 + ready_rc=$? + set -e + + if [[ "$ready_rc" -eq 0 ]]; then + log_boot_status "bootstatus: simulator ready after ${elapsed}s" + log_migration_status "$device" || true + return 0 + fi + + if [[ "$ready_rc" -eq 124 ]]; then + log_boot_status "bootstatus: still booting (probe timed out after ${BOOT_PROBE_TIMEOUT_SECONDS}s)" + else + log_boot_status "bootstatus: probe exited with status ${ready_rc}" + fi + + sleep "$BOOT_POLL_INTERVAL_SECONDS" + done + + log_boot_status "ERROR: timed out after ${BOOT_MAX_WAIT_SECONDS}s waiting for simulator to become ready" + return 1 +} + +if is_udid "$DEVICE"; then + log_boot_status "phase=resolve_device udid=\"${DEVICE}\"" +else + log_boot_status "phase=resolve_device name=\"${DEVICE}\"" +fi + +if ! is_device_booted "$DEVICE"; then + if [[ "$ENSURE_BOOT_IF_NEEDED" == "1" ]]; then + log_boot_status "phase=boot_command device not Booted; starting simctl boot..." + set +e + boot_output="$(xcrun simctl boot "$DEVICE" 2>&1)" + boot_rc=$? + set -e + if [[ "$boot_rc" -ne 0 ]]; then + log_boot_status "simctl boot exited ${boot_rc}: ${boot_output}" + else + log_boot_status "simctl boot command returned (device may still be migrating data)" + fi + if [[ "$ENSURE_OPEN_SIMULATOR_APP" == "1" ]]; then + log_boot_status "phase=foreground_simulator opening Simulator.app..." + open -a Simulator.app || true + fi + else + log_boot_status "phase=boot_command skipped (ENSURE_BOOT_IF_NEEDED=0); waiting for existing boot..." + fi +else + log_boot_status "phase=boot_command device already Booted; waiting for full readiness..." +fi + +if ! wait_for_simulator_ready "$DEVICE"; then + exit 1 +fi + +log_boot_status "phase=complete device=\"${DEVICE}\" ready for flutter test"