Tests/OrgCoreTests/RoundTripTests.swift
83 lines · 3727 bytes
1import Foundation
2import Testing
3@testable import OrgCore
4
5/// SplitMix64, so failures reproduce from the seed.
6struct SeededGenerator: RandomNumberGenerator {
7 var state: UInt64
8 mutating func next() -> UInt64 {
9 state &+= 0x9E37_79B9_7F4A_7C15
10 var z = state
11 z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9
12 z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB
13 return z ^ (z >> 31)
14 }
15}
16
17let fragments = [
18 "* ", "** TODO [#A] title :a:b:", "*** DONE", "#+TODO: NEXT | DONE", "#+begin_src sh", "#+end_src",
19 "#+BEGIN_QUOTE", "#+end_quote", "#+BEGIN: clocktable", "#+END:", ":PROPERTIES:", ":ID: x", ":END:",
20 ":LOGBOOK:", "CLOCK: [2026-10-04 Sun 10:00]", "SCHEDULED: <2026-10-04 Sun>", "- item", " - nested",
21 "\t+ tab", "1. one", "| a | b |", "|---+---|", "#+TBLFM: $2=$1", "# comment", ": fixed", "-----",
22 "[fn:1] note", "#+NAME: x", "plain text", "é", "😀", "e\u{301}", " ", "\t", "\n", "\n", "\r\n", "\r", "",
23 "*bold*", "/it/ ", "=v=", "~c~", "_u_", "+s+", "*", "/", "=", "[[https://a.b][d *b*]]", "[[x]]", "[[", "]]",
24 "<2026-10-04 Sun 10:00 +1w -2d>", "[2026-10-04]--[2026-10-05]", "<", ">", "[fn:2]", "[fn::in [x]]", "[1/3]",
25 "[50%]", "<<t>>", "{{{m(a)}}}", "\\(x\\)", "a\\\\", "x^2", "^{y}", "src_sh{echo}", "https://e.com.",
26 "<https://e.com>", "| *a* | b |", "||", "- [X] done", "+ [ ] todo", "-",
27 "[cite:@a; @b]", "[cite/t:see @k p. 2;]", "@@html:<b>@@", "call_f(x=1)[:results raw]", "%%(diary-float t 4 2)",
28 "+---+---+", "+-", "- term :: def", "1. [@3] three", "- [@2] [X] x :: y", " :: ", "@", "call_",
29]
30
31func randomDocument(_ rng: inout SeededGenerator) -> String {
32 (0..<Int.random(in: 0...40, using: &rng)).map { _ in
33 fragments.randomElement(using: &rng)! + (Bool.random(using: &rng) ? "\n" : "")
34 }.joined()
35}
36
37func checkLengths(_ node: SyntaxNode) -> Bool {
38 let sum = node.green.children.reduce(0) { $0 + $1.length }
39 return sum == node.green.length && node.children.allSatisfy(checkLengths)
40}
41
42struct RoundTripTests {
43 @Test func fuzzedDocumentsRoundTrip() {
44 var rng = SeededGenerator(state: 20261004)
45 for n in 0..<2_000 {
46 let text = randomDocument(&rng)
47 let tree = OrgParser.parse(text)
48 #expect(tree.text == text, "document \(n)")
49 #expect(checkLengths(tree.root), "document \(n)")
50 _ = DocumentModel(tree: tree)
51 }
52 }
53
54 @Test(arguments: [
55 [0xEF, 0xBB, 0xBF] + Array("* a\r\n".utf8),
56 Array("* a\r\n- b\n\tc".utf8),
57 Array("* a".utf8),
58 Array("😀 e\u{301}\n".utf8),
59 [0x2A, 0x20, 0xFF, 0x0A] as [UInt8],
60 ])
61 func encodingFixturesRoundTrip(bytes: [UInt8]) {
62 let source = SourceText(bytes: bytes)
63 let tree = OrgParser.parse(source.text)
64 #expect(source.encode(tree.text) == bytes)
65 }
66
67 /// Private corpus: `ORGSTAR_CORPUS=/path/to/org swift test --filter corpus`.
68 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_CORPUS"] != nil))
69 func corpusRoundTrips() throws {
70 let root = URL(fileURLWithPath: ProcessInfo.processInfo.environment["ORGSTAR_CORPUS"]!)
71 let files = FileManager.default.enumerator(at: root, includingPropertiesForKeys: nil)!
72 .compactMap { $0 as? URL }
73 .filter { ["org", "org_archive"].contains($0.pathExtension) }
74 #expect(!files.isEmpty)
75 for file in files {
76 let bytes = try [UInt8](Data(contentsOf: file))
77 let source = SourceText(bytes: bytes)
78 let tree = OrgParser.parse(source.text)
79 #expect(source.encode(tree.text) == bytes, "\(file.path)")
80 _ = DocumentModel(tree: tree)
81 }
82 }
83}