mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
227 lines
12 KiB
Swift
227 lines
12 KiB
Swift
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)
|
||
}
|
||
}
|