mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
129 lines
6.4 KiB
Swift
129 lines
6.4 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import LegadoCore
|
|
|
|
struct ChapterContentCacheTests {
|
|
@Test func savesLoadsAndSeparatesBooks() async throws {
|
|
let directory = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString, directoryHint: .isDirectory)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let cache = ChapterContentCache(directory: directory)
|
|
let first = CachedChapterContent(bookURL: "book-a", chapterURL: "same", title: "一", content: "正文 A")
|
|
let second = CachedChapterContent(bookURL: "book-b", chapterURL: "same", title: "二", content: "正文 B")
|
|
|
|
try await cache.save(first)
|
|
try await cache.save(second)
|
|
|
|
#expect(try await cache.load(bookURL: "book-a", chapterURL: "same") == first)
|
|
#expect(try await cache.load(bookURL: "book-b", chapterURL: "same") == second)
|
|
let statistics = try await cache.statistics()
|
|
#expect(statistics.entries == 2)
|
|
}
|
|
|
|
@Test func removesOnlyRequestedBookAndRejectsEmptyEntries() async throws {
|
|
let directory = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString, directoryHint: .isDirectory)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let cache = ChapterContentCache(directory: directory)
|
|
try await cache.save(.init(bookURL: "a", chapterURL: "1", content: "A"))
|
|
try await cache.save(.init(bookURL: "b", chapterURL: "1", content: "B"))
|
|
|
|
try await cache.remove(bookURL: "a")
|
|
|
|
#expect(try await cache.load(bookURL: "a", chapterURL: "1") == nil)
|
|
#expect(try await cache.load(bookURL: "b", chapterURL: "1")?.content == "B")
|
|
await #expect(throws: ChapterContentCacheError.invalidEntry) {
|
|
try await cache.save(.init(bookURL: "a", chapterURL: "2", content: ""))
|
|
}
|
|
}
|
|
|
|
@Test func coordinatorUsesDiskCacheBeforeNetwork() async throws {
|
|
let directory = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString, directoryHint: .isDirectory)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let loader = StubChapterLoader(content: "网络正文")
|
|
let coordinator = ChapterContentCoordinator(loader: loader, cache: .init(directory: directory))
|
|
let book = Book(bookUrl: "book", name: "书", origin: "source")
|
|
let chapter = BookChapter(url: "chapter", title: "章", bookUrl: book.bookUrl, index: 0)
|
|
let source = BookSource(bookSourceUrl: "https://source.test", bookSourceName: "源")
|
|
|
|
let first = await coordinator.load(book: book, chapter: chapter, source: source)
|
|
let second = await coordinator.load(book: book, chapter: chapter, source: source)
|
|
|
|
#expect(!first.loadedFromCache)
|
|
#expect(second.loadedFromCache)
|
|
#expect(second.report.content == "网络正文")
|
|
#expect(await loader.calls() == 1)
|
|
}
|
|
|
|
@Test func removesOneChapterAndForceRefreshesIt() async throws {
|
|
let directory = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString, directoryHint: .isDirectory)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let cache = ChapterContentCache(directory: directory)
|
|
try await cache.save(.init(bookURL: "book", chapterURL: "one", content: "旧正文"))
|
|
try await cache.save(.init(bookURL: "book", chapterURL: "two", content: "保留正文"))
|
|
try await cache.remove(bookURL: "book", chapterURL: "one")
|
|
#expect(try await cache.load(bookURL: "book", chapterURL: "one") == nil)
|
|
#expect(try await cache.load(bookURL: "book", chapterURL: "two")?.content == "保留正文")
|
|
|
|
let loader = StubChapterLoader(content: "刷新正文")
|
|
let coordinator = ChapterContentCoordinator(loader: loader, cache: cache)
|
|
let book = Book(bookUrl: "book", name: "书", origin: "source")
|
|
let chapter = BookChapter(url: "two", title: "章", bookUrl: book.bookUrl, index: 0)
|
|
let source = BookSource(bookSourceUrl: "https://source.test", bookSourceName: "源")
|
|
let refreshed = await coordinator.load(book: book, chapter: chapter, source: source, forceRefresh: true)
|
|
|
|
#expect(!refreshed.loadedFromCache)
|
|
#expect(refreshed.report.content == "刷新正文")
|
|
#expect(await loader.calls() == 1)
|
|
}
|
|
|
|
@Test func coordinatorPrefetchesOnce() async {
|
|
let directory = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString, directoryHint: .isDirectory)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let loader = StubChapterLoader(content: "下一章")
|
|
let coordinator = ChapterContentCoordinator(loader: loader, cache: .init(directory: directory))
|
|
let book = Book(bookUrl: "book", name: "书", origin: "source")
|
|
let chapter = BookChapter(url: "next", title: "下一章", bookUrl: book.bookUrl, index: 1)
|
|
let source = BookSource(bookSourceUrl: "https://source.test", bookSourceName: "源")
|
|
|
|
#expect(await coordinator.prefetch(book: book, chapter: chapter, source: source))
|
|
#expect(!(await coordinator.prefetch(book: book, chapter: chapter, source: source)))
|
|
#expect(await loader.calls() == 1)
|
|
}
|
|
|
|
@Test func cachePlannerSelectsRangeAndSkipsVolumeRows() {
|
|
var chapters = (0..<5).map { BookChapter(url: "c\($0)", title: "第 \($0) 章", bookUrl: "book", index: $0) }
|
|
chapters[2].isVolume = true
|
|
|
|
let jobs = ChapterCachePlanner.plan(chapters: chapters, startIndex: 1, limit: 2)
|
|
|
|
#expect(jobs.map(\.chapter.index) == [1, 3])
|
|
#expect(jobs.map(\.nextChapterURL) == ["c3", "c4"])
|
|
}
|
|
|
|
@Test func cachePlannerClampsStartAndRejectsEmptyLimit() {
|
|
let chapters = (0..<2).map { BookChapter(url: "c\($0)", title: "章", bookUrl: "book", index: $0) }
|
|
|
|
#expect(ChapterCachePlanner.plan(chapters: chapters, startIndex: 99).map(\.chapter.index) == [1])
|
|
#expect(ChapterCachePlanner.plan(chapters: chapters, startIndex: 0, limit: 0).isEmpty)
|
|
}
|
|
}
|
|
|
|
private actor StubChapterLoader: ChapterContentLoading {
|
|
private let content: String
|
|
private var callCount = 0
|
|
|
|
init(content: String) { self.content = content }
|
|
|
|
func loadContent(
|
|
book: Book,
|
|
chapter: BookChapter,
|
|
source: BookSource,
|
|
nextChapterURL: String?,
|
|
maxPages: Int
|
|
) async -> ChapterContentReport {
|
|
callCount += 1
|
|
return .init(title: chapter.title, content: content)
|
|
}
|
|
|
|
func calls() -> Int { callCount }
|
|
}
|