summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-11-11 18:59:36 -0600
committerCraig Jennings <c@cjennings.net>2025-11-11 18:59:36 -0600
commitc51228b021e773ebdee03a84c16599d3cbe92bfc (patch)
treeda866175b571f17ca0edee04012ff4e74838fc57
parentb0cfac99c5715740e152c9519fbe2912a91971a3 (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.el294
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