From fe5a31072bfa2f6451769008a63ad5b0d9a3de17 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Tue, 24 Feb 2026 04:00:38 -0600 Subject: 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. --- tests/test-integration-startup.el | 284 +++++++++++++++++--------------------- tests/testutil-events.el | 57 ++++++++ 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 -- cgit v1.2.3