diff options
Diffstat (limited to 'tests/test-org-refile-build-targets.el')
| -rw-r--r-- | tests/test-org-refile-build-targets.el | 305 |
1 files changed, 305 insertions, 0 deletions
diff --git a/tests/test-org-refile-build-targets.el b/tests/test-org-refile-build-targets.el new file mode 100644 index 00000000..e7ab5c42 --- /dev/null +++ b/tests/test-org-refile-build-targets.el @@ -0,0 +1,305 @@ +;;; test-org-refile-build-targets.el --- Tests for cj/build-org-refile-targets -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for cj/build-org-refile-targets caching logic. +;; Tests cache behavior, TTL expiration, force rebuild, and async build flag. + +;;; Code: + +(require 'ert) + +;; Add modules to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub dependencies before loading the module +(defvar inbox-file "/tmp/test-inbox.org") +(defvar reference-file "/tmp/test-reference.org") +(defvar schedule-file "/tmp/test-schedule.org") +(defvar user-emacs-directory "/tmp/test-emacs.d/") +(defvar code-dir "/tmp/test-code/") +(defvar projects-dir "/tmp/test-projects/") + +;; Now load the actual production module +(require 'org-refile-config) + +;;; Setup and Teardown + +(defun test-org-refile-setup () + "Reset cache and state before each test." + (setq cj/org-refile-targets-cache nil) + (setq cj/org-refile-targets-cache-time nil) + (setq cj/org-refile-targets-building nil) + (setq org-refile-targets nil)) + +(defun test-org-refile-teardown () + "Clean up after each test." + (setq cj/org-refile-targets-cache nil) + (setq cj/org-refile-targets-cache-time nil) + (setq cj/org-refile-targets-building nil) + (setq org-refile-targets nil)) + +;;; Normal Cases + +(ert-deftest test-org-refile-build-targets-normal-first-call-builds-cache () + "Test that first call builds cache from scratch. + +When cache is empty, function should: +1. Scan directories for todo.org files +2. Build refile targets list +3. Populate cache +4. Set cache timestamp" + (test-org-refile-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern) '("/tmp/todo.org"))) + ((symbol-function 'fboundp) (lambda (_sym) nil))) + + ;; Before call: cache empty + (should (null cj/org-refile-targets-cache)) + (should (null cj/org-refile-targets-cache-time)) + + ;; Build targets + (cj/build-org-refile-targets) + + ;; After call: cache populated + (should cj/org-refile-targets-cache) + (should cj/org-refile-targets-cache-time) + (should org-refile-targets) + + ;; Cache matches org-refile-targets + (should (equal cj/org-refile-targets-cache org-refile-targets)) + + ;; Contains base files (inbox, reference, schedule) + (should (>= (length org-refile-targets) 3))) + (test-org-refile-teardown))) + +(ert-deftest test-org-refile-build-targets-normal-second-call-uses-cache () + "Test that second call uses cache instead of rebuilding. + +When cache is valid (not expired): +1. Should NOT scan directories again +2. Should restore targets from cache +3. Should NOT update cache timestamp" + (test-org-refile-setup) + (unwind-protect + (let ((scan-count 0)) + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern) + (setq scan-count (1+ scan-count)) + '("/tmp/todo.org"))) + ((symbol-function 'fboundp) (lambda (_sym) nil))) + + ;; First call: builds cache + (cj/build-org-refile-targets) + (should (= scan-count 3)) ; 3 directories scanned + + (let ((cached-time cj/org-refile-targets-cache-time) + (cached-targets cj/org-refile-targets-cache)) + + ;; Second call: uses cache + (cj/build-org-refile-targets) + + ;; Scan count unchanged (cache hit) + (should (= scan-count 3)) + + ;; Cache unchanged + (should (equal cj/org-refile-targets-cache-time cached-time)) + (should (equal cj/org-refile-targets-cache cached-targets))))) + (test-org-refile-teardown))) + +(ert-deftest test-org-refile-build-targets-normal-force-rebuild-bypasses-cache () + "Test that force-rebuild parameter bypasses cache. + +When force-rebuild is non-nil: +1. Should ignore valid cache +2. Should rebuild from scratch +3. Should update cache with new data" + (test-org-refile-setup) + (unwind-protect + (let ((scan-count 0)) + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern) + (setq scan-count (1+ scan-count)) + (if (> scan-count 3) + '("/tmp/todo.org" "/tmp/todo2.org") ; New file on rebuild + '("/tmp/todo.org")))) + ((symbol-function 'fboundp) (lambda (_sym) nil))) + + ;; First call: builds cache + (cj/build-org-refile-targets) + (let ((initial-count (length org-refile-targets))) + + ;; Force rebuild + (cj/build-org-refile-targets 'force) + + ;; Scanned again (3 more directories) + (should (= scan-count 6)) + + ;; New targets include additional file + (should (> (length org-refile-targets) initial-count))))) + (test-org-refile-teardown))) + +;;; Boundary Cases + +(ert-deftest test-org-refile-build-targets-boundary-cache-expires-after-ttl () + "Test that cache expires after TTL period. + +When cache timestamp exceeds TTL: +1. Should rebuild targets +2. Should update cache timestamp +3. Should rescan directories" + (test-org-refile-setup) + (unwind-protect + (let ((scan-count 0)) + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern) + (setq scan-count (1+ scan-count)) + '("/tmp/todo.org"))) + ((symbol-function 'fboundp) (lambda (_sym) nil))) + + ;; First call: builds cache + (cj/build-org-refile-targets) + (should (= scan-count 3)) + + ;; Simulate cache expiration (set time to 2 hours ago) + (setq cj/org-refile-targets-cache-time + (- (float-time) (* 2 3600))) + + ;; Second call: cache expired, rebuild + (cj/build-org-refile-targets) + + ;; Scanned again (cache was expired) + (should (= scan-count 6)) + + ;; Cache timestamp updated to current time + (should (< (- (float-time) cj/org-refile-targets-cache-time) 1)))) + (test-org-refile-teardown))) + +(ert-deftest test-org-refile-build-targets-boundary-empty-directories-creates-minimal-targets () + "Test behavior when directories contain no todo.org files. + +When directory scans return empty: +1. Should still create base targets (inbox, reference, schedule) +2. Should not fail or error +3. Should cache the minimal result" + (test-org-refile-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern) nil)) ; No files found + ((symbol-function 'fboundp) (lambda (_sym) nil))) + + (cj/build-org-refile-targets) + + ;; Should have base files only + (should (= (length org-refile-targets) 3)) + + ;; Cache should contain base files + (should cj/org-refile-targets-cache) + (should (= (length cj/org-refile-targets-cache) 3))) + (test-org-refile-teardown))) + +(ert-deftest test-org-refile-build-targets-boundary-building-flag-set-during-build () + "Test that building flag is set during build and cleared after. + +During build: +1. Flag should be set to prevent concurrent builds +2. Flag should clear even if build fails +3. Flag state should be consistent" + (test-org-refile-setup) + (unwind-protect + (let ((flag-during-build nil)) + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern) + ;; Capture flag state during directory scan + (setq flag-during-build cj/org-refile-targets-building) + '("/tmp/todo.org"))) + ((symbol-function 'fboundp) (lambda (_sym) nil))) + + ;; Before build + (should (null cj/org-refile-targets-building)) + + ;; Build + (cj/build-org-refile-targets) + + ;; Flag was set during build + (should flag-during-build) + + ;; Flag cleared after build + (should (null cj/org-refile-targets-building)))) + (test-org-refile-teardown))) + +(ert-deftest test-org-refile-build-targets-boundary-building-flag-clears-on-error () + "Test that building flag clears even if build errors. + +When build encounters error: +1. Flag should still be cleared (unwind-protect) +2. Prevents permanently locked state +3. Next build can proceed" + (test-org-refile-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern) + (error "Simulated scan failure"))) + ((symbol-function 'fboundp) (lambda (_sym) nil))) + + ;; Build will error + (should-error (cj/build-org-refile-targets)) + + ;; Flag cleared despite error (unwind-protect) + (should (null cj/org-refile-targets-building))) + (test-org-refile-teardown))) + +;;; Error Cases + +(ert-deftest test-org-refile-build-targets-error-nil-cache-with-old-timestamp () + "Test handling of inconsistent state (nil cache but timestamp set). + +When cache is nil but timestamp exists: +1. Should recognize cache as invalid +2. Should rebuild targets +3. Should set both cache and timestamp" + (test-org-refile-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern) '("/tmp/todo.org"))) + ((symbol-function 'fboundp) (lambda (_sym) nil))) + + ;; Set inconsistent state + (setq cj/org-refile-targets-cache nil) + (setq cj/org-refile-targets-cache-time (float-time)) + + ;; Build should recognize invalid state + (cj/build-org-refile-targets) + + ;; Cache now populated + (should cj/org-refile-targets-cache) + (should cj/org-refile-targets-cache-time) + (should org-refile-targets)) + (test-org-refile-teardown))) + +(ert-deftest test-org-refile-build-targets-error-directory-scan-failure-propagates () + "Test that directory scan failures propagate as errors. + +When directory-files-recursively errors: +1. Error should propagate to caller +2. Cache should not be corrupted +3. Building flag should clear" + (test-org-refile-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern) + (error "Permission denied"))) + ((symbol-function 'fboundp) (lambda (_sym) nil))) + + ;; Should propagate error + (should-error (cj/build-org-refile-targets)) + + ;; Cache not corrupted (still nil) + (should (null cj/org-refile-targets-cache)) + + ;; Building flag cleared + (should (null cj/org-refile-targets-building))) + (test-org-refile-teardown))) + +(provide 'test-org-refile-build-targets) +;;; test-org-refile-build-targets.el ends here |
