mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 15:05:24 +00:00
Make nearby notes tests parallel-safe (#1471)
Injects the location-notes enabled getter and settings-change publisher into NearbyNotesCounter (defaults reproduce the prior wiring exactly), so tests no longer mutate the persistent UserDefaults key shared across parallel test worker processes. No production behavior change. Un-stacked from the Codex chain (both files were byte-identical between main and the parent branch) and rebased onto the post-#1479 deflaked test suite.
This commit is contained in:
@@ -33,15 +33,25 @@ final class NearbyNotesCounter: ObservableObject {
|
|||||||
private let locationManager: LocationChannelManager
|
private let locationManager: LocationChannelManager
|
||||||
private let managerFactory: @MainActor (String) -> LocationNotesManager
|
private let managerFactory: @MainActor (String) -> LocationNotesManager
|
||||||
private let releaseManager: @MainActor (LocationNotesManager?) -> Void
|
private let releaseManager: @MainActor (LocationNotesManager?) -> Void
|
||||||
|
private let locationNotesEnabled: @MainActor () -> Bool
|
||||||
|
private let locationNotesSettingsPublisher: AnyPublisher<Void, Never>
|
||||||
|
|
||||||
init(
|
init(
|
||||||
locationManager: LocationChannelManager = .shared,
|
locationManager: LocationChannelManager = .shared,
|
||||||
managerFactory: @escaping @MainActor (String) -> LocationNotesManager = { LocationNotesPool.shared.acquire($0) },
|
managerFactory: @escaping @MainActor (String) -> LocationNotesManager = { LocationNotesPool.shared.acquire($0) },
|
||||||
releaseManager: @escaping @MainActor (LocationNotesManager?) -> Void = { LocationNotesPool.shared.release($0) }
|
releaseManager: @escaping @MainActor (LocationNotesManager?) -> Void = { LocationNotesPool.shared.release($0) },
|
||||||
|
locationNotesEnabled: @escaping @MainActor () -> Bool = { LocationNotesSettings.enabled },
|
||||||
|
locationNotesSettings: AnyPublisher<Void, Never>? = nil
|
||||||
) {
|
) {
|
||||||
self.locationManager = locationManager
|
self.locationManager = locationManager
|
||||||
self.managerFactory = managerFactory
|
self.managerFactory = managerFactory
|
||||||
self.releaseManager = releaseManager
|
self.releaseManager = releaseManager
|
||||||
|
self.locationNotesEnabled = locationNotesEnabled
|
||||||
|
self.locationNotesSettingsPublisher = locationNotesSettings
|
||||||
|
?? NotificationCenter.default
|
||||||
|
.publisher(for: LocationNotesSettings.didChangeNotification)
|
||||||
|
.map { _ in () }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the empty-timeline "check for notes" hint should render.
|
/// Whether the empty-timeline "check for notes" hint should render.
|
||||||
@@ -53,7 +63,7 @@ final class NearbyNotesCounter: ObservableObject {
|
|||||||
/// passes its own observed permission state so the hint re-renders when
|
/// passes its own observed permission state so the hint re-renders when
|
||||||
/// authorization changes.
|
/// authorization changes.
|
||||||
func offersRevealHint(permissionState: LocationChannelManager.PermissionState) -> Bool {
|
func offersRevealHint(permissionState: LocationChannelManager.PermissionState) -> Bool {
|
||||||
!revealed && LocationNotesSettings.enabled && permissionState == .authorized
|
!revealed && locationNotesEnabled() && permissionState == .authorized
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks the one explicit act that lets the counter subscribe. Sticky for
|
/// Marks the one explicit act that lets the counter subscribe. Sticky for
|
||||||
@@ -83,8 +93,7 @@ final class NearbyNotesCounter: ObservableObject {
|
|||||||
.sink { [weak self] _ in self?.retarget() }
|
.sink { [weak self] _ in self?.retarget() }
|
||||||
// The app-info kill switch must take effect immediately, not on the
|
// The app-info kill switch must take effect immediately, not on the
|
||||||
// next location change or remount.
|
// next location change or remount.
|
||||||
settingCancellable = NotificationCenter.default
|
settingCancellable = locationNotesSettingsPublisher
|
||||||
.publisher(for: LocationNotesSettings.didChangeNotification)
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] _ in self?.retarget() }
|
.sink { [weak self] _ in self?.retarget() }
|
||||||
retarget()
|
retarget()
|
||||||
@@ -105,7 +114,7 @@ final class NearbyNotesCounter: ObservableObject {
|
|||||||
private func retarget() {
|
private func retarget() {
|
||||||
guard activeHolders > 0,
|
guard activeHolders > 0,
|
||||||
revealed,
|
revealed,
|
||||||
LocationNotesSettings.enabled,
|
locationNotesEnabled(),
|
||||||
locationManager.permissionState == .authorized,
|
locationManager.permissionState == .authorized,
|
||||||
let geohash = locationManager.availableChannels
|
let geohash = locationManager.availableChannels
|
||||||
.first(where: { $0.level == .building })?.geohash
|
.first(where: { $0.level == .building })?.geohash
|
||||||
|
|||||||
@@ -8,25 +8,9 @@ import XCTest
|
|||||||
/// the pooled subscription must come up exactly once and go down exactly once.
|
/// the pooled subscription must come up exactly once and go down exactly once.
|
||||||
@MainActor
|
@MainActor
|
||||||
final class NearbyNotesCounterTests: XCTestCase {
|
final class NearbyNotesCounterTests: XCTestCase {
|
||||||
private var previousNotesEnabled: Any?
|
|
||||||
|
|
||||||
override func setUp() {
|
|
||||||
super.setUp()
|
|
||||||
previousNotesEnabled = UserDefaults.standard.object(forKey: "locationNotes.enabled")
|
|
||||||
UserDefaults.standard.set(true, forKey: "locationNotes.enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDown() {
|
|
||||||
if let previous = previousNotesEnabled as? Bool {
|
|
||||||
UserDefaults.standard.set(previous, forKey: "locationNotes.enabled")
|
|
||||||
} else {
|
|
||||||
UserDefaults.standard.removeObject(forKey: "locationNotes.enabled")
|
|
||||||
}
|
|
||||||
super.tearDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
func test_counterOnlySubscribesAfterReveal_countsUnexpiredNotes_andUnsubscribesOnDeactivate() async throws {
|
func test_counterOnlySubscribesAfterReveal_countsUnexpiredNotes_andUnsubscribesOnDeactivate() async throws {
|
||||||
let relays = SubscriptionRecorder()
|
let relays = SubscriptionRecorder()
|
||||||
|
let settings = LocationNotesSettingsStub()
|
||||||
let locationManager = try await makeAuthorizedLocationManager()
|
let locationManager = try await makeAuthorizedLocationManager()
|
||||||
let buildingGeohash = try XCTUnwrap(
|
let buildingGeohash = try XCTUnwrap(
|
||||||
locationManager.availableChannels.first(where: { $0.level == .building })?.geohash
|
locationManager.availableChannels.first(where: { $0.level == .building })?.geohash
|
||||||
@@ -35,7 +19,9 @@ final class NearbyNotesCounterTests: XCTestCase {
|
|||||||
let counter = NearbyNotesCounter(
|
let counter = NearbyNotesCounter(
|
||||||
locationManager: locationManager,
|
locationManager: locationManager,
|
||||||
managerFactory: { LocationNotesManager(geohash: $0, dependencies: relays.dependencies) },
|
managerFactory: { LocationNotesManager(geohash: $0, dependencies: relays.dependencies) },
|
||||||
releaseManager: { $0?.cancel() }
|
releaseManager: { $0?.cancel() },
|
||||||
|
locationNotesEnabled: { settings.enabled },
|
||||||
|
locationNotesSettings: settings.changes
|
||||||
)
|
)
|
||||||
|
|
||||||
counter.activate()
|
counter.activate()
|
||||||
@@ -93,11 +79,14 @@ final class NearbyNotesCounterTests: XCTestCase {
|
|||||||
|
|
||||||
func test_permissionRevocation_releasesBuildingSubscriptionDespiteCachedChannels() async throws {
|
func test_permissionRevocation_releasesBuildingSubscriptionDespiteCachedChannels() async throws {
|
||||||
let relays = SubscriptionRecorder()
|
let relays = SubscriptionRecorder()
|
||||||
|
let settings = LocationNotesSettingsStub()
|
||||||
let locationManager = try await makeAuthorizedLocationManager()
|
let locationManager = try await makeAuthorizedLocationManager()
|
||||||
let counter = NearbyNotesCounter(
|
let counter = NearbyNotesCounter(
|
||||||
locationManager: locationManager,
|
locationManager: locationManager,
|
||||||
managerFactory: { LocationNotesManager(geohash: $0, dependencies: relays.dependencies) },
|
managerFactory: { LocationNotesManager(geohash: $0, dependencies: relays.dependencies) },
|
||||||
releaseManager: { $0?.cancel() }
|
releaseManager: { $0?.cancel() },
|
||||||
|
locationNotesEnabled: { settings.enabled },
|
||||||
|
locationNotesSettings: settings.changes
|
||||||
)
|
)
|
||||||
|
|
||||||
counter.activate()
|
counter.activate()
|
||||||
@@ -122,24 +111,27 @@ final class NearbyNotesCounterTests: XCTestCase {
|
|||||||
|
|
||||||
func test_locationNotesKillSwitch_releasesAndCanReacquireBuildingSubscription() async throws {
|
func test_locationNotesKillSwitch_releasesAndCanReacquireBuildingSubscription() async throws {
|
||||||
let relays = SubscriptionRecorder()
|
let relays = SubscriptionRecorder()
|
||||||
|
let settings = LocationNotesSettingsStub()
|
||||||
let locationManager = try await makeAuthorizedLocationManager()
|
let locationManager = try await makeAuthorizedLocationManager()
|
||||||
let counter = NearbyNotesCounter(
|
let counter = NearbyNotesCounter(
|
||||||
locationManager: locationManager,
|
locationManager: locationManager,
|
||||||
managerFactory: { LocationNotesManager(geohash: $0, dependencies: relays.dependencies) },
|
managerFactory: { LocationNotesManager(geohash: $0, dependencies: relays.dependencies) },
|
||||||
releaseManager: { $0?.cancel() }
|
releaseManager: { $0?.cancel() },
|
||||||
|
locationNotesEnabled: { settings.enabled },
|
||||||
|
locationNotesSettings: settings.changes
|
||||||
)
|
)
|
||||||
|
|
||||||
counter.activate()
|
counter.activate()
|
||||||
counter.reveal()
|
counter.reveal()
|
||||||
XCTAssertEqual(relays.subscribeCount, 1)
|
XCTAssertEqual(relays.subscribeCount, 1)
|
||||||
|
|
||||||
LocationNotesSettings.enabled = false
|
settings.setEnabled(false)
|
||||||
|
|
||||||
let released = await waitUntil { relays.unsubscribeCount == 1 }
|
let released = await waitUntil { relays.unsubscribeCount == 1 }
|
||||||
XCTAssertTrue(released)
|
XCTAssertTrue(released)
|
||||||
XCTAssertEqual(counter.noteCount, 0)
|
XCTAssertEqual(counter.noteCount, 0)
|
||||||
|
|
||||||
LocationNotesSettings.enabled = true
|
settings.setEnabled(true)
|
||||||
|
|
||||||
let reacquired = await waitUntil { relays.subscribeCount == 2 }
|
let reacquired = await waitUntil { relays.subscribeCount == 2 }
|
||||||
XCTAssertTrue(reacquired)
|
XCTAssertTrue(reacquired)
|
||||||
@@ -149,10 +141,13 @@ final class NearbyNotesCounterTests: XCTestCase {
|
|||||||
|
|
||||||
func test_checkNotesHint_requiresAuthorizedLocationPermission() {
|
func test_checkNotesHint_requiresAuthorizedLocationPermission() {
|
||||||
let relays = SubscriptionRecorder()
|
let relays = SubscriptionRecorder()
|
||||||
|
let settings = LocationNotesSettingsStub()
|
||||||
let counter = NearbyNotesCounter(
|
let counter = NearbyNotesCounter(
|
||||||
locationManager: makeBareLocationManager(),
|
locationManager: makeBareLocationManager(),
|
||||||
managerFactory: { LocationNotesManager(geohash: $0, dependencies: relays.dependencies) },
|
managerFactory: { LocationNotesManager(geohash: $0, dependencies: relays.dependencies) },
|
||||||
releaseManager: { $0?.cancel() }
|
releaseManager: { $0?.cancel() },
|
||||||
|
locationNotesEnabled: { settings.enabled },
|
||||||
|
locationNotesSettings: settings.changes
|
||||||
)
|
)
|
||||||
|
|
||||||
// An unauthorized install must never see the hint: the tap can't
|
// An unauthorized install must never see the hint: the tap can't
|
||||||
@@ -164,9 +159,9 @@ final class NearbyNotesCounterTests: XCTestCase {
|
|||||||
XCTAssertTrue(counter.offersRevealHint(permissionState: .authorized))
|
XCTAssertTrue(counter.offersRevealHint(permissionState: .authorized))
|
||||||
|
|
||||||
// The app-info kill switch hides it too.
|
// The app-info kill switch hides it too.
|
||||||
LocationNotesSettings.enabled = false
|
settings.setEnabled(false)
|
||||||
XCTAssertFalse(counter.offersRevealHint(permissionState: .authorized))
|
XCTAssertFalse(counter.offersRevealHint(permissionState: .authorized))
|
||||||
LocationNotesSettings.enabled = true
|
settings.setEnabled(true)
|
||||||
|
|
||||||
// Once revealed, the hint yields to the live strip and count.
|
// Once revealed, the hint yields to the live strip and count.
|
||||||
counter.reveal()
|
counter.reveal()
|
||||||
@@ -411,6 +406,21 @@ final class NearbyNotesCounterTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class LocationNotesSettingsStub {
|
||||||
|
private let changesSubject = PassthroughSubject<Void, Never>()
|
||||||
|
private(set) var enabled = true
|
||||||
|
|
||||||
|
var changes: AnyPublisher<Void, Never> {
|
||||||
|
changesSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEnabled(_ enabled: Bool) {
|
||||||
|
self.enabled = enabled
|
||||||
|
changesSubject.send(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stub relay layer: counts REQs, captures the last filter/handler, and never
|
/// Stub relay layer: counts REQs, captures the last filter/handler, and never
|
||||||
/// touches the network.
|
/// touches the network.
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
Reference in New Issue
Block a user