mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 17:45:21 +00:00
Harden iOS-sim CI: destination fallback + Noise reconnect test determinism (#1483)
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user