/ tests / test_orgnode.py
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