From 726906681f2d6899544ec8404d8a3dc6baa07f70 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 12 Jul 2026 12:08:06 -0400 Subject: [PATCH] Make Noise generation race test deterministic --- bitchatTests/Noise/NoiseCoverageTests.swift | 38 ++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/bitchatTests/Noise/NoiseCoverageTests.swift b/bitchatTests/Noise/NoiseCoverageTests.swift index 61de0795..f1e85e4a 100644 --- a/bitchatTests/Noise/NoiseCoverageTests.swift +++ b/bitchatTests/Noise/NoiseCoverageTests.swift @@ -703,30 +703,48 @@ struct NoiseCoverageTests { oldSession.pauseNextDecrypt() let decryptResult = ConcurrentTestResult<(plaintext: Data, sessionGeneration: UUID)>() - DispatchQueue.global().async { + var promotionResultForCleanup: ConcurrentTestResult? + 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 { 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 promotionResult = ConcurrentTestResult() - DispatchQueue.global().async { + promotionResultForCleanup = promotionResult + let promotionThread = Thread { promotionStarted.signal() promotionResult.capture { 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( promotionResult.wait(timeout: 0.05) == nil, "Promotion must wait for the exact decrypting-session lease" ) oldSession.resumeDecrypt() - let decrypted = try #require(decryptResult.wait(timeout: 2)).get() - _ = try #require(promotionResult.wait(timeout: 2)).get() + let decrypted = try #require(decryptResult.wait(timeout: 5)).get() + _ = try #require(promotionResult.wait(timeout: 5)).get() #expect(decrypted.plaintext == Data("old session".utf8)) #expect(decrypted.sessionGeneration == oldGeneration) @@ -1008,15 +1026,19 @@ private final class BlockingDecryptNoiseSession: NoiseSession, @unchecked Sendab private final class ConcurrentTestResult: @unchecked Sendable { private let lock = NSLock() - private let completed = DispatchSemaphore(value: 0) + private let completed = DispatchGroup() private var storedResult: Result? + init() { + completed.enter() + } + func capture(_ operation: () throws -> Value) { let result = Result(catching: operation) lock.lock() storedResult = result lock.unlock() - completed.signal() + completed.leave() } func wait(timeout: TimeInterval) -> Result? {