Files
legado-ios/Tests/LegadoCoreTests/LocalBookServiceTests.swift
T

227 lines
12 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import Testing
@testable import LegadoCore
struct LocalBookServiceTests {
@Test func decodesDefaultTocRuleWithMissingReplacement() throws {
let data = Data(#"{"id":-1,"name":"目录","rule":"^第.+章$","serialNumber":0,"enable":true}"#.utf8)
let value = try JSONDecoder().decode(TxtTocRule.self, from: data)
#expect(value.replacement == "")
#expect(value.example == nil)
#expect(value.enable)
}
@Test func importsUTF8TextSplitsChaptersAndReadsContent() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let source = root.appending(path: "测试书.txt")
let sourceData = Data("序言\n第一章 相遇\n这是第一章正文\n第二章 远行\n这是第二章正文".utf8)
try sourceData.write(to: source)
let service = LocalBookService()
let report = try service.importTextFile(at: source, into: root.appending(path: "library"))
#expect(report.book.origin == "local")
#expect(report.book.charset == "UTF-8")
#expect(report.chapters.map(\.title) == ["前言", "第一章 相遇", "第二章 远行"])
#expect(report.chapters[1].url == "82e56047094237f3")
let firstTitle = try #require(sourceData.range(of: Data("第一章 相遇".utf8)))
let secondTitle = try #require(sourceData.range(of: Data("第二章 远行".utf8)))
#expect(report.chapters[1].start == Int64(firstTitle.upperBound))
#expect(report.chapters[1].end == Int64(secondTitle.lowerBound))
#expect(report.chapters.map(\.wordCount) == ["3字", "9字", "8字"])
#expect(report.book.wordCount == "20字")
#expect(report.book.intro == "序言\n")
let content = try service.loadContent(book: report.book, chapter: report.chapters[1])
#expect(content.content == "  这是第一章正文\n")
#expect(FileManager.default.fileExists(atPath: try #require(URL(string: report.book.bookUrl)).path))
}
@Test func createsSingleChapterWhenNoHeadingExists() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let source = root.appending(path: "短篇.txt")
try Data("这是一个没有章节标题的短篇。".utf8).write(to: source)
let report = try LocalBookService().importTextFile(at: source, into: root.appending(path: "library"))
#expect(report.chapters.count == 1)
#expect(report.chapters[0].title == "短篇")
#expect(try LocalBookService().loadContent(book: report.book, chapter: report.chapters[0]).content == "这是一个没有章节标题的短篇。")
}
@Test func splitsLongHeadinglessTextAtTenKiBThreshold() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let source = root.appending(path: "无目录长文.txt")
let paragraph = String(repeating: "a", count: 10_500)
try Data("\(paragraph)\n\(paragraph)\n尾声".utf8).write(to: source)
let report = try LocalBookService(tocRules: []).importTextFile(
at: source,
into: root.appending(path: "library")
)
#expect(report.chapters.count == 2)
#expect(report.chapters.map(\.title) == ["第1章(1)", "第1章(2)"])
#expect(report.chapters[0].end == report.chapters[1].start)
#expect(report.book.wordCount == "2.1万字")
#expect(report.book.intro == nil)
}
@Test func numbersHeadinglessChaptersBy512KBBlock() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let source = root.appending(path: "多块无目录.txt")
let paragraph = String(repeating: "a", count: 10_500) + "\n"
try Data(String(repeating: paragraph, count: 55).utf8).write(to: source)
let report = try LocalBookService(tocRules: []).importTextFile(
at: source,
into: root.appending(path: "library")
)
#expect(report.chapters.first?.title == "第1章(1)")
#expect(report.chapters.contains { $0.title.hasPrefix("第2章(") })
for index in 1..<report.chapters.count {
#expect(report.chapters[index - 1].end == report.chapters[index].start)
}
}
@Test func selectsCustomTocRuleAndRunsTitleScript() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let source = root.appending(path: "自定义目录.txt")
let body = String(repeating: "正文", count: 650)
try Data("卷1 开始\n\(body)\n卷2 继续\n\(body)\n卷3 结束\n\(body)".utf8).write(to: source)
let rule = TxtTocRule(
name: "卷标题",
rule: #"(?m)^卷\d+[^\r\n]*$"#,
replacement: "result.replace('卷', '第')",
serialNumber: 1
)
let report = try LocalBookService(tocRules: [rule]).importTextFile(
at: source,
into: root.appending(path: "library")
)
#expect(report.book.tocUrl == rule.rule + TxtTocRuleSelector.separator + rule.replacement)
#expect(report.chapters.map(\.title) == ["第1 开始", "第2 继续", "第3 结束"])
}
@Test func tocRuleSelectorKeepsEarlierRuleUnlessLaterWinsByMoreThanTwo() {
let body = String(repeating: "字", count: 1_100)
let content = "A1\n\(body)\nA2\n\(body)\nA3\n\(body)\nB1\n\(body)\nB2\n\(body)\nB3\n\(body)\nB4"
let first = TxtTocRule(id: 1, name: "A", rule: #"(?m)^A\d$"#, serialNumber: 1)
let second = TxtTocRule(id: 2, name: "B", rule: #"(?m)^B\d$"#, serialNumber: 2)
#expect(TxtTocRuleSelector.select(content: content, rules: [first, second])?.id == first.id)
}
@Test func titleScriptPutVolumeInsertsVolumeChapter() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let source = root.appending(path: "分卷.txt")
let body = String(repeating: "正文", count: 650)
try Data("章1\n\(body)\n章2\n\(body)\n章3\n\(body)".utf8).write(to: source)
let rule = TxtTocRule(
name: "脚本分卷",
rule: #"(?m)^章\d$"#,
replacement: "if (index === 2) java.putVolume('第二卷'); result;",
serialNumber: 1
)
let report = try LocalBookService(tocRules: [rule]).importTextFile(
at: source,
into: root.appending(path: "library")
)
#expect(report.chapters.map(\.title) == ["章1", "第二卷", "章2", "章3"])
#expect(report.chapters.map(\.isVolume) == [false, true, false, false])
#expect(report.chapters[1].start == report.chapters[1].end)
}
@Test func scansHeadingsAcross512KBBlocksAndReadsByByteRange() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let source = root.appending(path: "跨块.txt")
let longBody = String(repeating: "段落内容\n", count: 50_000)
let sourceData = Data("第一章 开始\n\(longBody)第二章 结束\n结尾".utf8)
try sourceData.write(to: source)
let report = try LocalBookService().importTextFile(
at: source,
into: root.appending(path: "library")
)
let secondTitle = try #require(sourceData.range(of: Data("第二章 结束".utf8)))
#expect(report.chapters.map(\.title) == ["第一章 开始", "第二章 结束"])
#expect(report.chapters[0].end == Int64(secondTitle.lowerBound))
#expect(report.chapters[1].start == Int64(secondTitle.upperBound))
#expect(try LocalBookService().loadContent(book: report.book, chapter: report.chapters[1]).content == "  结尾")
}
@Test func preservesUTF16EndianBoundariesAcross512KBBlocks() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let text = "第一章 开始\n\(String(repeating: "段落内容\n", count: 70_000))第二章 结束\n结尾"
let fixtures: [(String, String.Encoding, [UInt8], String)] = [
("LE", .utf16LittleEndian, [0xFF, 0xFE], "UTF-16LE"),
("BE", .utf16BigEndian, [0xFE, 0xFF], "UTF-16BE")
]
for (suffix, encoding, bom, charset) in fixtures {
let source = root.appending(path: "UTF16\(suffix).txt")
var sourceData = Data(bom)
sourceData.append(try #require(text.data(using: encoding)))
try sourceData.write(to: source)
let report = try LocalBookService().importTextFile(
at: source,
into: root.appending(path: "library-\(suffix)")
)
let titleData = try #require("第二章 结束".data(using: encoding))
let titleRange = try #require(sourceData.range(of: titleData))
let second = try #require(report.chapters.first(where: { $0.title == "第二章 结束" }))
#expect(report.book.charset == charset)
#expect(second.start == Int64(titleRange.upperBound))
#expect(try LocalBookService().loadContent(book: report.book, chapter: second).content == "  结尾")
}
}
@Test func splitsOversizedTocChapterIntoVolumeAndUpdatesFollowingScriptState() throws {
let root = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let source = root.appending(path: "超长章节.txt")
let longBody = String(repeating: "正文内容\n", count: 12_000)
try Data("第一章\n\(longBody)第二章\n结尾".utf8).write(to: source)
let rule = TxtTocRule(
name: "长章脚本",
rule: #"(?m)^第[一二]章$"#,
replacement: "result === '第二章' ? lastVolumeTitle + '|' + index + '|' + result : result",
serialNumber: 1
)
let report = try LocalBookService(tocRules: [rule]).importTextFile(
at: source,
into: root.appending(path: "library")
)
#expect(report.chapters.first?.title == "第一章")
#expect(report.chapters.first?.isVolume == true)
#expect(report.chapters.dropFirst().contains { $0.title == "第一章(1)" })
#expect(report.chapters.last?.title.hasPrefix("第一章|") == true)
#expect(report.chapters.last?.title.hasSuffix("|第二章") == true)
}
}