aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-24 04:00:38 -0600
committerCraig Jennings <c@cjennings.net>2026-02-26 05:21:33 -0600
commitfe5a31072bfa2f6451769008a63ad5b0d9a3de17 (patch)
tree6c99acc9091a50f04fded3249520e083888162eb
parente5e44c9aab0e4ead5085232b88ebcb7bbdff6729 (diff)
downloadchime-fe5a31072bfa2f6451769008a63ad5b0d9a3de17.tar.gz
chime-fe5a31072bfa2f6451769008a63ad5b0d9a3de17.zip
Add test config macros and migrate startup tests to use them
Add with-chime-config and with-org-event-file macros to testutil-events.el, replacing the manual defvar/save/restore pattern with let-bindings that auto-restore on exit. Migrate test-integration-startup.el as first adopter.
-rw-r--r--tests/test-integration-startup.el284
-rw-r--r--tests/testutil-events.el57
2 files changed, 186 insertions, 155 deletions
diff --git a/tests/test-integration-startup.el b/tests/test-integration-startup.el
index 63d7c76..f292b4e 100644
--- a/tests/test-integration-startup.el
+++ b/tests/test-integration-startup.el
@@ -60,42 +60,7 @@
;; Load test utilities
(require 'testutil-general (expand-file-name "testutil-general.el"))
(require 'testutil-time (expand-file-name "testutil-time.el"))
-
-;;; Setup and Teardown
-
-(defvar test-integration-startup--orig-agenda-files nil
- "Original org-agenda-files value before test.")
-
-(defvar test-integration-startup--orig-startup-delay nil
- "Original chime-startup-delay value.")
-
-(defvar test-integration-startup--orig-modeline-lookahead nil
- "Original chime-modeline-lookahead-minutes value.")
-
-(defvar test-integration-startup--orig-tooltip-lookahead nil
- "Original chime-tooltip-lookahead-hours value.")
-
-(defun test-integration-startup-setup ()
- "Setup function run before each test."
- (chime-create-test-base-dir)
- ;; Save original values
- (setq test-integration-startup--orig-agenda-files org-agenda-files)
- (setq test-integration-startup--orig-startup-delay chime-startup-delay)
- (setq test-integration-startup--orig-modeline-lookahead chime-modeline-lookahead-minutes)
- (setq test-integration-startup--orig-tooltip-lookahead chime-tooltip-lookahead-hours)
- ;; Set short lookahead for faster tests
- (setq chime-modeline-lookahead-minutes (* 24 60)) ; 24 hours
- (setq chime-tooltip-lookahead-hours 24) ; 24 hours
- (setq chime-startup-delay 1)) ; 1 second for tests
-
-(defun test-integration-startup-teardown ()
- "Teardown function run after each test."
- ;; Restore original values
- (setq org-agenda-files test-integration-startup--orig-agenda-files)
- (setq chime-startup-delay test-integration-startup--orig-startup-delay)
- (setq chime-modeline-lookahead-minutes test-integration-startup--orig-modeline-lookahead)
- (setq chime-tooltip-lookahead-hours test-integration-startup--orig-tooltip-lookahead)
- (chime-delete-test-base-dir))
+(require 'testutil-events (expand-file-name "testutil-events.el"))
;;; Helper Functions
@@ -114,6 +79,26 @@ Returns the file path."
(setq org-agenda-files (list org-file))
org-file))
+(defmacro with-startup-config (&rest body)
+ "Execute BODY with standard startup test config and temp dir management.
+
+Combines two concerns that every test in this file needs:
+ 1. `with-test-setup' creates/cleans up the temp test directory.
+ 2. `with-chime-config' `let'-binds config overrides so they auto-restore
+ when BODY exits (even on error), replacing the old pattern of saving
+ originals to defvars in setup and restoring them in teardown.
+
+The overrides set a 24-hour lookahead window and a 1-second startup delay
+so tests can exercise the full startup path without waiting or creating
+events far in the future."
+ (declare (indent 0))
+ `(with-test-setup
+ (with-chime-config
+ chime-modeline-lookahead-minutes (* 24 60)
+ chime-tooltip-lookahead-hours 24
+ chime-startup-delay 1
+ ,@body)))
+
;;; Normal Cases - Valid Startup Configuration
(ert-deftest test-integration-startup-valid-config-finds-events ()
@@ -135,21 +120,20 @@ Components integrated:
- chime-check (async wrapper around event gathering)
- chime--gather-info (extracts event details)
- chime--update-modeline (updates modeline display)"
- (test-integration-startup-setup)
- (unwind-protect
- (let* ((now (test-time-now))
- ;; Create events at various times
- (event1-time (test-time-at 0 2 0)) ; 2 hours from now
- (event2-time (test-time-at 0 5 0)) ; 5 hours from now
- (event3-time (test-time-at 1 0 0)) ; Tomorrow same time
- (event4-time (test-time-at -1 0 0)) ; Yesterday (overdue)
- ;; Generate timestamps
- (ts1 (test-timestamp-string event1-time))
- (ts2 (test-timestamp-string event2-time))
- (ts3 (test-timestamp-string event3-time))
- (ts4 (test-timestamp-string event4-time))
- ;; Create org file content
- (content (format "#+TITLE: Startup Test Events
+ (with-startup-config
+ (let* ((now (test-time-now))
+ ;; Create events at various times
+ (event1-time (test-time-at 0 2 0)) ; 2 hours from now
+ (event2-time (test-time-at 0 5 0)) ; 5 hours from now
+ (event3-time (test-time-at 1 0 0)) ; Tomorrow same time
+ (event4-time (test-time-at -1 0 0)) ; Yesterday (overdue)
+ ;; Generate timestamps
+ (ts1 (test-timestamp-string event1-time))
+ (ts2 (test-timestamp-string event2-time))
+ (ts3 (test-timestamp-string event3-time))
+ (ts4 (test-timestamp-string event4-time))
+ ;; Create org file content
+ (content (format "#+TITLE: Startup Test Events
* TODO Event in 2 hours
SCHEDULED: %s
@@ -167,39 +151,38 @@ SCHEDULED: %s
SCHEDULED: %s
" ts1 ts2 ts3 ts4 ts1)))
- ;; Create org file and set as agenda files
- (test-integration-startup--create-org-file content)
+ ;; Create org file and set as agenda files
+ (test-integration-startup--create-org-file content)
- ;; Validate configuration should pass
- (let ((issues (chime-validate-configuration)))
- (should (null issues)))
+ ;; Validate configuration should pass
+ (let ((issues (chime-validate-configuration)))
+ (should (null issues)))
- (with-test-time now
- ;; Call chime-check synchronously (bypasses async/timer for test reliability)
- ;; In real startup, this is called via run-at-time after chime-startup-delay
- (let ((event-count 0))
- ;; Mock the async-start to run synchronously for testing
- (cl-letf (((symbol-function 'async-start)
- (lambda (start-func finish-func)
- ;; Call start-func synchronously and pass result to finish-func
- (funcall finish-func (funcall start-func)))))
- ;; Now call chime-check - it will run synchronously
- (chime-check)
+ (with-test-time now
+ ;; Call chime-check synchronously (bypasses async/timer for test reliability)
+ ;; In real startup, this is called via run-at-time after chime-startup-delay
+ (let ((event-count 0))
+ ;; Mock the async-start to run synchronously for testing
+ (cl-letf (((symbol-function 'async-start)
+ (lambda (start-func finish-func)
+ ;; Call start-func synchronously and pass result to finish-func
+ (funcall finish-func (funcall start-func)))))
+ ;; Now call chime-check - it will run synchronously
+ (chime-check)
- ;; Give it a moment to process
- (sleep-for 0.1)
+ ;; Give it a moment to process
+ (sleep-for 0.1)
- ;; Verify modeline was updated
- (should chime-modeline-string)
+ ;; Verify modeline was updated
+ (should chime-modeline-string)
- ;; Verify we found events (should be 4 TODO events, DONE excluded)
- ;; Note: The exact behavior depends on chime's filtering logic
- (should chime--upcoming-events)
- (setq event-count (length chime--upcoming-events))
+ ;; Verify we found events (should be 4 TODO events, DONE excluded)
+ ;; Note: The exact behavior depends on chime's filtering logic
+ (should chime--upcoming-events)
+ (setq event-count (length chime--upcoming-events))
- ;; Should find at least the non-DONE events within lookahead window
- (should (>= event-count 3)))))) ; At least 3 events (2h, 5h, tomorrow)
- (test-integration-startup-teardown)))
+ ;; Should find at least the non-DONE events within lookahead window
+ (should (>= event-count 3)))))))) ; At least 3 events (2h, 5h, tomorrow)
(ert-deftest test-integration-startup-validation-passes-minimal-config ()
"Test validation passes with minimal valid configuration.
@@ -211,16 +194,14 @@ Validates that chime-validate-configuration returns nil (no issues) when:
- All other dependencies are available
This ensures the startup validation doesn't block legitimate configurations."
- (test-integration-startup-setup)
- (unwind-protect
- (let ((content "#+TITLE: Minimal Test\n\n* TODO Test event\nSCHEDULED: <2025-12-01 Mon 10:00>\n"))
- ;; Create minimal org file
- (test-integration-startup--create-org-file content)
+ (with-startup-config
+ (let ((content "#+TITLE: Minimal Test\n\n* TODO Test event\nSCHEDULED: <2025-12-01 Mon 10:00>\n"))
+ ;; Create minimal org file
+ (test-integration-startup--create-org-file content)
- ;; Validation should pass
- (let ((issues (chime-validate-configuration)))
- (should (null issues))))
- (test-integration-startup-teardown)))
+ ;; Validation should pass
+ (let ((issues (chime-validate-configuration)))
+ (should (null issues))))))
;;; Error Cases - Configuration Failures
@@ -237,36 +218,33 @@ When validation fails on first check, chime-check should:
- NOT proceed to event gathering
This validates the early-return mechanism works correctly."
- (test-integration-startup-setup)
- (unwind-protect
- (progn
- ;; Set up invalid configuration (empty org-agenda-files)
- (setq org-agenda-files nil)
+ (with-startup-config
+ ;; Set up invalid configuration (empty org-agenda-files)
+ (setq org-agenda-files nil)
- ;; Reset validation state so chime-check will validate on next call
- (setq chime--validation-done nil)
- (setq chime--validation-retry-count 0)
+ ;; Reset validation state so chime-check will validate on next call
+ (setq chime--validation-done nil)
+ (setq chime--validation-retry-count 0)
- ;; Clear state from any previous tests so we can verify
- ;; early return doesn't set these
- (setq chime--upcoming-events nil)
- (setq chime-modeline-string nil)
+ ;; Clear state from any previous tests so we can verify
+ ;; early return doesn't set these
+ (setq chime--upcoming-events nil)
+ (setq chime-modeline-string nil)
- ;; Call chime-check - should return early without error
- ;; Before the fix, this would throw: (no-catch --cl-block-chime-check-- nil)
- (let ((result (chime-check)))
+ ;; Call chime-check - should return early without error
+ ;; Before the fix, this would throw: (no-catch --cl-block-chime-check-- nil)
+ (let ((result (chime-check)))
- ;; Should return nil (early return from validation failure)
- (should (null result))
+ ;; Should return nil (early return from validation failure)
+ (should (null result))
- ;; Validation should NOT be marked done when it fails
- ;; (so it can retry on next check in case dependencies load later)
- (should (null chime--validation-done))
+ ;; Validation should NOT be marked done when it fails
+ ;; (so it can retry on next check in case dependencies load later)
+ (should (null chime--validation-done))
- ;; Should NOT have processed any events (early return worked)
- (should (null chime--upcoming-events))
- (should (null chime-modeline-string))))
- (test-integration-startup-teardown)))
+ ;; Should NOT have processed any events (early return worked)
+ (should (null chime--upcoming-events))
+ (should (null chime-modeline-string)))))
;;; Boundary Cases - Edge Conditions
@@ -275,60 +253,56 @@ This validates the early-return mechanism works correctly."
Boundary case: org-agenda-files with only one event.
Validates that the gathering and modeline logic work with minimal data."
- (test-integration-startup-setup)
- (unwind-protect
- (let* ((now (test-time-now))
- (event-time (test-time-at 0 1 0)) ; 1 hour from now
- (ts (test-timestamp-string event-time))
- (content (format "* TODO Single Event\nSCHEDULED: %s\n" ts)))
+ (with-startup-config
+ (let* ((now (test-time-now))
+ (event-time (test-time-at 0 1 0)) ; 1 hour from now
+ (ts (test-timestamp-string event-time))
+ (content (format "* TODO Single Event\nSCHEDULED: %s\n" ts)))
- (test-integration-startup--create-org-file content)
+ (test-integration-startup--create-org-file content)
- (with-test-time now
- (cl-letf (((symbol-function 'async-start)
- (lambda (start-func finish-func)
- (funcall finish-func (funcall start-func)))))
- (chime-check)
- (sleep-for 0.1)
+ (with-test-time now
+ (cl-letf (((symbol-function 'async-start)
+ (lambda (start-func finish-func)
+ (funcall finish-func (funcall start-func)))))
+ (chime-check)
+ (sleep-for 0.1)
- ;; Should find exactly 1 event
- (should (= 1 (length chime--upcoming-events)))
+ ;; Should find exactly 1 event
+ (should (= 1 (length chime--upcoming-events)))
- ;; Modeline should be populated
- (should chime-modeline-string)
- (should (string-match-p "Single Event" chime-modeline-string)))))
- (test-integration-startup-teardown)))
+ ;; Modeline should be populated
+ (should chime-modeline-string)
+ (should (string-match-p "Single Event" chime-modeline-string)))))))
(ert-deftest test-integration-startup-no-upcoming-events ()
"Test chime-check when org file has no upcoming events within lookahead.
Boundary case: Events exist but are far in the future (beyond lookahead window).
Validates that chime doesn't error and modeline shows appropriate state."
- (test-integration-startup-setup)
- (unwind-protect
- (let* ((now (test-time-now))
- ;; Event 30 days from now (beyond 24-hour lookahead)
- (event-time (test-time-at 30 0 0))
- (ts (test-timestamp-string event-time))
- (content (format "* TODO Future Event\nSCHEDULED: %s\n" ts)))
-
- (test-integration-startup--create-org-file content)
-
- (with-test-time now
- (cl-letf (((symbol-function 'async-start)
- (lambda (start-func finish-func)
- (funcall finish-func (funcall start-func)))))
- (chime-check)
- (sleep-for 0.1)
-
- ;; Should find 0 events within lookahead window
- (should (or (null chime--upcoming-events)
- (= 0 (length chime--upcoming-events))))
-
- ;; Modeline should handle this gracefully (nil or empty)
- ;; No error should occur
- )))
- (test-integration-startup-teardown)))
+ (with-startup-config
+ (let* ((now (test-time-now))
+ ;; Event 30 days from now (beyond 24-hour lookahead)
+ (event-time (test-time-at 30 0 0))
+ (ts (test-timestamp-string event-time))
+ (content (format "* TODO Future Event\nSCHEDULED: %s\n" ts)))
+
+ (test-integration-startup--create-org-file content)
+
+ (with-test-time now
+ (cl-letf (((symbol-function 'async-start)
+ (lambda (start-func finish-func)
+ (funcall finish-func (funcall start-func)))))
+ (chime-check)
+ (sleep-for 0.1)
+
+ ;; Should find 0 events within lookahead window
+ (should (or (null chime--upcoming-events)
+ (= 0 (length chime--upcoming-events))))
+
+ ;; Modeline should handle this gracefully (nil or empty)
+ ;; No error should occur
+ )))))
(provide 'test-integration-startup)
;;; test-integration-startup.el ends here
diff --git a/tests/testutil-events.el b/tests/testutil-events.el
index 19b3d55..0ee3d57 100644
--- a/tests/testutil-events.el
+++ b/tests/testutil-events.el
@@ -210,5 +210,62 @@ Example:
(progn ,@body)
(test-standard-teardown))))
+;;; Config Override Macro
+
+(defmacro with-chime-config (&rest args)
+ "Temporarily override chime config variables for testing.
+ARGS are alternating VARIABLE VALUE pairs, followed by BODY forms.
+
+Expands to a `let' form, so overridden values are automatically restored
+when BODY exits - including on error. This replaces the manual pattern of
+saving originals to defvars in setup, then restoring them in teardown:
+
+ ;; Before (manual save/restore - error-prone, verbose):
+ ;; (defvar saved-foo nil)
+ ;; (setq saved-foo chime-foo)
+ ;; (setq chime-foo 42)
+ ;; (unwind-protect (progn ...) (setq chime-foo saved-foo))
+ ;;
+ ;; After (let-binding - automatic, concise):
+ ;; (with-chime-config chime-foo 42 ...)
+
+Example:
+ (with-chime-config
+ chime-modeline-lookahead-minutes 1440
+ chime-tooltip-lookahead-hours 24
+ (should (= chime-modeline-lookahead-minutes 1440)))"
+ (declare (indent 0))
+ (let ((bindings nil)
+ (body nil)
+ (remaining args))
+ ;; Parse alternating symbol/value pairs until we hit a non-symbol or list
+ (while (and remaining
+ (symbolp (car remaining))
+ (not (null (car remaining)))
+ (cdr remaining))
+ (push (list (pop remaining) (pop remaining)) bindings))
+ (setq body remaining)
+ `(let ,(nreverse bindings)
+ ,@body)))
+
+;;; Org Event File Macro
+
+(defmacro with-org-event-file (events-spec file-var &rest body)
+ "Create temp org file from EVENTS-SPEC, bind path to FILE-VAR, execute BODY.
+Each element of EVENTS-SPEC is (TITLE TIME &optional SCHEDULED-P ALL-DAY-P).
+The temp file is created and cleaned up automatically.
+
+Example:
+ (with-org-event-file
+ ((\"Meeting\" event-time t)
+ (\"Birthday\" bday-time nil t))
+ org-file
+ (setq org-agenda-files (list org-file))
+ ...)"
+ (declare (indent 2))
+ `(let* ((content (test-create-org-events (list ,@events-spec)))
+ (,file-var (chime-create-temp-test-file-with-content content)))
+ ,@body))
+
(provide 'testutil-events)
;;; testutil-events.el ends here