Tests/OrgCoreTests/IncrementalTests.swift
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}