Fix automated test issues

Closes: https://github.com/damus-io/damus/issues/3275
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2025-10-20 17:33:40 -07:00
parent 690f8b891e
commit 9555145359
8 changed files with 65 additions and 49 deletions

View File

@@ -35,9 +35,9 @@ class NostrNetworkManager {
let reader: SubscriptionManager
let profilesManager: ProfilesManager
init(delegate: Delegate) {
init(delegate: Delegate, addNdbToRelayPool: Bool = true) {
self.delegate = delegate
let pool = RelayPool(ndb: delegate.ndb, keypair: delegate.keypair)
let pool = RelayPool(ndb: addNdbToRelayPool ? delegate.ndb : nil, keypair: delegate.keypair)
self.pool = pool
let reader = SubscriptionManager(pool: pool, ndb: delegate.ndb, experimentalLocalRelayModelSupport: self.delegate.experimentalLocalRelayModelSupport)
let userRelayList = UserRelayListManager(delegate: delegate, pool: pool, reader: reader)

View File

@@ -25,8 +25,6 @@ extension NostrNetworkManager {
category: "subscription_manager"
)
let EXTRA_VERBOSE_LOGGING: Bool = false
init(pool: RelayPool, ndb: Ndb, experimentalLocalRelayModelSupport: Bool) {
self.pool = pool
self.ndb = ndb
@@ -127,7 +125,7 @@ extension NostrNetworkManager {
// In normal mode: Issuing EOSE requires EOSE from both NDB and the network, since they are all considered separate relays
// In experimental local relay model mode: Issuing EOSE requires only EOSE from NDB, since that is the only relay that "matters"
let canIssueEOSE = switch streamMode {
case .ndbFirst: (ndbEOSEIssued)
case .ndbFirst, .ndbOnly: (ndbEOSEIssued)
case .ndbAndNetworkParallel: (ndbEOSEIssued && (networkEOSEIssued || !connectedToNetwork))
}
@@ -142,6 +140,7 @@ extension NostrNetworkManager {
var latestNoteTimestampSeen: UInt32? = nil
let startNetworkStreamTask = {
guard streamMode.shouldStreamFromNetwork else { return }
networkStreamTask = Task {
while !Task.isCancelled {
let optimizedFilters = filters.map {
@@ -171,7 +170,7 @@ extension NostrNetworkManager {
}
}
if streamMode.optimizeNetworkFilter == false {
if streamMode.optimizeNetworkFilter == false && streamMode.shouldStreamFromNetwork {
// Start streaming from the network straight away
startNetworkStreamTask()
}
@@ -199,7 +198,7 @@ extension NostrNetworkManager {
logStreamPipelineStats("SubscriptionManager_Advanced_Stream_\(id)", "Consumer_\(id)")
continuation.yield(item)
ndbEOSEIssued = true
if streamMode.optimizeNetworkFilter {
if streamMode.optimizeNetworkFilter && streamMode.shouldStreamFromNetwork {
startNetworkStreamTask()
}
yieldEOSEIfReady()
@@ -237,11 +236,8 @@ extension NostrNetworkManager {
logStreamPipelineStats("RelayPool_Handler_\(id)", "SubscriptionManager_Network_Stream_\(id)")
switch item {
case .event(let event):
if EXTRA_VERBOSE_LOGGING {
Self.logger.debug("Session subscription \(id.uuidString, privacy: .public): Received kind \(event.kind, privacy: .public) event with id \(event.id.hex(), privacy: .private) from the network")
}
switch streamMode {
case .ndbFirst:
case .ndbFirst, .ndbOnly:
break // NO-OP
case .ndbAndNetworkParallel:
continuation.yield(.event(lender: NdbNoteLender(ownedNdbNote: event)))
@@ -534,6 +530,8 @@ extension NostrNetworkManager {
/// Returns notes from both NostrDB and the network, in parallel, treating it with similar importance against the network relays. Generic EOSE is fired when EOSE is received from both the network and NostrDB
/// `optimizeNetworkFilter`: Returns notes from ndb, then streams from the network with an added "since" filter set to the latest note stored on ndb.
case ndbAndNetworkParallel(optimizeNetworkFilter: Bool)
/// Ignores the network. Used for testing purposes
case ndbOnly
var optimizeNetworkFilter: Bool {
switch self {
@@ -541,6 +539,19 @@ extension NostrNetworkManager {
return optimizeNetworkFilter
case .ndbAndNetworkParallel(optimizeNetworkFilter: let optimizeNetworkFilter):
return optimizeNetworkFilter
case .ndbOnly:
return false
}
}
var shouldStreamFromNetwork: Bool {
switch self {
case .ndbFirst:
return true
case .ndbAndNetworkParallel:
return true
case .ndbOnly:
return false
}
}
}

View File

@@ -35,7 +35,7 @@ actor RelayPool {
var request_queue: [QueuedRequest] = []
var seen: [NoteId: Set<RelayURL>] = [:]
var counts: [RelayURL: UInt64] = [:]
var ndb: Ndb
var ndb: Ndb?
/// The keypair used to authenticate with relays
var keypair: Keypair?
var message_received_function: (((String, RelayDescriptor)) -> Void)?
@@ -71,7 +71,7 @@ actor RelayPool {
seen.removeAll()
}
init(ndb: Ndb, keypair: Keypair? = nil) {
init(ndb: Ndb?, keypair: Keypair? = nil) {
self.ndb = ndb
self.keypair = keypair
@@ -179,7 +179,7 @@ actor RelayPool {
case .string(let str) = msg
else { return }
let _ = self.ndb.processEvent(str, originRelayURL: relay_id)
let _ = self.ndb?.processEvent(str, originRelayURL: relay_id)
self.message_received_function?((str, desc))
})
let relay = Relay(descriptor: desc, connection: conn)
@@ -400,10 +400,10 @@ actor RelayPool {
switch req {
case .typical(let r):
if case .event = r, let rstr = make_nostr_req(r) {
let _ = ndb.process_client_event(rstr)
let _ = ndb?.process_client_event(rstr)
}
case .custom(let string):
let _ = ndb.process_client_event(string)
let _ = ndb?.process_client_event(string)
}
}

View File

@@ -39,7 +39,7 @@ class DamusState: HeadlessDamusState, ObservableObject {
let favicon_cache: FaviconCache
private(set) var nostrNetwork: NostrNetworkManager
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache, addNdbToRelayPool: Bool = true) {
self.keypair = keypair
self.likes = likes
self.boosts = boosts
@@ -72,7 +72,7 @@ class DamusState: HeadlessDamusState, ObservableObject {
self.favicon_cache = FaviconCache()
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
let nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
let nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate, addNdbToRelayPool: addNdbToRelayPool)
self.nostrNetwork = nostrNetwork
self.wallet.nostrNetwork = nostrNetwork
}

View File

@@ -103,13 +103,16 @@ final class AuthIntegrationTests: XCTestCase {
try! await pool.add_relay(relay_descriptor)
XCTAssertEqual(pool.relays.count, 1)
let connection_expectation = XCTestExpectation(description: "Waiting for connection")
await pool.connect()
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
if pool.num_connected == 1 {
connection_expectation.fulfill()
timer.invalidate()
Task {
if await pool.num_connected == 1 {
connection_expectation.fulfill()
timer.invalidate()
}
}
}
wait(for: [connection_expectation], timeout: 30.0)
await fulfillment(of: [connection_expectation], timeout: 30.0)
XCTAssertEqual(pool.num_connected, 1)
// Assert that no AUTH messages have been received
XCTAssertEqual(received_messages.count, 0)
@@ -148,13 +151,16 @@ final class AuthIntegrationTests: XCTestCase {
try! await pool.add_relay(relay_descriptor)
XCTAssertEqual(pool.relays.count, 1)
let connection_expectation = XCTestExpectation(description: "Waiting for connection")
await pool.connect()
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
if pool.num_connected == 1 {
connection_expectation.fulfill()
timer.invalidate()
Task {
if pool.num_connected == 1 {
connection_expectation.fulfill()
timer.invalidate()
}
}
}
wait(for: [connection_expectation], timeout: 30.0)
await fulfillment(of: [connection_expectation], timeout: 30.0)
XCTAssertEqual(pool.num_connected, 1)
// Assert that no AUTH messages have been received
XCTAssertEqual(received_messages.count, 0)
@@ -173,13 +179,15 @@ final class AuthIntegrationTests: XCTestCase {
timer.invalidate()
}
}
wait(for: [msg_expectation], timeout: 30.0)
await fulfillment(of: [msg_expectation], timeout: 30.0)
// Assert that AUTH message has been received
XCTAssertTrue(received_messages.count >= 1, "expected recieved_messages to be >= 1")
if received_messages.count < 1 { return } // End test early
let json_received = try! JSONSerialization.jsonObject(with: received_messages[0].data(using: .utf8)!, options: []) as! [Any]
XCTAssertEqual(json_received[0] as! String, "AUTH")
// Assert that we've replied with the AUTH response
XCTAssertEqual(sent_messages.count, 2)
if sent_messages.count < 2 { return }
let json_sent = try! JSONSerialization.jsonObject(with: sent_messages[1].data(using: .utf8)!, options: []) as! [Any]
XCTAssertEqual(json_sent[0] as! String, "AUTH")
let sent_msg = json_sent[1] as! [String: Any]

View File

@@ -12,12 +12,12 @@ import EmojiPicker
// Generates a test damus state with configurable mock parameters
func generate_test_damus_state(
mock_profile_info: [Pubkey: Profile]?,
home: HomeModel? = nil
home: HomeModel? = nil,
addNdbToRelayPool: Bool = true
) -> DamusState {
// Create a unique temporary directory
let ndb = Ndb.test
let our_pubkey = test_pubkey
let pool = RelayPool(ndb: ndb)
let settings = UserSettingsStore()
let profiles: Profiles = {
@@ -51,7 +51,8 @@ func generate_test_damus_state(
ndb: ndb,
quote_reposts: .init(our_pubkey: our_pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: false),
favicon_cache: .init()
favicon_cache: .init(),
addNdbToRelayPool: addNdbToRelayPool
)
home?.damus_state = damus

View File

@@ -14,7 +14,10 @@ class NostrNetworkManagerTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
damusState = generate_test_damus_state(mock_profile_info: nil)
damusState = generate_test_damus_state(
mock_profile_info: nil,
addNdbToRelayPool: false // Don't give RelayPool any access to Ndb. This will prevent incoming notes from affecting our test
)
let notesJSONL = getTestNotesJSONL()
@@ -37,13 +40,12 @@ class NostrNetworkManagerTests: XCTestCase {
return try! String(contentsOf: fileURL, encoding: .utf8)
}
func ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter, expectedCount: Int) {
func ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter, expectedCount: Int) async {
let endOfStream = XCTestExpectation(description: "Stream should receive EOSE")
let gotAtLeastExpectedCount = XCTestExpectation(description: "Stream should receive at least the expected number of items")
var receivedCount = 0
var eventIds: Set<NoteId> = []
Task {
for await item in self.damusState!.nostrNetwork.reader.advancedStream(filters: [filter], streamMode: .ndbFirst) {
for await item in self.damusState!.nostrNetwork.reader.advancedStream(filters: [filter], streamMode: .ndbOnly) {
switch item {
case .event(let lender):
try? lender.borrow { event in
@@ -53,9 +55,6 @@ class NostrNetworkManagerTests: XCTestCase {
}
eventIds.insert(event.id)
}
if receivedCount == expectedCount {
gotAtLeastExpectedCount.fulfill()
}
case .eose:
continue
case .ndbEose:
@@ -67,7 +66,7 @@ class NostrNetworkManagerTests: XCTestCase {
}
}
}
wait(for: [endOfStream, gotAtLeastExpectedCount], timeout: 10.0)
await fulfillment(of: [endOfStream], timeout: 15.0)
XCTAssertEqual(receivedCount, expectedCount, "Event IDs: \(eventIds.map({ $0.hex() }))")
}
@@ -83,14 +82,11 @@ class NostrNetworkManagerTests: XCTestCase {
/// nak req --kind 1 ws://localhost:10547 | wc -l
/// ```
func testNdbSubscription() async {
try! await damusState?.nostrNetwork.userRelayList.set(userRelayList: NIP65.RelayList())
await damusState?.nostrNetwork.connect()
ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text]), expectedCount: 57)
ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(authors: [Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!]), expectedCount: 22)
ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.boost], referenced_ids: [NoteId(hex: "64b26d0a587f5f894470e1e4783756b4d8ba971226de975ee30ac1b69970d5a1")!]), expectedCount: 5)
ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text, .boost, .zap], referenced_ids: [NoteId(hex: "64b26d0a587f5f894470e1e4783756b4d8ba971226de975ee30ac1b69970d5a1")!], limit: 500), expectedCount: 5)
ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text], limit: 10), expectedCount: 10)
ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text], until: UInt32(Date.now.timeIntervalSince1970), limit: 10), expectedCount: 10)
await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text]), expectedCount: 57)
await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(authors: [Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!]), expectedCount: 22)
await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.boost], referenced_ids: [NoteId(hex: "64b26d0a587f5f894470e1e4783756b4d8ba971226de975ee30ac1b69970d5a1")!]), expectedCount: 5)
await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text, .boost, .zap], referenced_ids: [NoteId(hex: "64b26d0a587f5f894470e1e4783756b4d8ba971226de975ee30ac1b69970d5a1")!], limit: 500), expectedCount: 5)
await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text], limit: 10), expectedCount: 10)
await ensureSubscribeGetsAllExpectedNotes(filter: NostrFilter(kinds: [.text], until: UInt32(Date.now.timeIntervalSince1970), limit: 10), expectedCount: 10)
}
}

View File

@@ -1,5 +1,5 @@
build:
xcodebuild -scheme damus -sdk iphoneos -destination 'platform=iOS Simulator,OS=18.2,name=iPhone 16' -quiet | xcbeautify --quieter
xcodebuild -scheme damus -sdk iphoneos -destination 'platform=iOS Simulator,OS=26.0,name=iPhone 16e' -quiet | xcbeautify --quieter
test:
xcodebuild test -scheme damus -destination 'platform=iOS Simulator,OS=18.2,name=iPhone 16' -quiet | xcbeautify --quieter
xcodebuild test -scheme damus -destination 'platform=iOS Simulator,OS=26.0,name=iPhone 16e' | xcbeautify