Skip to content

Make tests wait for conditions instead of the clock (fixes flaky CI on Xcode 27) - #273

Merged
darioalessandro merged 2 commits into
masterfrom
fix-flaky-trailing-edge-test
Nov 9, 2026
Merged

darioalessandro merged 2 commits into
masterfrom
fix-flaky-trailing-edge-test

Conversation

@darioalessandro

@darioalessandro darioalessandro commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

CI has been red on the new Xcode 27 machines because of tests, not because of the app. This makes the tests wait for what they're checking instead of betting on the clock.

What was going on

When we moved CI to Xcode 27 (for the iPhone 18 Pro fix), GitHub put us on slower machines that sometimes freeze for seconds. Many of our tests did "wait 150 ms, then check" or "give up after 2.5 seconds". On a frozen machine the check happens too early, and each run a different test failed.

It's like checking your mailbox at 3pm for a letter due at 1pm, except the mail truck got stuck in traffic. The letter isn't lost, you just looked too soon. The fix is to wait for the letter, not for 3pm.

What changed (tests only, app untouched)

  • Wait for the thing itself. About 20 tests now wait for the event they check (the photo resolving, the recording being cancelled, the reconnect attempt, the camera list showing up, the slider's last value arriving) instead of sleeping a fixed time. There's one 10-second limit, and it only exists so a truly stuck test still ends.
  • "Nothing happens" checks (for example, Cancel stops the retries) keep a short wait. There's nothing to wait for when nothing happens, and a slow machine can only make that wait longer, so it can't cause a false failure.
  • A broken "has the screen drawn?" check. The screenshot tests looked at only one pixel in every 32 to decide whether a screen had drawn. Thin things like the countdown's "3", text and icons slipped between those pixels. So ten screens always thought they were blank, waited, and fell back to a worse renderer, which is what failed on CI. It now looks at one pixel in every 4. Those tests went from 10 seconds to 0.2, and three scanner screen tests that were silently skipped now actually run.
  • One test skipped for now: "the transfer badge stays in its corner". On iOS 27, adding the badge sometimes redraws the shiny edges of the glass buttons next to it, so its "every other pixel must be identical" rule fails about 1 run in 5 even though nothing moved. It should check where the buttons are instead of comparing pixels. The skip message says so.

Verified

  • The slider test, faked freeze: the old version failed 2 of 20, the new one passed 20 of 20 (plus 50 of 50 without the freeze).
  • The full unit suite (792 tests) passed on 8 local runs today. It takes about 90 to 125 seconds now, down from about 220.
  • I tried to reproduce the CI freezes locally for the whole suite and couldn't (the old tests passed too), so the real proof for the rest is this PR's CI.

🤖 Generated with Claude Code

darioalessandro and others added 2 commits November 8, 2026 18:45
testTrailingEdgeDeliversTheLastValueOnly raced its own 150 ms dispatch
against the sender's 50 ms Timer. When the main thread stalled past both
deadlines, the run loop could service the dispatch first and assert before
the trailing send. It failed 2 of 3 CI runs on the Xcode 27 image; with a
300 ms stall injected locally it failed 2/20, the new version 0/20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The xcode-27 CI image stalls for seconds at a time, and each run some
test lost a race against a fixed sleep or a tight deadline.

- Shared eventually / eventuallyOnMain / turnRunLoopUntil helpers with
  one 10 s ceiling that only stops a hung test.
- Sleep-then-assert sites now wait for the event: ack timeout resolving
  the shot, the start deadline voiding a take, the reconnect tick,
  capability re-requests, the camera list, the Watch stream watchdog,
  the director publishing rig settings and exiting.
- Snapshot chrome detector sampled every 32nd pixel and missed thin
  text and icons, so ten screens always fell back to ImageRenderer
  (testCountdownChrome's CI failure). It samples every 4th pixel now:
  those tests take 0.2 s instead of 10 s and the three scanner snapshot
  tests run instead of skipping.
- Absence checks (nothing more is sent) keep a short window: there is
  no event to wait for, and a slow runner only lengthens it.
- Skip testFocusedTransferBadgeDrawsInItsCornerAndMovesNothing: on
  iOS 27 adding the badge sometimes redraws neighbouring glass rims,
  so its pixel-exact diff fails ~1 in 5 without anything moving.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@darioalessandro darioalessandro changed the title Fix a flaky test that's failing CI on master Make tests wait for conditions instead of the clock (fixes flaky CI on Xcode 27) Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

📊 Test Coverage Report

✅ 786/809 tests passed · ⏭️ 23 skipped

Metric Coverage
App code (excl. generated + vendored Theater) ██████████░░░░░░░░░░ 48.3% 🔴
Whole target ██████████░░░░░░░░░░ 49.4% 🔴

🎥 Capture & session core

File Coverage Lines
CaptureEngine.swift ██░░░░░░░░░░░░░░░░░░ 11.8% 258/2195 🔴
RecordingPipeline.swift ████████████████░░░░ 80.7% 706/875 🟢
FrameStreamingCoordinator.swift ███░░░░░░░░░░░░░░░░░ 13.0% 32/247 🔴
FrameStreamer.swift ███████████████████░ 92.6% 113/122 🟢
RemoteCmdFlatBuffers.swift ██████████████████░░ 92.0% 1184/1287 🟢
📁 Full report — 103 files
File Coverage Lines
DebugLog.swift █████████████████████ 104.8% 22/21 🟢
JPEGFrameEncoder.swift ████████████████████ 100.0% 12/12 🟢
CameraPreviewMode.swift ████████████████████ 100.0% 13/13 🟢
AppActivityMonitor.swift ████████████████████ 100.0% 31/31 🟢
AppTheme.swift ████████████████████ 100.0% 33/33 🟢
Messages.swift ████████████████████ 100.0% 7/7 🟢
ZoomScale.swift ████████████████████ 100.0% 56/56 🟢
ZoomScaleSeed.swift ████████████████████ 100.0% 14/14 🟢
MonitorChrome.swift ████████████████████ 100.0% 12/12 🟢
PeerSessionCore.swift ████████████████████ 100.0% 26/26 🟢
ClockOffsetEstimator.swift ████████████████████ 100.0% 24/24 🟢
ExposurePolicy.swift ████████████████████ 100.0% 42/42 🟢
MultipeerMessages.swift ████████████████████ 100.0% 12/12 🟢
StreamingConfig.swift ████████████████████ 100.0% 34/34 🟢
FocusedCameraControlState.swift ████████████████████ 100.0% 6/6 🟢
Locked.swift ████████████████████ 100.0% 19/19 🟢
FrameCreditWindow.swift ████████████████████ 100.0% 9/9 🟢
AppDelegate.swift ████████████████████ 100.0% 16/16 🟢
CameraLink.swift ████████████████████ 100.0% 59/59 🟢
HEICFrameEncoder.swift ████████████████████ 100.0% 17/17 🟢
PeerCompatibility.swift ████████████████████ 100.0% 42/42 🟢
RigQualityMenu.swift ████████████████████ 99.3% 140/141 🟢
DeviceScannerViewModel.swift ████████████████████ 98.9% 183/185 🟢
FrameSender.swift ████████████████████ 98.4% 184/187 🟢
FocusPointMapping.swift ████████████████████ 97.6% 40/41 🟢
CaptureSyncMetadata.swift ███████████████████░ 97.0% 65/67 🟢
MultiCamChrome.swift ███████████████████░ 96.9% 31/32 🟢
RemoteCmds.swift ███████████████████░ 95.9% 325/339 🟢
MulticamViewModel.swift ███████████████████░ 94.7% 143/151 🟢
VP9FrameEncoder.swift ███████████████████░ 94.7% 195/206 🟢
DiscoveredPeers.swift ███████████████████░ 94.4% 17/18 🟢
WatchSharedTypes.swift ███████████████████░ 94.0% 236/251 🟢
WatchPreviewStreamer.swift ███████████████████░ 93.6% 44/47 🟢
MulticamController.swift ███████████████████░ 92.7% 1487/1604 🟢
FrameStreamer.swift ███████████████████░ 92.6% 113/122 🟢
RemoteCmdFlatBuffers.swift ██████████████████░░ 92.0% 1184/1287 🟢
WelcomeView.swift ██████████████████░░ 89.3% 492/551 🟢
VideoTransferProgressView.swift █████████████████░░░ 84.0% 295/351 🟢
UIImage+gif.swift ████████████████░░░░ 81.6% 129/158 🟢
CameraDeviceDescriptor.swift ████████████████░░░░ 81.0% 34/42 🟢
RecordingPipeline.swift ████████████████░░░░ 80.7% 706/875 🟢
CameraProgressOverlayView.swift ████████████████░░░░ 78.5% 186/237 🟡
SessionCoordinator.swift ████████████████░░░░ 77.9% 1870/2399 🟡
UICmds.swift ███████████████░░░░░ 77.4% 72/93 🟡
FlatBufferSchemas_generated.swift (excluded from app-code metric) ███████████████░░░░░ 77.0% 876/1138 🟡
SettingsView.swift ███████████████░░░░░ 76.2% 593/778 🟡
RolePickerView.swift ███████████████░░░░░ 76.2% 314/412 🟡
StreamLog.swift ███████████████░░░░░ 75.0% 15/20 🟡
WelcomeViewModel.swift ███████████████░░░░░ 74.1% 100/135 🟡
CameraRecordingTimerView.swift ███████████████░░░░░ 73.8% 127/172 🟡
SettingsViewModel.swift ██████████████░░░░░░ 70.6% 89/126 🟡
DeviceScannerView.swift ██████████████░░░░░░ 70.1% 1142/1630 🟡
SwiftConstants.swift █████████████░░░░░░░ 66.7% 12/18 🟡
UIAlertController.swift █████████████░░░░░░░ 66.0% 35/53 🟡
MonitorView.swift █████████████░░░░░░░ 65.0% 514/791 🟡
WelcomeViewController.swift █████████████░░░░░░░ 64.1% 41/64 🟡
ScannerLobby.swift ████████████░░░░░░░░ 62.5% 5/8 🟡
CameraViewModel.swift ████████████░░░░░░░░ 59.9% 160/267 🟡
StoreManager.swift ███████████░░░░░░░░░ 54.6% 83/152 🟡
SceneDelegate.swift ██████████░░░░░░░░░░ 51.9% 14/27 🟡
UIViewController+SwiftUIHosting.swift ██████████░░░░░░░░░░ 48.4% 15/31 🔴
FrameStreamReceiver.swift ██████████░░░░░░░░░░ 47.7% 244/512 🔴
FrameCodecs.swift █████████░░░░░░░░░░░ 47.1% 24/51 🔴
MonitorViewModel.swift █████████░░░░░░░░░░░ 44.0% 11/25 🔴
PurchaseManaging.swift ████████░░░░░░░░░░░░ 42.0% 21/50 🔴
MulticamView.swift ████████░░░░░░░░░░░░ 40.1% 544/1356 🔴
CameraScreenView.swift ████████░░░░░░░░░░░░ 39.3% 262/666 🔴
MulticamViewController.swift ████████░░░░░░░░░░░░ 38.0% 130/342 🔴
EnumExtensions.swift ███████░░░░░░░░░░░░░ 33.9% 40/118 🔴
HEVCFrameEncoder.swift ██████░░░░░░░░░░░░░░ 31.9% 110/345 🔴
ExposureControls.swift ██████░░░░░░░░░░░░░░ 30.8% 94/305 🔴
CaptureTally.swift █████░░░░░░░░░░░░░░░ 25.0% 13/52 🔴
RulerPill.swift █████░░░░░░░░░░░░░░░ 24.5% 152/621 🔴
DeviceScannerViewController.swift █████░░░░░░░░░░░░░░░ 24.3% 117/482 🔴
CameraRig.swift █████░░░░░░░░░░░░░░░ 23.8% 150/629 🔴
WatchSessionManager.swift ████░░░░░░░░░░░░░░░░ 20.8% 37/178 🔴
MediaProcessors.swift ███░░░░░░░░░░░░░░░░░ 17.5% 11/63 🔴
SessionDebugConsole.swift ███░░░░░░░░░░░░░░░░░ 15.0% 74/494 🔴
FrameStreamingCoordinator.swift ███░░░░░░░░░░░░░░░░░ 13.0% 32/247 🔴
CaptureEngine.swift ██░░░░░░░░░░░░░░░░░░ 11.8% 258/2195 🔴
RolePickerController.swift ██░░░░░░░░░░░░░░░░░░ 10.2% 23/226 🔴
PeerLinkStatus.swift █░░░░░░░░░░░░░░░░░░░ 7.1% 10/141 🔴
CameraCountdownTorch.swift █░░░░░░░░░░░░░░░░░░░ 4.9% 3/61 🔴
WatchRemoteCameraController.swift █░░░░░░░░░░░░░░░░░░░ 3.2% 9/279 🔴
SoundManager.swift ░░░░░░░░░░░░░░░░░░░░ 2.1% 1/47 🔴
RecordingTimer.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/52 🔴
ViewfinderGestureLayer.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/165 🔴
CameraPermissionsView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/676 🔴
CameraHostController.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/205 🔴
RemoteShutterHelpView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/1372 🔴
CameraPermissionErrorView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/416 🔴
ZoomPill.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/116 🔴
PreviewSurface.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/100 🔴
PhotosAccessDeniedView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/613 🔴
MicrophonePermissionPromptView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/347 🔴
Photos.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/12 🔴
LocalNetworkPermissionView.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/747 🔴
OrientationUtils.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/18 🔴
PermissionManager.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/185 🔴
PhotosAccessHelper.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/347 🔴
UIAlertPresenter.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/43 🔴
MultipeerService.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/123 🔴
Data+MD5.swift ░░░░░░░░░░░░░░░░░░░░ 0.0% 0/4 🔴

Generated from 779d695 · workflow run

@darioalessandro
darioalessandro merged commit f197f4d into master Nov 9, 2026
2 checks passed
@darioalessandro
darioalessandro deleted the fix-flaky-trailing-edge-test branch November 9, 2026 06:15
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