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

1464 lines
72 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import XCTest
@testable import Signal
@testable import SignalServiceKit
private extension Date {
/// Initialize a date using a compressed notation: `Date(compressedDate: 2022_04_28)`
init(compressedDate: UInt32, calendar: Calendar = .current) {
let day = Int(compressedDate % 100)
assert((1...31).contains(day))
let month = Int((compressedDate / 100) % 100)
assert((1...12).contains(month))
let year = Int(compressedDate / 1_00_00)
assert((1970...).contains(year))
self = calendar.date(from: DateComponents(year: year, month: month, day: day))!
}
}
private extension GalleryDate {
/// Initialize a GalleryDate using a compressed notation: `GalleryDate(2022_04_28)`.
///
/// Note that GalleryDates represent intervals; the above example will produce a GalleryDate for all of April 2022.
init(_ compressedDate: UInt32) {
self.init(date: Date(compressedDate: compressedDate))
}
}
private extension MediaGallerySections {
/// A functional version of the normal, inout-based API. More convenient for testing.
func trimLoadedItemsAtStart(from naiveRange: Range<Int>, relativeToSection sectionIndex: Int) -> Range<Int> {
var result = naiveRange
trimLoadedItemsAtStart(from: &result, relativeToSection: sectionIndex)
return result
}
/// A functional version of the normal, inout-based API. More convenient for testing.
func trimLoadedItemsAtEnd(from naiveRange: Range<Int>, relativeToSection sectionIndex: Int) -> Range<Int> {
var result = naiveRange
trimLoadedItemsAtEnd(from: &result, relativeToSection: sectionIndex)
return result
}
}
// MARK: -
private struct FakeItem: MediaGallerySectionItem, Equatable {
private static var _nextRowID = Int64(0)
private static func allocateItemID() -> AttachmentReferenceId {
defer { _nextRowID += 1 }
return .init(ownerId: .messageBodyAttachment(messageRowId: _nextRowID), orderInOwner: nil)
}
var itemId: AttachmentReferenceId
var attachmentId: AttachmentReferenceId
var timestamp: Date
var galleryDate: GalleryDate { GalleryDate(date: timestamp) }
/// Generates an item with the given timestamp in compressed notation: `FakeItem(2022_04_28)`
///
/// The item's unique ID will be randomly generated.
init(_ compressedDate: UInt32) {
self.itemId = FakeItem.allocateItemID()
self.attachmentId = .init(ownerId: .messageBodyAttachment(messageRowId: .random(in: 0...Int64.max)), orderInOwner: nil)
self.timestamp = Date(compressedDate: compressedDate)
}
init(_ compressedDate: UInt32, attachmentId: AttachmentReferenceId?, itemId: AttachmentReferenceId) {
self.itemId = itemId
self.attachmentId = attachmentId ?? .init(ownerId: .messageBodyAttachment(messageRowId: .random(in: 0...Int64.max)), orderInOwner: nil)
self.timestamp = Date(compressedDate: compressedDate)
}
}
/// Takes the place of the database for MediaGallerySection tests.
private final class FakeGalleryStore: MediaGallerySectionLoader {
func rowIdsAndDatesOfItemsInSection(
for date: GalleryDate,
offset: Int,
ascending: Bool,
transaction: SignalServiceKit.SDSAnyReadTransaction
) -> [DatedAttachmentReferenceId] {
guard let items = itemsBySection[date] else {
return []
}
let sortedItems = ascending ? items : items.reversed()
return sortedItems[offset...].map {
DatedAttachmentReferenceId(
id: $0.itemId,
receivedAtTimestamp: $0.timestamp.ows_millisecondsSince1970
)
}
}
typealias Item = FakeItem
typealias EnumerationCompletion = MediaGalleryAttachmentFinder.EnumerationCompletion
var allItems: [Item]
var itemsBySection: OrderedDictionary<GalleryDate, [Item]>
var mostRecentRequest: Range<Int> = 0..<0
/// Sorts and groups the given items for traversal as a flat array (`allItems`)
/// as well as by section (`itemsBySection`).
init(_ items: [Item]) {
self.allItems = items.sorted { $0.timestamp < $1.timestamp }
let itemsBySection = Dictionary(grouping: allItems) { $0.galleryDate }
self.itemsBySection = OrderedDictionary(keyValueMap: itemsBySection, orderedKeys: itemsBySection.keys.sorted())
}
func set(items: [Item]) {
self.allItems = items.sorted { $0.timestamp < $1.timestamp }
let itemsBySection = Dictionary(grouping: allItems) { $0.galleryDate }
self.itemsBySection = OrderedDictionary(keyValueMap: itemsBySection, orderedKeys: itemsBySection.keys.sorted())
}
func clone() -> Self {
return Self(self.allItems)
}
private func numberOfItemsInSection(for date: GalleryDate) -> Int {
itemsBySection[date]?.count ?? 0
}
/// Iterates over `items` and calls `block` up to `count` times.
///
/// Items with IDs matching `deletedAttachmentIds` are skipped and do not count against `count`.
/// Returns `finished` if `count` items were visited and `reachedEnd` if there were not enough items to visit.
private static func enumerate<Items>(_ items: Items, count: Int, visit: (Item) -> Void) -> EnumerationCompletion
where Items: Collection, Items.Element == Item {
items.prefix(count).forEach(visit)
if items.count < count {
return .reachedEnd
}
return .finished
}
func enumerateTimestamps(
before date: Date,
count: Int,
transaction: SDSAnyReadTransaction,
block: (DatedAttachmentReferenceId) -> Void
) -> EnumerationCompletion {
// It would be more efficient to binary search here, but this is for testing.
let itemsInRange = allItems.reversed().drop { $0.timestamp >= date }
return Self.enumerate(itemsInRange, count: count) {
block(.init(id: $0.itemId, receivedAtTimestamp: $0.timestamp.ows_millisecondsSince1970))
}
}
func enumerateTimestamps(
after date: Date,
count: Int,
transaction: SDSAnyReadTransaction,
block: (DatedAttachmentReferenceId) -> Void
) -> EnumerationCompletion {
// It would be more efficient to binary search here, but this is for testing.
let itemsInRange = allItems.drop { $0.timestamp < date }
return Self.enumerate(itemsInRange, count: count) {
block(.init(id: $0.itemId, receivedAtTimestamp: $0.timestamp.ows_millisecondsSince1970))
}
}
func enumerateItems(
in interval: DateInterval,
range: Range<Int>,
transaction: SDSAnyReadTransaction,
block: (_ offset: Int, _ attachmentId: AttachmentReferenceId, _ buildItem: () -> Item) -> Void
) {
// It would be more efficient to binary search here, but this is for testing.
// DateInterval is usually a *closed* range, but we're using it as a half-open one here.
let itemsInInterval = allItems.drop { $0.timestamp < interval.start }.prefix { $0.timestamp < interval.end }
let itemsInRange = itemsInInterval.dropFirst(range.startIndex).prefix(range.count)
mostRecentRequest = itemsInRange.indices
for (offset, item) in zip(range, itemsInRange) {
block(offset, item.attachmentId, { item })
}
}
}
/// A store initialized with items in Jan, Apr, and Sep 2021.
private let standardFakeStore: FakeGalleryStore = FakeGalleryStore([
2021_01_01, // rowid 0
2021_01_02, // rowid 1
2021_01_20, // rowid 2
2021_04_01, // rowid 3
2021_04_13, // rowid 4
2021_09_09, // rowid 5
2021_09_09, // rowid 6
2021_09_09, // rowid 7
2021_09_30, // rowid 8
2021_09_30 // rowid 9
].map { FakeItem($0) })
// Before we test the actual MediaGallerySections, make sure our fake store is behaving as expected.
class MediaGallerySectionsFakeStoreTest: SignalBaseTest {
func testFakeItem() {
let item1 = FakeItem(1970_01_01)
XCTAssertEqual(Calendar.current.dateComponents([.year, .month, .day], from: item1.timestamp),
DateComponents(year: 1970, month: 1, day: 1))
let item2 = FakeItem(2021_04_28)
XCTAssertEqual(Calendar.current.dateComponents([.year, .month, .day], from: item2.timestamp),
DateComponents(year: 2021, month: 4, day: 28))
XCTAssertNotEqual(item1.attachmentId, item2.attachmentId)
}
func testNumberOfItemsInSection() {
let store = standardFakeStore
SSKEnvironment.shared.databaseStorageRef.read { transaction in
XCTAssertEqual(3,
store.rowIdsAndDatesOfItemsInSection(
for: GalleryDate(2021_01_01),
offset: 0,
ascending: true,
transaction: transaction).count)
XCTAssertEqual(0,
store.rowIdsAndDatesOfItemsInSection(
for: GalleryDate(2021_02_01),
offset: 0,
ascending: true,
transaction: transaction).count)
XCTAssertEqual(2,
store.rowIdsAndDatesOfItemsInSection(
for: GalleryDate(2021_04_01),
offset: 0,
ascending: true,
transaction: transaction).count)
XCTAssertEqual(5,
store.rowIdsAndDatesOfItemsInSection(
for: GalleryDate(2021_09_01),
offset: 0,
ascending: true,
transaction: transaction).count)
}
}
func testEnumerateAfter() {
let store = standardFakeStore
SSKEnvironment.shared.databaseStorageRef.read { transaction in
var results: [Date] = []
XCTAssertEqual(
.finished,
store.enumerateTimestamps(
after: .distantPast,
count: 4,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems.prefix(4).map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.finished,
store.enumerateTimestamps(
after: .distantPast,
count: 10,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems.map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.reachedEnd,
store.enumerateTimestamps(
after: .distantPast,
count: 11,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems.map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.finished,
store.enumerateTimestamps(
after: Date(compressedDate: 2021_04_01),
count: 3,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems[3..<6].map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.reachedEnd,
store.enumerateTimestamps(
after: Date(compressedDate: 2021_04_01),
count: 17,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems[3...].map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.reachedEnd,
store.enumerateTimestamps(
after: Date(compressedDate: 2022_04_01),
count: 17,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, [])
results.removeAll()
XCTAssertEqual(
.finished,
store.enumerateTimestamps(
after: Date(compressedDate: 2022_04_01),
count: 0,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, [])
}
}
func testEnumerateBefore() {
let store = standardFakeStore
SSKEnvironment.shared.databaseStorageRef.read { transaction in
var results: [Date] = []
XCTAssertEqual(
.finished,
store.enumerateTimestamps(
before: .distantFuture,
count: 4,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems.reversed().prefix(4).map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.finished,
store.enumerateTimestamps(
before: .distantFuture,
count: 10,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems.reversed().map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.reachedEnd,
store.enumerateTimestamps(
before: .distantFuture,
count: 11,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems.reversed().map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.finished,
store.enumerateTimestamps(
before: Date(compressedDate: 2021_04_01),
count: 2,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems[1..<3].reversed().map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.reachedEnd,
store.enumerateTimestamps(
before: Date(compressedDate: 2021_04_01),
count: 17,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, store.allItems[..<3].reversed().map { $0.timestamp })
results.removeAll()
XCTAssertEqual(
.reachedEnd,
store.enumerateTimestamps(
before: Date(compressedDate: 2020_01_01),
count: 17,
transaction: transaction
) { item in
results.append(item.date)
}
)
XCTAssertEqual(results, [])
}
}
func testEnumerateItems() {
let store = standardFakeStore
SSKEnvironment.shared.databaseStorageRef.read { transaction in
var results: [AttachmentReferenceId] = []
let saveToResults = { (offset: Int, attachmentId: AttachmentReferenceId, buildItem: () -> FakeItem) in
results.append(attachmentId)
XCTAssertEqual(attachmentId, buildItem().attachmentId)
}
store.enumerateItems(in: GalleryDate(2021_01_01).interval,
range: 1..<3,
transaction: transaction,
block: saveToResults)
XCTAssertEqual(store.allItems[1..<3].map { $0.attachmentId }, results)
results.removeAll()
store.enumerateItems(in: GalleryDate(2021_09_01).interval,
range: 2..<4,
transaction: transaction,
block: saveToResults)
XCTAssertEqual(store.allItems[7..<9].map { $0.attachmentId }, results)
results.removeAll()
store.enumerateItems(in: GalleryDate(2021_09_01).interval,
range: 0..<20,
transaction: transaction,
block: saveToResults)
XCTAssertEqual(store.allItems[5...].map { $0.attachmentId }, results)
results.removeAll()
store.enumerateItems(in: GalleryDate(2021_10_01).interval,
range: 0..<1,
transaction: transaction,
block: saveToResults)
XCTAssertEqual([], results)
results.removeAll()
store.enumerateItems(in: GalleryDate(2021_09_01).interval,
range: 2..<2,
transaction: transaction,
block: saveToResults)
XCTAssertEqual([], results)
results.removeAll()
store.enumerateItems(in: GalleryDate(2021_09_01).interval,
range: 20..<25,
transaction: transaction,
block: saveToResults)
XCTAssertEqual([], results)
}
}
}
class MediaGallerySectionsTest: SignalBaseTest {
fileprivate typealias Sections = MediaGallerySections<FakeGalleryStore, Int>
fileprivate struct SectionsWrapper {
private(set) var sections: Sections
var userData: [Int] = []
mutating func mutate<T>(_ closure: (inout Sections) -> (T)) -> T {
let result = closure(&sections)
sections.accessPendingUpdate { pendingUpdate in
userData.append(contentsOf: pendingUpdate.userData)
_ = pendingUpdate.commit()
}
return result
}
}
func testLoadSectionsBackward() {
var wrapper = SectionsWrapper(sections: Sections(loader: standardFakeStore))
XCTAssertEqual(wrapper.sections.itemsBySection.count, 0)
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
wrapper.mutate { sections in
XCTAssertEqual(1, sections.loadEarlierSections(batchSize: 4))
}
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([GalleryDate(2021_09_01)], wrapper.sections.itemsBySection.orderedKeys)
XCTAssertEqual([5], wrapper.sections.itemsBySection.orderedValues.map { $0.count })
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
wrapper.mutate { sections in
XCTAssertEqual(2, sections.loadEarlierSections(batchSize: 4))
}
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)],
wrapper.sections.itemsBySection.orderedKeys)
XCTAssertEqual([3, 2, 5], wrapper.sections.itemsBySection.orderedValues.map { $0.count })
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
wrapper.mutate { sections in
XCTAssertEqual(0, sections.loadEarlierSections(batchSize: 4))
}
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)],
wrapper.sections.itemsBySection.orderedKeys)
XCTAssertEqual([3, 2, 5], wrapper.sections.itemsBySection.orderedValues.map { $0.count })
XCTAssertTrue(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
}
func testLoadSectionsForward() {
var wrapper = SectionsWrapper(sections: Sections(loader: standardFakeStore))
XCTAssertEqual(wrapper.sections.itemsBySection.count, 0)
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
wrapper.mutate { sections in
XCTAssertEqual(2, sections.loadLaterSections(batchSize: 4))
}
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01)], wrapper.sections.itemsBySection.orderedKeys)
XCTAssertEqual([3, 2], wrapper.sections.itemsBySection.orderedValues.map { $0.count })
XCTAssertTrue(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
wrapper.mutate { sections in
XCTAssertEqual(1, sections.loadLaterSections(batchSize: 4))
}
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)],
wrapper.sections.itemsBySection.orderedKeys)
XCTAssertEqual([3, 2, 5], wrapper.sections.itemsBySection.orderedValues.map { $0.count })
XCTAssertTrue(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
wrapper.mutate { sections in
XCTAssertEqual(0, sections.loadLaterSections(batchSize: 4))
}
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([GalleryDate(2021_01_01), GalleryDate(2021_04_01), GalleryDate(2021_09_01)],
wrapper.sections.itemsBySection.orderedKeys)
XCTAssertEqual([3, 2, 5], wrapper.sections.itemsBySection.orderedValues.map { $0.count })
XCTAssertTrue(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
}
func testStartIndexResolution() {
var wrapper = SectionsWrapper(sections: Sections(loader: standardFakeStore))
// Load April and September
XCTAssertEqual(2, wrapper.mutate { sections in sections.loadEarlierSections(batchSize: 6) })
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 1),
wrapper.sections.resolveNaiveStartIndex(0, relativeToSection: 1))
XCTAssertEqual(MediaGalleryIndexPath(item: 4, section: 1),
wrapper.sections.resolveNaiveStartIndex(4, relativeToSection: 1))
XCTAssertEqual(MediaGalleryIndexPath(item: 5, section: 1),
wrapper.sections.resolveNaiveStartIndex(5, relativeToSection: 1))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 0),
wrapper.sections.resolveNaiveStartIndex(-1, relativeToSection: 1))
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 0),
wrapper.sections.resolveNaiveStartIndex(-2, relativeToSection: 1))
XCTAssertNil(wrapper.sections.resolveNaiveStartIndex(-3, relativeToSection: 1))
// Load January
do {
let actual = wrapper.mutate { sections in
sections.resolveNaiveStartIndex(-3, relativeToSection: 1, batchSize: 1)
}
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 0), actual.path)
XCTAssertEqual(1, actual.numberOfSectionsLoaded)
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 0),
wrapper.sections.resolveNaiveStartIndex(-3, relativeToSection: 2))
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 0),
wrapper.sections.resolveNaiveStartIndex(-5, relativeToSection: 2))
XCTAssertNil(wrapper.sections.resolveNaiveStartIndex(-6, relativeToSection: 2))
// Find out that January was the earliest section.
do {
let actual = wrapper.mutate { sections in
sections.resolveNaiveStartIndex(-6, relativeToSection: 2, batchSize: 1)
}
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 0), actual.0)
XCTAssertEqual(0, actual.1)
}
XCTAssertTrue(wrapper.sections.hasFetchedOldest)
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 0),
wrapper.sections.resolveNaiveStartIndex(-6, relativeToSection: 2))
}
func testEndIndexResolution() {
var wrapper = SectionsWrapper(sections: Sections(loader: standardFakeStore))
// Load January and April
XCTAssertEqual(2, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 4) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 0),
wrapper.sections.resolveNaiveEndIndex(0, relativeToSection: 0))
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 0),
wrapper.sections.resolveNaiveEndIndex(2, relativeToSection: 0))
// Note: (0, 3) rather than (1, 0), because this is an end index.
XCTAssertEqual(MediaGalleryIndexPath(item: 3, section: 0),
wrapper.sections.resolveNaiveEndIndex(3, relativeToSection: 0))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 1),
wrapper.sections.resolveNaiveEndIndex(4, relativeToSection: 0))
// Note: (1, 2) rather than nil.
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 1),
wrapper.sections.resolveNaiveEndIndex(5, relativeToSection: 0))
XCTAssertNil(wrapper.sections.resolveNaiveEndIndex(6, relativeToSection: 0))
// Load September
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 1),
wrapper.sections.resolveNaiveEndIndex(5, relativeToSection: 0))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 2),
wrapper.sections.resolveNaiveEndIndex(6, relativeToSection: 0))
XCTAssertEqual(MediaGalleryIndexPath(item: 4, section: 2),
wrapper.sections.resolveNaiveEndIndex(9, relativeToSection: 0))
XCTAssertEqual(MediaGalleryIndexPath(item: 5, section: 2),
wrapper.sections.resolveNaiveEndIndex(10, relativeToSection: 0))
// Reached end.
XCTAssertEqual(MediaGalleryIndexPath(item: 5, section: 2),
wrapper.sections.resolveNaiveEndIndex(11, relativeToSection: 0))
XCTAssertEqual(MediaGalleryIndexPath(item: 5, section: 2),
wrapper.sections.resolveNaiveEndIndex(12, relativeToSection: 0))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 1),
wrapper.sections.resolveNaiveEndIndex(1, relativeToSection: 1))
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 1),
wrapper.sections.resolveNaiveEndIndex(2, relativeToSection: 1))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 2),
wrapper.sections.resolveNaiveEndIndex(3, relativeToSection: 1))
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 2),
wrapper.sections.resolveNaiveEndIndex(4, relativeToSection: 1))
XCTAssertEqual(MediaGalleryIndexPath(item: 5, section: 2),
wrapper.sections.resolveNaiveEndIndex(10, relativeToSection: 1))
}
func testLoadingFromEnd() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load April and September
XCTAssertEqual(2, wrapper.mutate { sections in sections.loadEarlierSections(batchSize: 6) })
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual(
[3, 4].map { AttachmentReferenceId(ownerId: .messageBodyAttachment(messageRowId: $0), orderInOwner: nil) },
wrapper.sections.itemsBySection[0].value.map { $0.itemId }
)
XCTAssertEqual(
[5, 6, 7, 8, 9].map { AttachmentReferenceId(ownerId: .messageBodyAttachment(messageRowId: $0), orderInOwner: nil) },
wrapper.sections.itemsBySection[1].value.map { $0.itemId }
)
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 1..<3, relativeToSection: 1) }.isEmpty)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[6], store.allItems[7], nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 5..<5, relativeToSection: 1) }.isEmpty)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[6], store.allItems[7], nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: (-2)..<3, relativeToSection: 1) }.isEmpty)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([store.allItems[5], store.allItems[6], store.allItems[7], nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([store.allItems[3], store.allItems[4]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual(IndexSet(integer: 0), wrapper.mutate { sections in sections.ensureItemsLoaded(in: (-4)..<0, relativeToSection: 1) })
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([store.allItems[5], store.allItems[6], store.allItems[7], nil, nil], wrapper.sections.itemsBySection[2].value.map { $0.item })
XCTAssertEqual([store.allItems[3], store.allItems[4]], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
}
func testLoadingFromEndInBigJump() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load September
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadEarlierSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 1..<3, relativeToSection: 0) }.isEmpty)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[6], store.allItems[7], nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual(IndexSet(integersIn: 0...1),
wrapper.mutate { sections in sections.ensureItemsLoaded(in: (-4)..<0, relativeToSection: 0) })
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[6], store.allItems[7], nil, nil], wrapper.sections.itemsBySection[2].value.map { $0.item })
XCTAssertEqual([store.allItems[3], store.allItems[4]], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
}
func testLoadingFromStart() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January and April
XCTAssertEqual(2, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 4) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 1..<3, relativeToSection: 0) }.isEmpty)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 0..<0, relativeToSection: 0) }.isEmpty)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 3..<4, relativeToSection: 0) }.isEmpty)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([store.allItems[3], nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual(IndexSet(integer: 2), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 3..<7, relativeToSection: 0) })
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([store.allItems[3], store.allItems[4]], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([store.allItems[5], store.allItems[6], nil, nil, nil], wrapper.sections.itemsBySection[2].value.map { $0.item })
}
func testLoadingFromStartInBigJump() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 1..<3, relativeToSection: 0) }.isEmpty)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 0..<0, relativeToSection: 0) }.isEmpty)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual(IndexSet(integersIn: 1...2), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 3..<7, relativeToSection: 0) })
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([store.allItems[3], store.allItems[4]], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([store.allItems[5], store.allItems[6], nil, nil, nil], wrapper.sections.itemsBySection[2].value.map { $0.item })
}
func testLoadingFromMiddle() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
_ = read { transaction in
wrapper.mutate { sections in
sections.loadInitialSection(for: GalleryDate(2021_04_01), transaction: transaction)
}
}
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 1..<2, relativeToSection: 0) }.isEmpty)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[4]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual(IndexSet([0, 2]), wrapper.mutate { sections in sections.ensureItemsLoaded(in: (-2)..<5, relativeToSection: 0) })
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, store.allItems[1], store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([store.allItems[3], store.allItems[4]], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([store.allItems[5], store.allItems[6], store.allItems[7], nil, nil],
wrapper.sections.itemsBySection[2].value.map { $0.item })
}
func testReloadSection() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual(IndexSet(integersIn: 1...2), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 3..<7, relativeToSection: 0) })
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([store.allItems[3], store.allItems[4]], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([store.allItems[5], store.allItems[6], nil, nil, nil], wrapper.sections.itemsBySection[2].value.map { $0.item })
XCTAssertEqual(2, wrapper.mutate { sections in sections.reloadSection(for: GalleryDate(2021_04_01)) })
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([store.allItems[5], store.allItems[6], nil, nil, nil], wrapper.sections.itemsBySection[2].value.map { $0.item })
}
func testResetWhenEmpty() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
wrapper.mutate { sections in
sections.reset()
}
XCTAssert(wrapper.sections.itemsBySection.isEmpty)
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
}
func testResetOneSection() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
}
func testResetTwoSections() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual(IndexSet(integer: 1), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 3..<4, relativeToSection: 0) })
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([store.allItems[3], nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
}
func testResetFull() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual(IndexSet(integersIn: 1...2), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 2..<7, relativeToSection: 0) })
XCTAssertTrue(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, store.allItems[2]], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([store.allItems[3], store.allItems[4]], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([store.allItems[5], store.allItems[6], nil, nil, nil], wrapper.sections.itemsBySection[2].value.map { $0.item })
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
XCTAssertEqual([nil, nil, nil, nil, nil], wrapper.sections.itemsBySection[2].value.map { $0.item })
}
func testResetOneSectionAfterDeletingStart() {
let store = standardFakeStore.clone()
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
store.allItems.removeFirst(3)
store.itemsBySection.replace(key: GalleryDate(2021_01_01), value: [])
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([GalleryDate(2021_09_01)], wrapper.sections.itemsBySection.orderedKeys)
XCTAssertEqual([nil, nil, nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
}
func testResetTwoSectionsAfterDeletingStart() {
let store = standardFakeStore.clone()
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual(IndexSet(integer: 1), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 3..<4, relativeToSection: 0) })
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
store.allItems.removeFirst(3)
store.itemsBySection.replace(key: GalleryDate(2021_01_01), value: [])
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([GalleryDate(2021_09_01)], wrapper.sections.itemsBySection.orderedKeys)
XCTAssertEqual([nil, nil, nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
}
func testResetTwoSectionsAfterDeletingEndWithAnotherSectionFollowing() {
let store = standardFakeStore.clone()
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual(IndexSet(integer: 1), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 3..<4, relativeToSection: 0) })
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
store.allItems.removeSubrange(3..<5)
store.itemsBySection.replace(key: GalleryDate(2021_04_01), value: [])
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([nil, nil, nil, nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
}
func testResetTwoSectionsAfterDeletingEndWithNothingFollowing() {
let store = standardFakeStore.clone()
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load April and September
XCTAssertEqual(2, wrapper.mutate { sections in sections.loadEarlierSections(batchSize: 6) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
store.allItems.removeSubrange(5...)
store.itemsBySection.replace(key: GalleryDate(2021_09_01), value: [])
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([GalleryDate(2021_04_01)], wrapper.sections.itemsBySection.orderedKeys)
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
}
func testResetFullAfterDeletingStart() {
let store = standardFakeStore.clone()
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual(IndexSet(integersIn: 1...2), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 2..<7, relativeToSection: 0) })
XCTAssertTrue(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
store.allItems.removeFirst(3)
store.itemsBySection.replace(key: GalleryDate(2021_01_01), value: [])
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([nil, nil, nil, nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
}
func testResetFullAfterDeletingMiddle() {
let store = standardFakeStore.clone()
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual(IndexSet(integersIn: 1...2), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 2..<7, relativeToSection: 0) })
XCTAssertTrue(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
store.allItems.removeSubrange(3..<5)
store.itemsBySection.replace(key: GalleryDate(2021_04_01), value: [])
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([nil, nil, nil, nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
}
func testResetFullAfterDeletingEnd() {
let store = standardFakeStore.clone()
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual(IndexSet(integersIn: 1...2), wrapper.mutate { sections in sections.ensureItemsLoaded(in: 2..<7, relativeToSection: 0) })
XCTAssertTrue(wrapper.sections.hasFetchedOldest)
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
store.allItems.removeSubrange(5...)
store.itemsBySection.replace(key: GalleryDate(2021_09_01), value: [])
wrapper.mutate { sections in
sections.reset()
}
XCTAssertFalse(wrapper.sections.hasFetchedOldest)
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(2, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual([nil, nil], wrapper.sections.itemsBySection[1].value.map { $0.item })
}
func testGetOrAddItem() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
let fakeItem = FakeItem(2021_01_05)
let itemId = wrapper.sections.stateForTesting.itemsBySection[fakeItem.galleryDate]![1].itemId
let newItem = wrapper.mutate { sections in sections.getOrReplaceItem(fakeItem, itemId: itemId) }
XCTAssertEqual(fakeItem, newItem)
XCTAssertEqual([nil, fakeItem, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
let fakeItem2 = FakeItem(2021_01_06)
let itemId2 = wrapper.sections.stateForTesting.itemsBySection[fakeItem2.galleryDate]![1].itemId
let newItem2 = wrapper.mutate { sections in sections.getOrReplaceItem(fakeItem2, itemId: itemId2) }
XCTAssertEqual(fakeItem, newItem2)
XCTAssertEqual([nil, fakeItem, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
}
func testIndexAfter() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 0), wrapper.sections.indexPath(after: MediaGalleryIndexPath(item: 0, section: 0)))
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 0), wrapper.sections.indexPath(after: MediaGalleryIndexPath(item: 1, section: 0)))
XCTAssertNil(wrapper.sections.indexPath(after: MediaGalleryIndexPath(item: 2, section: 0)))
// Load remaining sections
XCTAssertEqual(2, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 0), wrapper.sections.indexPath(after: MediaGalleryIndexPath(item: 0, section: 0)))
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 0), wrapper.sections.indexPath(after: MediaGalleryIndexPath(item: 1, section: 0)))
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 1), wrapper.sections.indexPath(after: MediaGalleryIndexPath(item: 2, section: 0)))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 1), wrapper.sections.indexPath(after: MediaGalleryIndexPath(item: 0, section: 1)))
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 2), wrapper.sections.indexPath(after: MediaGalleryIndexPath(item: 1, section: 1)))
XCTAssertNil(wrapper.sections.indexPath(after: MediaGalleryIndexPath(item: 4, section: 2)))
}
func testIndexBefore() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 1) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
XCTAssertEqual([nil, nil, nil], wrapper.sections.itemsBySection[0].value.map { $0.item })
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 0), wrapper.sections.indexPath(before: MediaGalleryIndexPath(item: 2, section: 0)))
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 0), wrapper.sections.indexPath(before: MediaGalleryIndexPath(item: 1, section: 0)))
XCTAssertNil(wrapper.sections.indexPath(before: MediaGalleryIndexPath(item: 0, section: 0)))
// Load remaining sections
XCTAssertEqual(2, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 1), wrapper.sections.indexPath(before: MediaGalleryIndexPath(item: 0, section: 2)))
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 1), wrapper.sections.indexPath(before: MediaGalleryIndexPath(item: 1, section: 1)))
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 0), wrapper.sections.indexPath(before: MediaGalleryIndexPath(item: 0, section: 1)))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 0), wrapper.sections.indexPath(before: MediaGalleryIndexPath(item: 2, section: 0)))
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 0), wrapper.sections.indexPath(before: MediaGalleryIndexPath(item: 1, section: 0)))
XCTAssertNil(wrapper.sections.indexPath(before: MediaGalleryIndexPath(item: 0, section: 0)))
}
func testIndexPathOf() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load all months
XCTAssertEqual(3, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
// Load all items.
XCTAssert(wrapper.mutate { sections in sections.ensureItemsLoaded(in: 0..<20, relativeToSection: 0) }.isEmpty)
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 0), wrapper.sections.indexPath(for: store.allItems[0]))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 0), wrapper.sections.indexPath(for: store.allItems[1]))
XCTAssertEqual(MediaGalleryIndexPath(item: 2, section: 0), wrapper.sections.indexPath(for: store.allItems[2]))
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 1), wrapper.sections.indexPath(for: store.allItems[3]))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 1), wrapper.sections.indexPath(for: store.allItems[4]))
XCTAssertEqual(MediaGalleryIndexPath(item: 0, section: 2), wrapper.sections.indexPath(for: store.allItems[5]))
XCTAssertEqual(MediaGalleryIndexPath(item: 1, section: 2), wrapper.sections.indexPath(for: store.allItems[6]))
// Different uniqueId -> no match, even though the timestamp matches.
XCTAssert(store.allItems.contains { $0.timestamp == Date(compressedDate: 2021_09_09) })
XCTAssertNil(wrapper.sections.indexPath(for: FakeItem(2021_09_09)))
}
func testTrimFromStart() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load all months
XCTAssertEqual(3, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
// _ _ _ | _ _ | x _ x x _
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: 0..<1, relativeToSection: 2) }
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: 2..<4, relativeToSection: 2) }
XCTAssertEqual(1..<4, wrapper.sections.trimLoadedItemsAtStart(from: 0..<4, relativeToSection: 2))
XCTAssertEqual(1..<5, wrapper.sections.trimLoadedItemsAtStart(from: 0..<5, relativeToSection: 2))
// End index is not even checked.
XCTAssertEqual(1..<6, wrapper.sections.trimLoadedItemsAtStart(from: 0..<6, relativeToSection: 2))
XCTAssertEqual(1..<4, wrapper.sections.trimLoadedItemsAtStart(from: 1..<4, relativeToSection: 2))
XCTAssertEqual(4..<4, wrapper.sections.trimLoadedItemsAtStart(from: 2..<4, relativeToSection: 2))
XCTAssertEqual(4..<5, wrapper.sections.trimLoadedItemsAtStart(from: 2..<5, relativeToSection: 2))
XCTAssertEqual(-1 ..< 5, wrapper.sections.trimLoadedItemsAtStart(from: -1 ..< 5, relativeToSection: 2))
XCTAssertEqual(-5 ..< 5, wrapper.sections.trimLoadedItemsAtStart(from: -5 ..< 5, relativeToSection: 2))
// _ _ _ | _ x | x _ x x _
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: 1..<2, relativeToSection: 1) }
XCTAssertEqual(1..<5, wrapper.sections.trimLoadedItemsAtStart(from: -1 ..< 5, relativeToSection: 2))
XCTAssertEqual(-2 ..< 5, wrapper.sections.trimLoadedItemsAtStart(from: -2 ..< 5, relativeToSection: 2))
// trimLoadedItemsAtStart never goes past the current section.
XCTAssertEqual(2..<4, wrapper.sections.trimLoadedItemsAtStart(from: 1..<4, relativeToSection: 1))
XCTAssertEqual(0..<4, wrapper.sections.trimLoadedItemsAtStart(from: 0..<4, relativeToSection: 1))
// _ _ _ | x x | x _ x x _
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: 0..<2, relativeToSection: 1) }
XCTAssertEqual(1..<5, wrapper.sections.trimLoadedItemsAtStart(from: -1 ..< 5, relativeToSection: 2))
XCTAssertEqual(1..<5, wrapper.sections.trimLoadedItemsAtStart(from: -2 ..< 5, relativeToSection: 2))
XCTAssertEqual(-3 ..< 5, wrapper.sections.trimLoadedItemsAtStart(from: -3 ..< 5, relativeToSection: 2))
// trimLoadedItemsAtStart never goes past the current section.
XCTAssertEqual(2..<4, wrapper.sections.trimLoadedItemsAtStart(from: 1..<4, relativeToSection: 1))
XCTAssertEqual(2..<4, wrapper.sections.trimLoadedItemsAtStart(from: 0..<4, relativeToSection: 1))
// _ _ x | x _ | x _ x x _
_ = wrapper.mutate { sections in sections.reloadSection(for: sections.sectionDates[1]) }
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: -1 ..< 1, relativeToSection: 1) }
XCTAssertEqual(-1 ..< 5, wrapper.sections.trimLoadedItemsAtStart(from: -1 ..< 5, relativeToSection: 2))
XCTAssertEqual(-1 ..< 5, wrapper.sections.trimLoadedItemsAtStart(from: -2 ..< 5, relativeToSection: 2))
XCTAssertEqual(-1 ..< 5, wrapper.sections.trimLoadedItemsAtStart(from: -3 ..< 5, relativeToSection: 2))
XCTAssertEqual(-4 ..< 5, wrapper.sections.trimLoadedItemsAtStart(from: -4 ..< 5, relativeToSection: 2))
XCTAssertEqual(1..<4, wrapper.sections.trimLoadedItemsAtStart(from: 1..<4, relativeToSection: 1))
XCTAssertEqual(1..<4, wrapper.sections.trimLoadedItemsAtStart(from: 0..<4, relativeToSection: 1))
// x x x | x x | x x x x x
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: 0..<10, relativeToSection: 0) }
XCTAssertEqual(5..<5, wrapper.sections.trimLoadedItemsAtStart(from: -5 ..< 5, relativeToSection: 2))
XCTAssertEqual(3..<3, wrapper.sections.trimLoadedItemsAtStart(from: -5 ..< 3, relativeToSection: 2))
// trimLoadedItemsAtStart never goes past the current section.
XCTAssertEqual(2..<7, wrapper.sections.trimLoadedItemsAtStart(from: -3 ..< 7, relativeToSection: 1))
}
func testTrimFromEnd() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load all months
XCTAssertEqual(3, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
// x x _ | _ _ | _ _ _ _ _
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: 0..<2, relativeToSection: 0) }
XCTAssertEqual(0..<0, wrapper.sections.trimLoadedItemsAtEnd(from: 0..<1, relativeToSection: 0))
XCTAssertEqual(0..<0, wrapper.sections.trimLoadedItemsAtEnd(from: 0..<2, relativeToSection: 0))
XCTAssertEqual(0..<3, wrapper.sections.trimLoadedItemsAtEnd(from: 0..<3, relativeToSection: 0))
XCTAssertEqual(0..<4, wrapper.sections.trimLoadedItemsAtEnd(from: 0..<4, relativeToSection: 0))
// We don't actually check the start index.
XCTAssertEqual(-5 ..< 0, wrapper.sections.trimLoadedItemsAtEnd(from: -5 ..< 2, relativeToSection: 0))
}
func testLoadingTrimsRequestedRange() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load all months
XCTAssertEqual(3, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
// _ _ x | x x | x _ _ _ _
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: -1 ..< 3, relativeToSection: 1) }
XCTAssertEqual(2..<6, store.mostRecentRequest)
// _ _ x | x x | x x x _ _
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: 0 ..< 5, relativeToSection: 1) }
// We only trim up to the current section, so the first item in September gets reloaded.
XCTAssertEqual(5..<8, store.mostRecentRequest)
// x x x | x x | x x x _ _
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: -3 ..< 1, relativeToSection: 1) }
// We only trim down to the current section, so the last item in January gets reloaded.
XCTAssertEqual(0..<3, store.mostRecentRequest)
// x x x | _ _ | x x x _ _
_ = wrapper.mutate { sections in sections.reloadSection(for: sections.sectionDates[1]) }
// x x x | x x | x x x _ _
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: -1 ..< 3, relativeToSection: 1) }
XCTAssertEqual(3..<5, store.mostRecentRequest)
store.mostRecentRequest = 5000..<5000
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: -1 ..< 3, relativeToSection: 1) }
// The request should be skipped entirely.
XCTAssertEqual(5000..<5000, store.mostRecentRequest)
}
func testReloadSections() {
let store = standardFakeStore.clone()
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load all months
XCTAssertEqual(3, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
// x x x | x x | x x x x x
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: -1 ..< 10, relativeToSection: 1) }
let modifiedItems = store.allItems.filter {
// Keep January unmodified, drop all of April, and drop one value from September.
[0, 1, 2, 5, 6, 8, 9]
.lazy
.map({ AttachmentReferenceId(ownerId: .messageBodyAttachment(messageRowId: $0), orderInOwner: nil) })
.contains($0.itemId)
}
store.set(items: modifiedItems)
let (update, delete) = wrapper.mutate { sections in
sections.reloadSections(for: Set(sections.sectionDates + [GalleryDate(2022_01_01)]))
}
XCTAssertEqual(update, IndexSet([0, 1, 2]))
XCTAssertEqual(delete, IndexSet(integer: 1))
}
func testHandleNewAttachments_NewSectionButMostRecentUnfetched() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load the first month, January 2021
XCTAssertEqual(1, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 3) })
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(1, wrapper.sections.itemsBySection.count)
let expected = Sections.NewAttachmentHandlingResult(update: IndexSet(),
didAddAtEnd: false,
didReset: false)
let actual = wrapper.mutate { sections in sections.handleNewAttachments([GalleryDate(2022_01_01)]) }
XCTAssertEqual(expected, actual)
}
func testHandleNewAttachments_NewSectionAndMostRecentFetched() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load all months
XCTAssertEqual(3, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
_ = wrapper.mutate { sections in sections.ensureItemsLoaded(in: -1 ..< 10, relativeToSection: 1) }
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
let expected = Sections.NewAttachmentHandlingResult(update: IndexSet(),
didAddAtEnd: true,
didReset: false)
let actual = wrapper.mutate { sections in sections.handleNewAttachments([GalleryDate(2022_01_01)]) }
XCTAssertFalse(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(expected, actual)
}
func testHandleNewAttachments_ExistingSection() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load all months
XCTAssertEqual(3, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
let expected = Sections.NewAttachmentHandlingResult(update: IndexSet(integer: 1),
didAddAtEnd: false,
didReset: false)
let actual = wrapper.mutate { sections in sections.handleNewAttachments([GalleryDate(2021_04_01)]) }
XCTAssertEqual(expected, actual)
}
func testHandleNewAttachments_FirstAttachmentInNewSectionNotAtEnd() {
let store = standardFakeStore
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load all months
XCTAssertEqual(3, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 20) })
XCTAssertTrue(wrapper.sections.hasFetchedMostRecent)
XCTAssertEqual(3, wrapper.sections.itemsBySection.count)
let expected = Sections.NewAttachmentHandlingResult(update: IndexSet(),
didAddAtEnd: false,
didReset: true)
let actual = wrapper.mutate { sections in sections.handleNewAttachments([GalleryDate(2020_07_01)]) }
XCTAssertEqual(expected, actual)
}
func testUserData() {
var wrapper = SectionsWrapper(sections: Sections(loader: standardFakeStore))
wrapper.mutate { sections in
_ = sections.loadEarlierSections(batchSize: 1, userData: 42)
_ = sections.loadEarlierSections(batchSize: 1, userData: 420)
}
XCTAssertEqual(wrapper.userData, [42, 420])
}
func testRemoveLoadedItems() {
let store = standardFakeStore.clone()
var wrapper = SectionsWrapper(sections: Sections(loader: store))
// Load January and April
XCTAssertEqual(2, wrapper.mutate { sections in sections.loadLaterSections(batchSize: 4) })
// Delete first and third item
var values = store.itemsBySection[0].value
let key = store.itemsBySection[0].key
values.remove(at: 2)
values.remove(at: 0)
store.itemsBySection.replace(key: key, value: values)
let indexes = wrapper.mutate { sections in
_ = sections.ensureItemsLoaded(in: 0..<3, relativeToSection: 0)
return sections.removeLoadedItems(atIndexPaths: [MediaGalleryIndexPath(item: 0, section: 0),
MediaGalleryIndexPath(item: 2, section: 0)])
}
XCTAssertEqual(IndexSet(), indexes, "Expected empty indexes but got \(indexes)")
XCTAssertEqual(values.map { $0.itemId },
wrapper.sections.itemsBySection[0].value.map { $0.itemId },
"Expected \(values.map { $0.itemId }) but got \(wrapper.sections.itemsBySection[0].value.map { $0.itemId })")
}
}
extension MediaGallerySections {
internal func resolveNaiveEndIndex(_ naiveIndex: Int,
relativeToSection initialSectionIndex: Int) -> MediaGalleryIndexPath? {
return stateForTesting.resolveNaiveEndIndex(naiveIndex, relativeToSection: initialSectionIndex)
}
internal func resolveNaiveStartIndex(_ naiveIndex: Int,
relativeToSection initialSectionIndex: Int) -> MediaGalleryIndexPath? {
return stateForTesting.resolveNaiveStartIndex(naiveIndex, relativeToSection: initialSectionIndex)
}
internal mutating func resolveNaiveStartIndex(
_ naiveIndex: Int,
relativeToSection initialSectionIndex: Int,
batchSize: Int,
userData: UpdateUserData? = nil
) -> (path: MediaGalleryIndexPath?, numberOfSectionsLoaded: Int) {
let request = State.LoadItemsRequest(date: stateForTesting.itemsBySection.orderedKeys[initialSectionIndex],
range: naiveIndex..<(naiveIndex + 1))
return snapshotManagerForTesting.mutate(userData: userData) { state, transaction in
return state.resolveNaiveStartIndex(request: request,
batchSize: batchSize,
transaction: transaction)
}
}
}