Make Noise generation race test deterministic

This commit is contained in:
jack
2026-07-12 12:08:06 -04:00
parent 34961b9ad6
commit 726906681f
+30 -8
View File
@@ -703,30 +703,48 @@ struct NoiseCoverageTests {
oldSession.pauseNextDecrypt() oldSession.pauseNextDecrypt()
let decryptResult = ConcurrentTestResult<(plaintext: Data, sessionGeneration: UUID)>() let decryptResult = ConcurrentTestResult<(plaintext: Data, sessionGeneration: UUID)>()
DispatchQueue.global().async { var promotionResultForCleanup: ConcurrentTestResult<Data?>?
defer {
// A failed startup requirement must not strand a late thread in
// the blocking test double after the test has returned.
oldSession.resumeDecrypt()
_ = decryptResult.wait(timeout: 5)
if let promotionResultForCleanup {
_ = promotionResultForCleanup.wait(timeout: 5)
}
}
let decryptThread = Thread {
decryptResult.capture { decryptResult.capture {
try aliceManager.decryptWithSessionGeneration(ciphertext, from: self.alicePeerID) try aliceManager.decryptWithSessionGeneration(ciphertext, from: self.alicePeerID)
} }
} }
#expect(oldSession.waitForDecryptStart(timeout: 2)) decryptThread.name = "NoiseCoverageTests.staleDecrypt.decrypt"
decryptThread.qualityOfService = .userInitiated
decryptThread.start()
try #require(oldSession.waitForDecryptStart(timeout: 5))
let promotionStarted = DispatchSemaphore(value: 0) let promotionStarted = DispatchSemaphore(value: 0)
let promotionResult = ConcurrentTestResult<Data?>() let promotionResult = ConcurrentTestResult<Data?>()
DispatchQueue.global().async { promotionResultForCleanup = promotionResult
let promotionThread = Thread {
promotionStarted.signal() promotionStarted.signal()
promotionResult.capture { promotionResult.capture {
try aliceManager.handleIncomingHandshake(from: self.alicePeerID, message: message3) try aliceManager.handleIncomingHandshake(from: self.alicePeerID, message: message3)
} }
} }
#expect(promotionStarted.wait(timeout: .now() + 2) == .success) promotionThread.name = "NoiseCoverageTests.staleDecrypt.promote"
promotionThread.qualityOfService = .userInitiated
promotionThread.start()
try #require(promotionStarted.wait(timeout: .now() + 5) == .success)
#expect( #expect(
promotionResult.wait(timeout: 0.05) == nil, promotionResult.wait(timeout: 0.05) == nil,
"Promotion must wait for the exact decrypting-session lease" "Promotion must wait for the exact decrypting-session lease"
) )
oldSession.resumeDecrypt() oldSession.resumeDecrypt()
let decrypted = try #require(decryptResult.wait(timeout: 2)).get() let decrypted = try #require(decryptResult.wait(timeout: 5)).get()
_ = try #require(promotionResult.wait(timeout: 2)).get() _ = try #require(promotionResult.wait(timeout: 5)).get()
#expect(decrypted.plaintext == Data("old session".utf8)) #expect(decrypted.plaintext == Data("old session".utf8))
#expect(decrypted.sessionGeneration == oldGeneration) #expect(decrypted.sessionGeneration == oldGeneration)
@@ -1008,15 +1026,19 @@ private final class BlockingDecryptNoiseSession: NoiseSession, @unchecked Sendab
private final class ConcurrentTestResult<Value>: @unchecked Sendable { private final class ConcurrentTestResult<Value>: @unchecked Sendable {
private let lock = NSLock() private let lock = NSLock()
private let completed = DispatchSemaphore(value: 0) private let completed = DispatchGroup()
private var storedResult: Result<Value, Error>? private var storedResult: Result<Value, Error>?
init() {
completed.enter()
}
func capture(_ operation: () throws -> Value) { func capture(_ operation: () throws -> Value) {
let result = Result(catching: operation) let result = Result(catching: operation)
lock.lock() lock.lock()
storedResult = result storedResult = result
lock.unlock() lock.unlock()
completed.signal() completed.leave()
} }
func wait(timeout: TimeInterval) -> Result<Value, Error>? { func wait(timeout: TimeInterval) -> Result<Value, Error>? {