krz/orgstar

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

Tests/OrgCoreTests/SemanticTests.swift

b98a6509c5ae75e5172dd333d1b6105fd6ceee0e
orgstar/Tests/OrgCoreTests/SemanticTests.swift history · blame · raw

159 lines · 6287 bytes

19 symbols in this file
  1import Testing
  2@testable import OrgCore
  3
  4func model(_ text: String, _ settings: SemanticSettings = .default) -> DocumentModel {
  5    DocumentModel(tree: OrgParser.parse(text), settings: settings)
  6}
  7
  8struct HeadingInfoTests {
  9    @Test func basics() {
 10        let m = model("* TODO [#A] Write *plan* :work:\n** DONE Sub\n")
 11        #expect(m.headings.count == 2)
 12        let top = m.headings[0]
 13        #expect(top.level == 1)
 14        #expect(top.todo == "TODO")
 15        #expect(!top.isDone)
 16        #expect(top.priority == "A")
 17        #expect(top.title == "Write *plan*")
 18        #expect(top.localTags == ["work"])
 19        #expect(top.parent == nil)
 20        #expect(m.headings[1].parent == 0)
 21        #expect(m.headings[1].isDone)
 22        #expect(m.outlinePath(of: 1) == ["Write *plan*", "Sub"])
 23    }
 24
 25    @Test func planningPropertiesClocksAndBody() throws {
 26        let text = """
 27        * a
 28        SCHEDULED: <2026-10-04 Sun> DEADLINE: <2026-10-10 Sat -2d>
 29        :PROPERTIES:
 30        :ID: abc
 31        :Effort: 1:00
 32        :END:
 33        :LOGBOOK:
 34        CLOCK: [2026-10-04 Sun 10:00]--[2026-10-04 Sun 11:30] =>  1:30
 35        CLOCK: [2026-10-05 Mon 09:00]
 36        :END:
 37        See <2026-10-07 Wed> and [[https://a.b][x]].
 38
 39        """
 40        let h = model(text).headings[0]
 41        #expect(h.scheduled?.start.day == 4)
 42        #expect(h.deadline?.warning?.interval.value == 2)
 43        #expect(h.closed == nil)
 44        #expect(h.id == "abc")
 45        #expect(h.properties == [Property(key: "ID", value: "abc"), Property(key: "Effort", value: "1:00")])
 46        #expect(h.clocks.count == 2)
 47        #expect(h.clocks[0].minutes == 90)
 48        #expect(h.clocks[0].end?.hour == 11)
 49        #expect(h.clocks[1].end == nil)
 50        #expect(h.timestamps == [try #require(Timestamp.parse("<2026-10-07 Wed>"))])
 51        #expect(h.links == ["https://a.b"])
 52    }
 53
 54    @Test func childContentStaysWithChild() {
 55        let m = model("* a\n** b\n<2026-10-04 Sun>\n")
 56        #expect(m.headings[0].timestamps.isEmpty)
 57        #expect(m.headings[1].timestamps.count == 1)
 58    }
 59
 60    @Test func titleTimestampsAndLinks() {
 61        let h = model("* Call <2026-10-04 Sun> [[id:x][y]]\n").headings[0]
 62        #expect(h.timestamps.count == 1)
 63        #expect(h.links == ["id:x"])
 64    }
 65
 66    @Test func doneStatesComeFromFileSettings() {
 67        let m = model("#+TODO: NEXT | SHIPPED\n* SHIPPED x\n")
 68        #expect(m.headings[0].isDone)
 69    }
 70}
 71
 72struct InheritanceTests {
 73    @Test func tagInheritance() {
 74        let text = "#+FILETAGS: :f:\n* a :x:noinherit:\n** b :y:\n"
 75        let m = model(text, SemanticSettings(tagsExcludedFromInheritance: ["noinherit"]))
 76        #expect(m.tags(of: 1).map(\.value) == ["f", "x", "y"])
 77        #expect(m.tags(of: 1).map(\.source) == [.file, .heading(0), .heading(1)])
 78        #expect(model(text, SemanticSettings(tagInheritance: false)).tags(of: 1).map(\.value) == ["y"])
 79    }
 80
 81    @Test func ownTagWinsOverInherited() {
 82        #expect(model("* a :x:\n** b :x:\n").tags(of: 1) == [Resolved("x", .heading(1))])
 83    }
 84
 85    @Test func propertyPolicies() {
 86        let text = "#+PROPERTY: owner team\n* a\n:PROPERTIES:\n:OWNER: alice\n:CATEGORY: work\n:ID: p\n:END:\n** b\n"
 87        let plain = model(text)
 88        #expect(plain.property("OWNER", of: 1) == nil)
 89        #expect(plain.property("CATEGORY", of: 1) == Resolved("work", .heading(0)))
 90        #expect(plain.property("OWNER", of: nil) == Resolved("team", .file))
 91
 92        let all = model(text, SemanticSettings(propertyInheritance: .all))
 93        #expect(all.property("owner", of: 1) == Resolved("alice", .heading(0)))
 94        #expect(all.property("ID", of: 1) == nil)
 95
 96        let some = model(text, SemanticSettings(propertyInheritance: .only(["OWNER"])))
 97        #expect(some.property("OWNER", of: 1) == Resolved("alice", .heading(0)))
 98    }
 99
100    @Test func additiveProperties() {
101        let text = "* a\n:PROPERTIES:\n:VAR: x=1\n:END:\n** b\n:PROPERTIES:\n:VAR+: y=2\n:END:\n"
102        #expect(model(text, SemanticSettings(propertyInheritance: .all)).property("VAR", of: 1) == Resolved("x=1 y=2", .heading(1)))
103        #expect(model(text).property("VAR", of: 1) == Resolved("y=2", .heading(1)))
104    }
105}
106
107struct SrcBlockTests {
108    @Test func resolution() throws {
109        let text = """
110        #+PROPERTY: header-args :dir /a :cache yes
111        #+PROPERTY: header-args:sh :results output
112        * a
113        :PROPERTIES:
114        :header-args+: :dir /b
115        :END:
116        #+NAME: hello
117        #+HEADER: :var x=1
118        #+begin_src sh -n :results silent
119        echo $x
120        #+end_src
121
122        """
123        let block = try #require(model(text).srcBlocks.first)
124        #expect(block.name == "hello")
125        #expect(block.language == "sh")
126        #expect(block.switches == ["-n"])
127        #expect(block.body == "echo $x\n")
128        #expect(block.heading == 0)
129        #expect(block.headerArguments[":dir"] == Resolved("/b", .heading(0)))
130        #expect(block.headerArguments[":cache"] == Resolved("yes", .file))
131        #expect(block.headerArguments[":results"] == Resolved("silent", .element))
132        #expect(block.headerArguments[":var"] == Resolved("x=1", .element))
133        #expect(block.headerArguments[":exports"] == Resolved("code", .defaults))
134    }
135
136    @Test func plainPropertyReplacesInheritedHeaderArguments() {
137        let text = "#+PROPERTY: header-args :dir /a\n* a\n:PROPERTIES:\n:header-args: :cache yes\n:END:\n#+begin_src sh\n#+end_src\n"
138        let block = model(text).srcBlocks[0]
139        #expect(block.headerArguments[":dir"] == nil)
140        #expect(block.headerArguments[":cache"] == Resolved("yes", .heading(0)))
141    }
142
143    @Test func blankLineDetachesAffiliatedKeywords() {
144        #expect(model("#+NAME: x\n\n#+begin_src sh\n#+end_src\n").srcBlocks[0].name == nil)
145    }
146
147    @Test func otherBlocksAreNotSourceBlocks() {
148        #expect(model("#+begin_example\nx\n#+end_example\n").srcBlocks.isEmpty)
149    }
150}
151
152struct TableModelTests {
153    @Test func rowsFormulasAndWidths() {
154        let table = model("| a | 日本 |\n|---+---|\n| 😀 | b |\n#+TBLFM: $2=$1\n").tables[0]
155        #expect(table.rows == [.cells(["a", "日本"]), .rule, .cells(["😀", "b"])])
156        #expect(table.formulas == ["$2=$1"])
157        #expect(table.columnWidths == [2, 4])
158    }
159}