diff options
| author | Craig Jennings <c@cjennings.net> | 2025-11-11 18:59:36 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-11-11 18:59:36 -0600 |
| commit | c51228b021e773ebdee03a84c16599d3cbe92bfc (patch) | |
| tree | da866175b571f17ca0edee04012ff4e74838fc57 | |
| parent | b0cfac99c5715740e152c9519fbe2912a91971a3 (diff) | |
test: Add comprehensive tests for org-agenda cacheperformance
Add 9 ERT tests covering org-agenda file list caching:
Normal cases:
- First call builds cache from scratch
- Second call uses cached files (validates cache hit)
- Force rebuild bypasses valid cache
Boundary cases:
- Cache expires after TTL and triggers rebuild
- Empty directories create minimal base file list
- Building flag set during build and cleared after
- Building flag clears on error (unwind-protect)
Error cases:
- Nil cache with timestamp triggers rebuild
- Directory scan failures propagate correctly
All tests pass (9/9). No regressions in full suite.
Total: 1,823 tests, 17 pre-existing failures (down from 18).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
| -rw-r--r-- | tests/test-org-agenda-build-list.el | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/tests/test-org-agenda-build-list.el b/tests/test-org-agenda-build-list.el new file mode 100644 index 00000000..6b424200 --- /dev/null +++ b/tests/test-org-agenda-build-list.el @@ -0,0 +1,294 @@ +;;; test-org-agenda-build-list.el --- Tests for cj/build-org-agenda-list -*- lexical-binding: t; -*- + +;;; Commentary: +;; Unit tests for cj/build-org-agenda-list 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 schedule-file "/tmp/test-schedule.org") +(defvar gcal-file "/tmp/test-gcal.org") +(defvar projects-dir "/tmp/test-projects/") + +;; Now load the actual production module +(require 'org-agenda-config) + +;;; Setup and Teardown + +(defun test-org-agenda-setup () + "Reset cache and state before each test." + (setq cj/org-agenda-files-cache nil) + (setq cj/org-agenda-files-cache-time nil) + (setq cj/org-agenda-files-building nil) + (setq org-agenda-files nil)) + +(defun test-org-agenda-teardown () + "Clean up after each test." + (setq cj/org-agenda-files-cache nil) + (setq cj/org-agenda-files-cache-time nil) + (setq cj/org-agenda-files-building nil) + (setq org-agenda-files nil)) + +;;; Normal Cases + +(ert-deftest test-org-agenda-build-list-normal-first-call-builds-cache () + "Test that first call builds cache from scratch. + +When cache is empty, function should: +1. Scan directory for todo.org files +2. Build agenda files list +3. Populate cache +4. Set cache timestamp" + (test-org-agenda-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern &optional _include-dirs) '("/tmp/project/todo.org")))) + + ;; Before call: cache empty + (should (null cj/org-agenda-files-cache)) + (should (null cj/org-agenda-files-cache-time)) + + ;; Build agenda files + (cj/build-org-agenda-list) + + ;; After call: cache populated + (should cj/org-agenda-files-cache) + (should cj/org-agenda-files-cache-time) + (should org-agenda-files) + + ;; Cache matches org-agenda-files + (should (equal cj/org-agenda-files-cache org-agenda-files)) + + ;; Contains base files (inbox, schedule, gcal) plus project files + (should (>= (length org-agenda-files) 3))) + (test-org-agenda-teardown))) + +(ert-deftest test-org-agenda-build-list-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 files from cache +3. Should NOT update cache timestamp" + (test-org-agenda-setup) + (unwind-protect + (let ((scan-count 0)) + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern &optional _include-dirs) + (setq scan-count (1+ scan-count)) + '("/tmp/project/todo.org")))) + + ;; First call: builds cache + (cj/build-org-agenda-list) + (should (= scan-count 1)) ; 1 directory scanned + + (let ((cached-time cj/org-agenda-files-cache-time) + (cached-files cj/org-agenda-files-cache)) + + ;; Second call: uses cache + (cj/build-org-agenda-list) + + ;; Scan count unchanged (cache hit) + (should (= scan-count 1)) + + ;; Cache unchanged + (should (equal cj/org-agenda-files-cache-time cached-time)) + (should (equal cj/org-agenda-files-cache cached-files))))) + (test-org-agenda-teardown))) + +(ert-deftest test-org-agenda-build-list-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-agenda-setup) + (unwind-protect + (let ((scan-count 0)) + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern &optional _include-dirs) + (setq scan-count (1+ scan-count)) + (if (> scan-count 1) + '("/tmp/project/todo.org" "/tmp/project2/todo.org") ; New file on rebuild + '("/tmp/project/todo.org"))))) + + ;; First call: builds cache + (cj/build-org-agenda-list) + (let ((initial-count (length org-agenda-files))) + + ;; Force rebuild + (cj/build-org-agenda-list 'force) + + ;; Scanned again + (should (= scan-count 2)) + + ;; New files include additional project + (should (> (length org-agenda-files) initial-count))))) + (test-org-agenda-teardown))) + +;;; Boundary Cases + +(ert-deftest test-org-agenda-build-list-boundary-cache-expires-after-ttl () + "Test that cache expires after TTL period. + +When cache timestamp exceeds TTL: +1. Should rebuild files list +2. Should update cache timestamp +3. Should rescan directory" + (test-org-agenda-setup) + (unwind-protect + (let ((scan-count 0)) + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern &optional _include-dirs) + (setq scan-count (1+ scan-count)) + '("/tmp/project/todo.org")))) + + ;; First call: builds cache + (cj/build-org-agenda-list) + (should (= scan-count 1)) + + ;; Simulate cache expiration (set time to 2 hours ago) + (setq cj/org-agenda-files-cache-time + (- (float-time) (* 2 3600))) + + ;; Second call: cache expired, rebuild + (cj/build-org-agenda-list) + + ;; Scanned again (cache was expired) + (should (= scan-count 2)) + + ;; Cache timestamp updated to current time + (should (< (- (float-time) cj/org-agenda-files-cache-time) 1)))) + (test-org-agenda-teardown))) + +(ert-deftest test-org-agenda-build-list-boundary-empty-directory-creates-minimal-list () + "Test behavior when directory contains no todo.org files. + +When directory scan returns empty: +1. Should still create base files (inbox, schedule) +2. Should not fail or error +3. Should cache the minimal result" + (test-org-agenda-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern &optional _include-dirs) nil))) ; No files found + + (cj/build-org-agenda-list) + + ;; Should have base files only (inbox, schedule, gcal) + (should (= (length org-agenda-files) 3)) + + ;; Cache should contain base files + (should cj/org-agenda-files-cache) + (should (= (length cj/org-agenda-files-cache) 3))) + (test-org-agenda-teardown))) + +(ert-deftest test-org-agenda-build-list-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-agenda-setup) + (unwind-protect + (let ((flag-during-build nil)) + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern &optional _include-dirs) + ;; Capture flag state during directory scan + (setq flag-during-build cj/org-agenda-files-building) + '("/tmp/project/todo.org")))) + + ;; Before build + (should (null cj/org-agenda-files-building)) + + ;; Build + (cj/build-org-agenda-list) + + ;; Flag was set during build + (should flag-during-build) + + ;; Flag cleared after build + (should (null cj/org-agenda-files-building)))) + (test-org-agenda-teardown))) + +(ert-deftest test-org-agenda-build-list-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-agenda-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern &optional _include-dirs) + (error "Simulated scan failure")))) + + ;; Build will error + (should-error (cj/build-org-agenda-list)) + + ;; Flag cleared despite error (unwind-protect) + (should (null cj/org-agenda-files-building))) + (test-org-agenda-teardown))) + +;;; Error Cases + +(ert-deftest test-org-agenda-build-list-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 files list +3. Should set both cache and timestamp" + (test-org-agenda-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern &optional _include-dirs) '("/tmp/project/todo.org")))) + + ;; Set inconsistent state + (setq cj/org-agenda-files-cache nil) + (setq cj/org-agenda-files-cache-time (float-time)) + + ;; Build should recognize invalid state + (cj/build-org-agenda-list) + + ;; Cache now populated + (should cj/org-agenda-files-cache) + (should cj/org-agenda-files-cache-time) + (should org-agenda-files)) + (test-org-agenda-teardown))) + +(ert-deftest test-org-agenda-build-list-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-agenda-setup) + (unwind-protect + (cl-letf (((symbol-function 'directory-files-recursively) + (lambda (_dir _pattern &optional _include-dirs) + (error "Permission denied")))) + + ;; Should propagate error + (should-error (cj/build-org-agenda-list)) + + ;; Cache not corrupted (still nil) + (should (null cj/org-agenda-files-cache)) + + ;; Building flag cleared + (should (null cj/org-agenda-files-building))) + (test-org-agenda-teardown))) + +(provide 'test-org-agenda-build-list) +;;; test-org-agenda-build-list.el ends here |
