test_orgnode.py
1 import datetime 2 3 from khoj.processor.content.org_mode import orgnode 4 5 6 # Test 7 # ---------------------------------------------------------------------------------------------------- 8 def test_parse_entry_with_no_headings(tmp_path): 9 "Test parsing of entry with minimal fields" 10 # Arrange 11 entry = """Body Line 1""" 12 orgfile = create_file(tmp_path, entry) 13 14 # Act 15 entries = orgnode.makelist_with_filepath(orgfile) 16 17 # Assert 18 assert len(entries) == 1 19 assert entries[0].heading == f"{orgfile}" 20 assert entries[0].tags == list() 21 assert entries[0].body == "Body Line 1" 22 assert entries[0].priority == "" 23 assert entries[0].Property("ID") == "" 24 assert entries[0].closed == "" 25 assert entries[0].scheduled == "" 26 assert entries[0].deadline == "" 27 28 29 # ---------------------------------------------------------------------------------------------------- 30 def test_parse_minimal_entry(tmp_path): 31 "Test parsing of entry with minimal fields" 32 # Arrange 33 entry = """ 34 * Heading 35 Body Line 1""" 36 orgfile = create_file(tmp_path, entry) 37 38 # Act 39 entries = orgnode.makelist_with_filepath(orgfile) 40 41 # Assert 42 assert len(entries) == 1 43 assert entries[0].heading == "Heading" 44 assert entries[0].tags == list() 45 assert entries[0].body == "Body Line 1\n\n" 46 assert entries[0].priority == "" 47 assert entries[0].Property("ID") == "" 48 assert entries[0].closed == "" 49 assert entries[0].scheduled == "" 50 assert entries[0].deadline == "" 51 52 53 # ---------------------------------------------------------------------------------------------------- 54 def test_parse_complete_entry(tmp_path): 55 "Test parsing of entry with all important fields" 56 # Arrange 57 entry = """ 58 *** DONE [#A] Heading :Tag1:TAG2:tag3: 59 CLOSED: [1984-04-01 Sun 12:00] SCHEDULED: <1984-04-01 Sun 09:00> DEADLINE: <1984-04-01 Sun> 60 :PROPERTIES: 61 :ID: 123-456-789-4234-1231 62 :END: 63 :LOGBOOK: 64 CLOCK: [1984-04-01 Sun 09:00]--[1984-04-01 Sun 12:00] => 3:00 65 - Clocked Log 1 66 :END: 67 Body Line 1 68 Body Line 2""" 69 orgfile = create_file(tmp_path, entry) 70 71 # Act 72 entries = orgnode.makelist_with_filepath(orgfile) 73 74 # Assert 75 assert len(entries) == 1 76 assert entries[0].heading == "Heading" 77 assert entries[0].todo == "DONE" 78 assert entries[0].tags == ["Tag1", "TAG2", "tag3"] 79 assert entries[0].body == "- Clocked Log 1\n\nBody Line 1\n\nBody Line 2\n\n" 80 assert entries[0].priority == "A" 81 assert entries[0].Property("ID") == "id:123-456-789-4234-1231" 82 assert entries[0].closed == datetime.date(1984, 4, 1) 83 assert entries[0].scheduled == datetime.date(1984, 4, 1) 84 assert entries[0].deadline == datetime.date(1984, 4, 1) 85 assert entries[0].logbook == [(datetime.datetime(1984, 4, 1, 9, 0, 0), datetime.datetime(1984, 4, 1, 12, 0, 0))] 86 87 88 # ---------------------------------------------------------------------------------------------------- 89 def test_render_entry_with_property_drawer_and_empty_body(tmp_path): 90 "Render heading entry with property drawer" 91 # Arrange 92 entry_to_render = """ 93 *** [#A] Heading1 :tag1: 94 :PROPERTIES: 95 :ID: 111-111-111-1111-1111 96 :END: 97 \t\r \n 98 """ 99 orgfile = create_file(tmp_path, entry_to_render) 100 101 expected_entry = f"""*** [#A] Heading1 :tag1: 102 :PROPERTIES: 103 :LINE: file://{orgfile}#line=2 104 :ID: id:111-111-111-1111-1111 105 :END: 106 """ 107 108 # Act 109 parsed_entries = orgnode.makelist_with_filepath(orgfile) 110 111 # Assert 112 assert f"{parsed_entries[0]}" == expected_entry 113 114 115 # ---------------------------------------------------------------------------------------------------- 116 def test_all_links_to_entry_rendered(tmp_path): 117 "Ensure all links to entry rendered in property drawer from entry" 118 # Arrange 119 entry = """ 120 *** [#A] Heading :tag1: 121 :PROPERTIES: 122 :ID: 123-456-789-4234-1231 123 :END: 124 Body Line 1 125 *** Heading2 126 Body Line 2 127 """ 128 orgfile = create_file(tmp_path, entry) 129 130 # Act 131 entries = orgnode.makelist_with_filepath(orgfile) 132 133 # Assert 134 # SOURCE link rendered with Heading 135 # ID link rendered with ID 136 assert ":ID: id:123-456-789-4234-1231" in f"{entries[0]}" 137 # LINE link rendered with line number 138 assert f":LINE: file://{orgfile}#line=2" in f"{entries[0]}" 139 # LINE link rendered with line number 140 assert f":LINE: file://{orgfile}#line=7" in f"{entries[1]}" 141 142 143 # ---------------------------------------------------------------------------------------------------- 144 def test_parse_multiple_entries(tmp_path): 145 "Test parsing of multiple entries" 146 # Arrange 147 content = """ 148 *** FAILED [#A] Heading1 :tag1: 149 CLOSED: [1984-04-01 Sun 12:00] SCHEDULED: <1984-04-01 Sun 09:00> DEADLINE: <1984-04-01 Sun> 150 :PROPERTIES: 151 :ID: 123-456-789-4234-0001 152 :END: 153 :LOGBOOK: 154 CLOCK: [1984-04-01 Sun 09:00]--[1984-04-01 Sun 12:00] => 3:00 155 - Clocked Log 1 156 :END: 157 Body 1 158 159 *** CANCELLED [#A] Heading2 :tag2: 160 CLOSED: [1984-04-02 Sun 12:00] SCHEDULED: <1984-04-02 Sun 09:00> DEADLINE: <1984-04-02 Sun> 161 :PROPERTIES: 162 :ID: 123-456-789-4234-0002 163 :END: 164 :LOGBOOK: 165 CLOCK: [1984-04-02 Mon 09:00]--[1984-04-02 Mon 12:00] => 3:00 166 - Clocked Log 2 167 :END: 168 Body 2 169 170 """ 171 orgfile = create_file(tmp_path, content) 172 173 # Act 174 entries = orgnode.makelist_with_filepath(orgfile) 175 176 # Assert 177 assert len(entries) == 2 178 for index, entry in enumerate(entries): 179 assert entry.heading == f"Heading{index + 1}" 180 assert entry.todo == "FAILED" if index == 0 else "CANCELLED" 181 assert entry.tags == [f"tag{index + 1}"] 182 assert entry.body == f"- Clocked Log {index + 1}\n\nBody {index + 1}\n\n" 183 assert entry.priority == "A" 184 assert entry.Property("ID") == f"id:123-456-789-4234-000{index + 1}" 185 assert entry.closed == datetime.date(1984, 4, index + 1) 186 assert entry.scheduled == datetime.date(1984, 4, index + 1) 187 assert entry.deadline == datetime.date(1984, 4, index + 1) 188 assert entry.logbook == [ 189 (datetime.datetime(1984, 4, index + 1, 9, 0, 0), datetime.datetime(1984, 4, index + 1, 12, 0, 0)) 190 ] 191 192 193 # ---------------------------------------------------------------------------------------------------- 194 def test_parse_entry_with_empty_title(tmp_path): 195 "Test parsing of entry with minimal fields" 196 # Arrange 197 entry = """#+TITLE: 198 Body Line 1""" 199 orgfile = create_file(tmp_path, entry) 200 201 # Act 202 entries = orgnode.makelist_with_filepath(orgfile) 203 204 # Assert 205 assert len(entries) == 1 206 assert entries[0].heading == f"{orgfile}" 207 assert entries[0].tags == list() 208 assert entries[0].body == "Body Line 1" 209 assert entries[0].priority == "" 210 assert entries[0].Property("ID") == "" 211 assert entries[0].closed == "" 212 assert entries[0].scheduled == "" 213 assert entries[0].deadline == "" 214 215 216 # ---------------------------------------------------------------------------------------------------- 217 def test_parse_entry_with_title_and_no_headings(tmp_path): 218 "Test parsing of entry with minimal fields" 219 # Arrange 220 entry = """#+TITLE: test 221 Body Line 1""" 222 orgfile = create_file(tmp_path, entry) 223 224 # Act 225 entries = orgnode.makelist_with_filepath(orgfile) 226 227 # Assert 228 assert len(entries) == 1 229 assert entries[0].heading == "test" 230 assert entries[0].tags == list() 231 assert entries[0].body == "Body Line 1" 232 assert entries[0].priority == "" 233 assert entries[0].Property("ID") == "" 234 assert entries[0].closed == "" 235 assert entries[0].scheduled == "" 236 assert entries[0].deadline == "" 237 assert entries[0].ancestors == ["test"] 238 239 240 # ---------------------------------------------------------------------------------------------------- 241 def test_parse_entry_with_multiple_titles_and_no_headings(tmp_path): 242 "Test parsing of entry with minimal fields" 243 # Arrange 244 entry = """#+TITLE: title1 245 Body Line 1 246 #+TITLE: title2 """ 247 orgfile = create_file(tmp_path, entry) 248 249 # Act 250 entries = orgnode.makelist_with_filepath(orgfile) 251 252 # Assert 253 assert len(entries) == 1 254 assert entries[0].heading == "title1 title2" 255 assert entries[0].tags == list() 256 assert entries[0].body == "Body Line 1\n" 257 assert entries[0].priority == "" 258 assert entries[0].Property("ID") == "" 259 assert entries[0].closed == "" 260 assert entries[0].scheduled == "" 261 assert entries[0].deadline == "" 262 assert entries[0].ancestors == ["title1 title2"] 263 264 265 # ---------------------------------------------------------------------------------------------------- 266 def test_parse_org_with_intro_text_before_heading(tmp_path): 267 "Test parsing of org file with intro text before heading" 268 # Arrange 269 body = """#+TITLE: Title 270 intro body 271 * Entry Heading 272 entry body 273 """ 274 orgfile = create_file(tmp_path, body) 275 276 # Act 277 entries = orgnode.makelist_with_filepath(orgfile) 278 279 # Assert 280 assert len(entries) == 2 281 assert entries[0].heading == "Title" 282 assert entries[0].body == "intro body\n" 283 assert entries[0].ancestors == ["Title"] 284 assert entries[1].heading == "Entry Heading" 285 assert entries[1].body == "entry body\n\n" 286 assert entries[1].ancestors == ["Title"] 287 288 289 # ---------------------------------------------------------------------------------------------------- 290 def test_parse_org_with_intro_text_multiple_titles_and_heading(tmp_path): 291 "Test parsing of org file with intro text, multiple titles and heading entry" 292 # Arrange 293 body = """#+TITLE: Title1 294 intro body 295 * Entry Heading 296 entry body 297 #+TITLE: Title2 """ 298 orgfile = create_file(tmp_path, body) 299 300 # Act 301 entries = orgnode.makelist_with_filepath(orgfile) 302 303 # Assert 304 assert len(entries) == 2 305 assert entries[0].heading == "Title1 Title2" 306 assert entries[0].body == "intro body\n" 307 assert entries[0].ancestors == ["Title1 Title2"] 308 assert entries[1].heading == "Entry Heading" 309 assert entries[1].body == "entry body\n\n" 310 assert entries[0].ancestors == ["Title1 Title2"] 311 312 313 # ---------------------------------------------------------------------------------------------------- 314 def test_parse_org_with_single_ancestor_heading(tmp_path): 315 "Parse org entries with parent headings context" 316 # Arrange 317 body = """ 318 * Heading 1 319 body 1 320 ** Sub Heading 1 321 """ 322 orgfile = create_file(tmp_path, body) 323 324 # Act 325 entries = orgnode.makelist_with_filepath(orgfile) 326 327 # Assert 328 assert len(entries) == 2 329 assert entries[0].heading == "Heading 1" 330 assert entries[0].ancestors == [f"{orgfile}"] 331 assert entries[1].heading == "Sub Heading 1" 332 assert entries[1].ancestors == [f"{orgfile}", "Heading 1"] 333 334 335 # ---------------------------------------------------------------------------------------------------- 336 def test_parse_org_with_multiple_ancestor_headings(tmp_path): 337 "Parse org entries with parent headings context" 338 # Arrange 339 body = """ 340 * Heading 1 341 body 1 342 ** Sub Heading 1 343 *** Sub Sub Heading 1 344 sub sub body 1 345 """ 346 orgfile = create_file(tmp_path, body) 347 348 # Act 349 entries = orgnode.makelist_with_filepath(orgfile) 350 351 # Assert 352 assert len(entries) == 3 353 assert entries[0].heading == "Heading 1" 354 assert entries[0].ancestors == [f"{orgfile}"] 355 assert entries[1].heading == "Sub Heading 1" 356 assert entries[1].ancestors == [f"{orgfile}", "Heading 1"] 357 assert entries[2].heading == "Sub Sub Heading 1" 358 assert entries[2].ancestors == [f"{orgfile}", "Heading 1", "Sub Heading 1"] 359 360 361 # ---------------------------------------------------------------------------------------------------- 362 def test_parse_org_with_multiple_ancestor_headings_of_siblings(tmp_path): 363 "Parse org entries with parent headings context" 364 # Arrange 365 body = """ 366 * Heading 1 367 body 1 368 ** Sub Heading 1 369 *** Sub Sub Heading 1 370 sub sub body 1 371 *** Sub Sub Heading 2 372 ** Sub Heading 2 373 *** Sub Sub Heading 3 374 """ 375 orgfile = create_file(tmp_path, body) 376 377 # Act 378 entries = orgnode.makelist_with_filepath(orgfile) 379 380 # Assert 381 assert len(entries) == 6 382 assert entries[0].heading == "Heading 1" 383 assert entries[0].ancestors == [f"{orgfile}"] 384 assert entries[1].heading == "Sub Heading 1" 385 assert entries[1].ancestors == [f"{orgfile}", "Heading 1"] 386 assert entries[2].heading == "Sub Sub Heading 1" 387 assert entries[2].ancestors == [f"{orgfile}", "Heading 1", "Sub Heading 1"] 388 assert entries[3].heading == "Sub Sub Heading 2" 389 assert entries[3].ancestors == [f"{orgfile}", "Heading 1", "Sub Heading 1"] 390 assert entries[4].heading == "Sub Heading 2" 391 assert entries[4].ancestors == [f"{orgfile}", "Heading 1"] 392 assert entries[5].heading == "Sub Sub Heading 3" 393 assert entries[5].ancestors == [f"{orgfile}", "Heading 1", "Sub Heading 2"] 394 395 396 # Helper Functions 397 def create_file(tmp_path, entry, filename="test.org"): 398 org_file = tmp_path / f"notes/{filename}" 399 org_file.parent.mkdir() 400 org_file.touch() 401 org_file.write_text(entry) 402 return org_file