mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 23:45:19 +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
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
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
|
- name: Select available iPhone simulator
|
||||||
id: destination
|
id: destination
|
||||||
run: |
|
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:' '
|
destination_id=$(awk -F'id:' '
|
||||||
/platform:iOS Simulator/ && /name:iPhone/ && !found {
|
/platform:iOS Simulator/ && /name:iPhone/ \
|
||||||
|
&& !/error/ && !/unavailable/ && !found {
|
||||||
value=$2
|
value=$2
|
||||||
sub(/,.*/, "", value)
|
sub(/,.*/, "", value)
|
||||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
||||||
@@ -203,10 +210,47 @@ jobs:
|
|||||||
found=1
|
found=1
|
||||||
}
|
}
|
||||||
' <<< "$destinations")
|
' <<< "$destinations")
|
||||||
if [ -z "$destination_id" ]; then
|
|
||||||
echo "::error::No available iPhone simulator destination found"
|
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
|
exit 1
|
||||||
fi
|
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"
|
echo "id=$destination_id" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Run iOS tests
|
- name: Run iOS tests
|
||||||
|
|||||||
@@ -1235,9 +1235,17 @@ struct NoiseEncryptionServiceTests {
|
|||||||
@Test("Lost reconnect completion restores the quarantined transport")
|
@Test("Lost reconnect completion restores the quarantined transport")
|
||||||
func timedOutReconnectRestoresQuarantinedTransport() async throws {
|
func timedOutReconnectRestoresQuarantinedTransport() async throws {
|
||||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
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(
|
let bob = NoiseEncryptionService(
|
||||||
keychain: MockKeychain(),
|
keychain: MockKeychain(),
|
||||||
ordinaryResponderHandshakeTimeout: 0.02
|
ordinaryResponderHandshakeTimeout: 1.0
|
||||||
)
|
)
|
||||||
let mallory = NoiseEncryptionService(keychain: MockKeychain())
|
let mallory = NoiseEncryptionService(keychain: MockKeychain())
|
||||||
let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData())
|
let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData())
|
||||||
@@ -1253,9 +1261,17 @@ struct NoiseEncryptionServiceTests {
|
|||||||
)
|
)
|
||||||
#expect(!bob.hasEstablishedSession(with: alicePeerID))
|
#expect(!bob.hasEstablishedSession(with: alicePeerID))
|
||||||
|
|
||||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
// Poll instead of sleeping a fixed interval: the responder timeout
|
||||||
|
// fires on bob's manager queue at the quarantine deadline, and a
|
||||||
#expect(bob.hasEstablishedSession(with: alicePeerID))
|
// 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(
|
let oldTransport = try alice.encrypt(
|
||||||
Data("timeout rollback".utf8),
|
Data("timeout rollback".utf8),
|
||||||
for: bobPeerID
|
for: bobPeerID
|
||||||
|
|||||||
Reference in New Issue
Block a user