mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 21:45:22 +00:00
Stabilize VoiceRecorder async gate tests
This commit is contained in:
@@ -13,17 +13,16 @@ import Testing
|
|||||||
private final class VoiceRecorderTestSession: SessionApplying, @unchecked Sendable {
|
private final class VoiceRecorderTestSession: SessionApplying, @unchecked Sendable {
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private let activationGate = DispatchSemaphore(value: 0)
|
private let activationGate = DispatchSemaphore(value: 0)
|
||||||
|
private let activationBeganGate = DispatchSemaphore(value: 0)
|
||||||
private let shouldGateFirstActivation: Bool
|
private let shouldGateFirstActivation: Bool
|
||||||
private var gatedFirstActivation = false
|
private var gatedFirstActivation = false
|
||||||
private var _activationCalls: [Bool] = []
|
private var _activationCalls: [Bool] = []
|
||||||
private var _activationBegan = false
|
|
||||||
|
|
||||||
init(gateFirstActivation: Bool = false) {
|
init(gateFirstActivation: Bool = false) {
|
||||||
self.shouldGateFirstActivation = gateFirstActivation
|
self.shouldGateFirstActivation = gateFirstActivation
|
||||||
}
|
}
|
||||||
|
|
||||||
var activationCalls: [Bool] { lock.withLock { _activationCalls } }
|
var activationCalls: [Bool] { lock.withLock { _activationCalls } }
|
||||||
var activationBegan: Bool { lock.withLock { _activationBegan } }
|
|
||||||
|
|
||||||
func setCategory(_ category: AudioSessionCoordinator.Category) throws {}
|
func setCategory(_ category: AudioSessionCoordinator.Category) throws {}
|
||||||
|
|
||||||
@@ -32,14 +31,28 @@ private final class VoiceRecorderTestSession: SessionApplying, @unchecked Sendab
|
|||||||
_activationCalls.append(active)
|
_activationCalls.append(active)
|
||||||
guard active, shouldGateFirstActivation, !gatedFirstActivation else { return false }
|
guard active, shouldGateFirstActivation, !gatedFirstActivation else { return false }
|
||||||
gatedFirstActivation = true
|
gatedFirstActivation = true
|
||||||
_activationBegan = true
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if shouldWait {
|
if shouldWait {
|
||||||
|
activationBeganGate.signal()
|
||||||
activationGate.wait()
|
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() {
|
func resumeActivation() {
|
||||||
activationGate.signal()
|
activationGate.signal()
|
||||||
}
|
}
|
||||||
@@ -142,26 +155,38 @@ private final class TestVoiceAudioRecorderFactory: VoiceAudioRecorderCreating {
|
|||||||
/// this remains deterministic when the full test suite saturates the executor.
|
/// this remains deterministic when the full test suite saturates the executor.
|
||||||
private final class VoiceRecorderPaddingGate: @unchecked Sendable {
|
private final class VoiceRecorderPaddingGate: @unchecked Sendable {
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private var _entered = false
|
private let enteredGate = DispatchSemaphore(value: 0)
|
||||||
private var isOpen = false
|
private var isOpen = false
|
||||||
private var openWaiters: [CheckedContinuation<Void, Never>] = []
|
private var openWaiters: [CheckedContinuation<Void, Never>] = []
|
||||||
|
|
||||||
var entered: Bool { lock.withLock { _entered } }
|
|
||||||
|
|
||||||
func wait() async {
|
func wait() async {
|
||||||
await withCheckedContinuation { continuation in
|
await withCheckedContinuation { continuation in
|
||||||
let resumeImmediately = lock.withLock { () -> Bool in
|
let resumeImmediately = lock.withLock { () -> Bool in
|
||||||
_entered = true
|
|
||||||
guard !isOpen else { return true }
|
guard !isOpen else { return true }
|
||||||
openWaiters.append(continuation)
|
openWaiters.append(continuation)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
enteredGate.signal()
|
||||||
if resumeImmediately {
|
if resumeImmediately {
|
||||||
continuation.resume()
|
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() {
|
func open() {
|
||||||
let waiters = lock.withLock { () -> [CheckedContinuation<Void, Never>] in
|
let waiters = lock.withLock { () -> [CheckedContinuation<Void, Never>] in
|
||||||
isOpen = true
|
isOpen = true
|
||||||
@@ -181,18 +206,6 @@ struct VoiceRecorderTests {
|
|||||||
return url
|
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 {
|
@Test func cancelWhileSessionAcquireIsInFlightNeverCreatesARecorder() async throws {
|
||||||
let directory = try makeTemporaryDirectory()
|
let directory = try makeTemporaryDirectory()
|
||||||
defer { try? FileManager.default.removeItem(at: directory) }
|
defer { try? FileManager.default.removeItem(at: directory) }
|
||||||
@@ -210,7 +223,7 @@ struct VoiceRecorderTests {
|
|||||||
let owner = VoiceRecorder.RecordingOwner()
|
let owner = VoiceRecorder.RecordingOwner()
|
||||||
|
|
||||||
let startTask = Task { try await voiceRecorder.startRecording(owner: owner) }
|
let startTask = Task { try await voiceRecorder.startRecording(owner: owner) }
|
||||||
await waitUntil { session.activationBegan }
|
#expect(await session.waitUntilActivationBegan())
|
||||||
|
|
||||||
await voiceRecorder.cancelRecording(owner: owner)
|
await voiceRecorder.cancelRecording(owner: owner)
|
||||||
session.resumeActivation()
|
session.resumeActivation()
|
||||||
@@ -308,7 +321,7 @@ struct VoiceRecorderTests {
|
|||||||
try await finishingHold.start()
|
try await finishingHold.start()
|
||||||
let firstURL = try #require(factory.urls.first)
|
let firstURL = try #require(factory.urls.first)
|
||||||
let finishTask = Task { await finishingHold.finish() }
|
let finishTask = Task { await finishingHold.finish() }
|
||||||
await waitUntil { paddingGate.entered }
|
#expect(await paddingGate.waitUntilEntered())
|
||||||
|
|
||||||
await #expect(throws: VoiceRecorder.RecorderError.recordingInProgress) {
|
await #expect(throws: VoiceRecorder.RecorderError.recordingInProgress) {
|
||||||
try await rejectedHold.start()
|
try await rejectedHold.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user