From 44fadeeaba2a20e3f1dab99ad7b372814fa35f0c Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 26 Jul 2026 22:00:04 +0100 Subject: [PATCH] Raise two more starvation-prone test deadlines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both surfaced on the CI runs for this PR and #1487, both in tests neither PR touches, both the same shape as the two already fixed here: a deadline sized for the work rather than for a runner executing many suites at once. VoiceNotePlaybackControllerTests waited 5s for a @MainActor Task that playback schedules for the session acquire and its failure path. When that Task is not scheduled in time the helper reports *two* failures — the wait, and the `!isPlaying` the un-run failure path has not reset yet — which reads like a playback bug rather than a starved scheduler. That is exactly what CI showed. GeoRelayDirectoryTests waited 10s for work the directory runs in Task.detached(priority: .utility); utility priority competes with every other suite. The retry-scheduling case timed out at exactly 10.06s with the retry never scheduled, reading like a missing retry rather than a starved background task. Raised to 30s each with the reasoning recorded at the helper. Both helpers return as soon as their condition holds, so nothing slows down when tests pass — only the genuine-failure case takes longer to report. Verified 3x with all cores saturated: both suites clean. Co-Authored-By: Claude Opus 5 --- bitchatTests/Nostr/GeoRelayDirectoryTests.swift | 10 +++++++++- .../VoiceNotePlaybackControllerTests.swift | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/bitchatTests/Nostr/GeoRelayDirectoryTests.swift b/bitchatTests/Nostr/GeoRelayDirectoryTests.swift index 3a9c92ff..92ef6e4f 100644 --- a/bitchatTests/Nostr/GeoRelayDirectoryTests.swift +++ b/bitchatTests/Nostr/GeoRelayDirectoryTests.swift @@ -580,8 +580,16 @@ final class GeoRelayDirectoryTests: XCTestCase { /// constrained CI runners (2-core, serialized testing) can starve the /// detached utility-priority fetch task for seconds before it runs, and /// a successful wait returns as soon as the condition becomes true. + /// Default deliberately far larger than the work being awaited. + /// + /// The directory performs its fetch in a `Task.detached(priority: .utility)`, + /// and utility priority competes with every other suite on a CI runner. At + /// ten seconds the retry-scheduling test timed out at exactly 10.06s with + /// the retry never scheduled — which reads like a missing retry rather than + /// a starved background task. Returning as soon as the condition holds means + /// a longer deadline only extends the genuine-failure case. private func waitUntil( - timeout: TimeInterval = 10.0, + timeout: TimeInterval = 30.0, condition: @escaping @MainActor () async -> Bool ) async -> Bool { let deadline = Date().addingTimeInterval(timeout) diff --git a/bitchatTests/VoiceNotePlaybackControllerTests.swift b/bitchatTests/VoiceNotePlaybackControllerTests.swift index f7853f1d..d6221b15 100644 --- a/bitchatTests/VoiceNotePlaybackControllerTests.swift +++ b/bitchatTests/VoiceNotePlaybackControllerTests.swift @@ -59,11 +59,24 @@ struct VoiceNotePlaybackControllerTests { return url } + /// Waits for an async settle, then asserts. + /// + /// The deadline is deliberately far larger than the work it waits on. Every + /// condition here depends on a `@MainActor` Task that playback schedules + /// (the session acquire and its failure path), and on a CI runner executing + /// many suites in parallel that Task can simply not be scheduled for + /// seconds. At five seconds this timed out on CI and reported *two* + /// failures — the wait itself, and the `!isPlaying` that the un-run failure + /// path had not yet reset — which reads like a playback bug rather than a + /// starved scheduler. + /// + /// A generous deadline costs nothing when the condition holds, since this + /// returns as soon as it does; it only extends the genuine-failure case. private func waitUntil( _ condition: () -> Bool, sourceLocation: SourceLocation = #_sourceLocation ) async { - let deadline = ContinuousClock.now.advanced(by: .seconds(5)) + let deadline = ContinuousClock.now.advanced(by: .seconds(30)) while !condition(), ContinuousClock.now < deadline { await Task.yield() try? await Task.sleep(nanoseconds: 1_000_000)