TM-SGNL-iOS/Signal/test/ViewControllers/MessageLoaderTest.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

436 lines
16 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import XCTest
@testable import Signal
@testable import SignalServiceKit
class MessageLoaderTest: XCTestCase {
private var batchFetcher: MockBatchFetcher!
private var interactionFetcher: MockInteractionFetcher!
private var messageLoader: MessageLoader!
private var mockDB: InMemoryDB!
private class MockBatchFetcher: MessageLoaderBatchFetcher {
var interactions = [TSInteraction]()
func fetchUniqueIds(filter: InteractionFinder.RowIdFilter, limit: Int, tx: DBReadTransaction) throws -> [String] {
switch filter {
case .newest:
return Array(interactions.lazy.suffix(limit).map { $0.uniqueId })
case .after(let rowId):
return Array(interactions.lazy.filter { $0.sqliteRowId! > rowId }.prefix(limit).map { $0.uniqueId })
case .atOrBefore(let rowId):
return Array(interactions.lazy.filter { $0.sqliteRowId! <= rowId }.suffix(limit).map { $0.uniqueId })
case .before(let rowId):
return Array(interactions.lazy.filter { $0.sqliteRowId! < rowId }.suffix(limit).map { $0.uniqueId })
case .range(let rowIds):
return Array(interactions.lazy.filter { rowIds.contains($0.sqliteRowId!) }.map { $0.uniqueId })
}
}
}
private class MockInteractionFetcher: MessageLoaderInteractionFetcher {
var interactions = [TSInteraction]()
func fetchInteractions(for uniqueIds: [String], tx: DBReadTransaction) -> [String: TSInteraction] {
return Dictionary(
uniqueKeysWithValues: interactions.lazy.filter { uniqueIds.contains($0.uniqueId) }.map { ($0.uniqueId, $0) }
)
}
}
override func setUp() {
super.setUp()
batchFetcher = MockBatchFetcher()
interactionFetcher = MockInteractionFetcher()
messageLoader = MessageLoader(
batchFetcher: batchFetcher,
interactionFetchers: [interactionFetcher]
)
mockDB = InMemoryDB()
}
private func createInteractions(_ count: Int64) -> [TSInteraction] {
return ((1 as Int64)...count).map { rowId in
TSInteraction(grdbId: rowId, uniqueId: UUID().uuidString, receivedAtTimestamp: 0, sortId: 0, timestamp: 0, uniqueThreadId: "")
}
}
private func setInteractions(_ interactions: [TSInteraction]) {
batchFetcher.interactions = interactions
interactionFetcher.interactions = interactions
}
func test_loadInitialMessagePage_empty() throws {
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: nil,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
XCTAssertEqual(messageLoader.loadedInteractions, [])
}
func test_loadInitialMessagePage_nonempty() throws {
let initialMessages = createInteractions(5)
setInteractions(initialMessages)
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: nil,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
XCTAssertEqual(messageLoader.loadedInteractions.count, 5)
XCTAssertEqual(initialMessages.map { $0.uniqueId }, messageLoader.loadedInteractions.map { $0.uniqueId })
}
func test_reloadInteractions_deletes() throws {
let initialMessages = createInteractions(5)
setInteractions(initialMessages)
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: nil,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
let remainingMessages = [initialMessages[1], initialMessages[3], initialMessages[4]]
setInteractions(remainingMessages)
XCTAssertEqual(initialMessages.map { $0.uniqueId }, messageLoader.loadedInteractions.map { $0.uniqueId })
let deletedInteractionIds: Set<String> = Set([initialMessages[0], initialMessages[2]].map { $0.uniqueId })
try mockDB.read { tx in
return try self.messageLoader.loadSameLocation(
reusableInteractions: [:],
deletedInteractionIds: deletedInteractionIds,
tx: tx
)
}
XCTAssertEqual(remainingMessages.map { $0.uniqueId }, messageLoader.loadedInteractions.map { $0.uniqueId })
}
func test_reloadInteractions_inserts() throws {
let allMessages = createInteractions(5)
setInteractions(Array(allMessages.prefix(2)))
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: nil,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
setInteractions(allMessages)
try mockDB.read { tx in
return try self.messageLoader.loadSameLocation(
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
XCTAssertEqual(allMessages.map { $0.uniqueId }, messageLoader.loadedInteractions.map { $0.uniqueId })
}
func test_reloadInteractions_insertsAndDeletes() throws {
let allMessages = createInteractions(5)
let initialMessages = Array(allMessages.prefix(4))
setInteractions(initialMessages)
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: nil,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
let remainingMessages = [allMessages[1], allMessages[2], allMessages[4]]
setInteractions(remainingMessages)
let deletedInteractionIds: Set<String> = Set([allMessages[0], allMessages[3]].map { $0.uniqueId })
try mockDB.read { tx in
return try self.messageLoader.loadSameLocation(
reusableInteractions: [:],
deletedInteractionIds: deletedInteractionIds,
tx: tx
)
}
XCTAssertEqual(remainingMessages.map { $0.uniqueId }, messageLoader.loadedInteractions.map { $0.uniqueId })
}
func test_reloadInteractions_removeAll() throws {
let initialMessages = createInteractions(5)
setInteractions(initialMessages)
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: nil,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
XCTAssert(!messageLoader.canLoadNewer)
XCTAssert(!messageLoader.canLoadOlder)
setInteractions([])
let removedIds = Set(initialMessages.map { $0.uniqueId })
try mockDB.read { tx in
return try self.messageLoader.loadSameLocation(
reusableInteractions: [:],
deletedInteractionIds: removedIds,
tx: tx
)
}
XCTAssertEqual(messageLoader.loadedInteractions, [])
}
func test_reloadInteractions_fix_crash() throws {
// Create more messages than are part of the first batch.
let initialMessages = createInteractions(400)
setInteractions(initialMessages)
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: nil,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
XCTAssertLessThan(messageLoader.loadedInteractions.count, initialMessages.count)
XCTAssert(!messageLoader.canLoadNewer)
XCTAssert(messageLoader.canLoadOlder)
setInteractions([])
let removedIds = Set(initialMessages.map { $0.uniqueId })
try mockDB.read { tx in
return try self.messageLoader.loadSameLocation(
reusableInteractions: [:],
deletedInteractionIds: removedIds,
tx: tx
)
}
XCTAssertEqual(messageLoader.loadedInteractions, [])
}
func test_loadAroundEdge() throws {
// Create more messages than are part of the first batch.
let initialMessages = createInteractions(100)
setInteractions(initialMessages)
// For each message, load the end of the chat, and then jump to that
// message. (This would be similar to opening the chat and tapping a reply
// to jump earlier in the history.)
for idx in stride(from: initialMessages.startIndex, to: initialMessages.endIndex, by: 10) {
let message = initialMessages[idx]
let messageLoader = MessageLoader(batchFetcher: batchFetcher, interactionFetchers: [interactionFetcher])
try mockDB.read { tx in
try messageLoader.loadInitialMessagePage(
focusMessageId: nil,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
XCTAssertLessThan(messageLoader.loadedInteractions.count, initialMessages.count)
try messageLoader.loadMessagePage(
aroundInteractionId: message.uniqueId,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
XCTAssertNotNil(messageLoader.loadedInteractions.map { $0.uniqueId }.firstIndex(of: message.uniqueId))
}
}
func testTotalDeletion() throws {
let allMessages = createInteractions(15)
let batch1 = Array(allMessages[0..<5])
let batch2 = Array(allMessages[5..<10])
let batch3 = Array(allMessages[10..<15])
setInteractions(batch1)
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: nil,
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
XCTAssertEqual(batch1.map { $0.uniqueId }, messageLoader.loadedInteractions.map { $0.uniqueId })
setInteractions([])
try mockDB.read { tx in
return try self.messageLoader.loadSameLocation(
reusableInteractions: [:],
deletedInteractionIds: Set(batch1.map { $0.uniqueId }),
tx: tx
)
}
XCTAssertEqual(messageLoader.loadedInteractions, [])
setInteractions(batch2)
try mockDB.read { tx in
return try self.messageLoader.loadSameLocation(
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
XCTAssertEqual(batch2.map { $0.uniqueId }, messageLoader.loadedInteractions.map { $0.uniqueId })
setInteractions(batch3)
try mockDB.read { tx in
return try self.messageLoader.loadSameLocation(
reusableInteractions: [:],
deletedInteractionIds: Set(batch2.map { $0.uniqueId }),
tx: tx
)
}
XCTAssertEqual(batch3.map { $0.uniqueId }, messageLoader.loadedInteractions.map { $0.uniqueId })
}
func testDeletionAndLoadOlder() throws {
var messages = createInteractions(200)
setInteractions(messages)
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: messages[100].uniqueId, // pretend this is the first unread message
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
// Make sure the load window is pretty small.
let loadCount1 = messageLoader.loadedInteractions.count
XCTAssertLessThan(loadCount1, 50)
// Remove the first, middle and last interactions.
// This will break "sort index" continuity and make things interesting.
let removedInteractions1 = [
messages.remove(at: (messages.startIndex + messages.endIndex) / 2),
messages.remove(at: messages.endIndex - 1),
messages.remove(at: messages.startIndex)
]
setInteractions(messages)
try mockDB.read { tx in
return try self.messageLoader.loadOlderMessagePage(
reusableInteractions: [:],
deletedInteractionIds: Set(removedInteractions1.map { $0.uniqueId }),
tx: tx
)
}
let loadCount2 = messageLoader.loadedInteractions.count
XCTAssertLessThan(loadCount1, loadCount2)
XCTAssertLessThan(loadCount2, 100)
// Remove the first, middle and last interactions.
// This will break "sort index" continuity and make things interesting.
let removedInteractions2 = [
messages.remove(at: (messages.startIndex + messages.endIndex) / 2),
messages.remove(at: messages.endIndex - 1),
messages.remove(at: messages.startIndex)
]
setInteractions(messages)
try mockDB.read { tx in
return try self.messageLoader.loadOlderMessagePage(
reusableInteractions: [:],
deletedInteractionIds: Set(removedInteractions2.map { $0.uniqueId }),
tx: tx
)
}
let loadCount3 = messageLoader.loadedInteractions.count
XCTAssertLessThan(loadCount2, loadCount3)
XCTAssertLessThan(loadCount3, 150)
}
func testDeletionAndLoadNewer() throws {
var messages = createInteractions(200)
setInteractions(messages)
try mockDB.read { tx in
try self.messageLoader.loadInitialMessagePage(
focusMessageId: messages[100].uniqueId, // pretend this is the first unread message
reusableInteractions: [:],
deletedInteractionIds: [],
tx: tx
)
}
// Make sure the load window is pretty small.
let loadCount1 = messageLoader.loadedInteractions.count
XCTAssertLessThan(loadCount1, 50)
// Remove the first, middle and last interactions.
// This will break "sort index" continuity and make things interesting.
let removedInteractions1 = [
messages.remove(at: (messages.startIndex + messages.endIndex) / 2),
messages.remove(at: messages.endIndex - 1),
messages.remove(at: messages.startIndex)
]
setInteractions(messages)
try mockDB.read { tx in
return try self.messageLoader.loadNewerMessagePage(
reusableInteractions: [:],
deletedInteractionIds: Set(removedInteractions1.map { $0.uniqueId }),
tx: tx
)
}
let loadCount2 = messageLoader.loadedInteractions.count
XCTAssertLessThan(loadCount1, loadCount2)
XCTAssertLessThan(loadCount2, 100)
// Remove the first, middle and last interactions.
// This will break "sort index" continuity and make things interesting.
let removedInteractions2 = [
messages.remove(at: (messages.startIndex + messages.endIndex) / 2),
messages.remove(at: messages.endIndex - 1),
messages.remove(at: messages.startIndex)
]
setInteractions(messages)
try mockDB.read { tx in
return try self.messageLoader.loadNewerMessagePage(
reusableInteractions: [:],
deletedInteractionIds: Set(removedInteractions2.map { $0.uniqueId }),
tx: tx
)
}
let loadCount3 = messageLoader.loadedInteractions.count
XCTAssertLessThan(loadCount2, loadCount3)
XCTAssertLessThan(loadCount3, 150)
}
}