krz/orgstar

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

Tests/OrgCoreTests/IncrementalTests.swift

191ddb506f3a91b93c755ba6e8123a4e8ac4f90d
orgstar/Tests/OrgCoreTests/IncrementalTests.swift history · blame · raw

117 lines · 5105 bytes

  1import Foundation
  2import Testing
  3@testable import OrgCore
  4
  5/// Reparses `text` after an edit, checks the result against a full parse, and returns the
  6/// strategy used.
  7@discardableResult
  8func checkReparse(_ text: String, _ range: Range<Int>, _ replacement: String) -> ReparseStrategy {
  9    let old = OrgParser.parse(text)
 10    let edit = TextEdit(range: range, replacement: replacement)
 11    let result = OrgParser.reparseWithStrategy(old, oldText: text, edit: edit)
 12    let expected = OrgParser.parse(edit.apply(to: text))
 13    #expect(result.tree.text == expected.text)
 14    #expect(result.tree.green == expected.green)
 15    return result.strategy
 16}
 17
 18/// UTF-16 offsets of every unicode scalar boundary.
 19func scalarOffsets(_ text: String) -> [Int] {
 20    var offsets = [0]
 21    var offset = 0
 22    for scalar in text.unicodeScalars {
 23        offset += scalar.utf16.count
 24        offsets.append(offset)
 25    }
 26    return offsets
 27}
 28
 29struct IncrementalTests {
 30    @Test func applyKeepsCRLFHalves() {
 31        #expect(TextEdit(range: 1..<1, replacement: "x").apply(to: "\r\n") == "\rx\n")
 32        #expect(TextEdit(range: 1..<3, replacement: "").apply(to: "a😀b") == "ab")
 33    }
 34
 35    @Test func typingInAParagraphReparsesOneElement() {
 36        #expect(checkReparse("* a\nhello world\n* b\n", 10..<10, "big ") == .element)
 37        #expect(checkReparse("- item *one*\n  - two\n", 8..<8, "x") == .element)
 38    }
 39
 40    @Test func typingAtTheStartOfAnItemParagraph() {
 41        #expect(checkReparse(" >\n1. one", 6..<6, " ") != .element)
 42    }
 43
 44    @Test func changingATodoKeywordReparsesOneElement() {
 45        #expect(checkReparse("* TODO a\nbody\n", 2..<6, "DONE") == .element)
 46    }
 47
 48    @Test func newHeadingReparsesSections() {
 49        #expect(checkReparse("* a\nx\ny\n* b\n", 6..<6, "* c\n") == .sections)
 50    }
 51
 52    @Test func joiningParagraphsReparsesARegion() {
 53        #expect(checkReparse("a\n\nb\n", 1..<2, "") == .region)
 54        #expect(checkReparse("* h\nfirst\nsecond\n\nthird\n** child\n", 15..<15, "\n\n") == .region)
 55    }
 56
 57    @Test func regionGrowsUntilABoundaryHolds() {
 58        // Splitting a paragraph next to a table and a list.
 59        #expect(checkReparse("* h\nx\ntext\nmore\n| a |\n- i\n", 9..<9, "\n") == .region)
 60        // A new begin line without its end runs to the end of the body.
 61        #expect(checkReparse("* h\nx\none\ntwo\n#+end_src\nthree\n", 6..<6, "\n#+begin_src") == .region)
 62    }
 63
 64    @Test func regionDefersToSectionsNearHeadings() {
 65        // The first body line could become a planning line.
 66        #expect(checkReparse("* h\nbody\n", 4..<4, "\n") != .region)
 67        // An added end delimiter could close a block that starts before the window.
 68        #expect(checkReparse("x\n#+begin_src\na\n\nb\n", 16..<16, "#+end_src\n") != .region)
 69    }
 70
 71    @Test func runGrowsUntilAHeadingEndsIt() {
 72        #expect(checkReparse("** a\n** b\n* c\n", 0..<0, "* x\n") == .sections)
 73    }
 74
 75    @Test func settingsLineForcesFullParse() {
 76        #expect(checkReparse("#+TODO: A | B\n* A x\n", 8..<9, "C") == .full)
 77    }
 78
 79    @Test func exposingAKeywordInsideABlockForcesFullParse() {
 80        let text = "#+begin_example\n#+TODO: X\n#+end_example\n* X a\n"
 81        #expect(checkReparse(text, 26..<40, "") == .full)
 82    }
 83
 84    @Test func editsAtTheEdges() {
 85        checkReparse("", 0..<0, "* a\n")
 86        checkReparse("* a\n", 0..<4, "")
 87        checkReparse("* a", 3..<3, "\n")
 88        checkReparse("a\r\nb\r\n", 1..<1, "x")
 89    }
 90
 91    /// Set `ORGSTAR_FUZZ_EDITS` to run more (the phase 1 gate is 100,000 in a release build) and
 92    /// `ORGSTAR_FUZZ_SEED` to try another sequence.
 93    @Test func incrementalEqualsFull() {
 94        let environment = ProcessInfo.processInfo.environment
 95        var rng = SeededGenerator(state: UInt64(environment["ORGSTAR_FUZZ_SEED"] ?? "") ?? 20261005)
 96        let count = Int(environment["ORGSTAR_FUZZ_EDITS"] ?? "") ?? 3_000
 97        let small = ["a", " ", "*", "/", "=", "[", "]", "<", ">", "-", ":", "|", "#", "+", "\n", "\r\n", "😀", "é"]
 98        for n in 0..<count {
 99            let text = randomDocument(&rng)
100            let offsets = scalarOffsets(text)
101            let a = offsets.randomElement(using: &rng)!
102            let later = offsets.filter { $0 >= a && $0 <= a + 12 }
103            let b = Bool.random(using: &rng) ? a : later.randomElement(using: &rng)!
104            let replacement: String
105            if Int.random(in: 0..<10, using: &rng) < 7 {
106                replacement = (0..<Int.random(in: 0...2, using: &rng)).map { _ in small.randomElement(using: &rng)! }.joined()
107            } else {
108                replacement = fragments.randomElement(using: &rng)! + (Bool.random(using: &rng) ? "\n" : "")
109            }
110            let old = OrgParser.parse(text)
111            let edit = TextEdit(range: a..<b, replacement: replacement)
112            let tree = OrgParser.reparse(old, oldText: text, edit: edit)
113            let expected = OrgParser.parse(edit.apply(to: text))
114            #expect(tree.green == expected.green, "edit \(n): \(a)..<\(b) \(replacement.debugDescription) in \(text.debugDescription)")
115        }
116    }
117}