From a30c7078a2984f8e77e88d067f3ffe626458b55c Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 26 Jul 2026 08:25:06 +0200 Subject: [PATCH] Stabilize VoiceRecorder async gate tests --- bitchatTests/VoiceRecorderTests.swift | 55 +++++++++++++++++---------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/bitchatTests/VoiceRecorderTests.swift b/bitchatTests/VoiceRecorderTests.swift index 116e73fe..c9b6839e 100644 --- a/bitchatTests/VoiceRecorderTests.swift +++ b/bitchatTests/VoiceRecorderTests.swift @@ -13,17 +13,16 @@ import Testing private final class VoiceRecorderTestSession: SessionApplying, @unchecked Sendable { private let lock = NSLock() private let activationGate = DispatchSemaphore(value: 0) + private let activationBeganGate = DispatchSemaphore(value: 0) private let shouldGateFirstActivation: Bool private var gatedFirstActivation = false private var _activationCalls: [Bool] = [] - private var _activationBegan = false init(gateFirstActivation: Bool = false) { self.shouldGateFirstActivation = gateFirstActivation } var activationCalls: [Bool] { lock.withLock { _activationCalls } } - var activationBegan: Bool { lock.withLock { _activationBegan } } func setCategory(_ category: AudioSessionCoordinator.Category) throws {} @@ -32,14 +31,28 @@ private final class VoiceRecorderTestSession: SessionApplying, @unchecked Sendab _activationCalls.append(active) guard active, shouldGateFirstActivation, !gatedFirstActivation else { return false } gatedFirstActivation = true - _activationBegan = true return true } if shouldWait { + activationBeganGate.signal() activationGate.wait() } } + func waitUntilActivationBegan( + timeout: DispatchTimeInterval = .seconds(5) + ) async -> Bool { + await withCheckedContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { + continuation.resume( + returning: self.activationBeganGate.wait( + timeout: DispatchTime.now() + timeout + ) == .success + ) + } + } + } + func resumeActivation() { activationGate.signal() } @@ -142,26 +155,38 @@ private final class TestVoiceAudioRecorderFactory: VoiceAudioRecorderCreating { /// this remains deterministic when the full test suite saturates the executor. private final class VoiceRecorderPaddingGate: @unchecked Sendable { private let lock = NSLock() - private var _entered = false + private let enteredGate = DispatchSemaphore(value: 0) private var isOpen = false private var openWaiters: [CheckedContinuation] = [] - var entered: Bool { lock.withLock { _entered } } - func wait() async { await withCheckedContinuation { continuation in let resumeImmediately = lock.withLock { () -> Bool in - _entered = true guard !isOpen else { return true } openWaiters.append(continuation) return false } + enteredGate.signal() if resumeImmediately { continuation.resume() } } } + func waitUntilEntered( + timeout: DispatchTimeInterval = .seconds(5) + ) async -> Bool { + await withCheckedContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { + continuation.resume( + returning: self.enteredGate.wait( + timeout: DispatchTime.now() + timeout + ) == .success + ) + } + } + } + func open() { let waiters = lock.withLock { () -> [CheckedContinuation] in isOpen = true @@ -181,18 +206,6 @@ struct VoiceRecorderTests { return url } - private func waitUntil( - _ condition: () -> Bool, - sourceLocation: SourceLocation = #_sourceLocation - ) async { - let deadline = ContinuousClock.now.advanced(by: .seconds(5)) - while !condition(), ContinuousClock.now < deadline { - await Task.yield() - try? await Task.sleep(nanoseconds: 1_000_000) - } - #expect(condition(), sourceLocation: sourceLocation) - } - @Test func cancelWhileSessionAcquireIsInFlightNeverCreatesARecorder() async throws { let directory = try makeTemporaryDirectory() defer { try? FileManager.default.removeItem(at: directory) } @@ -210,7 +223,7 @@ struct VoiceRecorderTests { let owner = VoiceRecorder.RecordingOwner() let startTask = Task { try await voiceRecorder.startRecording(owner: owner) } - await waitUntil { session.activationBegan } + #expect(await session.waitUntilActivationBegan()) await voiceRecorder.cancelRecording(owner: owner) session.resumeActivation() @@ -308,7 +321,7 @@ struct VoiceRecorderTests { try await finishingHold.start() let firstURL = try #require(factory.urls.first) let finishTask = Task { await finishingHold.finish() } - await waitUntil { paddingGate.entered } + #expect(await paddingGate.waitUntilEntered()) await #expect(throws: VoiceRecorder.RecorderError.recordingInProgress) { try await rejectedHold.start()