Files
legado-ios/Tests/LegadoCoreTests/ChapterContentCacheTests.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 }
}