mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 17:05:21 +00:00
Deflake GeoRelayDirectoryTests under constrained runners (#1479)
Raises the file-local waitUntil default from 1s to 10s (returns early on success, so zero cost on healthy runs) and fixes a latent race in the observers test: it waited on the fetch-probe request count, which increments before handleFetchSuccess settles isFetching/lastFetchAt on the MainActor, so the next trigger notification posted into that gap was silently swallowed by the guard. Now waits on the .geoRelayDirectoryDidRefresh notification posted at the end of the synchronous success handler. Negative checks assert against captured baselines. No assertion weakened; 15/15 x 9 local runs including under 2x-ncpu CPU load.
This commit is contained in:
@@ -290,11 +290,15 @@ final class GeoRelayDirectoryTests: XCTestCase {
|
||||
harness.userDefaults.set(harness.clock.now, forKey: "georelay.lastFetchAt")
|
||||
let directory = GeoRelayDirectory(dependencies: harness.dependencies)
|
||||
|
||||
let baselineRequestCount = await harness.fetcher.recordedRequestCount()
|
||||
directory.prefetchIfNeeded()
|
||||
// Negative check: nothing should have been scheduled, so there is no
|
||||
// condition to wait on. A modest delay gives a wrongly spawned fetch
|
||||
// task a chance to run before we compare against the baseline.
|
||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
||||
|
||||
let requestCount = await harness.fetcher.recordedRequestCount()
|
||||
XCTAssertEqual(requestCount, 0)
|
||||
XCTAssertEqual(requestCount, baselineRequestCount)
|
||||
XCTAssertFalse(directory.debugHasRetryTask)
|
||||
}
|
||||
|
||||
@@ -319,9 +323,11 @@ final class GeoRelayDirectoryTests: XCTestCase {
|
||||
XCTAssertFalse(directory.debugHasRetryTask)
|
||||
|
||||
directory.prefetchIfNeeded(force: true)
|
||||
// Negative check against the captured baseline: the forced refetch
|
||||
// must be skipped, so there is no condition to wait on.
|
||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
||||
let forcedRequestCount = await harness.fetcher.recordedRequestCount()
|
||||
XCTAssertEqual(forcedRequestCount, 1)
|
||||
XCTAssertEqual(forcedRequestCount, requestCount)
|
||||
}
|
||||
|
||||
func test_prefetchIfNeeded_runsRemoteFetchOffMainThread() async {
|
||||
@@ -436,26 +442,43 @@ final class GeoRelayDirectoryTests: XCTestCase {
|
||||
autoStart: true,
|
||||
activeNotificationName: activeNotification
|
||||
)
|
||||
var directory: GeoRelayDirectory? = GeoRelayDirectory(dependencies: harness.dependencies)
|
||||
let initialFetch = await waitUntil {
|
||||
await harness.fetcher.recordedRequestCount() == 1
|
||||
// Wait on the refresh notification rather than the raw request count:
|
||||
// the request count increments before the directory finishes handling
|
||||
// the response on the main actor (resetting `isFetching`, recording
|
||||
// `lastFetchAt`). Posting the next trigger inside that gap would get
|
||||
// swallowed by the in-flight guard. The notification is posted at the
|
||||
// end of the synchronous success handler, so once it fires the next
|
||||
// trigger is guaranteed to be accepted.
|
||||
var refreshCount = 0
|
||||
let refreshObserver = harness.notificationCenter.addObserver(
|
||||
forName: .geoRelayDirectoryDidRefresh,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
refreshCount += 1
|
||||
}
|
||||
defer { harness.notificationCenter.removeObserver(refreshObserver) }
|
||||
|
||||
var directory: GeoRelayDirectory? = GeoRelayDirectory(dependencies: harness.dependencies)
|
||||
let initialFetch = await waitUntil { refreshCount == 1 }
|
||||
XCTAssertTrue(initialFetch)
|
||||
var requestCount = await harness.fetcher.recordedRequestCount()
|
||||
XCTAssertEqual(requestCount, 1)
|
||||
XCTAssertEqual(directory?.debugObserverCount, 2)
|
||||
|
||||
harness.clock.now = harness.clock.now.addingTimeInterval(6)
|
||||
harness.notificationCenter.post(name: .TorDidBecomeReady, object: nil)
|
||||
let torTriggered = await waitUntil {
|
||||
await harness.fetcher.recordedRequestCount() == 2
|
||||
}
|
||||
let torTriggered = await waitUntil { refreshCount == 2 }
|
||||
XCTAssertTrue(torTriggered)
|
||||
requestCount = await harness.fetcher.recordedRequestCount()
|
||||
XCTAssertEqual(requestCount, 2)
|
||||
|
||||
harness.clock.now = harness.clock.now.addingTimeInterval(61)
|
||||
harness.notificationCenter.post(name: activeNotification, object: nil)
|
||||
let activeTriggered = await waitUntil {
|
||||
await harness.fetcher.recordedRequestCount() == 3
|
||||
}
|
||||
let activeTriggered = await waitUntil { refreshCount == 3 }
|
||||
XCTAssertTrue(activeTriggered)
|
||||
requestCount = await harness.fetcher.recordedRequestCount()
|
||||
XCTAssertEqual(requestCount, 3)
|
||||
|
||||
weak var weakDirectory: GeoRelayDirectory?
|
||||
weakDirectory = directory
|
||||
@@ -553,8 +576,12 @@ final class GeoRelayDirectoryTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
/// Polls until `condition` holds. The timeout is deliberately generous:
|
||||
/// constrained CI runners (2-core, serialized testing) can starve the
|
||||
/// detached utility-priority fetch task for seconds before it runs, and
|
||||
/// a successful wait returns as soon as the condition becomes true.
|
||||
private func waitUntil(
|
||||
timeout: TimeInterval = 1.0,
|
||||
timeout: TimeInterval = 10.0,
|
||||
condition: @escaping @MainActor () async -> Bool
|
||||
) async -> Bool {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
|
||||
Reference in New Issue
Block a user