Stabilize VoiceRecorder async gate tests

This commit is contained in:
jack
2026-07-26 08:30:37 +02:00
parent 5d92dca135
commit 84bed52d08
+34 -21
View File
@@ -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<Void, Never>] = []
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<Void, Never>] 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()