Tests/OrgCoreTests/SemanticTests.swift
159 lines · 6287 bytes
19 symbols in this file
modelHeadingInfoTestsbasicsplanningPropertiesClocksAndBodychildContentStaysWithChildtitleTimestampsAndLinksdoneStatesComeFromFileSettingsInheritanceTeststagInheritanceownTagWinsOverInheritedpropertyPoliciesadditivePropertiesSrcBlockTestsresolutionplainPropertyReplacesInheritedHeaderArgumentsblankLineDetachesAffiliatedKeywordsotherBlocksAreNotSourceBlocksTableModelTestsrowsFormulasAndWidths
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}