auto-update-daily-20260202
1;;; batch-functions-test.el --- Tests for org-mode batch operations -*- lexical-binding: t; no-byte-compile: t -*-
2
3;; Copyright (C) 2025 Vincent Demeester
4
5;; Author: Vincent Demeester <vincent@sbr.pm>
6;; Keywords: org, batch, automation, testing
7
8;;; Commentary:
9
10;; ERT tests for batch-functions.el
11;; Run with: emacs --batch -L .. -l batch-functions.el -l batch-functions-test.el -f ert-run-tests-batch-and-exit
12
13;;; Code:
14
15(require 'ert)
16(require 'org)
17(require 'org-element)
18
19;; Load batch-functions from parent directory
20(eval-and-compile
21 (let* ((current-file (or load-file-name buffer-file-name))
22 (parent-dir (when current-file
23 (file-name-directory (directory-file-name (file-name-directory current-file))))))
24 (when parent-dir
25 (add-to-list 'load-path parent-dir)))
26 (require 'batch-functions))
27
28;;; Test Fixtures
29
30(defvar batch-test-fixture-dir
31 (expand-file-name "fixtures" (file-name-directory load-file-name))
32 "Directory containing test fixture files.")
33
34(defvar batch-test-fixture-file
35 (expand-file-name "test-todos.org" batch-test-fixture-dir)
36 "Main test fixture file.")
37
38;;; Helper Functions
39
40(defun batch-test--count-items (items)
41 "Count number of items in ITEMS list."
42 (length items))
43
44(defun batch-test--find-item-by-heading (items heading)
45 "Find item in ITEMS with matching HEADING."
46 (seq-find (lambda (item)
47 (string= (alist-get 'heading item) heading))
48 items))
49
50(defun batch-test--with-temp-org-file (content fn)
51 "Create temp org file with CONTENT, call FN with filepath, then cleanup."
52 (let ((temp-file (make-temp-file "org-test-" nil ".org")))
53 (unwind-protect
54 (progn
55 (with-temp-file temp-file
56 (insert content))
57 (funcall fn temp-file))
58 (when (file-exists-p temp-file)
59 (delete-file temp-file)))))
60
61;;; Tests for Read Operations
62
63(ert-deftest test-org-batch-list-todos ()
64 "Test listing all TODOs from file."
65 (let ((todos (org-batch-list-todos batch-test-fixture-file)))
66 (should (> (batch-test--count-items todos) 0))
67 ;; Should find "Review PR #123"
68 (should (batch-test--find-item-by-heading todos "Review PR #123"))
69 ;; Should find "Buy groceries"
70 (should (batch-test--find-item-by-heading todos "Buy groceries"))))
71
72(ert-deftest test-org-batch-list-todos-filter-state ()
73 "Test filtering TODOs by state."
74 (let ((next-todos (org-batch-list-todos batch-test-fixture-file "NEXT")))
75 (should (> (batch-test--count-items next-todos) 0))
76 ;; All items should have NEXT state
77 (dolist (todo next-todos)
78 (should (string= (alist-get 'todo todo) "NEXT")))
79 ;; Should find "Implement authentication"
80 (should (batch-test--find-item-by-heading next-todos "Implement authentication"))))
81
82(ert-deftest test-org-batch-list-todos-filter-priority ()
83 "Test filtering TODOs by priority."
84 (let ((high-priority (org-batch-list-todos batch-test-fixture-file nil 1)))
85 (should (> (batch-test--count-items high-priority) 0))
86 ;; All items should have priority 1
87 (dolist (todo high-priority)
88 (should (= (alist-get 'priority todo) 1)))
89 ;; Should find "Buy groceries"
90 (should (batch-test--find-item-by-heading high-priority "Buy groceries"))))
91
92(ert-deftest test-org-batch-list-todos-filter-tags ()
93 "Test filtering TODOs by tags."
94 (let ((work-todos (org-batch-list-todos batch-test-fixture-file nil nil '("work"))))
95 (should (> (batch-test--count-items work-todos) 0))
96 ;; All items should have :work: tag
97 (dolist (todo work-todos)
98 (should (member "work" (alist-get 'tags todo))))
99 ;; Should find "Review PR #123"
100 (should (batch-test--find-item-by-heading work-todos "Review PR #123"))))
101
102(ert-deftest test-org-batch-scheduled-today ()
103 "Test getting scheduled items for a specific date."
104 (let ((scheduled (org-batch-scheduled-today batch-test-fixture-file "2025-12-23")))
105 (should (>= (batch-test--count-items scheduled) 1))
106 ;; Should include "Review PR #123" and "Buy groceries"
107 (should (batch-test--find-item-by-heading scheduled "Review PR #123"))))
108
109(ert-deftest test-org-batch-by-section ()
110 "Test getting TODOs by section."
111 (let ((work-section (org-batch-by-section batch-test-fixture-file "Work")))
112 (should (> (batch-test--count-items work-section) 0))
113 ;; Should find work-related tasks
114 (should (batch-test--find-item-by-heading work-section "Review PR #123")))
115
116 (let ((personal-section (org-batch-by-section batch-test-fixture-file "Personal")))
117 (should (> (batch-test--count-items personal-section) 0))
118 ;; Should find personal tasks
119 (should (batch-test--find-item-by-heading personal-section "Buy groceries"))))
120
121(ert-deftest test-org-batch-count-by-state ()
122 "Test counting TODOs by state."
123 (let ((counts (org-batch-count-by-state batch-test-fixture-file)))
124 (should (> (alist-get 'total counts) 0))
125 (should (> (alist-get 'TODO counts) 0))
126 (should (>= (alist-get 'NEXT counts) 1))
127 (should (>= (alist-get 'DONE counts) 1))
128 (should (>= (alist-get 'WAIT counts) 1))))
129
130(ert-deftest test-org-batch-search ()
131 "Test searching for content in TODOs."
132 (let ((matches (org-batch-search batch-test-fixture-file "OAuth2")))
133 (should (> (batch-test--count-items matches) 0))
134 ;; Should find "Implement authentication"
135 (should (batch-test--find-item-by-heading matches "Implement authentication"))))
136
137(ert-deftest test-org-batch-get-sections ()
138 "Test getting list of top-level sections."
139 (let ((sections (org-batch-get-sections batch-test-fixture-file)))
140 (should (>= (length sections) 3))
141 (should (member "Work" sections))
142 (should (member "Personal" sections))
143 (should (member "Archive" sections))))
144
145(ert-deftest test-org-batch-get-children ()
146 "Test getting direct children of a heading."
147 (let ((work-children (org-batch-get-children batch-test-fixture-file "Work")))
148 (should (>= (batch-test--count-items work-children) 2))
149 ;; Should find direct children only
150 (should (batch-test--find-item-by-heading work-children "Review PR #123"))
151 (should (batch-test--find-item-by-heading work-children "Implement authentication"))))
152
153;;; Tests for Write Operations
154
155(ert-deftest test-org-batch-add-todo ()
156 "Test adding a new TODO item."
157 (batch-test--with-temp-org-file
158 "* Work\n"
159 (lambda (temp-file)
160 (let ((result (org-batch-add-todo temp-file "Work" "New Task"
161 "2025-12-25" 2 '("test" "task"))))
162 (should result)
163 ;; Verify it was added
164 (let ((todos (org-batch-list-todos temp-file)))
165 (let ((new-task (batch-test--find-item-by-heading todos "New Task")))
166 (should new-task)
167 (should (string= (alist-get 'todo new-task) "TODO"))
168 (should (= (alist-get 'priority new-task) 2))
169 (should (member "test" (alist-get 'tags new-task)))
170 (should (member "task" (alist-get 'tags new-task)))))))))
171
172(ert-deftest test-org-batch-update-state ()
173 "Test updating TODO state."
174 (batch-test--with-temp-org-file
175 "* Work\n** TODO Test Task\n"
176 (lambda (temp-file)
177 (let ((result (org-batch-update-state temp-file "Test Task" "DONE")))
178 (should result)
179 ;; Verify state was updated
180 (let ((todos (org-batch-list-todos temp-file)))
181 (let ((task (batch-test--find-item-by-heading todos "Test Task")))
182 (should task)
183 (should (string= (alist-get 'todo task) "DONE"))))))))
184
185(ert-deftest test-org-batch-schedule-task ()
186 "Test scheduling a task."
187 (batch-test--with-temp-org-file
188 "* Work\n** TODO Test Task\n"
189 (lambda (temp-file)
190 (let ((result (org-batch-schedule-task temp-file "Test Task" "2025-12-25")))
191 (should result)
192 ;; Verify schedule was set
193 (let ((scheduled (org-batch-scheduled-today temp-file "2025-12-25")))
194 (should (batch-test--find-item-by-heading scheduled "Test Task")))))))
195
196(ert-deftest test-org-batch-set-priority ()
197 "Test setting task priority."
198 (batch-test--with-temp-org-file
199 "* Work\n** TODO Test Task\n"
200 (lambda (temp-file)
201 (let ((result (org-batch-set-priority temp-file "Test Task" 1)))
202 (should result)
203 ;; Verify priority was set
204 (let ((todos (org-batch-list-todos temp-file)))
205 (let ((task (batch-test--find-item-by-heading todos "Test Task")))
206 (should task)
207 (should (= (alist-get 'priority task) 1))))))))
208
209;;; Tests for Utility Functions
210
211(ert-deftest test-org-batch--priority-conversion ()
212 "Test priority number/character conversion.
213Priority mapping: '1'=1, '2'=2, '3'=3, '4'=4, '5'=5."
214 (should (= (org-batch--priority-to-number ?1) 1))
215 (should (= (org-batch--priority-to-number ?5) 5))
216 (should (= (org-batch--number-to-priority 1) ?1))
217 (should (= (org-batch--number-to-priority 5) ?5)))
218
219;;; Placeholder Tests for New Features
220
221(ert-deftest test-org-batch-get-todo-content ()
222 "Test getting full TODO content with metadata and body."
223 (let ((result (org-batch-get-todo-content batch-test-fixture-file "Review PR #123")))
224 (should result)
225 ;; Check basic metadata
226 (should (string= (alist-get 'heading result) "Review PR #123"))
227 (should (string= (alist-get 'todo result) "TODO"))
228 (should (member "work" (alist-get 'tags result)))
229 (should (member "code" (alist-get 'tags result)))
230 ;; Check properties
231 (let ((props (alist-get 'properties result)))
232 (should props)
233 (should (assoc "CREATED" props))
234 (should (assoc "PR_URL" props))
235 (should (string-match "github.com" (cdr (assoc "PR_URL" props)))))
236 ;; Check content
237 (let ((content (alist-get 'content result)))
238 (should content)
239 (should (string-match "Check for security" content))
240 (should (string-match "error handling" content))))
241
242 ;; Test non-existent heading
243 (let ((result (org-batch-get-todo-content batch-test-fixture-file "NonExistent Task")))
244 (should-not result)))
245
246(ert-deftest test-org-batch-add-tags ()
247 "Test adding tags to existing TODO."
248 (batch-test--with-temp-org-file
249 "* Work\n** TODO Test Task :existing:\n"
250 (lambda (temp-file)
251 (let ((result (org-batch-add-tags temp-file "Test Task" '("new" "tags"))))
252 (should result)
253 ;; Verify tags were added
254 (let ((todos (org-batch-list-todos temp-file)))
255 (let ((task (batch-test--find-item-by-heading todos "Test Task")))
256 (should task)
257 (should (member "existing" (alist-get 'tags task)))
258 (should (member "new" (alist-get 'tags task)))
259 (should (member "tags" (alist-get 'tags task)))))))))
260
261(ert-deftest test-org-batch-remove-tags ()
262 "Test removing tags from TODO."
263 (batch-test--with-temp-org-file
264 "* Work\n** TODO Test Task :tag1:tag2:tag3:\n"
265 (lambda (temp-file)
266 (let ((result (org-batch-remove-tags temp-file "Test Task" '("tag2"))))
267 (should result)
268 ;; Verify tag was removed
269 (let ((todos (org-batch-list-todos temp-file)))
270 (let ((task (batch-test--find-item-by-heading todos "Test Task")))
271 (should task)
272 (should (member "tag1" (alist-get 'tags task)))
273 (should-not (member "tag2" (alist-get 'tags task)))
274 (should (member "tag3" (alist-get 'tags task)))))))))
275
276(ert-deftest test-org-batch-list-all-tags ()
277 "Test listing all unique tags."
278 (let ((tags (org-batch-list-all-tags batch-test-fixture-file)))
279 (should (> (length tags) 0))
280 (should (member "work" tags))
281 (should (member "code" tags))
282 (should (member "personal" tags))
283 ;; Tags should be sorted and unique
284 (should (equal tags (sort (delete-dups tags) #'string<)))))
285
286(ert-deftest test-org-batch-get-overdue ()
287 "Test getting overdue tasks."
288 ;; Create a file with overdue task
289 (batch-test--with-temp-org-file
290 "* Work\n** TODO Overdue Task\nDEADLINE: <2020-01-01>\n"
291 (lambda (temp-file)
292 (let ((overdue (org-batch-get-overdue temp-file)))
293 (should (> (batch-test--count-items overdue) 0))
294 (should (batch-test--find-item-by-heading overdue "Overdue Task"))))))
295
296(ert-deftest test-org-batch-get-upcoming ()
297 "Test getting upcoming tasks."
298 ;; Create a file with upcoming task
299 (let ((future-date (format-time-string "%Y-%m-%d" (time-add (current-time) (days-to-time 3)))))
300 (batch-test--with-temp-org-file
301 (format "* Work\n** TODO Upcoming Task\nSCHEDULED: <%s>\n" future-date)
302 (lambda (temp-file)
303 (let ((upcoming (org-batch-get-upcoming temp-file 7)))
304 (should (> (batch-test--count-items upcoming) 0))
305 (should (batch-test--find-item-by-heading upcoming "Upcoming Task")))))))
306
307(ert-deftest test-org-batch-get-property ()
308 "Test getting property value."
309 (let ((prop-value (org-batch-get-property batch-test-fixture-file "Review PR #123" "PR_URL")))
310 (should prop-value)
311 (should (string-match "github.com" prop-value))))
312
313(ert-deftest test-org-batch-set-property ()
314 "Test setting property value."
315 (batch-test--with-temp-org-file
316 "* Work\n** TODO Test Task\n:PROPERTIES:\n:CREATED: [2025-12-23]\n:END:\n"
317 (lambda (temp-file)
318 (let ((result (org-batch-set-property temp-file "Test Task" "CUSTOM_PROP" "test-value")))
319 (should result)
320 ;; Verify property was set
321 (let ((value (org-batch-get-property temp-file "Test Task" "CUSTOM_PROP")))
322 (should value)
323 (should (string= value "test-value")))))))
324
325;;; Tests for Bulk Operations
326
327(ert-deftest test-org-batch-bulk-update-state ()
328 "Test bulk updating state of multiple tasks."
329 (batch-test--with-temp-org-file
330 "* Work\n** TODO Task 1\n** TODO Task 2 :urgent:\n** NEXT Task 3\n"
331 (lambda (temp-file)
332 (let ((count (org-batch-bulk-update-state temp-file "TODO" "DONE")))
333 (should (= count 2))
334 ;; Verify states were updated
335 (let ((todos (org-batch-list-todos temp-file)))
336 (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
337 (task2 (batch-test--find-item-by-heading todos "Task 2")))
338 (should (string= (alist-get 'todo task1) "DONE"))
339 (should (string= (alist-get 'todo task2) "DONE"))))))))
340
341(ert-deftest test-org-batch-bulk-update-state-with-tag-filter ()
342 "Test bulk updating state with tag filter."
343 (batch-test--with-temp-org-file
344 "* Work\n** TODO Task 1 :urgent:\n** TODO Task 2\n** TODO Task 3 :urgent:\n"
345 (lambda (temp-file)
346 (let ((count (org-batch-bulk-update-state temp-file "TODO" "NEXT" '("urgent"))))
347 (should (= count 2))
348 ;; Verify only urgent tasks were updated
349 (let ((todos (org-batch-list-todos temp-file)))
350 (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
351 (task2 (batch-test--find-item-by-heading todos "Task 2"))
352 (task3 (batch-test--find-item-by-heading todos "Task 3")))
353 (should (string= (alist-get 'todo task1) "NEXT"))
354 (should (string= (alist-get 'todo task2) "TODO"))
355 (should (string= (alist-get 'todo task3) "NEXT"))))))))
356
357(ert-deftest test-org-batch-bulk-add-tags ()
358 "Test bulk adding tags to multiple tasks."
359 (batch-test--with-temp-org-file
360 "* Work\n** TODO Task 1\n** TODO Task 2\n** NEXT Task 3\n"
361 (lambda (temp-file)
362 (let ((count (org-batch-bulk-add-tags temp-file "TODO" '("review" "urgent"))))
363 (should (= count 2))
364 ;; Verify tags were added
365 (let ((todos (org-batch-list-todos temp-file)))
366 (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
367 (task2 (batch-test--find-item-by-heading todos "Task 2")))
368 (should (member "review" (alist-get 'tags task1)))
369 (should (member "urgent" (alist-get 'tags task1)))
370 (should (member "review" (alist-get 'tags task2)))
371 (should (member "urgent" (alist-get 'tags task2)))))))))
372
373(ert-deftest test-org-batch-bulk-set-priority ()
374 "Test bulk setting priority for multiple tasks."
375 (batch-test--with-temp-org-file
376 "* Work\n** TODO Task 1\n** TODO Task 2\n** NEXT Task 3\n"
377 (lambda (temp-file)
378 (let ((count (org-batch-bulk-set-priority temp-file "TODO" 1)))
379 (should (= count 2))
380 ;; Verify priorities were set
381 (let ((todos (org-batch-list-todos temp-file)))
382 (let ((task1 (batch-test--find-item-by-heading todos "Task 1"))
383 (task2 (batch-test--find-item-by-heading todos "Task 2"))
384 (task3 (batch-test--find-item-by-heading todos "Task 3")))
385 (should (= (alist-get 'priority task1) 1))
386 (should (= (alist-get 'priority task2) 1))
387 (should-not (alist-get 'priority task3))))))))
388
389;;; Tests for Time Tracking
390
391(ert-deftest test-org-batch-clock-in ()
392 "Test clocking in to a task."
393 (batch-test--with-temp-org-file
394 "* Work\n** TODO Test Task\n"
395 (lambda (temp-file)
396 (let ((result (org-batch-clock-in temp-file "Test Task")))
397 (should result)
398 ;; Verify clock entry was added
399 (with-temp-buffer
400 (insert-file-contents temp-file)
401 (should (string-match-p "CLOCK: \\[" (buffer-string))))))))
402
403(ert-deftest test-org-batch-clock-out ()
404 "Test clocking out of a task."
405 (batch-test--with-temp-org-file
406 "* Work\n** TODO Test Task\n:LOGBOOK:\nCLOCK: [2025-12-23 Mon 10:00]\n:END:\n"
407 (lambda (temp-file)
408 (let ((result (org-batch-clock-out temp-file)))
409 (should result)
410 ;; Verify clock was closed with end time
411 (with-temp-buffer
412 (insert-file-contents temp-file)
413 (should (string-match-p "CLOCK: \\[.*?\\]--\\[.*?\\] =>" (buffer-string))))))))
414
415(ert-deftest test-org-batch-get-active-clock ()
416 "Test getting active clock."
417 (batch-test--with-temp-org-file
418 "* Work\n** TODO Test Task\n:LOGBOOK:\nCLOCK: [2025-12-23 Mon 10:00]\n:END:\n"
419 (lambda (temp-file)
420 (let ((result (org-batch-get-active-clock temp-file)))
421 (should result)
422 (should (string= (alist-get 'heading result) "Test Task"))
423 (should (alist-get 'clock_start result))))))
424
425(ert-deftest test-org-batch-get-active-clock-none ()
426 "Test getting active clock when none exists."
427 (batch-test--with-temp-org-file
428 "* Work\n** TODO Test Task\n"
429 (lambda (temp-file)
430 (let ((result (org-batch-get-active-clock temp-file)))
431 (should-not result)))))
432
433(ert-deftest test-org-batch-get-clocked-time ()
434 "Test getting total clocked time for a task."
435 (batch-test--with-temp-org-file
436 "* Work\n** TODO Test Task\n:LOGBOOK:\nCLOCK: [2025-12-23 Mon 10:00]--[2025-12-23 Mon 11:30] => 1:30\n:END:\n"
437 (lambda (temp-file)
438 (let ((minutes (org-batch-get-clocked-time temp-file "Test Task")))
439 (should (> minutes 0))
440 (should (= minutes 90)))))) ; 1:30 = 90 minutes
441
442;;; Tests for Statistics & Analytics
443
444(ert-deftest test-org-batch-get-statistics ()
445 "Test getting comprehensive statistics."
446 (let ((stats (org-batch-get-statistics batch-test-fixture-file)))
447 (should stats)
448 (should (> (alist-get 'total stats) 0))
449 (should (alist-get 'by_state stats))
450 (should (alist-get 'by_priority stats))
451 (should (alist-get 'by_tag stats))
452 ;; Verify we have state counts
453 (let ((by-state (alist-get 'by_state stats)))
454 (should (assoc 'TODO by-state))
455 (should (assoc 'NEXT by-state)))))
456
457(ert-deftest test-org-batch-get-priority-distribution ()
458 "Test getting priority distribution."
459 (let ((distribution (org-batch-get-priority-distribution batch-test-fixture-file)))
460 (should distribution)
461 ;; Should have entries for priorities 1-5
462 (should (assoc 1 distribution))
463 (should (assoc 2 distribution))
464 (should (assoc 3 distribution))
465 (should (assoc 4 distribution))
466 (should (assoc 5 distribution))))
467
468(ert-deftest test-org-batch-get-tag-statistics ()
469 "Test getting tag statistics."
470 (let ((tag-stats (org-batch-get-tag-statistics batch-test-fixture-file)))
471 (should tag-stats)
472 (should (> (length tag-stats) 0))
473 ;; Should be sorted by count (descending)
474 (when (>= (length tag-stats) 2)
475 (should (>= (cdr (nth 0 tag-stats)) (cdr (nth 1 tag-stats)))))))
476
477;;; Tests for Export & Reporting
478
479(ert-deftest test-org-batch-export-csv ()
480 "Test exporting TODOs to CSV."
481 (let ((output-file (make-temp-file "org-export-" nil ".csv")))
482 (unwind-protect
483 (progn
484 (should (org-batch-export-csv batch-test-fixture-file output-file))
485 ;; Verify file was created
486 (should (file-exists-p output-file))
487 ;; Verify it has content
488 (with-temp-buffer
489 (insert-file-contents output-file)
490 (should (> (buffer-size) 0))
491 ;; Should have CSV header
492 (should (string-match-p "heading,state,priority" (buffer-string)))))
493 (when (file-exists-p output-file)
494 (delete-file output-file)))))
495
496(ert-deftest test-org-batch-export-json ()
497 "Test exporting TODOs to JSON."
498 (let ((output-file (make-temp-file "org-export-" nil ".json")))
499 (unwind-protect
500 (progn
501 (should (org-batch-export-json batch-test-fixture-file output-file))
502 ;; Verify file was created
503 (should (file-exists-p output-file))
504 ;; Verify it has valid JSON
505 (with-temp-buffer
506 (insert-file-contents output-file)
507 (should (> (buffer-size) 0))
508 ;; Should be valid JSON (starts with [)
509 (should (string-match-p "^\\[" (buffer-string)))))
510 (when (file-exists-p output-file)
511 (delete-file output-file)))))
512
513(provide 'batch-functions-test)
514;;; batch-functions-test.el ends here