krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Tests/OrgCoreTests/RoundTripTests.swift

ab6ccec56eca03d0dadf2c9332aab10c4490eb62
orgstar/Tests/OrgCoreTests/RoundTripTests.swift history · blame · raw

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}