mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-15 07:06:11 +00:00
test: remove concurrent suite timing races
This commit is contained in:
parent
b7e11ff3f4
commit
255cc88d80
@ -816,6 +816,8 @@ struct BLEServiceCoreTests {
|
||||
let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData())
|
||||
let reconciled = SessionReconcileCounter()
|
||||
ble._test_onPrivateMediaSessionReconciled = reconciled.record
|
||||
let outbound = OutboundPacketTap()
|
||||
ble._test_onOutboundPacket = outbound.record
|
||||
|
||||
// Establish BLE as responder so the inbound reconnect below is not
|
||||
// coalesced by the initiator-completion grace path.
|
||||
@ -838,12 +840,23 @@ struct BLEServiceCoreTests {
|
||||
)
|
||||
await ble._test_drainNoiseMessagePipeline()
|
||||
#expect(ble.canDeliverSecurely(to: alicePeerID))
|
||||
let initialEncryptedFrameSent = await TestHelpers.waitUntil(
|
||||
{ outbound.count(ofType: .noiseEncrypted) >= 1 },
|
||||
timeout: TestConstants.longTimeout
|
||||
)
|
||||
try #require(initialEncryptedFrameSent)
|
||||
#expect(outbound.count(ofType: .noiseEncrypted) == 1)
|
||||
let initialReconcileRan = await TestHelpers.waitUntil(
|
||||
{ reconciled.count(for: alicePeerID) == 1 },
|
||||
timeout: TestConstants.longTimeout
|
||||
)
|
||||
try #require(initialReconcileRan)
|
||||
await ble._test_drainNoiseMessagePipeline()
|
||||
// Keep one tap installed for the service's whole lifetime. After the
|
||||
// explicit initial-frame observation and queue fence, start a fresh,
|
||||
// lock-protected capture epoch so every assertion below measures only
|
||||
// rollback/convergence output under a saturated concurrent suite.
|
||||
outbound.removeAll()
|
||||
|
||||
// The convergence retry only prepares for reachable peers.
|
||||
ble._test_seedConnectedPeer(alicePeerID, nickname: "Alice")
|
||||
@ -854,8 +867,6 @@ struct BLEServiceCoreTests {
|
||||
let recoveryGate = HandshakeRecoveryEnqueueGate()
|
||||
defer { recoveryGate.release() }
|
||||
ble._test_beforeHandshakeRecoveryEnqueued = { _ in recoveryGate.pause() }
|
||||
let outbound = OutboundPacketTap()
|
||||
ble._test_onOutboundPacket = outbound.record
|
||||
|
||||
// Park the traffic the race would lose directly in the pending
|
||||
// queues — the same place live sends land during quarantine — so no
|
||||
@ -1371,6 +1382,10 @@ private final class OutboundPacketTap {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
return packets
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
lock.lock(); packets.removeAll(); lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/// Blocks the convergence-recovery callback on its global-queue thread so a
|
||||
|
||||
@ -10,10 +10,41 @@ import Foundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
/// One-shot event that bridges synchronous production seams to async tests
|
||||
/// without blocking a shared dispatch worker while waiting for the seam.
|
||||
private final class VoiceRecorderAsyncEvent: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var isSignaled = false
|
||||
private var waiters: [CheckedContinuation<Void, Never>] = []
|
||||
|
||||
func wait() async {
|
||||
await withCheckedContinuation { continuation in
|
||||
let resumeImmediately = lock.withLock { () -> Bool in
|
||||
guard !isSignaled else { return true }
|
||||
waiters.append(continuation)
|
||||
return false
|
||||
}
|
||||
if resumeImmediately {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func signal() {
|
||||
let continuations = lock.withLock { () -> [CheckedContinuation<Void, Never>] in
|
||||
guard !isSignaled else { return [] }
|
||||
isSignaled = true
|
||||
defer { waiters.removeAll() }
|
||||
return waiters
|
||||
}
|
||||
continuations.forEach { $0.resume() }
|
||||
}
|
||||
}
|
||||
|
||||
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 activationBegan = VoiceRecorderAsyncEvent()
|
||||
private let shouldGateFirstActivation: Bool
|
||||
private var gatedFirstActivation = false
|
||||
private var _activationCalls: [Bool] = []
|
||||
@ -34,23 +65,13 @@ private final class VoiceRecorderTestSession: SessionApplying, @unchecked Sendab
|
||||
return true
|
||||
}
|
||||
if shouldWait {
|
||||
activationBeganGate.signal()
|
||||
activationBegan.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 waitUntilActivationBegan() async {
|
||||
await activationBegan.wait()
|
||||
}
|
||||
|
||||
func resumeActivation() {
|
||||
@ -155,7 +176,7 @@ 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 let enteredGate = DispatchSemaphore(value: 0)
|
||||
private let entered = VoiceRecorderAsyncEvent()
|
||||
private var isOpen = false
|
||||
private var openWaiters: [CheckedContinuation<Void, Never>] = []
|
||||
|
||||
@ -166,25 +187,15 @@ private final class VoiceRecorderPaddingGate: @unchecked Sendable {
|
||||
openWaiters.append(continuation)
|
||||
return false
|
||||
}
|
||||
enteredGate.signal()
|
||||
entered.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 waitUntilEntered() async {
|
||||
await entered.wait()
|
||||
}
|
||||
|
||||
func open() {
|
||||
@ -223,7 +234,7 @@ struct VoiceRecorderTests {
|
||||
let owner = VoiceRecorder.RecordingOwner()
|
||||
|
||||
let startTask = Task { try await voiceRecorder.startRecording(owner: owner) }
|
||||
#expect(await session.waitUntilActivationBegan())
|
||||
await session.waitUntilActivationBegan()
|
||||
|
||||
await voiceRecorder.cancelRecording(owner: owner)
|
||||
session.resumeActivation()
|
||||
@ -321,7 +332,7 @@ struct VoiceRecorderTests {
|
||||
try await finishingHold.start()
|
||||
let firstURL = try #require(factory.urls.first)
|
||||
let finishTask = Task { await finishingHold.finish() }
|
||||
#expect(await paddingGate.waitUntilEntered())
|
||||
await paddingGate.waitUntilEntered()
|
||||
|
||||
await #expect(throws: VoiceRecorder.RecorderError.recordingInProgress) {
|
||||
try await rejectedHold.start()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user