From 934b2cd2d36976cd8afb0afade60296f12049b15 Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:13:04 +0200 Subject: [PATCH] Harden iOS-sim CI: destination fallback + Noise reconnect test determinism (#1483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two CI-robustness fixes for the iOS-simulator job seen on main: 1. The destination picker no longer dies on runner images with placeholder/unavailable simulator lists (macos-26-arm64 20260720.0258): it filters error/unavailable rows, falls back to xcrun simctl's available-device list, and as a last resort creates a simulator from the newest installed iOS runtime's own supported iPhone device types. Each path logs which route was taken. 2. Deflakes NoiseEncryptionServiceTests' quarantine-restore test: the injected 20ms responder timeout also armed during the SETUP handshake, so a loaded runner tore down the half-open responder session before the scenario began (production quarantine/restore verified correct — the failure signature was a fresh msg2 to a session-less peer). Timeout raised to 1s and the fixed 100ms sleep replaced with a bounded waitUntil on the restore condition. No assertion weakened; 10/10 sequential + 3/3 under full-core CPU load. --- .github/workflows/swift-tests.yml | 54 +++++++++++++++++-- .../NoiseEncryptionServiceTests.swift | 24 +++++++-- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/.github/workflows/swift-tests.yml b/.github/workflows/swift-tests.yml index c2789c03..81c42eb2 100644 --- a/.github/workflows/swift-tests.yml +++ b/.github/workflows/swift-tests.yml @@ -190,12 +190,19 @@ jobs: - name: Checkout code uses: actions/checkout@v5 + # Some runner images list only placeholder destinations (rows carrying + # "error:" or "unavailable") or ship the selected Xcode without a + # matching iOS simulator runtime, so this walks three paths in order: + # a usable xcodebuild destination, an existing simctl device, and + # finally creating a device from the newest installed iOS runtime. - name: Select available iPhone simulator id: destination run: | - destinations=$(xcodebuild -project bitchat.xcodeproj -scheme "bitchat (iOS)" -showdestinations) + set -uo pipefail + destinations=$(xcodebuild -project bitchat.xcodeproj -scheme "bitchat (iOS)" -showdestinations 2>/dev/null || true) destination_id=$(awk -F'id:' ' - /platform:iOS Simulator/ && /name:iPhone/ && !found { + /platform:iOS Simulator/ && /name:iPhone/ \ + && !/error/ && !/unavailable/ && !found { value=$2 sub(/,.*/, "", value) gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) @@ -203,10 +210,47 @@ jobs: found=1 } ' <<< "$destinations") - if [ -z "$destination_id" ]; then - echo "::error::No available iPhone simulator destination found" - exit 1 + + if [ -n "$destination_id" ]; then + echo "Selected destination via xcodebuild -showdestinations: $destination_id" + else + echo "No usable iPhone destination in -showdestinations output; falling back to simctl" + destination_id=$(xcrun simctl list devices available --json | jq -r ' + [.devices | to_entries[] + | select(.key | contains("iOS")) + | .value[] + | select(.isAvailable and (.name | startswith("iPhone")))] + | first.udid // empty') + if [ -n "$destination_id" ]; then + echo "Selected existing simctl device: $destination_id" + else + echo "No available iPhone simulator device; creating one" + # Newest installed iOS runtime plus an iPhone device type that + # runtime itself reports as supported, so the pair always match. + create_spec=$(xcrun simctl list runtimes --json | jq -r ' + [.runtimes[] | select(.platform == "iOS" and .isAvailable)] + | sort_by(.version | split(".") | map(tonumber)) + | last // empty + | .identifier as $runtime + | ([(.supportedDeviceTypes // [])[] + | select(.productFamily == "iPhone" + or (.name // "" | startswith("iPhone")))] + | first.identifier // empty) as $devicetype + | "\($devicetype) \($runtime)"') + read -r devicetype runtime <<< "$create_spec" || true + if [ -z "${devicetype:-}" ] || [ -z "${runtime:-}" ]; then + echo "::error::No iPhone simulator destination found and none creatable (no installed iOS runtime with an iPhone device type)" + exit 1 + fi + destination_id=$(xcrun simctl create ci-iphone "$devicetype" "$runtime") || { + echo "::error::simctl create failed for $devicetype on $runtime" + exit 1 + } + echo "Created simulator $destination_id ($devicetype, $runtime)" + fi fi + + echo "Using iPhone simulator destination id: $destination_id" echo "id=$destination_id" >> "$GITHUB_OUTPUT" - name: Run iOS tests diff --git a/bitchatTests/Services/NoiseEncryptionServiceTests.swift b/bitchatTests/Services/NoiseEncryptionServiceTests.swift index 436e427c..7409cbba 100644 --- a/bitchatTests/Services/NoiseEncryptionServiceTests.swift +++ b/bitchatTests/Services/NoiseEncryptionServiceTests.swift @@ -1235,9 +1235,17 @@ struct NoiseEncryptionServiceTests { @Test("Lost reconnect completion restores the quarantined transport") func timedOutReconnectRestoresQuarantinedTransport() async throws { let alice = NoiseEncryptionService(keychain: MockKeychain()) + // The injected responder timeout also arms during the ordinary setup + // handshake below (bob is its responder), where the only work between + // message 1 and message 3 is two consecutive synchronous statements. + // It must be generous enough that a preempted runner cannot let the + // timeout fire mid-setup and tear down the half-open responder — at + // 20ms a loaded 2-core CI runner did exactly that, so message 3 was + // answered as a fresh initiation (96-byte message 2) and nothing was + // ever quarantined. let bob = NoiseEncryptionService( keychain: MockKeychain(), - ordinaryResponderHandshakeTimeout: 0.02 + ordinaryResponderHandshakeTimeout: 1.0 ) let mallory = NoiseEncryptionService(keychain: MockKeychain()) let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData()) @@ -1253,9 +1261,17 @@ struct NoiseEncryptionServiceTests { ) #expect(!bob.hasEstablishedSession(with: alicePeerID)) - try? await Task.sleep(nanoseconds: 100_000_000) - - #expect(bob.hasEstablishedSession(with: alicePeerID)) + // Poll instead of sleeping a fixed interval: the responder timeout + // fires on bob's manager queue at the quarantine deadline, and a + // starved runner can delay that work item well past the deadline. + let restored = await TestHelpers.waitUntil( + { bob.hasEstablishedSession(with: alicePeerID) }, + timeout: TestConstants.longTimeout + ) + try #require( + restored, + "Responder timeout should restore the quarantined transport" + ) let oldTransport = try alice.encrypt( Data("timeout rollback".utf8), for: bobPeerID