diff options
65 files changed, 3083 insertions, 2552 deletions
@@ -20,3 +20,6 @@ CLAUDE.md githooks/ /coverage-makefile.txt + +# Task archive — follows todo.org's privacy +/archive/task-archive.org @@ -725,6 +725,59 @@ supported by your system." :type '(choice (const :tag "No sound" nil) (file :tag "Sound file path"))) +(defcustom chime-sound-player 'auto + "How `chime-sound-file' is played. + +Emacs' built-in `play-sound-file' opens an ALSA device directly and +blocks until the clip finishes. On a system whose ALSA `default' PCM +does not resolve -- common under PipeWire, where nothing points +`pcm.!default' at the sound server -- it fails outright with \"No usable +sound device driver found\". An external player avoids both problems: it +speaks to the sound server and it runs asynchronously. + +The value is one of: + +`auto' Use the first available player from + `chime--sound-player-candidates', falling back to + `play-sound-file' when none is installed. The default. +`emacs' Always use `play-sound-file'. +STRING Name or path of an external player command. It is invoked as + \"COMMAND SOUND-FILE\". Falls back to `play-sound-file' when + the command is not on `exec-path'." + :package-version '(chime . "0.8.0") + :group 'chime + :type '(choice (const :tag "Auto-detect an external player" auto) + (const :tag "Emacs' built-in play-sound-file" emacs) + (string :tag "External player command")) + :set (lambda (symbol value) + (unless (or (memq value '(auto emacs)) (stringp value)) + (user-error "%s must be `auto', `emacs', or a command name, got: %S" + symbol value)) + (set-default symbol value))) + +(defcustom chime-sound-device nil + "Sound device passed to Emacs' built-in `play-sound-file'. + +Only consulted when `play-sound-file' is used -- that is, when +`chime-sound-player' is `emacs', or when it is `auto' and no external +player was found. nil lets the system choose, which on Linux means the +ALSA `default' PCM. + +Set this when `default' is unusable but a named device works. Under +PipeWire that is usually \"pipewire\" or \"pulse\"; test a candidate with + + (play-sound-file \"/path/to/chime.wav\" nil \"pipewire\") + +before setting it here." + :package-version '(chime . "0.8.0") + :group 'chime + :type '(choice (const :tag "System default" nil) + (string :tag "Device name")) + :set (lambda (symbol value) + (unless (or (null value) (stringp value)) + (user-error "%s must be nil or a string, got: %S" symbol value)) + (set-default symbol value))) + (defcustom chime-startup-delay 10 "Seconds to wait before first event check after chime-mode is enabled. This delay allows org-agenda-files and related infrastructure to finish @@ -861,6 +914,14 @@ you theme it." Consulted by the watchdog in `chime--fetch-and-process' to detect a child that exceeded `chime-async-timeout'.") +(defvar chime--process-generation 0 + "Counter identifying the current async child. +Incremented on every spawn, and on every event that abandons a child (the +watchdog interrupt, and `chime--stop'). Each child's callback captures the +generation it was spawned under and compares it here, so a straggler that +returns after its child was abandoned can be discarded rather than clobber +the replacement child's state.") + (defvar chime--consecutive-async-failures 0 "Count of consecutive async check failures. After `chime-max-consecutive-failures' failures, a warning is displayed.") @@ -1257,20 +1318,26 @@ For timed events, checks if the time is today (past or future)." (defun chime--days-until-event (all-times) "Calculate minimum days until the soonest all-day timestamp in ALL-TIMES. ALL-TIMES is a list of (TIMESTAMP-STR . TIME-OBJECT) cons cells. -Returns integer days (ceiling), or nil if no all-day timestamps found." - (let ((now (current-time))) - (-min - (--map - (when-let* ((timestamp-str (car it)) - (is-all-day (not (chime--has-timestamp timestamp-str))) - (parsed (org-parse-time-string timestamp-str)) - (year (nth 5 parsed)) - (month (nth 4 parsed)) - (day (nth 3 parsed))) - (let* ((event-time (encode-time 0 0 0 day month year)) - (seconds-until (time-subtract event-time now))) - (ceiling (/ (float-time seconds-until) 86400.0)))) - all-times)))) +Returns integer days (ceiling), or nil if no all-day timestamps found. + +A timed timestamp contributes no day count, so an event mixing timed and +all-day timestamps yields nils that must be dropped before `-min' sees +them. An event with no all-day timestamp at all leaves nothing to +minimize, hence the nil return the docstring promises." + (let* ((now (current-time)) + (days (-non-nil + (--map + (when-let* ((timestamp-str (car it)) + (is-all-day (not (chime--has-timestamp timestamp-str))) + (parsed (org-parse-time-string timestamp-str)) + (year (nth 5 parsed)) + (month (nth 4 parsed)) + (day (nth 3 parsed))) + (let* ((event-time (encode-time 0 0 0 day month year)) + (seconds-until (time-subtract event-time now))) + (ceiling (/ (float-time seconds-until) 86400.0)))) + all-times)))) + (when days (-min days)))) (defun chime--day-wide-notification-text (event) "Generate notification text for day-wide EVENT. @@ -1285,6 +1352,10 @@ Handles both same-day events and advance notices." (is-advance-notice (let ((days-until (chime--days-until-event (chime--event-times event)))) (cond + ;; No all-day timestamp means no day count to report. The + ;; advance-notice window should have excluded such an event, so + ;; this is a belt-and-braces guard against a nil reaching `='. + ((null days-until) (format "%s is due or scheduled today" title)) ((= days-until 1) (format "%s is tomorrow" title)) ((= days-until 2) (format "%s is in 2 days" title)) (t (format "%s is in %d days" title days-until))))) @@ -1776,7 +1847,13 @@ because that is what real org-gcal exports use." "org-todo-keywords" "chime-alert-intervals" "chime-include-filters" - "chime-exclude-filters"))) + "chime-exclude-filters" + ;; The child sizes its agenda span from these two. The + ;; child requires chime, so an uninjected one is bound at + ;; its default there and a user's customization is + ;; silently ignored rather than erroring. + "chime-modeline-lookahead-minutes" + "chime-tooltip-lookahead-hours"))) string-end))) (defun chime--environment-regex () @@ -1838,6 +1915,71 @@ list of internal event alists." (chime--apply-exclude-filters) (-map 'chime--gather-info)))) +;;;; Sound Playback + +(defconst chime--sound-player-candidates + '("pw-play" "paplay" "afplay" "aplay") + "External player commands tried, in order, when `chime-sound-player' is `auto'. +Each is invoked as \"COMMAND SOUND-FILE\". Sound-server players come +first: they respect the user's default sink and per-application volume. +`aplay' is last because it talks to ALSA directly and so shares +`play-sound-file''s failure mode on a system with no usable `default' +PCM. `afplay' is the macOS player.") + +(defun chime--find-sound-player () + "Return the external player command to use, or nil to use `play-sound-file'. +Honors `chime-sound-player': `emacs' selects no player, a string selects +that command when it is installed, and `auto' picks the first installed +candidate from `chime--sound-player-candidates'." + (cond + ((eq chime-sound-player 'emacs) nil) + ((stringp chime-sound-player) + (and (executable-find chime-sound-player) chime-sound-player)) + (t (cl-find-if #'executable-find chime--sound-player-candidates)))) + +(defun chime--play-sound-external (player file) + "Play FILE by spawning PLAYER asynchronously. +Return the process on success, or nil if the spawn failed. Playback +itself is not awaited, so a clip never blocks the notification. + +Because the clip is not awaited, a player that starts and *then* fails -- +an unsupported file, or a device that turns out to be unusable -- can +only be caught after the fact. A sentinel reports that non-zero exit, so +the failure lands in *Messages* rather than vanishing into silence." + (condition-case nil + (let ((process (start-process "chime-sound" nil player file))) + (set-process-query-on-exit-flag process nil) + (set-process-sentinel + process + (lambda (proc _event) + (when (memq (process-status proc) '(exit signal)) + (let ((status (process-exit-status proc))) + (unless (zerop status) + (message "chime: Failed to play sound: %s exited with status %s" + player status)))))) + process) + (error nil))) + +(defun chime--play-sound () + "Play `chime-sound-file', when one is configured and present. +Prefer an external player and fall back to Emacs' `play-sound-file'. +A playback failure is reported but never signalled -- losing the sound +must not cost the user the visual notification. + +Return the player process when an external player was used, t when Emacs +played the file, and nil when nothing was played." + (when (and chime-sound-file (file-exists-p chime-sound-file)) + (let* ((player (chime--find-sound-player)) + (process (and player + (chime--play-sound-external player chime-sound-file)))) + (or process + (condition-case err + (progn (play-sound-file chime-sound-file nil chime-sound-device) t) + (error + (message "chime: Failed to play sound: %s" + (error-message-string err)) + nil)))))) + ;;;; Notification Dispatch (defun chime--notify (msg-severity) @@ -1847,21 +1989,23 @@ notification text and SEVERITY is one of high, medium, or low." (let* ((event-msg (if (consp msg-severity) (car msg-severity) msg-severity)) (severity (if (consp msg-severity) (cdr msg-severity) 'medium))) ;; Play sound if a file is configured (set chime-sound-file to nil to disable) - (when chime-sound-file - (condition-case err - (when (file-exists-p chime-sound-file) - (play-sound-file chime-sound-file)) - (error - (message "chime: Failed to play sound: %s" - (error-message-string err))))) - ;; Show visual notification - (apply - 'alert event-msg - :icon chime-notification-icon - :title chime-notification-title - :severity severity - :category 'chime - chime-extra-alert-plist))) + (chime--play-sound) + ;; Show visual notification. `chime--process-notifications' maps this + ;; over every due event, so an alert that signals -- a dbus error, a + ;; misbehaving notification daemon -- would drop every notification + ;; after it and be miscounted as a fetch failure by the caller's + ;; condition-case. Guard each event's alert on its own. + (condition-case err + (apply + 'alert event-msg + :icon chime-notification-icon + :title chime-notification-title + :severity severity + :category 'chime + chime-extra-alert-plist) + (error + (message "chime: Failed to show notification: %s" + (error-message-string err)))))) ;;;; Timestamp Parsing @@ -2038,48 +2182,28 @@ delimiters for unmatched openings. Returns sanitized title or empty string if TITLE is nil." (if (not title) "" - (let ((chars (string-to-list title)) - (stack '()) ; Stack to track opening delimiters in order + (let ((pairs '((?\( . ?\)) (?\[ . ?\]) (?\{ . ?\}))) + (stack '()) ; opening delimiters seen, most recent first (result '())) - ;; Process each character - (dolist (char chars) + (dolist (char (string-to-list title)) (cond - ;; Opening delimiters - add to stack and result - ((memq char '(?\( ?\[ ?\{)) + ;; Opening delimiter - remember it and keep it. + ((assq char pairs) (push char stack) (push char result)) - ;; Closing delimiters - check if they match - ((eq char ?\)) - (if (and stack (eq (car stack) ?\()) - (progn - (pop stack) - (push char result)) - ;; Unmatched closing paren - skip it - nil)) - ((eq char ?\]) - (if (and stack (eq (car stack) ?\[)) - (progn - (pop stack) - (push char result)) - ;; Unmatched closing bracket - skip it - nil)) - ((eq char ?\}) - (if (and stack (eq (car stack) ?\{)) - (progn - (pop stack) - (push char result)) - ;; Unmatched closing brace - skip it - nil)) - ;; Regular characters - add to result + ;; Closing delimiter - keep only when it closes the current opener; + ;; an unmatched closer is dropped. + ((rassq char pairs) + (when (and stack (eq (cdr (assq (car stack) pairs)) char)) + (pop stack) + (push char result))) + ;; Regular character. (t (push char result)))) - ;; Add closing delimiters for any remaining opening delimiters + ;; Close any still-open delimiters, innermost first. (dolist (opener stack) - (cond - ((eq opener ?\() (push ?\) result)) - ((eq opener ?\[) (push ?\] result)) - ((eq opener ?\{) (push ?\} result)))) - ;; Convert back to string (reverse because we built it backwards) + (push (cdr (assq opener pairs)) result)) + ;; Convert back to string (reverse because we built it backwards). (concat (nreverse result))))) (defun chime--extract-title (marker) @@ -2265,12 +2389,18 @@ When called programmatically, returns structured validation results." ;;;; Core Lifecycle (defun chime--stop () - "Stop the notification timer and cancel any in-progress check." + "Stop the notification timer and cancel any in-progress check. +Leaves no state behind: a later `chime-mode' must not resume with the old +failure count, a stale spawn time, or a callback from the child this stop +abandoned." (-some-> chime--timer (cancel-timer)) (setq chime--timer nil) - (when chime--process - (interrupt-process chime--process) - (setq chime--process nil)) + (chime--kill-async-process chime--process) + (setq chime--process nil) + (setq chime--process-start-time nil) + ;; Orphan the abandoned child's callback, if it is still in flight. + (cl-incf chime--process-generation) + (setq chime--consecutive-async-failures 0) ;; Reset validation state so it runs again on next start (setq chime--validation-done nil) (setq chime--validation-retry-count 0)) @@ -2363,15 +2493,40 @@ deprecated per-event property." (chime--maybe-warn-deprecated-properties events) (funcall callback events)) +(defun chime--kill-async-process (process) + "Kill PROCESS and reap the buffer async.el leaves behind. +Does nothing when PROCESS is nil or already dead. + +`interrupt-process' only asks: a child stuck in a blocking read ignores +SIGINT and survives as a zombie, invisible because `chime--process' has +already been cleared. `delete-process' does not ask. + +async.el's sentinel kills the child's process buffer only on a zero exit, +so a signalled child leaks its buffer. A persistent hang leaks one per +`chime-async-timeout' until Emacs restarts, which is why the buffer is +killed here rather than left to async. The sentinel is silenced first so +async cannot act on a child we have already abandoned." + (when (processp process) + (let ((buffer (process-buffer process))) + (set-process-sentinel process #'ignore) + (when (process-live-p process) + (delete-process process)) + (when (buffer-live-p buffer) + (kill-buffer buffer))))) + (defun chime--interrupt-stale-process () - "Interrupt an async child that has outlived `chime-async-timeout'. + "Kill an async child that has outlived `chime-async-timeout'. A child that never returns (e.g. stuck on an interactive prompt in batch mode) is invisible to every failure path — it returns no error sexp, so `chime--consecutive-async-failures' never increments while the overlap -guard silently skips every subsequent check. Interrupting it and -recording the timeout through `chime--record-async-failure' routes the -hang into the existing failure machinery and frees the next check to -spawn a fresh child. Does nothing when `chime-async-timeout' is nil." +guard silently skips every subsequent check. Killing it and recording the +timeout through `chime--record-async-failure' routes the hang into the +existing failure machinery and frees the next check to spawn a fresh +child. Does nothing when `chime-async-timeout' is nil. + +Bumping `chime--process-generation' orphans the abandoned child's +callback, so a straggler that returns just past the timeout cannot clobber +the replacement child's state." (when (and chime-async-timeout chime--process (process-live-p chime--process) @@ -2379,38 +2534,54 @@ spawn a fresh child. Does nothing when `chime-async-timeout' is nil." (> (float-time (time-subtract (current-time) chime--process-start-time)) chime-async-timeout)) - (interrupt-process chime--process) + (chime--kill-async-process chime--process) (setq chime--process nil) + (setq chime--process-start-time nil) + (cl-incf chime--process-generation) (chime--record-async-failure (list 'error (format "Async fetch exceeded chime-async-timeout (%ds)" chime-async-timeout)) "Async watchdog"))) +(defun chime--handle-async-result (generation callback events) + "Handle EVENTS returned by the async child spawned under GENERATION. +CALLBACK receives the events on success. + +Discards the result when GENERATION is no longer current — the child was +abandoned by the watchdog or by `chime--stop', and a replacement may +already be running. Acting on a straggler would clear the replacement's +process handle (breaking the overlap guard, so a third child could spawn) +and reset the failure counter the watchdog had just incremented." + (when (= generation chime--process-generation) + (setq chime--process nil) + (setq chime--process-start-time nil) + (setq chime--last-check-time (current-time)) + (condition-case err + (if (and (listp events) + (eq (car events) 'async-signal)) + (chime--record-async-failure (cdr events) "Async error") + (chime--handle-async-success callback events)) + (error + (chime--record-async-failure err "Error processing events"))))) + (defun chime--fetch-and-process (callback) "Asynchronously fetch events from agenda and invoke CALLBACK with them. Manages async process state and last-check-time internally. Does nothing if a check is already in progress, unless that check has -exceeded `chime-async-timeout' — then it is interrupted and replaced." +exceeded `chime-async-timeout' — then it is killed and replaced." (chime--interrupt-stale-process) (unless (and chime--process (process-live-p chime--process)) (setq chime--process-start-time (current-time)) - (setq chime--process - (let ((default-directory user-emacs-directory) - (async-prompt-for-password nil) - (async-process-noquery-on-exit t)) - (async-start - (chime--retrieve-events) - (lambda (events) - (setq chime--process nil) - (setq chime--last-check-time (current-time)) - (condition-case err - (if (and (listp events) - (eq (car events) 'async-signal)) - (chime--record-async-failure (cdr events) "Async error") - (chime--handle-async-success callback events)) - (error - (chime--record-async-failure err "Error processing events"))))))))) + (let ((generation (cl-incf chime--process-generation))) + (setq chime--process + (let ((default-directory user-emacs-directory) + (async-prompt-for-password nil) + (async-process-noquery-on-exit t)) + (async-start + (chime--retrieve-events) + (lambda (events) + (chime--handle-async-result generation callback events)))))))) (defun chime--log-silently (format-string &rest args) "Append formatted message to *Messages* buffer without echoing. diff --git a/docs/CONFIGURATION.org b/docs/CONFIGURATION.org index 3565c4a..225b840 100644 --- a/docs/CONFIGURATION.org +++ b/docs/CONFIGURATION.org @@ -73,7 +73,50 @@ org-wild-notifier's =:WILD_NOTIFIER_NOTIFY_BEFORE:= property is honored as a dep (setq chime-sound-file "/path/to/chime.wav") #+END_SRC -Chime uses Emacs's built-in =play-sound-file=. WAV and AU are the safest formats. +WAV and AU are the safest formats. + +** How the sound gets played + +By default chime hands the file to an external player — the first of +=pw-play=, =paplay=, =afplay= or =aplay= found on =exec-path=. That player runs +asynchronously, so a clip never blocks Emacs, and it goes through the +system's sound server, so it respects the default sink and the +per-application volume. + +When no external player is installed, chime falls back to Emacs's built-in +=play-sound-file=. That function opens an audio device directly and blocks +until the clip finishes. + +#+BEGIN_SRC elisp +;; Always use Emacs's built-in player +(setq chime-sound-player 'emacs) + +;; Use a specific command, invoked as "COMMAND SOUND-FILE" +(setq chime-sound-player "paplay") +#+END_SRC + +An explicit command that isn't installed falls back to =play-sound-file= +rather than failing. + +** Choosing a device for the built-in player + +=chime-sound-device= is passed to =play-sound-file= as its DEVICE argument. +It only matters when the built-in player is in use. nil, the default, lets +the system choose — on Linux that means ALSA's =default= PCM. + +Set it when =default= is unusable but a named device works. Under PipeWire +that is usually "pipewire" or "pulse": + +#+BEGIN_SRC elisp +(setq chime-sound-player 'emacs) +(setq chime-sound-device "pipewire") +#+END_SRC + +Test a candidate before committing to it: + +#+BEGIN_SRC elisp +M-: (play-sound-file chime-sound-file nil "pipewire") +#+END_SRC * Notification Icon diff --git a/docs/TROUBLESHOOTING.org b/docs/TROUBLESHOOTING.org index b1fadde..fe78aa0 100644 --- a/docs/TROUBLESHOOTING.org +++ b/docs/TROUBLESHOOTING.org @@ -66,14 +66,59 @@ Check these in order: #+BEGIN_SRC elisp M-: (file-exists-p chime-sound-file) #+END_SRC -3. Test sound directly: +3. See which player chime picked: #+BEGIN_SRC elisp - M-: (play-sound-file chime-sound-file) + M-: (chime--find-sound-player) #+END_SRC -4. Ensure your system has audio support configured. + A string is the external player it will spawn. nil means it falls back + to Emacs's built-in =play-sound-file=. +4. Play the file the same way chime does: + #+BEGIN_SRC elisp + M-: (chime--play-sound) + #+END_SRC + A process object or t means playback started. nil means it failed, and + =*Messages*= carries the reason. An external player that starts and then + exits non-zero — an unsupported file, say — also reports to =*Messages*=. +5. Ensure your system has audio support configured. Set =chime-sound-file= to nil to disable sound. +** "No usable sound device driver found" + +Emacs's =play-sound-file= opens an ALSA device directly. It reports this +error when ALSA's =default= PCM doesn't resolve, which happens on a +PipeWire system where nothing points =pcm.!default= at the sound server. +Confirm it outside Emacs — =aplay <file>= fails the same way — and note +that =aplay -D pipewire <file>= usually works, which tells you the sound +server itself is healthy. + +Chime avoids the problem by preferring an external player. You only reach +this error when none of =pw-play=, =paplay=, =afplay= or =aplay= is installed, +or when =chime-sound-player= is set to ='emacs=. Two fixes: + +- Name a device that resolves: =(setq chime-sound-device "pipewire")=. +- Or repair the system so =default= routes to PipeWire. On Arch, the + needed config ships with =pipewire-audio= but is not enabled: + #+BEGIN_SRC sh + sudo ln -s /usr/share/alsa/alsa.conf.d/99-pipewire-default.conf /etc/alsa/conf.d/ + #+END_SRC + This one repairs every ALSA client on the machine, not just Emacs. + +*** The system fix doesn't reach a running Emacs + +alsa-lib parses its configuration once per process. A long-running +=emacs --daemon= that started before you added the ALSA config keeps the old +view, so =play-sound-file= still fails there while it succeeds in a fresh +=emacs -Q=. Restart the daemon, or set =chime-sound-device= to a device that +was already defined ("pipewire" or "pulse" both work without the =default= +fix, because =50-pipewire.conf= defines them). + +This is worth knowing before you conclude the system fix didn't work. + +On macOS, Emacs is built without sound support, so =play-sound-file= +signals rather than playing. Chime uses =afplay=, which ships with the OS, +so this only bites if =chime-sound-player= is forced to ='emacs=. + * Events Not Being Detected Common causes: diff --git a/tests/Makefile b/tests/Makefile index 8fd2e0c..dc12732 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -100,8 +100,10 @@ endif fi; \ printf "$(YELLOW)Running tests in $$TESTFILE...$(NC)\n"; \ $(EMACS_BATCH) -l ert -l "$$TESTFILE" \ - --eval "$(ERT_FAST_SELECTOR)" 2>&1 | tee $(PROJECT_ROOT)/tests/test-file-output.log; \ - if [ $$? -eq 0 ]; then \ + --eval "$(ERT_FAST_SELECTOR)" > $(PROJECT_ROOT)/tests/test-file-output.log 2>&1; \ + status=$$?; \ + cat $(PROJECT_ROOT)/tests/test-file-output.log; \ + if [ $$status -eq 0 ]; then \ printf "$(GREEN)✓ All tests in $$TESTFILE passed!$(NC)\n"; \ else \ printf "$(RED)✗ Some tests failed.$(NC)\n"; \ @@ -152,17 +154,22 @@ test-unit: check-deps # Run only integration tests (excluding :slow) test-integration: check-deps @printf "$(YELLOW)Running integration tests ($(words $(INTEGRATION_TESTS)) files, excluding :slow)...$(NC)\n" - @failed=0; \ + @failed=0; ran=0; \ for testfile in $(INTEGRATION_TESTS); do \ echo " Testing $$testfile..."; \ - $(EMACS_BATCH) -l ert -l "$$testfile" \ - --eval "$(ERT_FAST_SELECTOR)" || failed=$$((failed + 1)); \ + out=$$($(EMACS_BATCH) -l ert -l "$$testfile" \ + --eval "$(ERT_FAST_SELECTOR)" 2>&1) || failed=$$((failed + 1)); \ + printf '%s\n' "$$out"; \ + n=$$(printf '%s\n' "$$out" | sed -n 's/^Ran \([0-9][0-9]*\) tests.*/\1/p' | tail -1); \ + ran=$$((ran + $${n:-0})); \ done; \ - if [ $$failed -eq 0 ]; then \ - printf "$(GREEN)[✓] All integration tests passed$(NC)\n"; \ - else \ + if [ $$failed -gt 0 ]; then \ printf "$(RED)[✗] $$failed integration test file(s) failed$(NC)\n"; \ exit 1; \ + elif [ $$ran -eq 0 ]; then \ + printf "$(YELLOW)[i] Integration tests skipped: every one is tagged :slow. Run 'make test-all'.$(NC)\n"; \ + else \ + printf "$(GREEN)[✓] All integration tests passed ($$ran tests)$(NC)\n"; \ fi # Run tests matching a name pattern (ERT selector) @@ -201,7 +208,7 @@ validate: for file in ../chime.el test-*.el testutil-*.el; do \ if [ -f "$$file" ] && [ ! -d "$$file" ]; then \ total=$$((total + 1)); \ - output=$$(emacs --batch -Q --eval "(progn \ + raw=$$(emacs --batch -Q --eval "(progn \ (setq byte-compile-error-on-warn nil) \ (find-file \"$$file\") \ (condition-case err \ @@ -210,8 +217,10 @@ validate: (message \"✓ $$file - parentheses balanced\")) \ (error \ (message \"✗ $$file: %s\" (error-message-string err)) \ - (kill-emacs 1))))" 2>&1 | grep -E '(✓|✗)'); \ - if [ $$? -eq 0 ]; then \ + (kill-emacs 1))))" 2>&1); \ + status=$$?; \ + output=$$(printf '%s\n' "$$raw" | grep -E '(✓|✗)'); \ + if [ $$status -eq 0 ]; then \ printf "$(GREEN)$$output$(NC)\n"; \ else \ printf "$(RED)$$output$(NC)\n"; \ diff --git a/tests/test-bootstrap.el b/tests/test-bootstrap.el index f53b9f1..40c333a 100644 --- a/tests/test-bootstrap.el +++ b/tests/test-bootstrap.el @@ -45,5 +45,28 @@ ;; Load chime from parent directory (load (expand-file-name "../chime.el") nil t) +;; Load the near-universal test helpers so individual test files don't each +;; repeat the require. testutil-general carries the base-dir fixture used by +;; `chime-deftest' below; testutil-time carries the dynamic-time helpers. +(require 'testutil-general (expand-file-name "testutil-general.el")) +(require 'testutil-time (expand-file-name "testutil-time.el")) + +(defmacro chime-deftest (name arglist &rest body) + "Define an ERT test NAME with chime's standard base-dir fixture. +ARGLIST is the `ert-deftest' argument list (normally nil). When the +first form in BODY is a string it is kept as the test docstring. The +remaining forms run inside the test base directory, created beforehand +with `chime-create-test-base-dir' and removed afterward with +`chime-delete-test-base-dir' even if a form signals." + (declare (indent 2) (doc-string 3)) + (let* ((doc (and (stringp (car body)) (cdr body) (car body))) + (forms (if doc (cdr body) body))) + `(ert-deftest ,name ,arglist + ,@(and doc (list doc)) + (chime-create-test-base-dir) + (unwind-protect + (progn ,@forms) + (chime-delete-test-base-dir))))) + (provide 'test-bootstrap) ;;; test-bootstrap.el ends here diff --git a/tests/test-chime--deduplicate-events-by-title.el b/tests/test-chime--deduplicate-events-by-title.el index 66ec37a..13066eb 100644 --- a/tests/test-chime--deduplicate-events-by-title.el +++ b/tests/test-chime--deduplicate-events-by-title.el @@ -30,10 +30,6 @@ (setq chime-debug t) (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Test Helpers (defun test-make-event (title) diff --git a/tests/test-chime-12hour-format.el b/tests/test-chime-12hour-format.el index 0fcb91a..c18cbc5 100644 --- a/tests/test-chime-12hour-format.el +++ b/tests/test-chime-12hour-format.el @@ -31,10 +31,6 @@ (setq chime-debug t) (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Tests for chime--convert-12hour-to-24hour (ert-deftest test-12hour-convert-1pm-to-13 () diff --git a/tests/test-chime-all-day-events.el b/tests/test-chime-all-day-events.el index 1cacf69..168f545 100644 --- a/tests/test-chime-all-day-events.el +++ b/tests/test-chime-all-day-events.el @@ -9,8 +9,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) ;;; Helper Functions diff --git a/tests/test-chime-apply-blacklist.el b/tests/test-chime-apply-blacklist.el index df2f317..3f16a3d 100644 --- a/tests/test-chime-apply-blacklist.el +++ b/tests/test-chime-apply-blacklist.el @@ -27,9 +27,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) - ;;; Setup and Teardown (defun test-chime-apply-blacklist-setup () @@ -41,7 +38,10 @@ (defun test-chime-apply-blacklist-teardown () "Teardown function run after each test." (chime-delete-test-base-dir) - (setq chime-exclude-filters nil)) + ;; Restore the defcustom default rather than leaving the global clobbered. + ;; Each test let-binds `chime-exclude-filters', so the global only needs to + ;; return to its standard value to keep cross-file test isolation. + (custom-reevaluate-setting 'chime-exclude-filters)) ;;; Normal Cases diff --git a/tests/test-chime-apply-whitelist.el b/tests/test-chime-apply-whitelist.el index 1552ee2..6e3be24 100644 --- a/tests/test-chime-apply-whitelist.el +++ b/tests/test-chime-apply-whitelist.el @@ -27,9 +27,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) - ;;; Setup and Teardown (defun test-chime-apply-whitelist-setup () diff --git a/tests/test-chime-async-helpers.el b/tests/test-chime-async-helpers.el index 3e0e562..c7eb5ba 100644 --- a/tests/test-chime-async-helpers.el +++ b/tests/test-chime-async-helpers.el @@ -27,7 +27,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) ;;; Setup and Teardown diff --git a/tests/test-chime-async-injection.el b/tests/test-chime-async-injection.el new file mode 100644 index 0000000..3c9e304 --- /dev/null +++ b/tests/test-chime-async-injection.el @@ -0,0 +1,101 @@ +;;; test-chime-async-injection.el --- Tests for async environment injection -*- lexical-binding: t; -*- + +;; Copyright (C) 2024-2026 Craig Jennings + +;; Author: Craig Jennings <c@cjennings.net> + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;;; Commentary: + +;; Tests that every chime defcustom the async child reads is injected into +;; the child by `chime--environment-regex'. +;; +;; The child calls (require 'chime), so an uninjected defcustom is still +;; *bound* there -- at its default. A user who customizes it sees no error +;; and no effect, which is the worst shape a bug can take. That is exactly +;; what happened to chime-tooltip-lookahead-hours: the docstring invites +;; 8760 "to see distant events", and the child kept fetching 168 hours. +;; +;; The load-bearing test walks the child form and fails on any chime +;; defcustom it reads that the regex doesn't cover, so a variable added to +;; the child later cannot reintroduce the bug quietly. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) + +;;; Helpers + +(defun test-chime-injection--symbols (form) + "Return every symbol appearing anywhere in FORM." + (cond + ((symbolp form) (and form (list form))) + ((consp form) (append (test-chime-injection--symbols (car form)) + (test-chime-injection--symbols (cdr form)))) + (t nil))) + +(defun test-chime-injection--chime-defcustoms (form) + "Return the chime defcustoms appearing in FORM, deduplicated." + (seq-uniq + (seq-filter (lambda (sym) + (and (custom-variable-p sym) + (string-prefix-p "chime-" (symbol-name sym)))) + (test-chime-injection--symbols form)))) + +;;; Normal Cases + +(ert-deftest test-chime-async-injection-covers-every-chime-defcustom-in-the-child () + "Every chime defcustom the async child reads is injected into it. +An uninjected one silently runs at its default in the child, so a user's +customization is ignored without any error." + (let* ((form (chime--retrieve-events)) + (regex (chime--environment-regex)) + (uncovered (seq-remove + (lambda (sym) (string-match-p regex (symbol-name sym))) + (test-chime-injection--chime-defcustoms form)))) + (should-not uncovered))) + +(ert-deftest test-chime-async-injection-covers-the-lookahead-variables () + "Both lookahead variables reach the child. +The child sizes its agenda span from them, so without injection it fetches +the default 168-hour window no matter what the user set." + (let ((regex (chime--environment-regex))) + (should (string-match-p regex "chime-modeline-lookahead-minutes")) + (should (string-match-p regex "chime-tooltip-lookahead-hours")))) + +(ert-deftest test-chime-async-injection-covers-the-pre-existing-variables () + "The variables injected before the lookahead fix still are." + (let ((regex (chime--environment-regex))) + (dolist (name '("org-agenda-files" "load-path" "org-todo-keywords" + "chime-alert-intervals" "chime-include-filters" + "chime-exclude-filters")) + (should (string-match-p regex name))))) + +;;; Boundary Cases + +(ert-deftest test-chime-async-injection-regex-is-anchored () + "The regex matches whole variable names, not substrings. +Without anchoring, a user's unrelated `my-org-agenda-files-backup' would +be swept into the child." + (let ((regex (chime--environment-regex))) + (should-not (string-match-p regex "my-org-agenda-files")) + (should-not (string-match-p regex "org-agenda-files-extra")))) + +(ert-deftest test-chime-async-injection-honors-additional-regexes () + "`chime-additional-environment-regexes' extends the injected set." + (let* ((chime-additional-environment-regexes '("\\`my-var\\'")) + (regex (chime--environment-regex))) + (should (string-match-p regex "my-var")) + (should (string-match-p regex "org-agenda-files")))) + +(provide 'test-chime-async-injection) +;;; test-chime-async-injection.el ends here diff --git a/tests/test-chime-async-lifecycle.el b/tests/test-chime-async-lifecycle.el new file mode 100644 index 0000000..9ee0bee --- /dev/null +++ b/tests/test-chime-async-lifecycle.el @@ -0,0 +1,184 @@ +;;; test-chime-async-lifecycle.el --- Tests for async process lifecycle -*- lexical-binding: t; -*- + +;; Copyright (C) 2024-2026 Craig Jennings + +;; Author: Craig Jennings <c@cjennings.net> + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Unit tests for the async child's lifecycle: the spawn-generation guard +;; that discards a late callback, and the kill path that reaps a child the +;; watchdog abandoned. +;; +;; Two failure modes motivate these. +;; +;; A watchdog interrupt races a child that finishes just past the timeout. +;; The late callback nils `chime--process' -- which by then holds the +;; *replacement* child -- so the overlap guard breaks and a third child can +;; spawn. It also resets the failure counter the watchdog just incremented. +;; +;; `interrupt-process' only asks. A child stuck in a blocking read ignores +;; SIGINT and lives on as a zombie, invisible because `chime--process' was +;; already nil'd. And async.el only kills a child's process buffer on a +;; zero exit, so every signalled child leaks its buffer. A persistent hang +;; leaks one per `chime-async-timeout' until Emacs restarts. +;; +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) + +;;; Helpers + +(defun test-chime-lifecycle--reset () + "Reset the async state these tests touch." + (setq chime--process nil) + (setq chime--process-start-time nil) + (setq chime--process-generation 0) + (setq chime--consecutive-async-failures 0)) + +(defun test-chime-lifecycle--sleeper () + "Spawn a real, long-running child with a process buffer." + (let ((process (start-process "chime-lifecycle-test" + (generate-new-buffer " *chime-test*") + "sleep" "60"))) + (set-process-query-on-exit-flag process nil) + process)) + +;;; Normal Cases + +(chime-deftest test-chime-lifecycle-fresh-result-is-processed () + "A result from the current generation is processed." + (test-chime-lifecycle--reset) + (let ((handled nil)) + (setq chime--process-generation 3) + (cl-letf (((symbol-function 'chime--handle-async-success) + (lambda (_callback events) (setq handled events)))) + (chime--handle-async-result 3 #'ignore '(:an-event)) + (should (equal handled '(:an-event)))))) + +(chime-deftest test-chime-lifecycle-fresh-result-clears-process-state () + "Processing a fresh result clears the process handle and its start time." + (test-chime-lifecycle--reset) + (setq chime--process 'a-process) + (setq chime--process-start-time '(1 2)) + (cl-letf (((symbol-function 'chime--handle-async-success) (lambda (&rest _) nil))) + (chime--handle-async-result 0 #'ignore '()) + (should-not chime--process) + (should-not chime--process-start-time))) + +;;; Boundary Cases + +(chime-deftest test-chime-lifecycle-stale-result-is-discarded () + "A result from a superseded generation is ignored entirely." + (test-chime-lifecycle--reset) + (let ((handled nil) + (failed nil)) + ;; Generation 1 was abandoned; generation 2 is the live child. + (setq chime--process-generation 2) + (setq chime--process 'the-replacement-child) + (setq chime--consecutive-async-failures 1) + (cl-letf (((symbol-function 'chime--handle-async-success) + (lambda (&rest _) (setq handled t))) + ((symbol-function 'chime--record-async-failure) + (lambda (&rest _) (setq failed t)))) + (chime--handle-async-result 1 #'ignore '(:late-events)) + (should-not handled) + (should-not failed) + ;; The replacement child's handle survives. + (should (eq chime--process 'the-replacement-child)) + ;; The watchdog's failure count is not reset by the straggler. + (should (= chime--consecutive-async-failures 1))))) + +(chime-deftest test-chime-lifecycle-stale-error-result-is-discarded () + "A late error sexp from a superseded child is ignored too." + (test-chime-lifecycle--reset) + (let ((failed nil)) + (setq chime--process-generation 5) + (cl-letf (((symbol-function 'chime--record-async-failure) + (lambda (&rest _) (setq failed t)))) + (chime--handle-async-result 4 #'ignore '(async-signal error "boom")) + (should-not failed)))) + +(chime-deftest test-chime-lifecycle-watchdog-supersedes-the-generation () + "Interrupting a stale child bumps the generation, orphaning its callback." + (test-chime-lifecycle--reset) + (let ((now (current-time)) + (chime-async-timeout 10)) + (setq chime--process 'stuck-child) + (setq chime--process-start-time (time-subtract now (seconds-to-time 60))) + (setq chime--process-generation 7) + (cl-letf (((symbol-function 'process-live-p) (lambda (_p) t)) + ((symbol-function 'chime--kill-async-process) (lambda (_p) nil)) + ((symbol-function 'chime--record-async-failure) (lambda (&rest _) nil))) + (chime--interrupt-stale-process) + (should (= chime--process-generation 8)) + (should-not chime--process) + (should-not chime--process-start-time)))) + +(chime-deftest test-chime-lifecycle-stop-supersedes-the-generation () + "`chime--stop' orphans an in-flight callback and clears the counters." + (test-chime-lifecycle--reset) + (setq chime--process 'a-child) + (setq chime--process-start-time '(1 2)) + (setq chime--process-generation 2) + (setq chime--consecutive-async-failures 4) + (cl-letf (((symbol-function 'chime--kill-async-process) (lambda (_p) nil))) + (chime--stop) + (should (= chime--process-generation 3)) + (should-not chime--process) + (should-not chime--process-start-time) + ;; A restart must not resume with the old failure count. + (should (= chime--consecutive-async-failures 0)))) + +;;; Error Cases + +(chime-deftest test-chime-lifecycle-kill-async-process-kills-child-and-buffer () + "`chime--kill-async-process' leaves neither a live child nor its buffer. +`interrupt-process' only asks; async.el only reaps the buffer on a zero exit." + (let* ((process (test-chime-lifecycle--sleeper)) + (buffer (process-buffer process))) + (should (process-live-p process)) + (should (buffer-live-p buffer)) + (chime--kill-async-process process) + (should-not (process-live-p process)) + (should-not (buffer-live-p buffer)))) + +(chime-deftest test-chime-lifecycle-kill-async-process-tolerates-a-dead-child () + "Killing an already-dead process is a no-op, not an error." + (let ((process (test-chime-lifecycle--sleeper))) + (chime--kill-async-process process) + (should-not (condition-case nil + (progn (chime--kill-async-process process) nil) + (error t))))) + +(chime-deftest test-chime-lifecycle-kill-async-process-tolerates-nil () + "Killing nil is a no-op. `chime--stop' calls it unconditionally." + (should-not (condition-case nil + (progn (chime--kill-async-process nil) nil) + (error t)))) + +(chime-deftest test-chime-lifecycle-kill-async-process-silences-the-sentinel () + "The killed child's sentinel does not run, so async.el can't act on it." + (let ((process (test-chime-lifecycle--sleeper)) + (sentinel-ran nil)) + (set-process-sentinel process (lambda (&rest _) (setq sentinel-ran t))) + (chime--kill-async-process process) + (should-not sentinel-ran))) + +(provide 'test-chime-async-lifecycle) +;;; test-chime-async-lifecycle.el ends here diff --git a/tests/test-chime-async-watchdog.el b/tests/test-chime-async-watchdog.el index 473131d..f1b9829 100644 --- a/tests/test-chime-async-watchdog.el +++ b/tests/test-chime-async-watchdog.el @@ -86,34 +86,40 @@ Interruption requires strictly exceeding the timeout." (should-not interrupted) (should-not spawned))) -(ert-deftest test-chime-fetch-and-process-stale-child-interrupted-and-respawned () - "Error: an over-age child is interrupted, recorded as a failure, replaced. +(ert-deftest test-chime-fetch-and-process-stale-child-killed-and-respawned () + "Error: an over-age child is killed, recorded as a failure, replaced. The hung child must feed the existing consecutive-failures machinery (via `chime--record-async-failure') instead of silently blocking every tick, and -the same tick spawns a fresh child." +the same tick spawns a fresh child. + +The child is killed rather than interrupted: SIGINT is a request a child +stuck in a blocking read can ignore." (let* ((now (current-time)) - (interrupted nil) + (killed nil) (recorded nil) (spawned nil) (chime--process 'fake-live-process) (chime--process-start-time (time-subtract now (seconds-to-time 121))) + (chime--process-generation 0) (chime-async-timeout 120)) (cl-letf (((symbol-function 'current-time) (lambda () now)) ((symbol-function 'process-live-p) (lambda (proc) (eq proc 'fake-live-process))) - ((symbol-function 'interrupt-process) - (lambda (proc) (setq interrupted proc))) + ((symbol-function 'chime--kill-async-process) + (lambda (proc) (setq killed proc))) ((symbol-function 'chime--record-async-failure) (lambda (err prefix) (setq recorded (cons prefix err)))) ((symbol-function 'async-start) (lambda (&rest _) (setq spawned t) 'new-fake-process))) - (chime--fetch-and-process (lambda (_events) nil))) - (should (eq 'fake-live-process interrupted)) - (should (equal "Async watchdog" (car recorded))) - (should (string-match-p "chime-async-timeout" - (error-message-string (cdr recorded)))) - (should spawned) - (should (eq 'new-fake-process chime--process)))) + (chime--fetch-and-process (lambda (_events) nil)) + (should (eq 'fake-live-process killed)) + (should (equal "Async watchdog" (car recorded))) + (should (string-match-p "chime-async-timeout" + (error-message-string (cdr recorded)))) + (should spawned) + (should (eq 'new-fake-process chime--process)) + ;; One bump for abandoning the stale child, one for the replacement. + (should (= chime--process-generation 2))))) (ert-deftest test-chime-fetch-and-process-nil-timeout-disables-watchdog () "Boundary: `chime-async-timeout' nil disables the watchdog entirely. diff --git a/tests/test-chime-check-event.el b/tests/test-chime-check-event.el index e362504..db63c9e 100644 --- a/tests/test-chime-check-event.el +++ b/tests/test-chime-check-event.el @@ -27,8 +27,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) ;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;; Normal Cases diff --git a/tests/test-chime-check-interval.el b/tests/test-chime-check-interval.el index 74cbe14..4a79197 100644 --- a/tests/test-chime-check-interval.el +++ b/tests/test-chime-check-interval.el @@ -26,9 +26,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) - ;;; Setup and Teardown (defun test-chime-check-interval-setup () diff --git a/tests/test-chime-day-wide-notifications.el b/tests/test-chime-day-wide-notifications.el index 260abfc..b5638fb 100644 --- a/tests/test-chime-day-wide-notifications.el +++ b/tests/test-chime-day-wide-notifications.el @@ -23,7 +23,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (ert-deftest test-chime-day-wide-notifications-normal-wraps-with-medium-severity () "Normal: each generated text is wrapped as (TEXT . \\='medium)." diff --git a/tests/test-chime-day-wide-time-matching.el b/tests/test-chime-day-wide-time-matching.el index 88242e2..1a87326 100644 --- a/tests/test-chime-day-wide-time-matching.el +++ b/tests/test-chime-day-wide-time-matching.el @@ -33,10 +33,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - (defmacro test-chime-with-restored-day-wide-alert-times (&rest body) "Run BODY and restore default `chime-day-wide-alert-times' afterwards." (declare (indent 0) (debug t)) diff --git a/tests/test-chime-days-until-event.el b/tests/test-chime-days-until-event.el new file mode 100644 index 0000000..cd96218 --- /dev/null +++ b/tests/test-chime-days-until-event.el @@ -0,0 +1,131 @@ +;;; test-chime-days-until-event.el --- Tests for chime--days-until-event -*- lexical-binding: t; -*- + +;; Copyright (C) 2024-2026 Craig Jennings + +;; Author: Craig Jennings <c@cjennings.net> + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Unit tests for `chime--days-until-event' and the notification text that +;; consumes it. +;; +;; The function maps over every timestamp on an event and keeps the day +;; count for the all-day ones. Timed timestamps map to nil, so an event +;; mixing a timed and an all-day timestamp used to hand `-min' a list with +;; a nil in it and signal wrong-type-argument. An event with no all-day +;; timestamp at all handed `-min' an empty list, which signals too. Both +;; crash on every tick once `chime-day-wide-advance-notice' is on, walking +;; the async failure counter up to the persistent-failure warning. +;; +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) + +;;; Helpers + +;; `chime--days-until-event' measures against `current-time', so these +;; timestamps hang off the real now. testutil-time's `test-time-now' is +;; deliberately 30 days ahead, which would make every expected day count +;; wrong here. + +(defun test-chime-days--offset-time (days-from-now) + "Return the time DAYS-FROM-NOW days from the real current time." + (time-add (current-time) (days-to-time days-from-now))) + +(defun test-chime-days--all-day (days-from-now) + "Return an all-day timestamp cons cell DAYS-FROM-NOW days out." + (let ((ts (test-timestamp-string (test-chime-days--offset-time days-from-now) t))) + (cons ts (chime--timestamp-parse ts)))) + +(defun test-chime-days--timed (days-from-now hour minute) + "Return a timed timestamp cons cell DAYS-FROM-NOW days out at HOUR:MINUTE." + (let* ((decoded (decode-time (test-chime-days--offset-time days-from-now))) + (time (encode-time 0 minute hour + (decoded-time-day decoded) + (decoded-time-month decoded) + (decoded-time-year decoded))) + (ts (test-timestamp-string time))) + (cons ts (chime--timestamp-parse ts)))) + +;;; Normal Cases + +(ert-deftest test-chime-days-until-event-single-all-day-timestamp () + "A lone all-day timestamp yields its day count." + (should (= (chime--days-until-event (list (test-chime-days--all-day 3))) 3))) + +(ert-deftest test-chime-days-until-event-returns-the-soonest () + "The soonest all-day timestamp wins." + (should (= (chime--days-until-event + (list (test-chime-days--all-day 5) + (test-chime-days--all-day 2) + (test-chime-days--all-day 9))) + 2))) + +;;; Boundary Cases + +(ert-deftest test-chime-days-until-event-mixed-timed-and-all-day () + "An event mixing a timed and an all-day timestamp ignores the timed one. +The timed timestamp maps to nil, which must not reach `-min'." + (should (= (chime--days-until-event + (list (test-chime-days--timed 8 14 0) + (test-chime-days--all-day 4))) + 4))) + +(ert-deftest test-chime-days-until-event-timed-only-returns-nil () + "With no all-day timestamp there is no day count, so the result is nil." + (should-not (chime--days-until-event + (list (test-chime-days--timed 2 9 30) + (test-chime-days--timed 6 17 0))))) + +(ert-deftest test-chime-days-until-event-empty-list-returns-nil () + "An event with no timestamps at all yields nil rather than signalling." + (should-not (chime--days-until-event '()))) + +;;; Error Cases + +(ert-deftest test-chime-days-until-event-mixed-timestamps-do-not-signal () + "The mixed-timestamp case must not signal wrong-type-argument. +This is the crash that recurred every tick and drove the async failure +counter to the persistent-failure warning." + (should-not (condition-case nil + (progn (chime--days-until-event + (list (test-chime-days--timed 8 14 0) + (test-chime-days--all-day 4))) + nil) + (error t)))) + +(ert-deftest test-chime-day-wide-notification-text-survives-nil-day-count () + "The notification text falls back rather than crashing on a nil day count. +`chime--days-until-event' returns nil when an event has no all-day +timestamp, so the advance-notice branch must not compare nil to a number." + (let ((chime-day-wide-advance-notice 7) + (event `((title . "Timed Only") + (times . (,(test-chime-days--timed 3 10 0))) + (intervals . ((0))) + (marker-file . "/tmp/test.org") + (marker-pos . 1)))) + (cl-letf (((symbol-function 'chime--event-has-any-passed-time) + (lambda (_event) nil)) + ((symbol-function 'chime--event-within-advance-notice-window) + (lambda (_event) t))) + (let ((text (chime--day-wide-notification-text event))) + (should (stringp text)) + (should (string-match-p "Timed Only" text)))))) + +(provide 'test-chime-days-until-event) +;;; test-chime-days-until-event.el ends here diff --git a/tests/test-chime-debug-functions.el b/tests/test-chime-debug-functions.el index 7ef85e5..01e6663 100644 --- a/tests/test-chime-debug-functions.el +++ b/tests/test-chime-debug-functions.el @@ -30,10 +30,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) (require 'chime-debug (expand-file-name "../chime-debug.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Setup and Teardown (defun test-chime-debug-functions-setup () diff --git a/tests/test-chime-edge-coverage.el b/tests/test-chime-edge-coverage.el index 1325e77..0133099 100644 --- a/tests/test-chime-edge-coverage.el +++ b/tests/test-chime-edge-coverage.el @@ -26,7 +26,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'cl-lib) ;;;; chime--day-wide-notification-text fallback (chime.el ~ "t branch") diff --git a/tests/test-chime-event-contract.el b/tests/test-chime-event-contract.el index 697dccb..deeb626 100644 --- a/tests/test-chime-event-contract.el +++ b/tests/test-chime-event-contract.el @@ -25,8 +25,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) (ert-deftest test-chime-event-contract-make-event-creates-valid-event () diff --git a/tests/test-chime-event-is-today.el b/tests/test-chime-event-is-today.el index 305749a..deb4f70 100644 --- a/tests/test-chime-event-is-today.el +++ b/tests/test-chime-event-is-today.el @@ -31,10 +31,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Helpers — build events at real dates (defun test--real-today-at (hour minute) diff --git a/tests/test-chime-extract-time.el b/tests/test-chime-extract-time.el index 120c45b..d2be145 100644 --- a/tests/test-chime-extract-time.el +++ b/tests/test-chime-extract-time.el @@ -28,31 +28,15 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - -;;; Setup and Teardown - -(defun test-chime-extract-time-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-extract-time-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Tests for org-gcal events -(ert-deftest test-chime-extract-time-gcal-event-from-drawer () +(chime-deftest test-chime-extract-time-gcal-event-from-drawer () "Test that org-gcal events extract timestamps ONLY from :org-gcal: drawer. REFACTORED: Uses dynamic timestamps" - (test-chime-extract-time-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp-str (format-time-string "<%Y-%m-%d %a %H:%M-15:00>" time)) - (test-content (format "* Meeting + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp-str (format-time-string "<%Y-%m-%d %a %H:%M-15:00>" time)) + (test-content (format "* Meeting :PROPERTIES: :entry-id: abc123@google.com :END: @@ -60,33 +44,30 @@ REFACTORED: Uses dynamic timestamps" %s :END: " timestamp-str)) - (test-file (chime-create-temp-test-file-with-content test-content)) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) - (goto-char (point-min)) - (let* ((marker (point-marker)) - (times (chime--extract-time marker))) - ;; Should extract the timestamp from :org-gcal: drawer - (should (= 1 (length times))) - (should (string-match-p "14:00" (car (car times)))))) - (kill-buffer test-buffer)) - (test-chime-extract-time-teardown))) - -(ert-deftest test-chime-extract-time-gcal-event-ignores-body-timestamps () + (test-file (chime-create-temp-test-file-with-content test-content)) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) + (goto-char (point-min)) + (let* ((marker (point-marker)) + (times (chime--extract-time marker))) + ;; Should extract the timestamp from :org-gcal: drawer + (should (= 1 (length times))) + (should (string-match-p "14:00" (car (car times)))))) + (kill-buffer test-buffer))) + +(chime-deftest test-chime-extract-time-gcal-event-ignores-body-timestamps () "Test that org-gcal events ignore plain timestamps in body text. When an event is rescheduled, old timestamps might remain in the body. The :org-gcal: drawer has the correct time, so we should ignore body text. REFACTORED: Uses dynamic timestamps" - (test-chime-extract-time-setup) - (unwind-protect - (let* ((new-time (test-time-tomorrow-at 14 0)) - (old-time (test-time-today-at 14 0)) - (new-timestamp (test-timestamp-string new-time)) - (old-timestamp (test-timestamp-string old-time)) - (test-content (format "* Meeting + (let* ((new-time (test-time-tomorrow-at 14 0)) + (old-time (test-time-today-at 14 0)) + (new-timestamp (test-timestamp-string new-time)) + (old-timestamp (test-timestamp-string old-time)) + (test-content (format "* Meeting :PROPERTIES: :entry-id: abc123@google.com :END: @@ -95,34 +76,31 @@ REFACTORED: Uses dynamic timestamps" :END: Old time was %s " new-timestamp old-timestamp)) - (test-file (chime-create-temp-test-file-with-content test-content)) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) - (goto-char (point-min)) - (let* ((marker (point-marker)) - (times (chime--extract-time marker))) - ;; Should extract ONLY from drawer (tomorrow), ignore body (today) - (should (= 1 (length times))) - (should (string-match-p "14:00" (car (car times)))) - ;; Verify it's the new timestamp, not the old one - (should (string-match-p (format-time-string "%Y-%m-%d" new-time) (car (car times)))))) - (kill-buffer test-buffer)) - (test-chime-extract-time-teardown))) - -(ert-deftest test-chime-extract-time-gcal-event-ignores-scheduled () + (test-file (chime-create-temp-test-file-with-content test-content)) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) + (goto-char (point-min)) + (let* ((marker (point-marker)) + (times (chime--extract-time marker))) + ;; Should extract ONLY from drawer (tomorrow), ignore body (today) + (should (= 1 (length times))) + (should (string-match-p "14:00" (car (car times)))) + ;; Verify it's the new timestamp, not the old one + (should (string-match-p (format-time-string "%Y-%m-%d" new-time) (car (car times)))))) + (kill-buffer test-buffer))) + +(chime-deftest test-chime-extract-time-gcal-event-ignores-scheduled () "Test that org-gcal events ignore SCHEDULED/DEADLINE properties. For org-gcal events, the :org-gcal: drawer is the source of truth. REFACTORED: Uses dynamic timestamps" - (test-chime-extract-time-setup) - (unwind-protect - (let* ((drawer-time (test-time-tomorrow-at 14 0)) - (scheduled-time (test-time-days-from-now 2)) - (drawer-timestamp (test-timestamp-string drawer-time)) - (scheduled-timestamp (test-timestamp-string scheduled-time)) - (test-content (format "* Meeting + (let* ((drawer-time (test-time-tomorrow-at 14 0)) + (scheduled-time (test-time-days-from-now 2)) + (drawer-timestamp (test-timestamp-string drawer-time)) + (scheduled-timestamp (test-timestamp-string scheduled-time)) + (test-content (format "* Meeting SCHEDULED: %s :PROPERTIES: :entry-id: abc123@google.com @@ -131,33 +109,30 @@ SCHEDULED: %s %s :END: " scheduled-timestamp drawer-timestamp)) - (test-file (chime-create-temp-test-file-with-content test-content)) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) - (goto-char (point-min)) - (let* ((marker (point-marker)) - (times (chime--extract-time marker))) - ;; Should extract ONLY from drawer (tomorrow), ignore SCHEDULED (day after) - (should (= 1 (length times))) - (should (string-match-p "14:00" (car (car times)))) - (should (string-match-p (format-time-string "%Y-%m-%d" drawer-time) (car (car times)))))) - (kill-buffer test-buffer)) - (test-chime-extract-time-teardown))) - -(ert-deftest test-chime-extract-time-gcal-event-multiple-in-drawer () + (test-file (chime-create-temp-test-file-with-content test-content)) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) + (goto-char (point-min)) + (let* ((marker (point-marker)) + (times (chime--extract-time marker))) + ;; Should extract ONLY from drawer (tomorrow), ignore SCHEDULED (day after) + (should (= 1 (length times))) + (should (string-match-p "14:00" (car (car times)))) + (should (string-match-p (format-time-string "%Y-%m-%d" drawer-time) (car (car times)))))) + (kill-buffer test-buffer))) + +(chime-deftest test-chime-extract-time-gcal-event-multiple-in-drawer () "Test that org-gcal events extract all timestamps from :org-gcal: drawer. Some recurring events might have multiple timestamps in the drawer. REFACTORED: Uses dynamic timestamps" - (test-chime-extract-time-setup) - (unwind-protect - (let* ((time1 (test-time-tomorrow-at 14 0)) - (time2 (test-time-days-from-now 2 14 0)) - (timestamp1 (test-timestamp-string time1)) - (timestamp2 (test-timestamp-string time2)) - (test-content (format "* Meeting + (let* ((time1 (test-time-tomorrow-at 14 0)) + (time2 (test-time-days-from-now 2 14 0)) + (timestamp1 (test-timestamp-string time1)) + (timestamp2 (test-timestamp-string time2)) + (test-content (format "* Meeting :PROPERTIES: :entry-id: abc123@google.com :END: @@ -166,153 +141,137 @@ REFACTORED: Uses dynamic timestamps" %s :END: " timestamp1 timestamp2)) - (test-file (chime-create-temp-test-file-with-content test-content)) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) - (goto-char (point-min)) - (let* ((marker (point-marker)) - (times (chime--extract-time marker))) - ;; Should extract both timestamps from drawer - (should (= 2 (length times))) - (should (string-match-p "14:00" (car (car times)))) - (should (string-match-p "14:00" (car (cadr times)))))) - (kill-buffer test-buffer)) - (test-chime-extract-time-teardown))) + (test-file (chime-create-temp-test-file-with-content test-content)) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) + (goto-char (point-min)) + (let* ((marker (point-marker)) + (times (chime--extract-time marker))) + ;; Should extract both timestamps from drawer + (should (= 2 (length times))) + (should (string-match-p "14:00" (car (car times)))) + (should (string-match-p "14:00" (car (cadr times)))))) + (kill-buffer test-buffer))) ;;; Tests for regular org events -(ert-deftest test-chime-extract-time-regular-event-scheduled () +(chime-deftest test-chime-extract-time-regular-event-scheduled () "Test that regular events extract SCHEDULED timestamp. REFACTORED: Uses dynamic timestamps" - (test-chime-extract-time-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp (test-timestamp-string time)) - (test-content (format "* Task + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp (test-timestamp-string time)) + (test-content (format "* Task SCHEDULED: %s " timestamp)) - (test-file (chime-create-temp-test-file-with-content test-content)) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) - (goto-char (point-min)) - (let* ((marker (point-marker)) - (times (chime--extract-time marker))) - ;; Should extract SCHEDULED timestamp - (should (= 1 (length times))) - (should (string-match-p "14:00" (car (car times)))))) - (kill-buffer test-buffer)) - (test-chime-extract-time-teardown))) - -(ert-deftest test-chime-extract-time-regular-event-deadline () + (test-file (chime-create-temp-test-file-with-content test-content)) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) + (goto-char (point-min)) + (let* ((marker (point-marker)) + (times (chime--extract-time marker))) + ;; Should extract SCHEDULED timestamp + (should (= 1 (length times))) + (should (string-match-p "14:00" (car (car times)))))) + (kill-buffer test-buffer))) + +(chime-deftest test-chime-extract-time-regular-event-deadline () "Test that regular events extract DEADLINE timestamp. REFACTORED: Uses dynamic timestamps" - (test-chime-extract-time-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 17 0)) - (timestamp (test-timestamp-string time)) - (test-content (format "* Task + (let* ((time (test-time-tomorrow-at 17 0)) + (timestamp (test-timestamp-string time)) + (test-content (format "* Task DEADLINE: %s " timestamp)) - (test-file (chime-create-temp-test-file-with-content test-content)) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) - (goto-char (point-min)) - (let* ((marker (point-marker)) - (times (chime--extract-time marker))) - ;; Should extract DEADLINE timestamp - (should (= 1 (length times))) - (should (string-match-p "17:00" (car (car times)))))) - (kill-buffer test-buffer)) - (test-chime-extract-time-teardown))) - -(ert-deftest test-chime-extract-time-regular-event-plain-timestamps () + (test-file (chime-create-temp-test-file-with-content test-content)) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) + (goto-char (point-min)) + (let* ((marker (point-marker)) + (times (chime--extract-time marker))) + ;; Should extract DEADLINE timestamp + (should (= 1 (length times))) + (should (string-match-p "17:00" (car (car times)))))) + (kill-buffer test-buffer))) + +(chime-deftest test-chime-extract-time-regular-event-plain-timestamps () "Test that regular events extract plain timestamps when no SCHEDULED/DEADLINE. REFACTORED: Uses dynamic timestamps" - (test-chime-extract-time-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp (test-timestamp-string time)) - (test-content (format "* Meeting notes + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp (test-timestamp-string time)) + (test-content (format "* Meeting notes Discussed: %s " timestamp)) - (test-file (chime-create-temp-test-file-with-content test-content)) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) - (goto-char (point-min)) - (let* ((marker (point-marker)) - (times (chime--extract-time marker))) - ;; Should extract plain timestamp - (should (= 1 (length times))) - (should (string-match-p "14:00" (car (car times)))))) - (kill-buffer test-buffer)) - (test-chime-extract-time-teardown))) - -(ert-deftest test-chime-extract-time-regular-event-scheduled-and-plain () + (test-file (chime-create-temp-test-file-with-content test-content)) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) + (goto-char (point-min)) + (let* ((marker (point-marker)) + (times (chime--extract-time marker))) + ;; Should extract plain timestamp + (should (= 1 (length times))) + (should (string-match-p "14:00" (car (car times)))))) + (kill-buffer test-buffer))) + +(chime-deftest test-chime-extract-time-regular-event-scheduled-and-plain () "Test that regular events extract both SCHEDULED and plain timestamps. SCHEDULED/DEADLINE appear first, then plain timestamps. REFACTORED: Uses dynamic timestamps" - (test-chime-extract-time-setup) - (unwind-protect - (let* ((scheduled-time (test-time-tomorrow-at 14 0)) - (plain-time (test-time-days-from-now 2 15 0)) - (scheduled-timestamp (test-timestamp-string scheduled-time)) - (plain-timestamp (format-time-string "<%Y-%m-%d %a %H:%M>" plain-time)) - (test-content (format "* Task + (let* ((scheduled-time (test-time-tomorrow-at 14 0)) + (plain-time (test-time-days-from-now 2 15 0)) + (scheduled-timestamp (test-timestamp-string scheduled-time)) + (plain-timestamp (format-time-string "<%Y-%m-%d %a %H:%M>" plain-time)) + (test-content (format "* Task SCHEDULED: %s Note: also happens %s " scheduled-timestamp plain-timestamp)) - (test-file (chime-create-temp-test-file-with-content test-content)) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) - (goto-char (point-min)) - (let* ((marker (point-marker)) - (times (chime--extract-time marker))) - ;; Should extract both: SCHEDULED first, then plain - (should (= 2 (length times))) - ;; First should be SCHEDULED - (should (string-match-p "14:00" (car (car times)))) - ;; Second should be plain at 15:00 - (should (string-match-p "15:00" (car (cadr times)))))) - (kill-buffer test-buffer)) - (test-chime-extract-time-teardown))) - -(ert-deftest test-chime-extract-time-regular-event-multiple-plain () + (test-file (chime-create-temp-test-file-with-content test-content)) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) + (goto-char (point-min)) + (let* ((marker (point-marker)) + (times (chime--extract-time marker))) + ;; Should extract both: SCHEDULED first, then plain + (should (= 2 (length times))) + ;; First should be SCHEDULED + (should (string-match-p "14:00" (car (car times)))) + ;; Second should be plain at 15:00 + (should (string-match-p "15:00" (car (cadr times)))))) + (kill-buffer test-buffer))) + +(chime-deftest test-chime-extract-time-regular-event-multiple-plain () "Test that regular events extract all plain timestamps. REFACTORED: Uses dynamic timestamps" - (test-chime-extract-time-setup) - (unwind-protect - (let* ((time1 (test-time-tomorrow-at 14 0)) - (time2 (test-time-days-from-now 2 15 0)) - (timestamp1 (test-timestamp-string time1)) - (timestamp2 (test-timestamp-string time2)) - (test-content (format "* Meeting notes + (let* ((time1 (test-time-tomorrow-at 14 0)) + (time2 (test-time-days-from-now 2 15 0)) + (timestamp1 (test-timestamp-string time1)) + (timestamp2 (test-timestamp-string time2)) + (test-content (format "* Meeting notes First discussion: %s Second discussion: %s " timestamp1 timestamp2)) - (test-file (chime-create-temp-test-file-with-content test-content)) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) - (goto-char (point-min)) - (let* ((marker (point-marker)) - (times (chime--extract-time marker))) - ;; Should extract both plain timestamps - (should (= 2 (length times))) - (should (string-match-p "14:00" (car (car times)))) - (should (string-match-p "15:00" (car (cadr times)))))) - (kill-buffer test-buffer)) - (test-chime-extract-time-teardown))) + (test-file (chime-create-temp-test-file-with-content test-content)) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) + (goto-char (point-min)) + (let* ((marker (point-marker)) + (times (chime--extract-time marker))) + ;; Should extract both plain timestamps + (should (= 2 (length times))) + (should (string-match-p "14:00" (car (car times)))) + (should (string-match-p "15:00" (car (cadr times)))))) + (kill-buffer test-buffer))) (provide 'test-chime-extract-time) ;;; test-chime-extract-time.el ends here diff --git a/tests/test-chime-extract-title.el b/tests/test-chime-extract-title.el index d778c7e..f7efd33 100644 --- a/tests/test-chime-extract-title.el +++ b/tests/test-chime-extract-title.el @@ -27,9 +27,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) - ;;; Normal Cases (ert-deftest test-chime-extract-title-plain-heading () diff --git a/tests/test-chime-filter-day-wide-events.el b/tests/test-chime-filter-day-wide-events.el index 6e24f00..415598d 100644 --- a/tests/test-chime-filter-day-wide-events.el +++ b/tests/test-chime-filter-day-wide-events.el @@ -28,10 +28,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Normal Cases (ert-deftest test-chime-filter-day-wide-events-keeps-timed-event () diff --git a/tests/test-chime-format-event-for-tooltip.el b/tests/test-chime-format-event-for-tooltip.el index 5e2cabb..0569477 100644 --- a/tests/test-chime-format-event-for-tooltip.el +++ b/tests/test-chime-format-event-for-tooltip.el @@ -26,222 +26,175 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - -;;; Setup and Teardown - -(defun test-chime-format-event-for-tooltip-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-format-event-for-tooltip-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Normal Cases -(ert-deftest test-chime-format-event-for-tooltip-normal-minutes () +(chime-deftest test-chime-format-event-for-tooltip-normal-minutes () "Test formatting event with minutes until event. REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 10)) - (timestamp (test-timestamp-string time)) - (result (chime--format-event-for-tooltip - timestamp - 10 - "Team Meeting"))) - (should (stringp result)) - (should (string-match-p "Team Meeting" result)) - (should (string-match-p "02:10 PM" result)) - (should (string-match-p "10 minutes" result))) - (test-chime-format-event-for-tooltip-teardown))) - -(ert-deftest test-chime-format-event-for-tooltip-normal-hours () + (let* ((time (test-time-tomorrow-at 14 10)) + (timestamp (test-timestamp-string time)) + (result (chime--format-event-for-tooltip + timestamp + 10 + "Team Meeting"))) + (should (stringp result)) + (should (string-match-p "Team Meeting" result)) + (should (string-match-p "02:10 PM" result)) + (should (string-match-p "10 minutes" result)))) + +(chime-deftest test-chime-format-event-for-tooltip-normal-hours () "Test formatting event with hours until event. REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 15 30)) - (timestamp (test-timestamp-string time)) - (result (chime--format-event-for-tooltip - timestamp - 90 - "Afternoon Meeting"))) - (should (stringp result)) - (should (string-match-p "Afternoon Meeting" result)) - (should (string-match-p "03:30 PM" result)) - (should (string-match-p "1 hour" result))) - (test-chime-format-event-for-tooltip-teardown))) - -(ert-deftest test-chime-format-event-for-tooltip-normal-multiple-hours () + (let* ((time (test-time-tomorrow-at 15 30)) + (timestamp (test-timestamp-string time)) + (result (chime--format-event-for-tooltip + timestamp + 90 + "Afternoon Meeting"))) + (should (stringp result)) + (should (string-match-p "Afternoon Meeting" result)) + (should (string-match-p "03:30 PM" result)) + (should (string-match-p "1 hour" result)))) + +(chime-deftest test-chime-format-event-for-tooltip-normal-multiple-hours () "Test formatting event with multiple hours until event. REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 17 0)) - (timestamp (test-timestamp-string time)) - (result (chime--format-event-for-tooltip - timestamp - 300 - "End of Day Review"))) - (should (stringp result)) - (should (string-match-p "End of Day Review" result)) - (should (string-match-p "05:00 PM" result)) - (should (string-match-p "5 hours" result))) - (test-chime-format-event-for-tooltip-teardown))) + (let* ((time (test-time-tomorrow-at 17 0)) + (timestamp (test-timestamp-string time)) + (result (chime--format-event-for-tooltip + timestamp + 300 + "End of Day Review"))) + (should (stringp result)) + (should (string-match-p "End of Day Review" result)) + (should (string-match-p "05:00 PM" result)) + (should (string-match-p "5 hours" result)))) ;;; Boundary Cases -(ert-deftest test-chime-format-event-for-tooltip-boundary-exactly-one-day () +(chime-deftest test-chime-format-event-for-tooltip-boundary-exactly-one-day () "Test formatting event exactly 1 day away (1440 minutes). REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-days-from-now 1 9 0)) - (timestamp (test-timestamp-string time)) - (result (chime--format-event-for-tooltip - timestamp - 1440 - "Tomorrow Event"))) - (should (stringp result)) - (should (string-match-p "Tomorrow Event" result)) - (should (string-match-p "09:00 AM" result)) - (should (string-match-p "in 1 day" result))) - (test-chime-format-event-for-tooltip-teardown))) - -(ert-deftest test-chime-format-event-for-tooltip-boundary-multiple-days () + (let* ((time (test-time-days-from-now 1 9 0)) + (timestamp (test-timestamp-string time)) + (result (chime--format-event-for-tooltip + timestamp + 1440 + "Tomorrow Event"))) + (should (stringp result)) + (should (string-match-p "Tomorrow Event" result)) + (should (string-match-p "09:00 AM" result)) + (should (string-match-p "in 1 day" result)))) + +(chime-deftest test-chime-format-event-for-tooltip-boundary-multiple-days () "Test formatting event multiple days away. REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-days-from-now 3 10 0)) - (timestamp (test-timestamp-string time)) - (result (chime--format-event-for-tooltip - timestamp - 4320 ; 3 days - "Future Meeting"))) - (should (stringp result)) - (should (string-match-p "Future Meeting" result)) - (should (string-match-p "10:00 AM" result)) - (should (string-match-p "in 3 days" result))) - (test-chime-format-event-for-tooltip-teardown))) - -(ert-deftest test-chime-format-event-for-tooltip-boundary-just-under-one-day () + (let* ((time (test-time-days-from-now 3 10 0)) + (timestamp (test-timestamp-string time)) + (result (chime--format-event-for-tooltip + timestamp + 4320 ; 3 days + "Future Meeting"))) + (should (stringp result)) + (should (string-match-p "Future Meeting" result)) + (should (string-match-p "10:00 AM" result)) + (should (string-match-p "in 3 days" result)))) + +(chime-deftest test-chime-format-event-for-tooltip-boundary-just-under-one-day () "Test formatting event just under 1 day away (1439 minutes). REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 8 59)) - (timestamp (test-timestamp-string time)) - (result (chime--format-event-for-tooltip - timestamp - 1439 - "Almost Tomorrow"))) - (should (stringp result)) - (should (string-match-p "Almost Tomorrow" result)) - (should (string-match-p "08:59 AM" result)) - ;; Should show hours/minutes, not days - (should (string-match-p "23 hours" result))) - (test-chime-format-event-for-tooltip-teardown))) - -(ert-deftest test-chime-format-event-for-tooltip-boundary-zero-minutes () + (let* ((time (test-time-tomorrow-at 8 59)) + (timestamp (test-timestamp-string time)) + (result (chime--format-event-for-tooltip + timestamp + 1439 + "Almost Tomorrow"))) + (should (stringp result)) + (should (string-match-p "Almost Tomorrow" result)) + (should (string-match-p "08:59 AM" result)) + ;; Should show hours/minutes, not days + (should (string-match-p "23 hours" result)))) + +(chime-deftest test-chime-format-event-for-tooltip-boundary-zero-minutes () "Test formatting event happening right now (0 minutes). REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-today-at 14 0)) - (timestamp (test-timestamp-string time)) - (result (chime--format-event-for-tooltip - timestamp - 0 - "Current Event"))) - (should (stringp result)) - (should (string-match-p "Current Event" result)) - (should (string-match-p "02:00 PM" result)) - (should (string-match-p "right now" result))) - (test-chime-format-event-for-tooltip-teardown))) - -(ert-deftest test-chime-format-event-for-tooltip-boundary-one-minute () + (let* ((time (test-time-today-at 14 0)) + (timestamp (test-timestamp-string time)) + (result (chime--format-event-for-tooltip + timestamp + 0 + "Current Event"))) + (should (stringp result)) + (should (string-match-p "Current Event" result)) + (should (string-match-p "02:00 PM" result)) + (should (string-match-p "right now" result)))) + +(chime-deftest test-chime-format-event-for-tooltip-boundary-one-minute () "Test formatting event 1 minute away. REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-today-at 14 1)) - (timestamp (test-timestamp-string time)) - (result (chime--format-event-for-tooltip - timestamp - 1 - "Imminent Event"))) - (should (stringp result)) - (should (string-match-p "Imminent Event" result)) - (should (string-match-p "02:01 PM" result)) - (should (string-match-p "1 minute" result))) - (test-chime-format-event-for-tooltip-teardown))) - -(ert-deftest test-chime-format-event-for-tooltip-boundary-long-title () + (let* ((time (test-time-today-at 14 1)) + (timestamp (test-timestamp-string time)) + (result (chime--format-event-for-tooltip + timestamp + 1 + "Imminent Event"))) + (should (stringp result)) + (should (string-match-p "Imminent Event" result)) + (should (string-match-p "02:01 PM" result)) + (should (string-match-p "1 minute" result)))) + +(chime-deftest test-chime-format-event-for-tooltip-boundary-long-title () "Test formatting event with very long title. REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-today-at 14 10)) - (timestamp (test-timestamp-string time)) - (long-title (make-string 200 ?x)) - (result (chime--format-event-for-tooltip - timestamp - 10 - long-title))) - (should (stringp result)) - (should (string-match-p long-title result))) - (test-chime-format-event-for-tooltip-teardown))) + (let* ((time (test-time-today-at 14 10)) + (timestamp (test-timestamp-string time)) + (long-title (make-string 200 ?x)) + (result (chime--format-event-for-tooltip + timestamp + 10 + long-title))) + (should (stringp result)) + (should (string-match-p long-title result)))) ;;; Error Cases -(ert-deftest test-chime-format-event-for-tooltip-error-nil-title () +(chime-deftest test-chime-format-event-for-tooltip-error-nil-title () "Test formatting with nil title doesn't crash. REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-today-at 14 10)) - (timestamp (test-timestamp-string time))) - ;; Should not crash with nil title - (should-not (condition-case nil - (progn - (chime--format-event-for-tooltip - timestamp - 10 - nil) - nil) - (error t)))) - (test-chime-format-event-for-tooltip-teardown))) - -(ert-deftest test-chime-format-event-for-tooltip-error-empty-title () + (let* ((time (test-time-today-at 14 10)) + (timestamp (test-timestamp-string time))) + ;; Should not crash with nil title + (should-not (condition-case nil + (progn + (chime--format-event-for-tooltip + timestamp + 10 + nil) + nil) + (error t))))) + +(chime-deftest test-chime-format-event-for-tooltip-error-empty-title () "Test formatting with empty title. REFACTORED: Uses dynamic timestamps" - (test-chime-format-event-for-tooltip-setup) - (unwind-protect - (let* ((time (test-time-today-at 14 10)) - (timestamp (test-timestamp-string time)) - (result (chime--format-event-for-tooltip - timestamp - 10 - ""))) - (should (stringp result)) - (should (string-match-p "02:10 PM" result))) - (test-chime-format-event-for-tooltip-teardown))) + (let* ((time (test-time-today-at 14 10)) + (timestamp (test-timestamp-string time)) + (result (chime--format-event-for-tooltip + timestamp + 10 + ""))) + (should (stringp result)) + (should (string-match-p "02:10 PM" result)))) (provide 'test-chime-format-event-for-tooltip) ;;; test-chime-format-event-for-tooltip.el ends here diff --git a/tests/test-chime-format-refresh.el b/tests/test-chime-format-refresh.el index 0e2b58b..8393119 100644 --- a/tests/test-chime-format-refresh.el +++ b/tests/test-chime-format-refresh.el @@ -26,10 +26,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Setup and Teardown (defun test-chime-format-refresh-setup () diff --git a/tests/test-chime-gather-info.el b/tests/test-chime-gather-info.el index 132f0ad..d21c06d 100644 --- a/tests/test-chime-gather-info.el +++ b/tests/test-chime-gather-info.el @@ -29,8 +29,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) ;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;; Setup and Teardown diff --git a/tests/test-chime-get-tags.el b/tests/test-chime-get-tags.el index aecd2ea..1fca101 100644 --- a/tests/test-chime-get-tags.el +++ b/tests/test-chime-get-tags.el @@ -27,9 +27,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) - ;;;; Tests for chime--get-tags ;;; Normal Cases diff --git a/tests/test-chime-group-events-by-day.el b/tests/test-chime-group-events-by-day.el index b8546ee..277e6e1 100644 --- a/tests/test-chime-group-events-by-day.el +++ b/tests/test-chime-group-events-by-day.el @@ -38,10 +38,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Setup and Teardown (defun test-chime-group-events-by-day-setup () diff --git a/tests/test-chime-has-timestamp.el b/tests/test-chime-has-timestamp.el index 1c79fb7..3e946ab 100644 --- a/tests/test-chime-has-timestamp.el +++ b/tests/test-chime-has-timestamp.el @@ -26,239 +26,171 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - -;;; Setup and Teardown - -(defun test-chime-has-timestamp-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-has-timestamp-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Normal Cases -(ert-deftest test-chime-has-timestamp-standard-timestamp-with-time-returns-non-nil () +(chime-deftest test-chime-has-timestamp-standard-timestamp-with-time-returns-non-nil () "Test that standard timestamp with time returns non-nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 30)) - (timestamp (test-timestamp-string time)) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-timestamp-without-brackets-returns-non-nil () + (let* ((time (test-time-tomorrow-at 14 30)) + (timestamp (test-timestamp-string time)) + (result (chime--has-timestamp timestamp))) + (should result))) + +(chime-deftest test-chime-has-timestamp-timestamp-without-brackets-returns-non-nil () "Test that timestamp without brackets but with time returns non-nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 30)) - (timestamp (format-time-string "%Y-%m-%d %a %H:%M" time)) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-timestamp-with-time-range-returns-non-nil () + (let* ((time (test-time-tomorrow-at 14 30)) + (timestamp (format-time-string "%Y-%m-%d %a %H:%M" time)) + (result (chime--has-timestamp timestamp))) + (should result))) + +(chime-deftest test-chime-has-timestamp-timestamp-with-time-range-returns-non-nil () "Test that timestamp with time range returns non-nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp (format-time-string "<%Y-%m-%d %a %H:%M-15:30>" time)) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-scheduled-with-time-returns-non-nil () + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp (format-time-string "<%Y-%m-%d %a %H:%M-15:30>" time)) + (result (chime--has-timestamp timestamp))) + (should result))) + +(chime-deftest test-chime-has-timestamp-scheduled-with-time-returns-non-nil () "Test that SCHEDULED timestamp with time returns non-nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 9 0)) - (timestamp (concat "SCHEDULED: " (test-timestamp-string time))) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-deadline-with-time-returns-non-nil () + (let* ((time (test-time-tomorrow-at 9 0)) + (timestamp (concat "SCHEDULED: " (test-timestamp-string time))) + (result (chime--has-timestamp timestamp))) + (should result))) + +(chime-deftest test-chime-has-timestamp-deadline-with-time-returns-non-nil () "Test that DEADLINE timestamp with time returns non-nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 17 0)) - (timestamp (concat "DEADLINE: " (test-timestamp-string time))) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-repeater-with-time-returns-non-nil () + (let* ((time (test-time-tomorrow-at 17 0)) + (timestamp (concat "DEADLINE: " (test-timestamp-string time))) + (result (chime--has-timestamp timestamp))) + (should result))) + +(chime-deftest test-chime-has-timestamp-repeater-with-time-returns-non-nil () "Test that timestamp with repeater and time returns non-nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp (format-time-string "<%Y-%m-%d %a %H:%M +1w>" time)) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-midnight-timestamp-returns-non-nil () + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp (format-time-string "<%Y-%m-%d %a %H:%M +1w>" time)) + (result (chime--has-timestamp timestamp))) + (should result))) + +(chime-deftest test-chime-has-timestamp-midnight-timestamp-returns-non-nil () "Test that midnight timestamp (00:00) returns non-nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 0 0)) - (timestamp (test-timestamp-string time)) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) + (let* ((time (test-time-tomorrow-at 0 0)) + (timestamp (test-timestamp-string time)) + (result (chime--has-timestamp timestamp))) + (should result))) ;;; Boundary Cases -(ert-deftest test-chime-has-timestamp-day-wide-timestamp-returns-nil () +(chime-deftest test-chime-has-timestamp-day-wide-timestamp-returns-nil () "Test that day-wide timestamp without time returns nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 0 0)) - (timestamp (test-timestamp-string time t)) - (result (chime--has-timestamp timestamp))) - (should-not result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-date-only-returns-nil () + (let* ((time (test-time-tomorrow-at 0 0)) + (timestamp (test-timestamp-string time t)) + (result (chime--has-timestamp timestamp))) + (should-not result))) + +(chime-deftest test-chime-has-timestamp-date-only-returns-nil () "Test that date-only timestamp without day name returns nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 0 0)) - (timestamp (format-time-string "<%Y-%m-%d>" time)) - (result (chime--has-timestamp timestamp))) - (should-not result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-single-digit-hour-returns-non-nil () + (let* ((time (test-time-tomorrow-at 0 0)) + (timestamp (format-time-string "<%Y-%m-%d>" time)) + (result (chime--has-timestamp timestamp))) + (should-not result))) + +(chime-deftest test-chime-has-timestamp-single-digit-hour-returns-non-nil () "Test that timestamp with single-digit hour returns non-nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 9 0)) - (timestamp (format-time-string "<%Y-%m-%d %a %-H:%M>" time)) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-embedded-in-text-returns-non-nil () + (let* ((time (test-time-tomorrow-at 9 0)) + (timestamp (format-time-string "<%Y-%m-%d %a %-H:%M>" time)) + (result (chime--has-timestamp timestamp))) + (should result))) + +(chime-deftest test-chime-has-timestamp-embedded-in-text-returns-non-nil () "Test that timestamp embedded in text returns non-nil. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp (concat "Meeting scheduled for " (test-timestamp-string time) " in conference room")) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-multiple-timestamps-returns-non-nil () + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp (concat "Meeting scheduled for " (test-timestamp-string time) " in conference room")) + (result (chime--has-timestamp timestamp))) + (should result))) + +(chime-deftest test-chime-has-timestamp-multiple-timestamps-returns-non-nil () "Test that string with multiple timestamps returns non-nil for first match. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time1 (test-time-tomorrow-at 14 0)) - (time2 (test-time-days-from-now 2)) - (timestamp (concat (test-timestamp-string time1) " and " - (format-time-string "<%Y-%m-%d %a %H:%M>" time2))) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) + (let* ((time1 (test-time-tomorrow-at 14 0)) + (time2 (test-time-days-from-now 2)) + (timestamp (concat (test-timestamp-string time1) " and " + (format-time-string "<%Y-%m-%d %a %H:%M>" time2))) + (result (chime--has-timestamp timestamp))) + (should result))) ;;; Error Cases -(ert-deftest test-chime-has-timestamp-empty-string-returns-nil () +(chime-deftest test-chime-has-timestamp-empty-string-returns-nil () "Test that empty string returns nil." - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((timestamp "") - (result (chime--has-timestamp timestamp))) - (should-not result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-nil-input-returns-nil () + (let* ((timestamp "") + (result (chime--has-timestamp timestamp))) + (should-not result))) + +(chime-deftest test-chime-has-timestamp-nil-input-returns-nil () "Test that nil input returns nil." - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((timestamp nil) - (result (chime--has-timestamp timestamp))) - (should-not result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-no-timestamp-returns-nil () + (let* ((timestamp nil) + (result (chime--has-timestamp timestamp))) + (should-not result))) + +(chime-deftest test-chime-has-timestamp-no-timestamp-returns-nil () "Test that string without timestamp returns nil." - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((timestamp "Just a regular string with no timestamp") - (result (chime--has-timestamp timestamp))) - (should-not result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-invalid-format-returns-nil () + (let* ((timestamp "Just a regular string with no timestamp") + (result (chime--has-timestamp timestamp))) + (should-not result))) + +(chime-deftest test-chime-has-timestamp-invalid-format-returns-nil () "Test that invalid timestamp format returns nil. REFACTORED: Uses dynamic timestamps (keeps invalid format for testing)" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - ;; Intentionally wrong format (MM-DD-YYYY instead of YYYY-MM-DD) for testing - (timestamp (format-time-string "<%m-%d-%Y %a %H:%M>" time)) - (result (chime--has-timestamp timestamp))) - (should-not result)) - (test-chime-has-timestamp-teardown))) - -(ert-deftest test-chime-has-timestamp-partial-timestamp-returns-nil () + (let* ((time (test-time-tomorrow-at 14 0)) + ;; Intentionally wrong format (MM-DD-YYYY instead of YYYY-MM-DD) for testing + (timestamp (format-time-string "<%m-%d-%Y %a %H:%M>" time)) + (result (chime--has-timestamp timestamp))) + (should-not result))) + +(chime-deftest test-chime-has-timestamp-partial-timestamp-returns-nil () "Test that partial timestamp returns nil. REFACTORED: Uses dynamic timestamps (keeps partial format for testing)" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 0 0)) - ;; Intentionally incomplete timestamp for testing - (timestamp (format-time-string "<%Y-%m-%d" time)) - (result (chime--has-timestamp timestamp))) - (should-not result)) - (test-chime-has-timestamp-teardown))) + (let* ((time (test-time-tomorrow-at 0 0)) + ;; Intentionally incomplete timestamp for testing + (timestamp (format-time-string "<%Y-%m-%d" time)) + (result (chime--has-timestamp timestamp))) + (should-not result))) ;;; org-gcal Integration Tests -(ert-deftest test-chime-has-timestamp-org-gcal-time-range-returns-non-nil () +(chime-deftest test-chime-has-timestamp-org-gcal-time-range-returns-non-nil () "Test that org-gcal time range format is detected. org-gcal uses format like <2025-10-24 Fri 17:30-18:00> which should be detected. REFACTORED: Uses dynamic timestamps" - (test-chime-has-timestamp-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 17 30)) - (timestamp (format-time-string "<%Y-%m-%d %a %H:%M-18:00>" time)) - (result (chime--has-timestamp timestamp))) - (should result)) - (test-chime-has-timestamp-teardown))) + (let* ((time (test-time-tomorrow-at 17 30)) + (timestamp (format-time-string "<%Y-%m-%d %a %H:%M-18:00>" time)) + (result (chime--has-timestamp timestamp))) + (should result))) (provide 'test-chime-has-timestamp) ;;; test-chime-has-timestamp.el ends here diff --git a/tests/test-chime-intervals-for-marker.el b/tests/test-chime-intervals-for-marker.el index 9b73394..295dcd5 100644 --- a/tests/test-chime-intervals-for-marker.el +++ b/tests/test-chime-intervals-for-marker.el @@ -32,7 +32,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'cl-lib) ;;; Helpers diff --git a/tests/test-chime-jump-to-event.el b/tests/test-chime-jump-to-event.el index 6db885b..29cb279 100644 --- a/tests/test-chime-jump-to-event.el +++ b/tests/test-chime-jump-to-event.el @@ -19,7 +19,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) (defun test-chime-jump-to-event--make-temp-org-file (content) "Write CONTENT to a temp .org file under the test base dir and return its path." diff --git a/tests/test-chime-lifecycle.el b/tests/test-chime-lifecycle.el index 8ce9c13..826de4c 100644 --- a/tests/test-chime-lifecycle.el +++ b/tests/test-chime-lifecycle.el @@ -21,32 +21,39 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) (require 'cl-lib) -(ert-deftest test-chime-stop-interrupts-running-process () - "Normal: when chime--process is set, `chime--stop' interrupts it and clears the var." - (let ((interrupted nil) +(ert-deftest test-chime-stop-kills-running-process () + "Normal: when chime--process is set, `chime--stop' kills it and clears the var. +The child is killed rather than interrupted, because SIGINT is a request a +child stuck in a blocking read can ignore." + (let ((killed nil) (chime--timer nil) (chime--process 'fake-process) + (chime--process-generation 0) (chime--validation-done t) (chime--validation-retry-count 5)) - (cl-letf (((symbol-function 'interrupt-process) - (lambda (proc) (setq interrupted proc)))) + (cl-letf (((symbol-function 'chime--kill-async-process) + (lambda (proc) (setq killed proc)))) (chime--stop)) - (should (eq 'fake-process interrupted)) + (should (eq 'fake-process killed)) (should (null chime--process)) + ;; The abandoned child's callback is orphaned. + (should (= 1 chime--process-generation)) (should-not chime--validation-done) (should (= 0 chime--validation-retry-count)))) -(ert-deftest test-chime-stop-no-process-skips-interrupt () - "Boundary: with chime--process nil, `interrupt-process' is never called." - (let ((interrupted nil) +(ert-deftest test-chime-stop-no-process-kills-nothing () + "Boundary: with chime--process nil, no process is killed." + (let ((killed 'untouched) (chime--timer nil) (chime--process nil) + (chime--process-generation 0) (chime--validation-done t) (chime--validation-retry-count 5)) - (cl-letf (((symbol-function 'interrupt-process) - (lambda (proc) (setq interrupted proc)))) + (cl-letf (((symbol-function 'chime--kill-async-process) + (lambda (proc) (setq killed proc)))) (chime--stop)) - (should (null interrupted)) + ;; chime--stop calls the helper unconditionally; it must tolerate nil. + (should (null killed)) (should-not chime--validation-done) (should (= 0 chime--validation-retry-count)))) diff --git a/tests/test-chime-make-tooltip.el b/tests/test-chime-make-tooltip.el index 7ace684..6b704b1 100644 --- a/tests/test-chime-make-tooltip.el +++ b/tests/test-chime-make-tooltip.el @@ -28,8 +28,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) ;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;; Setup and Teardown diff --git a/tests/test-chime-modeline-faces.el b/tests/test-chime-modeline-faces.el index dc8eae8..27c180a 100644 --- a/tests/test-chime-modeline-faces.el +++ b/tests/test-chime-modeline-faces.el @@ -29,8 +29,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;; Setup/Teardown diff --git a/tests/test-chime-modeline-no-events-text.el b/tests/test-chime-modeline-no-events-text.el index f64c71f..eaf0633 100644 --- a/tests/test-chime-modeline-no-events-text.el +++ b/tests/test-chime-modeline-no-events-text.el @@ -31,10 +31,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; 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-chime-modeline-no-events-text--orig-lookahead nil) diff --git a/tests/test-chime-modeline.el b/tests/test-chime-modeline.el index 63ba26c..27a1936 100644 --- a/tests/test-chime-modeline.el +++ b/tests/test-chime-modeline.el @@ -30,8 +30,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) ;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;; Setup and Teardown diff --git a/tests/test-chime-notification-boundaries.el b/tests/test-chime-notification-boundaries.el index 853dd4d..fa288f0 100644 --- a/tests/test-chime-notification-boundaries.el +++ b/tests/test-chime-notification-boundaries.el @@ -27,322 +27,269 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - -;;; Setup and Teardown - -(defun test-chime-notification-boundaries-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-notification-boundaries-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Exact Matching Tests -(ert-deftest test-chime-notification-boundary-exact-10-minutes () +(chime-deftest test-chime-notification-boundary-exact-10-minutes () "Test that notification fires exactly at 10-minute interval. Event at 14:10, interval 10 minutes, current time 14:00. Should match: current_time + 10 minutes = 14:10 = event_time" - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ((10 . medium))))) - (result (chime--notifications event))) - ;; Should match: event is exactly 10 minutes away - (should (= 1 (length result)))))) - (test-chime-notification-boundaries-teardown))) - -(ert-deftest test-chime-notification-boundary-9-minutes-no-match () + (let* ((now (test-time-today-at 14 0)) + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ((10 . medium))))) + (result (chime--notifications event))) + ;; Should match: event is exactly 10 minutes away + (should (= 1 (length result))))))) + +(chime-deftest test-chime-notification-boundary-9-minutes-no-match () "Test that notification does NOT fire at 9 minutes (1 minute before interval). Event at 14:10, interval 10 minutes, current time 14:01. Should NOT match: current_time + 10 minutes = 14:11 ≠ 14:10" - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 1)) ; 1 minute past the hour - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ((10 . medium))))) - (result (chime--notifications event))) - ;; Should NOT match: event is 9 minutes away, not 10 - (should (= 0 (length result)))))) - (test-chime-notification-boundaries-teardown))) - -(ert-deftest test-chime-notification-boundary-11-minutes-no-match () + (let* ((now (test-time-today-at 14 1)) ; 1 minute past the hour + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ((10 . medium))))) + (result (chime--notifications event))) + ;; Should NOT match: event is 9 minutes away, not 10 + (should (= 0 (length result))))))) + +(chime-deftest test-chime-notification-boundary-11-minutes-no-match () "Test that notification does NOT fire at 11 minutes (1 minute after interval). Event at 14:11, interval 10 minutes, current time 14:00. Should NOT match: current_time + 10 minutes = 14:10 ≠ 14:11" - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (event-time (test-time-today-at 14 11)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ((10 . medium))))) - (result (chime--notifications event))) - ;; Should NOT match: event is 11 minutes away, not 10 - (should (= 0 (length result)))))) - (test-chime-notification-boundaries-teardown))) + (let* ((now (test-time-today-at 14 0)) + (event-time (test-time-today-at 14 11)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ((10 . medium))))) + (result (chime--notifications event))) + ;; Should NOT match: event is 11 minutes away, not 10 + (should (= 0 (length result))))))) ;;; Multi-Day Interval Tests -(ert-deftest test-chime-notification-boundary-4-days-exact () +(chime-deftest test-chime-notification-boundary-4-days-exact () "Test notification for event exactly 4 days away. This tests the user's reported bug: event on Saturday (4 days from Tuesday). With default 10-minute interval, should NOT match. Event at Saturday 10:00am, current time Tuesday 10:00am, interval 10 minutes. Should NOT match: Tuesday 10:00 + 10 min = Tuesday 10:10 ≠ Saturday 10:00" - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-now)) - ;; Event 4 days from now at same time - (event-time (time-add now (seconds-to-time (* 4 24 3600)))) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Saturday Event") - (intervals . ((10 . medium))))) ; Default 10 minutes - (result (chime--notifications event))) - ;; Should NOT match: event is 4 days away, interval is 10 minutes - (should (= 0 (length result)))))) - (test-chime-notification-boundaries-teardown))) - -(ert-deftest test-chime-notification-boundary-4-days-with-4-day-interval () + (let* ((now (test-time-now)) + ;; Event 4 days from now at same time + (event-time (time-add now (seconds-to-time (* 4 24 3600)))) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Saturday Event") + (intervals . ((10 . medium))))) ; Default 10 minutes + (result (chime--notifications event))) + ;; Should NOT match: event is 4 days away, interval is 10 minutes + (should (= 0 (length result))))))) + +(chime-deftest test-chime-notification-boundary-4-days-with-4-day-interval () "Test notification for event 4 days away with 4-day interval. Event at Saturday 10:00am, current time Tuesday 10:00am, interval 5760 minutes (4 days). Should match: Tuesday 10:00 + 4 days = Saturday 10:00 = event_time" - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-now)) - ;; Event exactly 4 days from now - (event-time (time-add now (seconds-to-time (* 4 24 3600)))) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Saturday Event") - (intervals . ((5760 . medium))))) ; 4 days = 5760 minutes - (result (chime--notifications event))) - ;; Should match: event is exactly 4 days away, interval is 4 days - (should (= 1 (length result)))))) - (test-chime-notification-boundaries-teardown))) - -(ert-deftest test-chime-notification-boundary-1-week-interval () + (let* ((now (test-time-now)) + ;; Event exactly 4 days from now + (event-time (time-add now (seconds-to-time (* 4 24 3600)))) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Saturday Event") + (intervals . ((5760 . medium))))) ; 4 days = 5760 minutes + (result (chime--notifications event))) + ;; Should match: event is exactly 4 days away, interval is 4 days + (should (= 1 (length result))))))) + +(chime-deftest test-chime-notification-boundary-1-week-interval () "Test notification for event exactly 1 week away with 1-week interval. Event 7 days from now, interval 10080 minutes (7 days). Should match: current_time + 7 days = event_time" - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-now)) - ;; Event exactly 7 days from now - (event-time (time-add now (seconds-to-time (* 7 24 3600)))) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Next Week Event") - (intervals . ((10080 . medium))))) ; 7 days = 10080 minutes - (result (chime--notifications event))) - ;; Should match: event is exactly 7 days away, interval is 7 days - (should (= 1 (length result)))))) - (test-chime-notification-boundaries-teardown))) + (let* ((now (test-time-now)) + ;; Event exactly 7 days from now + (event-time (time-add now (seconds-to-time (* 7 24 3600)))) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Next Week Event") + (intervals . ((10080 . medium))))) ; 7 days = 10080 minutes + (result (chime--notifications event))) + ;; Should match: event is exactly 7 days away, interval is 7 days + (should (= 1 (length result))))))) ;;; Cross-Month Boundary Tests (Testing for day-of-month matching bug) -(ert-deftest test-chime-notification-boundary-same-day-different-month () +(chime-deftest test-chime-notification-boundary-same-day-different-month () "Test that events on same day-of-month but different months do NOT match. This tests for the bug in chime--time= that only compares day:hour:minute. Event on Nov 18 at 10:00, current time Oct 18 at 10:00 minus 10 minutes. Should NOT match even though day-of-month (18) is the same." - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* (;; Oct 18, 2024 at 9:50am - (now (encode-time 0 50 9 18 10 2024)) - ;; Nov 18, 2024 at 10:00am (31 days + 10 minutes later) - (event-time (encode-time 0 0 10 18 11 2024)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Next Month Event") - (intervals . ((10 . medium))))) - (result (chime--notifications event))) - ;; Should NOT match: different months, even though day-of-month matches - (should (= 0 (length result)))))) - (test-chime-notification-boundaries-teardown))) - -(ert-deftest test-chime-notification-boundary-same-day-different-year () + (let* (;; Oct 18, 2024 at 9:50am + (now (encode-time 0 50 9 18 10 2024)) + ;; Nov 18, 2024 at 10:00am (31 days + 10 minutes later) + (event-time (encode-time 0 0 10 18 11 2024)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Next Month Event") + (intervals . ((10 . medium))))) + (result (chime--notifications event))) + ;; Should NOT match: different months, even though day-of-month matches + (should (= 0 (length result))))))) + +(chime-deftest test-chime-notification-boundary-same-day-different-year () "Test that events on same day/month but different years do NOT match. Event on Nov 18, 2025 at 10:00, current time Nov 18, 2024 at 9:50. Should NOT match even though month and day-of-month are the same." - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* (;; Nov 18, 2024 at 9:50am - (now (encode-time 0 50 9 18 11 2024)) - ;; Nov 18, 2025 at 10:00am (1 year + 10 minutes later) - (event-time (encode-time 0 0 10 18 11 2025)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Next Year Event") - (intervals . ((10 . medium))))) - (result (chime--notifications event))) - ;; Should NOT match: different years - (should (= 0 (length result)))))) - (test-chime-notification-boundaries-teardown))) + (let* (;; Nov 18, 2024 at 9:50am + (now (encode-time 0 50 9 18 11 2024)) + ;; Nov 18, 2025 at 10:00am (1 year + 10 minutes later) + (event-time (encode-time 0 0 10 18 11 2025)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Next Year Event") + (intervals . ((10 . medium))))) + (result (chime--notifications event))) + ;; Should NOT match: different years + (should (= 0 (length result))))))) ;;; Cross-Day Boundary Tests -(ert-deftest test-chime-notification-boundary-crosses-midnight () +(chime-deftest test-chime-notification-boundary-crosses-midnight () "Test notification that crosses midnight boundary. Event at 00:10 (next day), current time 23:50 (today), interval 20 minutes. Should match: 23:50 + 20 minutes = 00:10 next day" - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-today-at 23 50)) - ;; 20 minutes later crosses midnight - (event-time (time-add now (seconds-to-time (* 20 60)))) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Midnight Event") - (intervals . ((20 . medium))))) - (result (chime--notifications event))) - ;; Should match: event is exactly 20 minutes away - (should (= 1 (length result)))))) - (test-chime-notification-boundaries-teardown))) - -(ert-deftest test-chime-notification-boundary-just-before-midnight-no-match () + (let* ((now (test-time-today-at 23 50)) + ;; 20 minutes later crosses midnight + (event-time (time-add now (seconds-to-time (* 20 60)))) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Midnight Event") + (intervals . ((20 . medium))))) + (result (chime--notifications event))) + ;; Should match: event is exactly 20 minutes away + (should (= 1 (length result))))))) + +(chime-deftest test-chime-notification-boundary-just-before-midnight-no-match () "Test that notification doesn't fire just before crossing midnight. Event at 00:10 (next day), current time 23:51 (today), interval 20 minutes. Should NOT match: 23:51 + 20 minutes = 00:11 ≠ 00:10" - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-today-at 23 51)) - ;; 19 minutes later - (event-time (time-add now (seconds-to-time (* 19 60)))) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Midnight Event") - (intervals . ((20 . medium))))) - (result (chime--notifications event))) - ;; Should NOT match: event is 19 minutes away, not 20 - (should (= 0 (length result)))))) - (test-chime-notification-boundaries-teardown))) + (let* ((now (test-time-today-at 23 51)) + ;; 19 minutes later + (event-time (time-add now (seconds-to-time (* 19 60)))) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Midnight Event") + (intervals . ((20 . medium))))) + (result (chime--notifications event))) + ;; Should NOT match: event is 19 minutes away, not 20 + (should (= 0 (length result))))))) ;;; Multiple Interval Tests -(ert-deftest test-chime-notification-boundary-multiple-intervals-one-matches () +(chime-deftest test-chime-notification-boundary-multiple-intervals-one-matches () "Test that only matching intervals trigger notifications. Event at 14:10, current time 14:00, intervals 10 and 20 minutes. Should match only the 10-minute interval." - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ((10 . medium) (20 . high))))) - (result (chime--notifications event))) - ;; Should match only 10-minute interval - (should (= 1 (length result)))))) - (test-chime-notification-boundaries-teardown))) - -(ert-deftest test-chime-notification-boundary-multiple-intervals-all-match () + (let* ((now (test-time-today-at 14 0)) + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ((10 . medium) (20 . high))))) + (result (chime--notifications event))) + ;; Should match only 10-minute interval + (should (= 1 (length result))))))) + +(chime-deftest test-chime-notification-boundary-multiple-intervals-all-match () "Test that multiple matching intervals all trigger. Event at 14:20, current time 14:00, intervals 10 and 20 minutes. At 14:00, only 20-minute interval should match. At 14:10, only 10-minute interval should match." - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (event-time (test-time-today-at 14 20)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ((10 . medium) (20 . high))))) - (result (chime--notifications event))) - ;; At 14:00, event is 20 minutes away, so only 20-minute interval matches - (should (= 1 (length result))))) - - ;; Now test at 14:10 (10 minutes before event) - (let ((now-2 (test-time-today-at 14 10))) - (with-test-time now-2 - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ((10 . medium) (20 . high))))) - (result (chime--notifications event))) - ;; At 14:10, event is 10 minutes away, so only 10-minute interval matches - (should (= 1 (length result))))))) - (test-chime-notification-boundaries-teardown))) + (let* ((now (test-time-today-at 14 0)) + (event-time (test-time-today-at 14 20)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ((10 . medium) (20 . high))))) + (result (chime--notifications event))) + ;; At 14:00, event is 20 minutes away, so only 20-minute interval matches + (should (= 1 (length result))))) + + ;; Now test at 14:10 (10 minutes before event) + (let ((now-2 (test-time-today-at 14 10))) + (with-test-time now-2 + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ((10 . medium) (20 . high))))) + (result (chime--notifications event))) + ;; At 14:10, event is 10 minutes away, so only 10-minute interval matches + (should (= 1 (length result)))))))) ;;; Escalating Notification Tests -(ert-deftest test-chime-notification-boundary-escalating-intervals () +(chime-deftest test-chime-notification-boundary-escalating-intervals () "Test escalating notifications: 1 day, 1 hour, 10 minutes before event. Simulates the common use case of multiple notifications at different times." - (test-chime-notification-boundaries-setup) - (unwind-protect - (let* ((base-time (test-time-today-at 10 0)) - (event-time (time-add base-time (seconds-to-time (* 24 3600)))) ; 1 day later - (timestamp-str (test-timestamp-string event-time))) - - ;; Test 1: 24 hours before (1 day interval) - (with-test-time base-time - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Important Event") - (intervals . ((1440 . low) (60 . medium) (10 . high))))) - (result (chime--notifications event))) - ;; Should match 1440-minute (1 day) interval - (should (= 1 (length result))))) - - ;; Test 2: 1 hour before (60-minute interval) - (let ((time-1h-before (time-add event-time (seconds-to-time (* -60 60))))) - (with-test-time time-1h-before - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Important Event") - (intervals . ((1440 . low) (60 . medium) (10 . high))))) - (result (chime--notifications event))) - ;; Should match 60-minute interval - (should (= 1 (length result)))))) - - ;; Test 3: 10 minutes before (10-minute interval) - (let ((time-10m-before (time-add event-time (seconds-to-time (* -10 60))))) - (with-test-time time-10m-before - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Important Event") - (intervals . ((1440 . low) (60 . medium) (10 . high))))) - (result (chime--notifications event))) - ;; Should match 10-minute interval - (should (= 1 (length result))))))) - (test-chime-notification-boundaries-teardown))) + (let* ((base-time (test-time-today-at 10 0)) + (event-time (time-add base-time (seconds-to-time (* 24 3600)))) ; 1 day later + (timestamp-str (test-timestamp-string event-time))) + + ;; Test 1: 24 hours before (1 day interval) + (with-test-time base-time + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Important Event") + (intervals . ((1440 . low) (60 . medium) (10 . high))))) + (result (chime--notifications event))) + ;; Should match 1440-minute (1 day) interval + (should (= 1 (length result))))) + + ;; Test 2: 1 hour before (60-minute interval) + (let ((time-1h-before (time-add event-time (seconds-to-time (* -60 60))))) + (with-test-time time-1h-before + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Important Event") + (intervals . ((1440 . low) (60 . medium) (10 . high))))) + (result (chime--notifications event))) + ;; Should match 60-minute interval + (should (= 1 (length result)))))) + + ;; Test 3: 10 minutes before (10-minute interval) + (let ((time-10m-before (time-add event-time (seconds-to-time (* -10 60))))) + (with-test-time time-10m-before + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Important Event") + (intervals . ((1440 . low) (60 . medium) (10 . high))))) + (result (chime--notifications event))) + ;; Should match 10-minute interval + (should (= 1 (length result)))))))) (provide 'test-chime-notification-boundaries) ;;; test-chime-notification-boundaries.el ends here diff --git a/tests/test-chime-notification-text.el b/tests/test-chime-notification-text.el index a707d29..7363cd2 100644 --- a/tests/test-chime-notification-text.el +++ b/tests/test-chime-notification-text.el @@ -26,10 +26,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Setup and Teardown (defun test-chime-notification-text-setup () diff --git a/tests/test-chime-notifications.el b/tests/test-chime-notifications.el index 17ce540..e3c7cc2 100644 --- a/tests/test-chime-notifications.el +++ b/tests/test-chime-notifications.el @@ -26,221 +26,177 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - -;;; Setup and Teardown - -(defun test-chime-notifications-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-notifications-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Normal Cases -(ert-deftest test-chime-notifications-single-time-single-interval-returns-pair () +(chime-deftest test-chime-notifications-single-time-single-interval-returns-pair () "Test that single time with single interval returns one notification pair. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Event at 14:10 (10 minutes from now) - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ((10 . medium))))) - (result (chime--notifications event))) - ;; Should return list with one pair - (should (listp result)) - (should (= 1 (length result)))))) - (test-chime-notifications-teardown))) - -(ert-deftest test-chime-notifications-single-time-multiple-intervals-returns-multiple-pairs () + (let* ((now (test-time-today-at 14 0)) + ;; Event at 14:10 (10 minutes from now) + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ((10 . medium))))) + (result (chime--notifications event))) + ;; Should return list with one pair + (should (listp result)) + (should (= 1 (length result))))))) + +(chime-deftest test-chime-notifications-single-time-multiple-intervals-returns-multiple-pairs () "Test that single time with multiple intervals returns multiple notification pairs. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Event at 14:10 - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ((10 . medium) (5 . medium))))) ; Two intervals, only 10 matches - (result (chime--notifications event))) - ;; Should return only matching interval - (should (listp result)) - (should (= 1 (length result)))))) - (test-chime-notifications-teardown))) - -(ert-deftest test-chime-notifications-multiple-times-single-interval-returns-matching-pairs () + (let* ((now (test-time-today-at 14 0)) + ;; Event at 14:10 + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ((10 . medium) (5 . medium))))) ; Two intervals, only 10 matches + (result (chime--notifications event))) + ;; Should return only matching interval + (should (listp result)) + (should (= 1 (length result))))))) + +(chime-deftest test-chime-notifications-multiple-times-single-interval-returns-matching-pairs () "Test that multiple times with single interval returns matching notifications. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Two events: one at 14:10, one at 14:05 - (event-time-1 (test-time-today-at 14 10)) - (event-time-2 (test-time-today-at 14 5)) - (timestamp-str-1 (test-timestamp-string event-time-1)) - (timestamp-str-2 (test-timestamp-string event-time-2))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str-1 . ,event-time-1) - (,timestamp-str-2 . ,event-time-2))) - (title . "Test Event") - (intervals . ((10 . medium))))) ; Only first time matches - (result (chime--notifications event))) - ;; Should return only matching time - (should (listp result)) - (should (= 1 (length result)))))) - (test-chime-notifications-teardown))) - -(ert-deftest test-chime-notifications-multiple-times-multiple-intervals-returns-all-matches () + (let* ((now (test-time-today-at 14 0)) + ;; Two events: one at 14:10, one at 14:05 + (event-time-1 (test-time-today-at 14 10)) + (event-time-2 (test-time-today-at 14 5)) + (timestamp-str-1 (test-timestamp-string event-time-1)) + (timestamp-str-2 (test-timestamp-string event-time-2))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str-1 . ,event-time-1) + (,timestamp-str-2 . ,event-time-2))) + (title . "Test Event") + (intervals . ((10 . medium))))) ; Only first time matches + (result (chime--notifications event))) + ;; Should return only matching time + (should (listp result)) + (should (= 1 (length result))))))) + +(chime-deftest test-chime-notifications-multiple-times-multiple-intervals-returns-all-matches () "Test that multiple times and intervals return all matching combinations. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Event at 14:10 and 14:05 - (event-time-1 (test-time-today-at 14 10)) - (event-time-2 (test-time-today-at 14 5)) - (timestamp-str-1 (test-timestamp-string event-time-1)) - (timestamp-str-2 (test-timestamp-string event-time-2))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str-1 . ,event-time-1) - (,timestamp-str-2 . ,event-time-2))) - (title . "Test Event") - (intervals . ((10 . medium) (5 . medium))))) ; Both match (10 with first, 5 with second) - (result (chime--notifications event))) - ;; Should return both matching pairs - (should (listp result)) - (should (= 2 (length result)))))) - (test-chime-notifications-teardown))) - -(ert-deftest test-chime-notifications-zero-interval-returns-current-time-match () + (let* ((now (test-time-today-at 14 0)) + ;; Event at 14:10 and 14:05 + (event-time-1 (test-time-today-at 14 10)) + (event-time-2 (test-time-today-at 14 5)) + (timestamp-str-1 (test-timestamp-string event-time-1)) + (timestamp-str-2 (test-timestamp-string event-time-2))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str-1 . ,event-time-1) + (,timestamp-str-2 . ,event-time-2))) + (title . "Test Event") + (intervals . ((10 . medium) (5 . medium))))) ; Both match (10 with first, 5 with second) + (result (chime--notifications event))) + ;; Should return both matching pairs + (should (listp result)) + (should (= 2 (length result))))))) + +(chime-deftest test-chime-notifications-zero-interval-returns-current-time-match () "Test that zero interval (notify now) works correctly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Event at exactly current time - (event-time (test-time-today-at 14 0)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ((0 . high))))) - (result (chime--notifications event))) - ;; Should return one matching pair - (should (listp result)) - (should (= 1 (length result)))))) - (test-chime-notifications-teardown))) - -(ert-deftest test-chime-notifications-filters-day-wide-events () + (let* ((now (test-time-today-at 14 0)) + ;; Event at exactly current time + (event-time (test-time-today-at 14 0)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ((0 . high))))) + (result (chime--notifications event))) + ;; Should return one matching pair + (should (listp result)) + (should (= 1 (length result))))))) + +(chime-deftest test-chime-notifications-filters-day-wide-events () "Test that day-wide events (without time) are filtered out. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Mix of day-wide and timed events - (event-time (test-time-today-at 14 10)) - (timestamp-str-day (test-timestamp-string event-time t)) ; Day-wide - (timestamp-str-timed (test-timestamp-string event-time))) ; Timed - (with-test-time now - (let* ((event `((times . ((,timestamp-str-day . ,event-time) ; Day-wide - (,timestamp-str-timed . ,event-time))) ; Timed - (title . "Test Event") - (intervals . ((10 . medium))))) - (result (chime--notifications event))) - ;; Should return only timed event - (should (listp result)) - (should (= 1 (length result)))))) - (test-chime-notifications-teardown))) + (let* ((now (test-time-today-at 14 0)) + ;; Mix of day-wide and timed events + (event-time (test-time-today-at 14 10)) + (timestamp-str-day (test-timestamp-string event-time t)) ; Day-wide + (timestamp-str-timed (test-timestamp-string event-time))) ; Timed + (with-test-time now + (let* ((event `((times . ((,timestamp-str-day . ,event-time) ; Day-wide + (,timestamp-str-timed . ,event-time))) ; Timed + (title . "Test Event") + (intervals . ((10 . medium))))) + (result (chime--notifications event))) + ;; Should return only timed event + (should (listp result)) + (should (= 1 (length result))))))) ;;; Boundary Cases -(ert-deftest test-chime-notifications-empty-times-returns-empty-list () +(chime-deftest test-chime-notifications-empty-times-returns-empty-list () "Test that event with no times returns empty list. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let ((now (test-time-today-at 14 0))) - (with-test-time now - (let* ((event `((times . (())) - (title . "Test Event") - (intervals . ((10 . medium))))) - (result (chime--notifications event))) - (should (listp result)) - (should (= 0 (length result)))))) - (test-chime-notifications-teardown))) - -(ert-deftest test-chime-notifications-empty-intervals-returns-empty-list () + (let ((now (test-time-today-at 14 0))) + (with-test-time now + (let* ((event `((times . (())) + (title . "Test Event") + (intervals . ((10 . medium))))) + (result (chime--notifications event))) + (should (listp result)) + (should (= 0 (length result))))))) + +(chime-deftest test-chime-notifications-empty-intervals-returns-empty-list () "Test that event with no intervals returns empty list. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . ()))) - (result (chime--notifications event))) - (should (listp result)) - (should (= 0 (length result)))))) - (test-chime-notifications-teardown))) + (let* ((now (test-time-today-at 14 0)) + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . ()))) + (result (chime--notifications event))) + (should (listp result)) + (should (= 0 (length result))))))) ;;; Error Cases -(ert-deftest test-chime-notifications-nil-times-returns-empty-list () +(chime-deftest test-chime-notifications-nil-times-returns-empty-list () "Test that event with nil times returns empty list. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let ((now (test-time-today-at 14 0))) - (with-test-time now - (let* ((event `((times . (nil)) - (title . "Test Event") - (intervals . ((10 . medium))))) - (result (chime--notifications event))) - (should (listp result)) - (should (= 0 (length result)))))) - (test-chime-notifications-teardown))) - -(ert-deftest test-chime-notifications-nil-intervals-returns-empty-list () + (let ((now (test-time-today-at 14 0))) + (with-test-time now + (let* ((event `((times . (nil)) + (title . "Test Event") + (intervals . ((10 . medium))))) + (result (chime--notifications event))) + (should (listp result)) + (should (= 0 (length result))))))) + +(chime-deftest test-chime-notifications-nil-intervals-returns-empty-list () "Test that event with nil intervals returns empty list. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time))) - (with-test-time now - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Test Event") - (intervals . nil))) - (result (chime--notifications event))) - (should (listp result)) - (should (= 0 (length result)))))) - (test-chime-notifications-teardown))) + (let* ((now (test-time-today-at 14 0)) + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time))) + (with-test-time now + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Test Event") + (intervals . nil))) + (result (chime--notifications event))) + (should (listp result)) + (should (= 0 (length result))))))) (provide 'test-chime-notifications) ;;; test-chime-notifications.el ends here diff --git a/tests/test-chime-notify.el b/tests/test-chime-notify.el index cd7b350..68cb938 100644 --- a/tests/test-chime-notify.el +++ b/tests/test-chime-notify.el @@ -26,9 +26,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) - ;;; Setup and Teardown (defun test-chime-notify-setup () @@ -39,7 +36,12 @@ (setq chime-notification-icon nil) (setq chime-extra-alert-plist nil) ;; Use a simple test path for sound file - (setq chime-sound-file "/tmp/test-chime.wav")) + (setq chime-sound-file "/tmp/test-chime.wav") + ;; Route sound through `play-sound-file' so these tests mock one function + ;; and never spawn a real external player. The player-selection contract + ;; itself is covered in test-chime-sound.el. + (setq chime-sound-player 'emacs) + (setq chime-sound-device nil)) (defun test-chime-notify-teardown () "Teardown function run after each test." @@ -60,7 +62,7 @@ ((symbol-function 'file-exists-p) (lambda (file) t)) ;; Mock play-sound-file to track if called ((symbol-function 'play-sound-file) - (lambda (file) + (lambda (&rest _) (setq sound-played t))) ;; Mock alert to track if called ((symbol-function 'alert) @@ -149,7 +151,7 @@ ;; Mock file-exists-p to return nil ((symbol-function 'file-exists-p) (lambda (file) nil)) ((symbol-function 'play-sound-file) - (lambda (file) (setq sound-played t))) + (lambda (&rest _) (setq sound-played t))) ((symbol-function 'alert) (lambda (msg &rest args) (setq alert-called t)))) (chime--notify "Test Event") @@ -172,7 +174,7 @@ ((symbol-function 'file-exists-p) (lambda (file) t)) ;; Mock play-sound-file to throw error ((symbol-function 'play-sound-file) - (lambda (file) (error "Sound playback failed"))) + (lambda (&rest _) (error "Sound playback failed"))) ((symbol-function 'alert) (lambda (msg &rest args) (setq alert-called t)))) ;; Should not throw error @@ -219,5 +221,48 @@ (should alert-called))) (test-chime-notify-teardown))) +(ert-deftest test-chime-notify-alert-error-does-not-propagate () + "An `alert' failure is reported, not signalled. +`chime--process-notifications' maps `chime--notify' over every due event, so +an alert that signals (a dbus error, say) would drop every remaining +notification in the batch and be miscounted as a fetch failure." + (test-chime-notify-setup) + (unwind-protect + (let ((messages nil)) + (cl-letf (((symbol-function 'chime--play-sound) (lambda () nil)) + ((symbol-function 'alert) + (lambda (&rest _) (error "Dbus is having a bad day"))) + ((symbol-function 'message) + (lambda (fmt &rest args) (push (apply #'format fmt args) messages)))) + (should-not (condition-case nil + (progn (chime--notify "Test Event") nil) + (error t))) + (let ((chime-messages + (seq-filter (lambda (m) (string-prefix-p "chime:" m)) messages))) + (should (= (length chime-messages) 1)) + (should (string-match-p "Failed to show notification" (car chime-messages)))))) + (test-chime-notify-teardown))) + +(ert-deftest test-chime-notify-alert-error-does-not-stop-the-batch () + "One event's alert failure does not drop the notifications after it." + (test-chime-notify-setup) + (unwind-protect + (let ((delivered nil)) + (cl-letf (((symbol-function 'chime--play-sound) (lambda () nil)) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil)) + ((symbol-function 'chime--check-event) + (lambda (event) (list event))) + ((symbol-function 'alert) + (lambda (msg &rest _) + (if (equal msg "second") + (error "Dbus is having a bad day") + (push msg delivered)))) + ((symbol-function 'message) (lambda (&rest _) nil))) + (chime--process-notifications (list "first" "second" "third")) + ;; The failing middle event must not cost us the third. + (should (equal (nreverse delivered) (list "first" "third"))))) + (test-chime-notify-teardown))) + (provide 'test-chime-notify) ;;; test-chime-notify.el ends here diff --git a/tests/test-chime-overdue-todos.el b/tests/test-chime-overdue-todos.el index 84bc091..32e50c3 100644 --- a/tests/test-chime-overdue-todos.el +++ b/tests/test-chime-overdue-todos.el @@ -31,10 +31,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Test Helper Functions (defun test-overdue--create-event (title timestamp has-time) @@ -47,19 +43,9 @@ HAS-TIME determines if timestamp has time component." (times . ,times) (intervals . (10))))) -;;; Setup and Teardown - -(defun test-chime-overdue-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-overdue-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Tests for chime--event-has-any-passed-time -(ert-deftest test-overdue-has-passed-time-yesterday-all-day () +(chime-deftest test-overdue-has-passed-time-yesterday-all-day () "Test that all-day event from yesterday is recognized as passed. TIME RELATIONSHIPS: @@ -70,20 +56,17 @@ EXPECTED BEHAVIOR: Should return t (yesterday is in the past) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (yesterday (test-time-yesterday-at 0 0)) - (yesterday-timestamp (test-timestamp-string yesterday t)) - (event (test-overdue--create-event - "Yesterday Event" - yesterday-timestamp - nil))) ; all-day event - (with-test-time now - (should (chime--event-has-any-passed-time event)))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-has-passed-time-today-all-day () + (let* ((now (test-time-now)) + (yesterday (test-time-yesterday-at 0 0)) + (yesterday-timestamp (test-timestamp-string yesterday t)) + (event (test-overdue--create-event + "Yesterday Event" + yesterday-timestamp + nil))) ; all-day event + (with-test-time now + (should (chime--event-has-any-passed-time event))))) + +(chime-deftest test-overdue-has-passed-time-today-all-day () "Test that all-day event from today is recognized as passed. TIME RELATIONSHIPS: @@ -111,19 +94,16 @@ REFACTORING NOTES: Simple case - just needs TODAY timestamp and TODAY current-time. REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (today-timestamp (test-timestamp-string now t)) - (event (test-overdue--create-event - "Today Event" - today-timestamp - nil))) ; all-day event - (with-test-time now - (should (chime--event-has-any-passed-time event)))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-has-passed-time-tomorrow-all-day () + (let* ((now (test-time-now)) + (today-timestamp (test-timestamp-string now t)) + (event (test-overdue--create-event + "Today Event" + today-timestamp + nil))) ; all-day event + (with-test-time now + (should (chime--event-has-any-passed-time event))))) + +(chime-deftest test-overdue-has-passed-time-tomorrow-all-day () "Test that all-day event from tomorrow is NOT recognized as passed. TIME RELATIONSHIPS: @@ -134,20 +114,17 @@ EXPECTED BEHAVIOR: Should return nil (tomorrow is in the future) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (tomorrow (test-time-tomorrow-at 0 0)) - (tomorrow-timestamp (test-timestamp-string tomorrow t)) - (event (test-overdue--create-event - "Tomorrow Event" - tomorrow-timestamp - nil))) ; all-day event - (with-test-time now - (should-not (chime--event-has-any-passed-time event)))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-has-passed-time-timed-event-past () + (let* ((now (test-time-now)) + (tomorrow (test-time-tomorrow-at 0 0)) + (tomorrow-timestamp (test-timestamp-string tomorrow t)) + (event (test-overdue--create-event + "Tomorrow Event" + tomorrow-timestamp + nil))) ; all-day event + (with-test-time now + (should-not (chime--event-has-any-passed-time event))))) + +(chime-deftest test-overdue-has-passed-time-timed-event-past () "Test that timed event in the past is recognized as passed. TIME RELATIONSHIPS: @@ -158,20 +135,17 @@ EXPECTED BEHAVIOR: Should return t (event time has passed) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (past-event (test-time-today-at 9 0)) - (past-timestamp (test-timestamp-string past-event)) - (event (test-overdue--create-event - "Past Meeting" - past-timestamp - t))) ; timed event - (with-test-time now - (should (chime--event-has-any-passed-time event)))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-has-passed-time-timed-event-future () + (let* ((now (test-time-today-at 14 0)) + (past-event (test-time-today-at 9 0)) + (past-timestamp (test-timestamp-string past-event)) + (event (test-overdue--create-event + "Past Meeting" + past-timestamp + t))) ; timed event + (with-test-time now + (should (chime--event-has-any-passed-time event))))) + +(chime-deftest test-overdue-has-passed-time-timed-event-future () "Test that timed event in the future is NOT recognized as passed. TIME RELATIONSHIPS: @@ -182,22 +156,19 @@ EXPECTED BEHAVIOR: Should return nil (event time is in future) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (future-event (test-time-today-at 16 0)) - (future-timestamp (test-timestamp-string future-event)) - (event (test-overdue--create-event - "Future Meeting" - future-timestamp - t))) ; timed event - (with-test-time now - (should-not (chime--event-has-any-passed-time event)))) - (test-chime-overdue-teardown))) + (let* ((now (test-time-today-at 14 0)) + (future-event (test-time-today-at 16 0)) + (future-timestamp (test-timestamp-string future-event)) + (event (test-overdue--create-event + "Future Meeting" + future-timestamp + t))) ; timed event + (with-test-time now + (should-not (chime--event-has-any-passed-time event))))) ;;; Tests for chime--display-as-day-wide-event with overdue setting -(ert-deftest test-overdue-display-yesterday-all-day-with-overdue-enabled () +(chime-deftest test-overdue-display-yesterday-all-day-with-overdue-enabled () "Test that yesterday's all-day event is displayed when overdue is enabled. TIME RELATIONSHIPS: @@ -209,133 +180,115 @@ EXPECTED BEHAVIOR: Should display (overdue enabled shows past events) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (yesterday (test-time-yesterday-at 0 0)) - (yesterday-timestamp (test-timestamp-string yesterday t)) - (chime-show-any-overdue-with-day-wide-alerts t) - (chime-day-wide-advance-notice nil) - (event (test-overdue--create-event - "Yesterday Birthday" - yesterday-timestamp - nil))) - (with-test-time now - (should (chime--display-as-day-wide-event event)))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-display-yesterday-all-day-with-overdue-disabled () + (let* ((now (test-time-now)) + (yesterday (test-time-yesterday-at 0 0)) + (yesterday-timestamp (test-timestamp-string yesterday t)) + (chime-show-any-overdue-with-day-wide-alerts t) + (chime-day-wide-advance-notice nil) + (event (test-overdue--create-event + "Yesterday Birthday" + yesterday-timestamp + nil))) + (with-test-time now + (should (chime--display-as-day-wide-event event))))) + +(chime-deftest test-overdue-display-yesterday-all-day-with-overdue-disabled () "Test that yesterday's all-day event is NOT displayed when overdue is disabled. This prevents showing old birthdays/holidays from the past. REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (yesterday (test-time-yesterday-at 0 0)) - (yesterday-timestamp (test-timestamp-string yesterday t)) - (chime-show-any-overdue-with-day-wide-alerts nil) - (chime-day-wide-advance-notice nil) - (event (test-overdue--create-event - "Yesterday Birthday" - yesterday-timestamp - nil))) - (with-test-time now - (should-not (chime--display-as-day-wide-event event)))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-display-yesterday-timed-with-overdue-enabled () + (let* ((now (test-time-now)) + (yesterday (test-time-yesterday-at 0 0)) + (yesterday-timestamp (test-timestamp-string yesterday t)) + (chime-show-any-overdue-with-day-wide-alerts nil) + (chime-day-wide-advance-notice nil) + (event (test-overdue--create-event + "Yesterday Birthday" + yesterday-timestamp + nil))) + (with-test-time now + (should-not (chime--display-as-day-wide-event event))))) + +(chime-deftest test-overdue-display-yesterday-timed-with-overdue-enabled () "Test that yesterday's timed event is displayed when overdue is enabled. TIME: TODAY 10am, Event: YESTERDAY 2pm, overdue=t EXPECTED: Display (show past timed events when enabled) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (yesterday (test-time-yesterday-at 14 0)) - (yesterday-timestamp (test-timestamp-string yesterday)) - (chime-show-any-overdue-with-day-wide-alerts t) - (chime-day-wide-advance-notice nil) - (event (test-overdue--create-event - "Yesterday Meeting" - yesterday-timestamp - t))) - (with-test-time now - (should (chime--display-as-day-wide-event event)))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-display-yesterday-timed-with-overdue-disabled () + (let* ((now (test-time-now)) + (yesterday (test-time-yesterday-at 14 0)) + (yesterday-timestamp (test-timestamp-string yesterday)) + (chime-show-any-overdue-with-day-wide-alerts t) + (chime-day-wide-advance-notice nil) + (event (test-overdue--create-event + "Yesterday Meeting" + yesterday-timestamp + t))) + (with-test-time now + (should (chime--display-as-day-wide-event event))))) + +(chime-deftest test-overdue-display-yesterday-timed-with-overdue-disabled () "Test that yesterday's timed event is NOT displayed when overdue is disabled. TIME: TODAY 10am, Event: YESTERDAY 2pm, overdue=nil EXPECTED: Hide (don't show past timed events when disabled) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (yesterday (test-time-yesterday-at 14 0)) - (yesterday-timestamp (test-timestamp-string yesterday)) - (chime-show-any-overdue-with-day-wide-alerts nil) - (chime-day-wide-advance-notice nil) - (event (test-overdue--create-event - "Yesterday Meeting" - yesterday-timestamp - t))) - (with-test-time now - (should-not (chime--display-as-day-wide-event event)))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-display-today-all-day-always-shown () + (let* ((now (test-time-now)) + (yesterday (test-time-yesterday-at 14 0)) + (yesterday-timestamp (test-timestamp-string yesterday)) + (chime-show-any-overdue-with-day-wide-alerts nil) + (chime-day-wide-advance-notice nil) + (event (test-overdue--create-event + "Yesterday Meeting" + yesterday-timestamp + t))) + (with-test-time now + (should-not (chime--display-as-day-wide-event event))))) + +(chime-deftest test-overdue-display-today-all-day-always-shown () "Test that today's all-day event is always displayed regardless of overdue setting. TIME: TODAY 10am, Event: TODAY (all-day), both overdue=t and =nil EXPECTED: Always display (today's events shown regardless of setting) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (today-timestamp (test-timestamp-string now t)) - (chime-day-wide-advance-notice nil) - (event (test-overdue--create-event - "Today Birthday" - today-timestamp - nil))) - (with-test-time now - ;; Should show with overdue enabled - (let ((chime-show-any-overdue-with-day-wide-alerts t)) - (should (chime--display-as-day-wide-event event))) - ;; Should also show with overdue disabled (it's today, not overdue) - (let ((chime-show-any-overdue-with-day-wide-alerts nil)) - (should (chime--display-as-day-wide-event event))))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-display-week-old-all-day-with-overdue-enabled () + (let* ((now (test-time-now)) + (today-timestamp (test-timestamp-string now t)) + (chime-day-wide-advance-notice nil) + (event (test-overdue--create-event + "Today Birthday" + today-timestamp + nil))) + (with-test-time now + ;; Should show with overdue enabled + (let ((chime-show-any-overdue-with-day-wide-alerts t)) + (should (chime--display-as-day-wide-event event))) + ;; Should also show with overdue disabled (it's today, not overdue) + (let ((chime-show-any-overdue-with-day-wide-alerts nil)) + (should (chime--display-as-day-wide-event event)))))) + +(chime-deftest test-overdue-display-week-old-all-day-with-overdue-enabled () "Test that week-old all-day event is displayed when overdue is enabled. TIME: TODAY (Oct 28), Event: 7 DAYS AGO (Oct 21), overdue=t EXPECTED: Display (show old events when enabled) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (week-ago (test-time-days-ago 7)) - (week-ago-timestamp (test-timestamp-string week-ago t)) - (chime-show-any-overdue-with-day-wide-alerts t) - (chime-day-wide-advance-notice nil) - (event (test-overdue--create-event - "Week Old Event" - week-ago-timestamp - nil))) - (with-test-time now - (should (chime--display-as-day-wide-event event)))) - (test-chime-overdue-teardown))) - -(ert-deftest test-overdue-display-week-old-all-day-with-overdue-disabled () + (let* ((now (test-time-now)) + (week-ago (test-time-days-ago 7)) + (week-ago-timestamp (test-timestamp-string week-ago t)) + (chime-show-any-overdue-with-day-wide-alerts t) + (chime-day-wide-advance-notice nil) + (event (test-overdue--create-event + "Week Old Event" + week-ago-timestamp + nil))) + (with-test-time now + (should (chime--display-as-day-wide-event event))))) + +(chime-deftest test-overdue-display-week-old-all-day-with-overdue-disabled () "Test that week-old all-day event is NOT displayed when overdue is disabled. This prevents showing old birthdays/holidays from past weeks. @@ -343,48 +296,42 @@ TIME: TODAY (Oct 28), Event: 7 DAYS AGO (Oct 21), overdue=nil EXPECTED: Hide (prevent old birthday spam) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (week-ago (test-time-days-ago 7)) - (week-ago-timestamp (test-timestamp-string week-ago t)) - (chime-show-any-overdue-with-day-wide-alerts nil) - (chime-day-wide-advance-notice nil) - (event (test-overdue--create-event - "Week Old Event" - week-ago-timestamp - nil))) - (with-test-time now - (should-not (chime--display-as-day-wide-event event)))) - (test-chime-overdue-teardown))) + (let* ((now (test-time-now)) + (week-ago (test-time-days-ago 7)) + (week-ago-timestamp (test-timestamp-string week-ago t)) + (chime-show-any-overdue-with-day-wide-alerts nil) + (chime-day-wide-advance-notice nil) + (event (test-overdue--create-event + "Week Old Event" + week-ago-timestamp + nil))) + (with-test-time now + (should-not (chime--display-as-day-wide-event event))))) ;;; Tests verifying overdue doesn't affect future events -(ert-deftest test-overdue-future-event-not-affected-by-overdue-setting () +(chime-deftest test-overdue-future-event-not-affected-by-overdue-setting () "Test that future events are not affected by overdue setting. TIME: TODAY (Oct 28), Event: 2 DAYS FROM NOW (Oct 30), both overdue settings EXPECTED: Never display (future events not shown without advance notice) REFACTORED: Uses dynamic timestamps via testutil-time.el" - (test-chime-overdue-setup) - (unwind-protect - (let* ((now (test-time-now)) - (future (test-time-days-from-now 2)) - (future-timestamp (test-timestamp-string future t)) - (chime-day-wide-advance-notice nil) - (event (test-overdue--create-event - "Future Event" - future-timestamp - nil))) - (with-test-time now - ;; Should NOT show with overdue enabled (it's future, not today) - (let ((chime-show-any-overdue-with-day-wide-alerts t)) - (should-not (chime--display-as-day-wide-event event))) - ;; Should NOT show with overdue disabled (it's future, not today) - (let ((chime-show-any-overdue-with-day-wide-alerts nil)) - (should-not (chime--display-as-day-wide-event event))))) - (test-chime-overdue-teardown))) + (let* ((now (test-time-now)) + (future (test-time-days-from-now 2)) + (future-timestamp (test-timestamp-string future t)) + (chime-day-wide-advance-notice nil) + (event (test-overdue--create-event + "Future Event" + future-timestamp + nil))) + (with-test-time now + ;; Should NOT show with overdue enabled (it's future, not today) + (let ((chime-show-any-overdue-with-day-wide-alerts t)) + (should-not (chime--display-as-day-wide-event event))) + ;; Should NOT show with overdue disabled (it's future, not today) + (let ((chime-show-any-overdue-with-day-wide-alerts nil)) + (should-not (chime--display-as-day-wide-event event)))))) (provide 'test-chime-overdue-todos) ;;; test-chime-overdue-todos.el ends here diff --git a/tests/test-chime-process-notifications.el b/tests/test-chime-process-notifications.el index 75cac9b..1a93842 100644 --- a/tests/test-chime-process-notifications.el +++ b/tests/test-chime-process-notifications.el @@ -26,363 +26,310 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - -;;; Setup and Teardown - -(defun test-chime-process-notifications-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-process-notifications-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Normal Cases -(ert-deftest test-chime-process-notifications-normal-single-event-calls-notify () +(chime-deftest test-chime-process-notifications-normal-single-event-calls-notify () "Test that single event with notification calls chime--notify. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Event at 14:10 (10 minutes from now) - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time)) - (notify-called nil) - (notify-messages '())) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) - (setq notify-called t) - (push msg notify-messages))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil))) - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Team Meeting") - (intervals . ((10 . medium))))) - (events (list event))) - (chime--process-notifications events) - ;; Should call notify - (should notify-called) - (should (= 1 (length notify-messages))) - (should (string-match-p "Team Meeting" (caar notify-messages))))))) - (test-chime-process-notifications-teardown))) - -(ert-deftest test-chime-process-notifications-normal-multiple-events-calls-notify-multiple-times () + (let* ((now (test-time-today-at 14 0)) + ;; Event at 14:10 (10 minutes from now) + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time)) + (notify-called nil) + (notify-messages '())) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) + (setq notify-called t) + (push msg notify-messages))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil))) + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Team Meeting") + (intervals . ((10 . medium))))) + (events (list event))) + (chime--process-notifications events) + ;; Should call notify + (should notify-called) + (should (= 1 (length notify-messages))) + (should (string-match-p "Team Meeting" (caar notify-messages)))))))) + +(chime-deftest test-chime-process-notifications-normal-multiple-events-calls-notify-multiple-times () "Test that multiple events with notifications call chime--notify multiple times. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Event 1 at 14:10 - (event-time-1 (test-time-today-at 14 10)) - (timestamp-str-1 (test-timestamp-string event-time-1)) - ;; Event 2 at 14:05 - (event-time-2 (test-time-today-at 14 5)) - (timestamp-str-2 (test-timestamp-string event-time-2)) - (notify-count 0)) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) (setq notify-count (1+ notify-count)))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil))) - (let* ((event1 `((times . ((,timestamp-str-1 . ,event-time-1))) - (title . "Meeting 1") - (intervals . ((10 . medium))))) - (event2 `((times . ((,timestamp-str-2 . ,event-time-2))) - (title . "Meeting 2") - (intervals . ((5 . medium))))) - (events (list event1 event2))) - (chime--process-notifications events) - ;; Should call notify twice (once per event) - (should (= 2 notify-count)))))) - (test-chime-process-notifications-teardown))) - -(ert-deftest test-chime-process-notifications-normal-deduplication-removes-duplicates () + (let* ((now (test-time-today-at 14 0)) + ;; Event 1 at 14:10 + (event-time-1 (test-time-today-at 14 10)) + (timestamp-str-1 (test-timestamp-string event-time-1)) + ;; Event 2 at 14:05 + (event-time-2 (test-time-today-at 14 5)) + (timestamp-str-2 (test-timestamp-string event-time-2)) + (notify-count 0)) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) (setq notify-count (1+ notify-count)))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil))) + (let* ((event1 `((times . ((,timestamp-str-1 . ,event-time-1))) + (title . "Meeting 1") + (intervals . ((10 . medium))))) + (event2 `((times . ((,timestamp-str-2 . ,event-time-2))) + (title . "Meeting 2") + (intervals . ((5 . medium))))) + (events (list event1 event2))) + (chime--process-notifications events) + ;; Should call notify twice (once per event) + (should (= 2 notify-count))))))) + +(chime-deftest test-chime-process-notifications-normal-deduplication-removes-duplicates () "Test that duplicate notification messages are deduplicated. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Two events with same title and time - should dedupe - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time)) - (notify-messages '())) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) (push msg notify-messages))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil))) - (let* ((event1 `((times . ((,timestamp-str . ,event-time))) - (title . "Team Meeting") - (intervals . ((10 . medium))))) - (event2 `((times . ((,timestamp-str . ,event-time))) - (title . "Team Meeting") - (intervals . ((10 . medium))))) - (events (list event1 event2))) - (chime--process-notifications events) - ;; Should only call notify once due to deduplication - (should (= 1 (length notify-messages))))))) - (test-chime-process-notifications-teardown))) - -(ert-deftest test-chime-process-notifications-normal-day-wide-notifications-called-at-right-time () + (let* ((now (test-time-today-at 14 0)) + ;; Two events with same title and time - should dedupe + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time)) + (notify-messages '())) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) (push msg notify-messages))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil))) + (let* ((event1 `((times . ((,timestamp-str . ,event-time))) + (title . "Team Meeting") + (intervals . ((10 . medium))))) + (event2 `((times . ((,timestamp-str . ,event-time))) + (title . "Team Meeting") + (intervals . ((10 . medium))))) + (events (list event1 event2))) + (chime--process-notifications events) + ;; Should only call notify once due to deduplication + (should (= 1 (length notify-messages)))))))) + +(chime-deftest test-chime-process-notifications-normal-day-wide-notifications-called-at-right-time () "Test that day-wide notifications are sent when current time matches. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 9 0)) - ;; Day-wide event - (event-time (test-time-today-at 0 0)) - (timestamp-str (test-timestamp-string event-time t)) ; Day-wide - (notify-count 0)) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) (setq notify-count (1+ notify-count)))) - ;; Mock day-wide time to return true - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () t)) - ((symbol-function 'chime--day-wide-notifications) - (lambda (events) (list "Day-wide alert")))) - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "All Day Event") - (intervals . ()))) - (events (list event))) - (chime--process-notifications events) - ;; Should call notify at least once for day-wide - (should (>= notify-count 1)))))) - (test-chime-process-notifications-teardown))) - -(ert-deftest test-chime-process-notifications-normal-no-day-wide-when-wrong-time () + (let* ((now (test-time-today-at 9 0)) + ;; Day-wide event + (event-time (test-time-today-at 0 0)) + (timestamp-str (test-timestamp-string event-time t)) ; Day-wide + (notify-count 0)) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) (setq notify-count (1+ notify-count)))) + ;; Mock day-wide time to return true + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () t)) + ((symbol-function 'chime--day-wide-notifications) + (lambda (events) (list "Day-wide alert")))) + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "All Day Event") + (intervals . ()))) + (events (list event))) + (chime--process-notifications events) + ;; Should call notify at least once for day-wide + (should (>= notify-count 1))))))) + +(chime-deftest test-chime-process-notifications-normal-no-day-wide-when-wrong-time () "Test that day-wide notifications are not sent when time doesn't match. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (event-time (test-time-today-at 0 0)) - (timestamp-str (test-timestamp-string event-time t)) ; Day-wide - (day-wide-called nil)) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) (lambda (msg) nil)) - ;; Mock day-wide time to return false - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil)) - ((symbol-function 'chime--day-wide-notifications) - (lambda (events) - (setq day-wide-called t) - '()))) - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "All Day Event") - (intervals . ()))) - (events (list event))) - (chime--process-notifications events) - ;; Day-wide function should not be called - (should-not day-wide-called))))) - (test-chime-process-notifications-teardown))) + (let* ((now (test-time-today-at 14 0)) + (event-time (test-time-today-at 0 0)) + (timestamp-str (test-timestamp-string event-time t)) ; Day-wide + (day-wide-called nil)) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) (lambda (msg) nil)) + ;; Mock day-wide time to return false + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil)) + ((symbol-function 'chime--day-wide-notifications) + (lambda (events) + (setq day-wide-called t) + '()))) + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "All Day Event") + (intervals . ()))) + (events (list event))) + (chime--process-notifications events) + ;; Day-wide function should not be called + (should-not day-wide-called)))))) ;;; Boundary Cases -(ert-deftest test-chime-process-notifications-boundary-empty-events-no-notifications () +(chime-deftest test-chime-process-notifications-boundary-empty-events-no-notifications () "Test that empty events list produces no notifications. REFACTORED: No timestamps used" - (test-chime-process-notifications-setup) - (unwind-protect - (let ((notify-called nil)) - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) (setq notify-called t))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil))) - (let ((events '())) - (chime--process-notifications events) - ;; Should not call notify - (should-not notify-called)))) - (test-chime-process-notifications-teardown))) - -(ert-deftest test-chime-process-notifications-boundary-events-with-no-matches-no-notifications () + (let ((notify-called nil)) + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) (setq notify-called t))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil))) + (let ((events '())) + (chime--process-notifications events) + ;; Should not call notify + (should-not notify-called))))) + +(chime-deftest test-chime-process-notifications-boundary-events-with-no-matches-no-notifications () "Test that events with no matching notifications don't call notify. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Event at 15:00 (60 minutes away, doesn't match 10 min interval) - (event-time (test-time-today-at 15 0)) - (timestamp-str (test-timestamp-string event-time)) - (notify-called nil)) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) (setq notify-called t))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil))) - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Future Event") - (intervals . ((10 . medium))))) - (events (list event))) - (chime--process-notifications events) - ;; Should not call notify - (should-not notify-called))))) - (test-chime-process-notifications-teardown))) - -(ert-deftest test-chime-process-notifications-boundary-single-event-edge-case () + (let* ((now (test-time-today-at 14 0)) + ;; Event at 15:00 (60 minutes away, doesn't match 10 min interval) + (event-time (test-time-today-at 15 0)) + (timestamp-str (test-timestamp-string event-time)) + (notify-called nil)) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) (setq notify-called t))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil))) + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Future Event") + (intervals . ((10 . medium))))) + (events (list event))) + (chime--process-notifications events) + ;; Should not call notify + (should-not notify-called)))))) + +(chime-deftest test-chime-process-notifications-boundary-single-event-edge-case () "Test processing single event works correctly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time)) - (notify-count 0)) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) (setq notify-count (1+ notify-count)))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil))) - (let* ((event `((times . ((,timestamp-str . ,event-time))) - (title . "Single Event") - (intervals . ((10 . medium))))) - (events (list event))) - (chime--process-notifications events) - ;; Should call notify exactly once - (should (= 1 notify-count)))))) - (test-chime-process-notifications-teardown))) + (let* ((now (test-time-today-at 14 0)) + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time)) + (notify-count 0)) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) (setq notify-count (1+ notify-count)))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil))) + (let* ((event `((times . ((,timestamp-str . ,event-time))) + (title . "Single Event") + (intervals . ((10 . medium))))) + (events (list event))) + (chime--process-notifications events) + ;; Should call notify exactly once + (should (= 1 notify-count))))))) ;;; Error Cases -(ert-deftest test-chime-process-notifications-error-nil-events-handles-gracefully () +(chime-deftest test-chime-process-notifications-error-nil-events-handles-gracefully () "Test that nil events parameter doesn't crash. REFACTORED: No timestamps used" - (test-chime-process-notifications-setup) - (unwind-protect - (let ((notify-called nil)) - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) (setq notify-called t))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil))) - ;; Should not error with nil events - (should-not (condition-case nil - (progn (chime--process-notifications nil) nil) - (error t))) - ;; Should not call notify - (should-not notify-called))) - (test-chime-process-notifications-teardown))) - -(ert-deftest test-chime-process-notifications-error-invalid-event-structure-handles-gracefully () + (let ((notify-called nil)) + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) (setq notify-called t))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil))) + ;; Should not error with nil events + (should-not (condition-case nil + (progn (chime--process-notifications nil) nil) + (error t))) + ;; Should not call notify + (should-not notify-called)))) + +(chime-deftest test-chime-process-notifications-error-invalid-event-structure-handles-gracefully () "Test that invalid event structure doesn't crash. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (notify-called nil)) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) (setq notify-called t))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil))) - (let* (;; Invalid event: missing required fields - (events (list '((invalid . "structure"))))) - ;; Should not crash even with invalid events - (should-not (condition-case nil - (progn (chime--process-notifications events) nil) - (error t))))))) - (test-chime-process-notifications-teardown))) - -(ert-deftest test-chime-process-notifications-error-mixed-valid-invalid-events-processes-valid () + (let* ((now (test-time-today-at 14 0)) + (notify-called nil)) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) (setq notify-called t))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil))) + (let* (;; Invalid event: missing required fields + (events (list '((invalid . "structure"))))) + ;; Should not crash even with invalid events + (should-not (condition-case nil + (progn (chime--process-notifications events) nil) + (error t)))))))) + +(chime-deftest test-chime-process-notifications-error-mixed-valid-invalid-events-processes-valid () "Test that mix of valid and invalid events processes valid ones. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Valid event - (event-time (test-time-today-at 14 10)) - (timestamp-str (test-timestamp-string event-time)) - (notify-count 0)) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) (setq notify-count (1+ notify-count)))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () nil))) - (let* ((valid-event `((times . ((,timestamp-str . ,event-time))) - (title . "Valid Event") - (intervals . ((10 . medium))))) - ;; Invalid event - (invalid-event '((invalid . "data"))) - (events (list valid-event invalid-event))) - ;; Should not crash - (should-not (condition-case nil - (progn (chime--process-notifications events) nil) - (error t))) - ;; Should process at least the valid event - (should (>= notify-count 1)))))) - (test-chime-process-notifications-teardown))) + (let* ((now (test-time-today-at 14 0)) + ;; Valid event + (event-time (test-time-today-at 14 10)) + (timestamp-str (test-timestamp-string event-time)) + (notify-count 0)) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) (setq notify-count (1+ notify-count)))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () nil))) + (let* ((valid-event `((times . ((,timestamp-str . ,event-time))) + (title . "Valid Event") + (intervals . ((10 . medium))))) + ;; Invalid event + (invalid-event '((invalid . "data"))) + (events (list valid-event invalid-event))) + ;; Should not crash + (should-not (condition-case nil + (progn (chime--process-notifications events) nil) + (error t))) + ;; Should process at least the valid event + (should (>= notify-count 1))))))) ;;; Day-wide bundling -(ert-deftest test-chime-process-notifications-day-wide-multiple-events-single-notify () +(chime-deftest test-chime-process-notifications-day-wide-multiple-events-single-notify () "Multiple day-wide events should produce a single bundled notification, not one notification per event." - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 8 0)) - (notify-count 0) - (notify-messages '())) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) - (setq notify-count (1+ notify-count)) - (push msg notify-messages))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () t)) - ((symbol-function 'chime--day-wide-notifications) - (lambda (events) - (list (cons "Blake's birthday is today" 'medium) - (cons "Holiday: Memorial Day is today" 'medium) - (cons "Submit expense report is due or scheduled today" 'medium))))) - (chime--process-notifications '()) - ;; Should fire exactly ONE notification for all day-wide events - (should (= 1 notify-count)) - ;; The single notification body should contain all event messages - (let ((body (caar notify-messages))) - (should (string-match-p "Blake's birthday" body)) - (should (string-match-p "Memorial Day" body)) - (should (string-match-p "expense report" body)))))) - (test-chime-process-notifications-teardown))) - -(ert-deftest test-chime-process-notifications-day-wide-single-event-no-bundling () + (let* ((now (test-time-today-at 8 0)) + (notify-count 0) + (notify-messages '())) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) + (setq notify-count (1+ notify-count)) + (push msg notify-messages))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () t)) + ((symbol-function 'chime--day-wide-notifications) + (lambda (events) + (list (cons "Blake's birthday is today" 'medium) + (cons "Holiday: Memorial Day is today" 'medium) + (cons "Submit expense report is due or scheduled today" 'medium))))) + (chime--process-notifications '()) + ;; Should fire exactly ONE notification for all day-wide events + (should (= 1 notify-count)) + ;; The single notification body should contain all event messages + (let ((body (caar notify-messages))) + (should (string-match-p "Blake's birthday" body)) + (should (string-match-p "Memorial Day" body)) + (should (string-match-p "expense report" body))))))) + +(chime-deftest test-chime-process-notifications-day-wide-single-event-no-bundling () "A single day-wide event should produce a normal notification, not bundled." - (test-chime-process-notifications-setup) - (unwind-protect - (let* ((now (test-time-today-at 8 0)) - (notify-count 0) - (notify-messages '())) - (with-test-time now - (cl-letf (((symbol-function 'chime--notify) - (lambda (msg) - (setq notify-count (1+ notify-count)) - (push msg notify-messages))) - ((symbol-function 'chime--current-time-is-day-wide-time) - (lambda () t)) - ((symbol-function 'chime--day-wide-notifications) - (lambda (events) - (list (cons "Blake's birthday is today" 'medium))))) - (chime--process-notifications '()) - ;; Single event: still one notification - (should (= 1 notify-count)) - ;; Should be the plain message, not wrapped in a bundle - (let ((body (caar notify-messages))) - (should (string= "Blake's birthday is today" body)))))) - (test-chime-process-notifications-teardown))) + (let* ((now (test-time-today-at 8 0)) + (notify-count 0) + (notify-messages '())) + (with-test-time now + (cl-letf (((symbol-function 'chime--notify) + (lambda (msg) + (setq notify-count (1+ notify-count)) + (push msg notify-messages))) + ((symbol-function 'chime--current-time-is-day-wide-time) + (lambda () t)) + ((symbol-function 'chime--day-wide-notifications) + (lambda (events) + (list (cons "Blake's birthday is today" 'medium))))) + (chime--process-notifications '()) + ;; Single event: still one notification + (should (= 1 notify-count)) + ;; Should be the plain message, not wrapped in a bundle + (let ((body (caar notify-messages))) + (should (string= "Blake's birthday is today" body))))))) (provide 'test-chime-process-notifications) ;;; test-chime-process-notifications.el ends here diff --git a/tests/test-chime-propertize-modeline.el b/tests/test-chime-propertize-modeline.el index cc0dbb2..c0a0874 100644 --- a/tests/test-chime-propertize-modeline.el +++ b/tests/test-chime-propertize-modeline.el @@ -27,8 +27,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) ;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;; Setup/Teardown diff --git a/tests/test-chime-sanitize-title.el b/tests/test-chime-sanitize-title.el index 672cf70..8856a8e 100644 --- a/tests/test-chime-sanitize-title.el +++ b/tests/test-chime-sanitize-title.el @@ -32,358 +32,254 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - -;;; Setup and Teardown - -(defun test-chime-sanitize-title-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-sanitize-title-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Normal Cases - Already Balanced -(ert-deftest test-chime-sanitize-title-balanced-parens-unchanged () +(chime-deftest test-chime-sanitize-title-balanced-parens-unchanged () "Test that balanced parentheses are unchanged. REFACTORED: No timestamps used" - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting (Team Sync)") - (result (chime--sanitize-title title))) - (should (string-equal title result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-balanced-brackets-unchanged () + (let* ((title "Meeting (Team Sync)") + (result (chime--sanitize-title title))) + (should (string-equal title result)))) + +(chime-deftest test-chime-sanitize-title-balanced-brackets-unchanged () "Test that balanced brackets are unchanged. REFACTORED: No timestamps used" - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Review [PR #123]") - (result (chime--sanitize-title title))) - (should (string-equal title result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-balanced-braces-unchanged () + (let* ((title "Review [PR #123]") + (result (chime--sanitize-title title))) + (should (string-equal title result)))) + +(chime-deftest test-chime-sanitize-title-balanced-braces-unchanged () "Test that balanced braces are unchanged. REFACTORED: No timestamps used" - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Code Review {urgent}") - (result (chime--sanitize-title title))) - (should (string-equal title result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-mixed-balanced-unchanged () + (let* ((title "Code Review {urgent}") + (result (chime--sanitize-title title))) + (should (string-equal title result)))) + +(chime-deftest test-chime-sanitize-title-mixed-balanced-unchanged () "Test that mixed balanced delimiters are unchanged. REFACTORED: No timestamps used" - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting [Team] (Sync) {Urgent}") - (result (chime--sanitize-title title))) - (should (string-equal title result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-nested-balanced-unchanged () + (let* ((title "Meeting [Team] (Sync) {Urgent}") + (result (chime--sanitize-title title))) + (should (string-equal title result)))) + +(chime-deftest test-chime-sanitize-title-nested-balanced-unchanged () "Test that nested balanced delimiters are unchanged. REFACTORED: No timestamps used" - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Review (PR [#123] {urgent})") - (result (chime--sanitize-title title))) - (should (string-equal title result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-no-delimiters-unchanged () + (let* ((title "Review (PR [#123] {urgent})") + (result (chime--sanitize-title title))) + (should (string-equal title result)))) + +(chime-deftest test-chime-sanitize-title-no-delimiters-unchanged () "Test that titles without delimiters are unchanged. REFACTORED: No timestamps used" - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Simple Meeting Title") - (result (chime--sanitize-title title))) - (should (string-equal title result))) - (test-chime-sanitize-title-teardown))) + (let* ((title "Simple Meeting Title") + (result (chime--sanitize-title title))) + (should (string-equal title result)))) ;;; Unmatched Opening Delimiters -(ert-deftest test-chime-sanitize-title-unmatched-opening-paren () +(chime-deftest test-chime-sanitize-title-unmatched-opening-paren () "Test that unmatched opening parenthesis is closed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "CTO/COO XLT (Extended Leadership") - (result (chime--sanitize-title title))) - (should (string-equal "CTO/COO XLT (Extended Leadership)" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-unmatched-opening-paren-at-end () + (let* ((title "CTO/COO XLT (Extended Leadership") + (result (chime--sanitize-title title))) + (should (string-equal "CTO/COO XLT (Extended Leadership)" result)))) + +(chime-deftest test-chime-sanitize-title-unmatched-opening-paren-at-end () "Test that unmatched opening parenthesis at end is closed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Spice Cake (") - (result (chime--sanitize-title title))) - (should (string-equal "Spice Cake ()" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-multiple-unmatched-opening-parens () + (let* ((title "Spice Cake (") + (result (chime--sanitize-title title))) + (should (string-equal "Spice Cake ()" result)))) + +(chime-deftest test-chime-sanitize-title-multiple-unmatched-opening-parens () "Test that multiple unmatched opening parentheses are closed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting (Team (Sync") - (result (chime--sanitize-title title))) - (should (string-equal "Meeting (Team (Sync))" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-unmatched-opening-bracket () + (let* ((title "Meeting (Team (Sync") + (result (chime--sanitize-title title))) + (should (string-equal "Meeting (Team (Sync))" result)))) + +(chime-deftest test-chime-sanitize-title-unmatched-opening-bracket () "Test that unmatched opening bracket is closed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Review [PR #123") - (result (chime--sanitize-title title))) - (should (string-equal "Review [PR #123]" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-unmatched-opening-brace () + (let* ((title "Review [PR #123") + (result (chime--sanitize-title title))) + (should (string-equal "Review [PR #123]" result)))) + +(chime-deftest test-chime-sanitize-title-unmatched-opening-brace () "Test that unmatched opening brace is closed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Code Review {urgent") - (result (chime--sanitize-title title))) - (should (string-equal "Code Review {urgent}" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-mixed-unmatched-opening-delimiters () + (let* ((title "Code Review {urgent") + (result (chime--sanitize-title title))) + (should (string-equal "Code Review {urgent}" result)))) + +(chime-deftest test-chime-sanitize-title-mixed-unmatched-opening-delimiters () "Test that mixed unmatched opening delimiters are all closed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting [Team (Sync {Urgent") - (result (chime--sanitize-title title))) - (should (string-equal "Meeting [Team (Sync {Urgent})]" result))) - (test-chime-sanitize-title-teardown))) + (let* ((title "Meeting [Team (Sync {Urgent") + (result (chime--sanitize-title title))) + (should (string-equal "Meeting [Team (Sync {Urgent})]" result)))) ;;; Unmatched Closing Delimiters -(ert-deftest test-chime-sanitize-title-unmatched-closing-paren () +(chime-deftest test-chime-sanitize-title-unmatched-closing-paren () "Test that unmatched closing parenthesis is removed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting Title)") - (result (chime--sanitize-title title))) - (should (string-equal "Meeting Title" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-multiple-unmatched-closing-parens () + (let* ((title "Meeting Title)") + (result (chime--sanitize-title title))) + (should (string-equal "Meeting Title" result)))) + +(chime-deftest test-chime-sanitize-title-multiple-unmatched-closing-parens () "Test that multiple unmatched closing parentheses are removed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting Title))") - (result (chime--sanitize-title title))) - (should (string-equal "Meeting Title" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-unmatched-closing-bracket () + (let* ((title "Meeting Title))") + (result (chime--sanitize-title title))) + (should (string-equal "Meeting Title" result)))) + +(chime-deftest test-chime-sanitize-title-unmatched-closing-bracket () "Test that unmatched closing bracket is removed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Review PR]") - (result (chime--sanitize-title title))) - (should (string-equal "Review PR" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-unmatched-closing-brace () + (let* ((title "Review PR]") + (result (chime--sanitize-title title))) + (should (string-equal "Review PR" result)))) + +(chime-deftest test-chime-sanitize-title-unmatched-closing-brace () "Test that unmatched closing brace is removed." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Code Review}") - (result (chime--sanitize-title title))) - (should (string-equal "Code Review" result))) - (test-chime-sanitize-title-teardown))) + (let* ((title "Code Review}") + (result (chime--sanitize-title title))) + (should (string-equal "Code Review" result)))) ;;; Complex Mixed Cases -(ert-deftest test-chime-sanitize-title-opening-and-closing-mixed () +(chime-deftest test-chime-sanitize-title-opening-and-closing-mixed () "Test title with both unmatched opening and closing delimiters." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting (Team) Extra)") - (result (chime--sanitize-title title))) - ;; Should remove the extra closing paren - (should (string-equal "Meeting (Team) Extra" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-complex-nesting-with-unmatched () + (let* ((title "Meeting (Team) Extra)") + (result (chime--sanitize-title title))) + ;; Should remove the extra closing paren + (should (string-equal "Meeting (Team) Extra" result)))) + +(chime-deftest test-chime-sanitize-title-complex-nesting-with-unmatched () "Test complex nested delimiters with some unmatched." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting [Team (Sync] Extra") - (result (chime--sanitize-title title))) - ;; The ']' doesn't match the '[' (because '(' is in between) - ;; So it's removed, and we close the '(' and '[' properly: ')' and ']' - (should (string-equal "Meeting [Team (Sync Extra)]" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-all-types-unmatched () + (let* ((title "Meeting [Team (Sync] Extra") + (result (chime--sanitize-title title))) + ;; The ']' doesn't match the '[' (because '(' is in between) + ;; So it's removed, and we close the '(' and '[' properly: ')' and ']' + (should (string-equal "Meeting [Team (Sync Extra)]" result)))) + +(chime-deftest test-chime-sanitize-title-all-types-unmatched () "Test with all three delimiter types unmatched." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting (Team [Project {Status") - (result (chime--sanitize-title title))) - (should (string-equal "Meeting (Team [Project {Status}])" result))) - (test-chime-sanitize-title-teardown))) + (let* ((title "Meeting (Team [Project {Status") + (result (chime--sanitize-title title))) + (should (string-equal "Meeting (Team [Project {Status}])" result)))) ;;; Edge Cases -(ert-deftest test-chime-sanitize-title-nil-returns-empty-string () +(chime-deftest test-chime-sanitize-title-nil-returns-empty-string () "Test that nil title returns empty string." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((result (chime--sanitize-title nil))) - (should (string-equal "" result))) - (test-chime-sanitize-title-teardown))) + (let* ((result (chime--sanitize-title nil))) + (should (string-equal "" result)))) -(ert-deftest test-chime-sanitize-title-empty-string-unchanged () +(chime-deftest test-chime-sanitize-title-empty-string-unchanged () "Test that empty string is unchanged." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "") - (result (chime--sanitize-title title))) - (should (string-equal "" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-only-opening-delimiters () + (let* ((title "") + (result (chime--sanitize-title title))) + (should (string-equal "" result)))) + +(chime-deftest test-chime-sanitize-title-only-opening-delimiters () "Test title with only opening delimiters." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "([{") - (result (chime--sanitize-title title))) - (should (string-equal "([{}])" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-only-closing-delimiters () + (let* ((title "([{") + (result (chime--sanitize-title title))) + (should (string-equal "([{}])" result)))) + +(chime-deftest test-chime-sanitize-title-only-closing-delimiters () "Test title with only closing delimiters." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title ")]}") - (result (chime--sanitize-title title))) - (should (string-equal "" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-very-long-title-with-unmatched () + (let* ((title ")]}") + (result (chime--sanitize-title title))) + (should (string-equal "" result)))) + +(chime-deftest test-chime-sanitize-title-very-long-title-with-unmatched () "Test very long title with unmatched delimiter." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "This is a very long meeting title that contains many words and might wrap in the notification display (Extended Info") - (result (chime--sanitize-title title))) - (should (string-equal "This is a very long meeting title that contains many words and might wrap in the notification display (Extended Info)" result))) - (test-chime-sanitize-title-teardown))) + (let* ((title "This is a very long meeting title that contains many words and might wrap in the notification display (Extended Info") + (result (chime--sanitize-title title))) + (should (string-equal "This is a very long meeting title that contains many words and might wrap in the notification display (Extended Info)" result)))) ;;; Real-World Bug Cases -(ert-deftest test-chime-sanitize-title-bug-case-extended-leadership () +(chime-deftest test-chime-sanitize-title-bug-case-extended-leadership () "Test the actual bug case from vineti.meetings.org." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "1:01pm CTO/COO XLT (Extended Leadership") - (result (chime--sanitize-title title))) - (should (string-equal "1:01pm CTO/COO XLT (Extended Leadership)" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-bug-case-spice-cake () + (let* ((title "1:01pm CTO/COO XLT (Extended Leadership") + (result (chime--sanitize-title title))) + (should (string-equal "1:01pm CTO/COO XLT (Extended Leadership)" result)))) + +(chime-deftest test-chime-sanitize-title-bug-case-spice-cake () "Test the actual bug case from journal/2023-11-22.org." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Spice Cake (") - (result (chime--sanitize-title title))) - (should (string-equal "Spice Cake ()" result))) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-sanitize-title-lisp-serialization-safety () - "Test that sanitized title can be safely read by Lisp reader." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((title "Meeting (Team Sync") - (sanitized (chime--sanitize-title title)) - ;; Simulate what happens in async serialization - (serialized (format "'((title . \"%s\"))" sanitized))) - ;; This should not signal an error - (should (listp (read serialized))) - (should (string-equal "Meeting (Team Sync)" sanitized))) - (test-chime-sanitize-title-teardown))) + (let* ((title "Spice Cake (") + (result (chime--sanitize-title title))) + (should (string-equal "Spice Cake ()" result)))) -(ert-deftest test-chime-sanitize-title-async-serialization-with-unmatched-parens () +(chime-deftest test-chime-sanitize-title-lisp-serialization-safety () + "Test that sanitized title can be safely read by Lisp reader." + (let* ((title "Meeting (Team Sync") + (sanitized (chime--sanitize-title title)) + ;; Simulate what happens in async serialization + (serialized (format "'((title . \"%s\"))" sanitized))) + ;; This should not signal an error + (should (listp (read serialized))) + (should (string-equal "Meeting (Team Sync)" sanitized)))) + +(chime-deftest test-chime-sanitize-title-async-serialization-with-unmatched-parens () "Test that titles with unmatched parens won't break async serialization." - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((problematic-titles '("Meeting (Team" - "Review [PR" - "Code {Status" - "Event ((" - "Task ))"))) - (dolist (title problematic-titles) - (let* ((sanitized (chime--sanitize-title title)) - (serialized (format "'((title . \"%s\"))" sanitized))) - ;; Should not signal 'invalid-read-syntax error - (should (listp (read serialized)))))) - (test-chime-sanitize-title-teardown))) + (let* ((problematic-titles '("Meeting (Team" + "Review [PR" + "Code {Status" + "Event ((" + "Task ))"))) + (dolist (title problematic-titles) + (let* ((sanitized (chime--sanitize-title title)) + (serialized (format "'((title . \"%s\"))" sanitized))) + ;; Should not signal 'invalid-read-syntax error + (should (listp (read serialized))))))) ;;; Integration with chime--extract-title -(ert-deftest test-chime-extract-title-sanitizes-output () +(chime-deftest test-chime-extract-title-sanitizes-output () "Test that chime--extract-title applies sanitization. REFACTORED: Uses dynamic timestamps" - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp (test-timestamp-string time)) - (test-file (chime-create-temp-test-file-with-content - (format "* TODO Meeting (Team Sync\n%s\n" timestamp))) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) ; Enable org-mode - (goto-char (point-min)) - ;; Search for the heading - (re-search-forward "^\\* TODO" nil t) - (beginning-of-line) - (let* ((marker (point-marker)) - (title (chime--extract-title marker))) - ;; Should be sanitized with closing paren added - (should (string-equal "Meeting (Team Sync)" title)))) - (kill-buffer test-buffer)) - (test-chime-sanitize-title-teardown))) - -(ert-deftest test-chime-extract-title-handles-nil () + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp (test-timestamp-string time)) + (test-file (chime-create-temp-test-file-with-content + (format "* TODO Meeting (Team Sync\n%s\n" timestamp))) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) ; Enable org-mode + (goto-char (point-min)) + ;; Search for the heading + (re-search-forward "^\\* TODO" nil t) + (beginning-of-line) + (let* ((marker (point-marker)) + (title (chime--extract-title marker))) + ;; Should be sanitized with closing paren added + (should (string-equal "Meeting (Team Sync)" title)))) + (kill-buffer test-buffer))) + +(chime-deftest test-chime-extract-title-handles-nil () "Test that chime--extract-title handles nil gracefully. REFACTORED: Uses dynamic timestamps" - (test-chime-sanitize-title-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp (test-timestamp-string time)) - (test-file (chime-create-temp-test-file-with-content - (format "* TODO\n%s\n" timestamp))) - (test-buffer (find-file-noselect test-file))) - (with-current-buffer test-buffer - (org-mode) ; Enable org-mode - (goto-char (point-min)) - ;; Search for the heading - (re-search-forward "^\\* TODO" nil t) - (beginning-of-line) - (let* ((marker (point-marker)) - (title (chime--extract-title marker))) - ;; Should return empty string for nil title - (should (string-equal "" title)))) - (kill-buffer test-buffer)) - (test-chime-sanitize-title-teardown))) + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp (test-timestamp-string time)) + (test-file (chime-create-temp-test-file-with-content + (format "* TODO\n%s\n" timestamp))) + (test-buffer (find-file-noselect test-file))) + (with-current-buffer test-buffer + (org-mode) ; Enable org-mode + (goto-char (point-min)) + ;; Search for the heading + (re-search-forward "^\\* TODO" nil t) + (beginning-of-line) + (let* ((marker (point-marker)) + (title (chime--extract-title marker))) + ;; Should return empty string for nil title + (should (string-equal "" title)))) + (kill-buffer test-buffer))) (provide 'test-chime-sanitize-title) ;;; test-chime-sanitize-title.el ends here diff --git a/tests/test-chime-sound.el b/tests/test-chime-sound.el new file mode 100644 index 0000000..c43231a --- /dev/null +++ b/tests/test-chime-sound.el @@ -0,0 +1,334 @@ +;;; test-chime-sound.el --- Tests for chime's sound playback -*- lexical-binding: t; -*- + +;; Copyright (C) 2024-2026 Craig Jennings + +;; Author: Craig Jennings <c@cjennings.net> + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Unit tests for `chime--play-sound', `chime--find-sound-player' and +;; `chime--play-sound-external'. +;; +;; Emacs' built-in `play-sound-file' talks to ALSA directly and dies with +;; "No usable sound device driver found" on any system whose ALSA `default' +;; PCM doesn't resolve -- which is every PipeWire box that never got +;; `pcm.!default' pointed at the sound server. It is also synchronous. +;; So chime prefers an external player and keeps `play-sound-file' as the +;; fallback. These tests pin that contract. +;; +;; `play-sound-file' is the system boundary here and is always mocked, so +;; no test opens an audio device. Where a test needs a real process -- +;; asynchrony and the exit-status sentinel cannot be observed through a +;; mock -- it spawns `true' or `false' as a stand-in player. No real audio +;; player is ever run. +;; +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) + +;;; Helpers + +(defconst test-chime-sound--real-start-process + (symbol-function 'start-process) + "The real `start-process', captured so mocks can still spawn a stand-in. +Mocking `set-process-query-on-exit-flag' would need a native-comp +trampoline, so tests hand the production code a real process instead.") + +(defun test-chime-sound--existing-file () + "Return the path of a real file inside the test base dir." + (let ((file (expand-file-name "chime-test.wav" chime-test-base-dir))) + (with-temp-file file (insert "not really a wav")) + file)) + +(defun test-chime-sound--stand-in-process () + "Spawn a real, immediately-exiting process to stand in for a player." + (funcall test-chime-sound--real-start-process "chime-sound-test" nil "true")) + +(defun test-chime-sound--chime-messages (messages) + "Return only the chime-emitted entries of MESSAGES. +A test that mocks `message' captures every message Emacs emits, not just +chime's. Under a coverage run the source is loaded uninstrumented and the +sentinel lambda is compiled on first call, so a byte-compiler warning lands +in the capture list and a bare count assertion fails. Assert on what chime +said, not on Emacs having said nothing." + (seq-filter (lambda (m) (string-prefix-p "chime:" m)) messages)) + +(defun test-chime-sound--await-exit (process &optional until) + "Wait for PROCESS to exit and for its sentinel to have run. +UNTIL, when given, is polled after each turn of the event loop; waiting +stops as soon as it returns non-nil. + +`process-live-p' goes nil the moment the child exits, but the sentinel is +queued and runs on a *later* turn of the event loop. A test that stops +waiting at process death therefore lets the sentinel's message land inside +the next test's `message' mock. That is not hypothetical: it put \"false +exited with status 1\" into the silent-player test under a coverage run, +where instrumentation slowed things just enough to expose the race." + (while (process-live-p process) + (accept-process-output process 0 50)) + (let ((deadline (time-add (current-time) + (seconds-to-time (if until 2 0.3))))) + (while (and (time-less-p (current-time) deadline) + (not (and until (funcall until)))) + (accept-process-output nil 0 20)))) + +;;; Normal Cases + +(chime-deftest test-chime-sound-find-sound-player-auto-returns-first-available () + "Auto mode returns the only candidate found on PATH." + (let ((chime-sound-player 'auto)) + (cl-letf (((symbol-function 'executable-find) + (lambda (cmd) (equal cmd "aplay")))) + ;; The sound-server players are absent, so aplay is all that is left. + (should (equal (chime--find-sound-player) "aplay"))))) + +(chime-deftest test-chime-sound-find-sound-player-prefers-sound-server-over-raw-alsa () + "Auto mode prefers a sound-server player over raw ALSA." + (let ((chime-sound-player 'auto)) + (cl-letf (((symbol-function 'executable-find) + (lambda (cmd) (member cmd '("pw-play" "paplay" "aplay"))))) + (should (equal (chime--find-sound-player) "pw-play"))))) + +(chime-deftest test-chime-sound-find-sound-player-explicit-string-is-used () + "An explicit player string is used when it is on PATH." + (let ((chime-sound-player "mpv")) + (cl-letf (((symbol-function 'executable-find) + (lambda (cmd) (equal cmd "mpv")))) + (should (equal (chime--find-sound-player) "mpv"))))) + +(chime-deftest test-chime-sound-find-sound-player-emacs-mode-returns-nil () + "Emacs mode never selects an external player." + (let ((chime-sound-player 'emacs)) + (cl-letf (((symbol-function 'executable-find) (lambda (_cmd) t))) + (should-not (chime--find-sound-player))))) + +(chime-deftest test-chime-sound-play-sound-spawns-external-player () + "The external player is spawned with the sound file, and Emacs' player is not used." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player 'auto) + (spawned nil) + (emacs-player-called nil)) + (cl-letf (((symbol-function 'executable-find) + (lambda (cmd) (equal cmd "pw-play"))) + ((symbol-function 'start-process) + (lambda (_name _buffer program &rest args) + (setq spawned (cons program args)) + (test-chime-sound--stand-in-process))) + ((symbol-function 'play-sound-file) + (lambda (&rest _) (setq emacs-player-called t)))) + (chime--play-sound) + (should (equal spawned (list "pw-play" file))) + (should-not emacs-player-called)))) + +(chime-deftest test-chime-sound-play-sound-external-returns-a-process () + "External playback hands back a process rather than awaiting the clip. +Runs the real spawn path against `true', a harmless stand-in player. +Returning a process is the evidence that playback is asynchronous: the +synchronous `play-sound-file' path returns t instead." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player "true") + (emacs-player-called nil)) + (cl-letf (((symbol-function 'play-sound-file) + (lambda (&rest _) (setq emacs-player-called t)))) + (should (processp (chime--play-sound))) + (should-not emacs-player-called)))) + +(chime-deftest test-chime-sound-play-sound-emacs-mode-passes-device () + "Emacs mode calls `play-sound-file' with `chime-sound-device' as DEVICE." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player 'emacs) + (chime-sound-device "pipewire") + (call-args nil)) + (cl-letf (((symbol-function 'play-sound-file) + (lambda (&rest args) (setq call-args args)))) + (chime--play-sound) + (should (equal call-args (list file nil "pipewire")))))) + +;;; Boundary Cases + +(chime-deftest test-chime-sound-play-sound-nil-device-passed-through () + "A nil `chime-sound-device' is still passed as the DEVICE argument." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player 'emacs) + (chime-sound-device nil) + (call-args nil)) + (cl-letf (((symbol-function 'play-sound-file) + (lambda (&rest args) (setq call-args args)))) + (chime--play-sound) + (should (equal call-args (list file nil nil)))))) + +(chime-deftest test-chime-sound-play-sound-nil-sound-file-plays-nothing () + "No sound file configured means nothing is played." + (let ((chime-sound-file nil) + (chime-sound-player 'auto) + (played nil)) + (cl-letf (((symbol-function 'executable-find) (lambda (_cmd) t)) + ((symbol-function 'start-process) + (lambda (&rest _) (setq played t) 'fake-process)) + ((symbol-function 'play-sound-file) + (lambda (&rest _) (setq played t)))) + (chime--play-sound) + (should-not played)))) + +(chime-deftest test-chime-sound-play-sound-missing-file-plays-nothing () + "A configured but absent sound file means nothing is played." + (let ((chime-sound-file (expand-file-name "absent.wav" chime-test-base-dir)) + (chime-sound-player 'auto) + (played nil)) + (cl-letf (((symbol-function 'executable-find) (lambda (_cmd) t)) + ((symbol-function 'start-process) + (lambda (&rest _) (setq played t) 'fake-process)) + ((symbol-function 'play-sound-file) + (lambda (&rest _) (setq played t)))) + (chime--play-sound) + (should-not played)))) + +(chime-deftest test-chime-sound-play-sound-no-player-found-falls-back-to-emacs () + "With no external player on PATH, Emacs' `play-sound-file' is used." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player 'auto) + (emacs-player-called nil)) + (cl-letf (((symbol-function 'executable-find) (lambda (_cmd) nil)) + ((symbol-function 'play-sound-file) + (lambda (&rest _) (setq emacs-player-called t)))) + (chime--play-sound) + (should emacs-player-called)))) + +(chime-deftest test-chime-sound-play-sound-explicit-player-absent-falls-back-to-emacs () + "An explicit player that is not installed falls back to Emacs' player." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player "no-such-player") + (emacs-player-called nil)) + (cl-letf (((symbol-function 'executable-find) (lambda (_cmd) nil)) + ((symbol-function 'play-sound-file) + (lambda (&rest _) (setq emacs-player-called t)))) + (chime--play-sound) + (should emacs-player-called)))) + +;;; Error Cases + +(chime-deftest test-chime-sound-external-spawn-failure-falls-back-to-emacs () + "When spawning the external player signals, Emacs' player is used instead." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player 'auto) + (emacs-player-called nil)) + (cl-letf (((symbol-function 'executable-find) + (lambda (cmd) (equal cmd "pw-play"))) + ((symbol-function 'start-process) + (lambda (&rest _) (error "Spawning failed"))) + ((symbol-function 'play-sound-file) + (lambda (&rest _) (setq emacs-player-called t)))) + (chime--play-sound) + (should emacs-player-called)))) + +(chime-deftest test-chime-sound-play-sound-external-returns-nil-on-spawn-error () + "`chime--play-sound-external' reports failure rather than signalling." + (cl-letf (((symbol-function 'start-process) + (lambda (&rest _) (error "Spawning failed")))) + (should-not (chime--play-sound-external "pw-play" "/tmp/whatever.wav")))) + +(chime-deftest test-chime-sound-external-nonzero-exit-is-reported () + "A player that spawns but fails is reported rather than failing silently. +`false' stands in for a player that cannot decode the file." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player "false") + (messages nil)) + (cl-letf (((symbol-function 'message) + (lambda (fmt &rest args) (push (apply #'format fmt args) messages)))) + (let ((process (chime--play-sound))) + (should (processp process)) + ;; The sentinel fires after the player exits, on a later turn of the + ;; event loop -- wait for the message, not merely for the exit. + (test-chime-sound--await-exit + process (lambda () (test-chime-sound--chime-messages messages)))) + (let ((chime-messages (test-chime-sound--chime-messages messages))) + (should (= (length chime-messages) 1)) + (should (string-match-p "Failed to play sound" (car chime-messages))) + (should (string-match-p "false" (car chime-messages))))))) + +(chime-deftest test-chime-sound-external-zero-exit-is-silent () + "A player that succeeds reports nothing." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player "true") + (messages nil)) + (cl-letf (((symbol-function 'message) + (lambda (fmt &rest args) (push (apply #'format fmt args) messages)))) + (let ((process (chime--play-sound))) + ;; Drain the sentinel too, so a late message can't slip past the + ;; assertion and into the next test. + (test-chime-sound--await-exit process)) + (should-not (test-chime-sound--chime-messages messages))))) + +(chime-deftest test-chime-sound-both-players-failing-reports-once-and-does-not-signal () + "When both players fail, the error is reported and not propagated." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player 'auto) + (messages nil)) + (cl-letf (((symbol-function 'executable-find) + (lambda (cmd) (equal cmd "pw-play"))) + ((symbol-function 'start-process) + (lambda (&rest _) (error "Spawning failed"))) + ((symbol-function 'play-sound-file) + (lambda (&rest _) (error "No usable sound device driver found"))) + ((symbol-function 'message) + (lambda (fmt &rest args) (push (apply #'format fmt args) messages)))) + ;; Must not signal. + (should-not (condition-case nil + (progn (chime--play-sound) nil) + (error t))) + (let ((chime-messages (test-chime-sound--chime-messages messages))) + (should (= (length chime-messages) 1)) + (should (string-match-p "Failed to play sound" (car chime-messages))))))) + +(chime-deftest test-chime-sound-emacs-player-error-is-reported-not-signalled () + "A `play-sound-file' failure is reported and swallowed." + (let* ((file (test-chime-sound--existing-file)) + (chime-sound-file file) + (chime-sound-player 'emacs) + (messages nil)) + (cl-letf (((symbol-function 'play-sound-file) + (lambda (&rest _) (error "No usable sound device driver found"))) + ((symbol-function 'message) + (lambda (fmt &rest args) (push (apply #'format fmt args) messages)))) + (should-not (condition-case nil + (progn (chime--play-sound) nil) + (error t))) + (let ((chime-messages (test-chime-sound--chime-messages messages))) + (should (= (length chime-messages) 1)) + (should (string-match-p "Failed to play sound" (car chime-messages))))))) + +(chime-deftest test-chime-sound-device-rejects-non-string-at-customize-time () + "Setting `chime-sound-device' to a non-string is rejected." + (should-error (customize-set-variable 'chime-sound-device 42) + :type 'user-error) + ;; nil is allowed -- it means "let the system decide". + (customize-set-variable 'chime-sound-device nil) + (should-not chime-sound-device)) + +(provide 'test-chime-sound) +;;; test-chime-sound.el ends here diff --git a/tests/test-chime-time-left.el b/tests/test-chime-time-left.el index b9bd176..5d27636 100644 --- a/tests/test-chime-time-left.el +++ b/tests/test-chime-time-left.el @@ -26,9 +26,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) - ;;; Setup and Teardown (defun test-chime-time-left-setup () diff --git a/tests/test-chime-time-utilities.el b/tests/test-chime-time-utilities.el index 26e4d96..29488df 100644 --- a/tests/test-chime-time-utilities.el +++ b/tests/test-chime-time-utilities.el @@ -28,10 +28,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;;; Tests for chime--get-minutes-into-day ;;; Normal Cases diff --git a/tests/test-chime-timestamp-parse.el b/tests/test-chime-timestamp-parse.el index 5dc30bf..e06b832 100644 --- a/tests/test-chime-timestamp-parse.el +++ b/tests/test-chime-timestamp-parse.el @@ -26,375 +26,283 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - -;;; Setup and Teardown - -(defun test-chime-timestamp-parse-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-timestamp-parse-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Normal Cases -(ert-deftest test-chime-timestamp-parse-standard-timestamp-returns-time-list () +(chime-deftest test-chime-timestamp-parse-standard-timestamp-returns-time-list () "Test that a standard timestamp with time component returns a time list. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 30)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - ;; Should return a time list (list of integers) - (should (listp result)) - (should (= (length result) 2)) - (should (integerp (car result))) - (should (integerp (cadr result))) - ;; Result should not be nil - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-scheduled-timestamp-returns-time-list () + (let* ((time (test-time-tomorrow-at 14 30)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + ;; Should return a time list (list of integers) + (should (listp result)) + (should (= (length result) 2)) + (should (integerp (car result))) + (should (integerp (cadr result))) + ;; Result should not be nil + (should result))) + +(chime-deftest test-chime-timestamp-parse-scheduled-timestamp-returns-time-list () "Test that a SCHEDULED timestamp parses correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 9 0)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-deadline-timestamp-returns-time-list () + (let* ((time (test-time-tomorrow-at 9 0)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-deadline-timestamp-returns-time-list () "Test that a DEADLINE timestamp parses correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 17 0)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-timestamp-with-weekly-repeater-returns-time-list () + (let* ((time (test-time-tomorrow-at 17 0)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-timestamp-with-weekly-repeater-returns-time-list () "Test that a timestamp with +1w repeater parses correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp (format-time-string "<%Y-%m-%d %a %H:%M +1w>" time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-timestamp-with-completion-repeater-returns-time-list () + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp (format-time-string "<%Y-%m-%d %a %H:%M +1w>" time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-timestamp-with-completion-repeater-returns-time-list () "Test that a timestamp with .+1d repeater parses correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 8 0)) - (timestamp (format-time-string "<%Y-%m-%d %a %H:%M .+1d>" time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-timestamp-with-catchup-repeater-returns-time-list () + (let* ((time (test-time-tomorrow-at 8 0)) + (timestamp (format-time-string "<%Y-%m-%d %a %H:%M .+1d>" time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-timestamp-with-catchup-repeater-returns-time-list () "Test that a timestamp with ++1w repeater parses correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 10 30)) - (timestamp (format-time-string "<%Y-%m-%d %a %H:%M ++1w>" time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-timestamp-with-time-range-returns-start-time () + (let* ((time (test-time-tomorrow-at 10 30)) + (timestamp (format-time-string "<%Y-%m-%d %a %H:%M ++1w>" time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-timestamp-with-time-range-returns-start-time () "Test that a timestamp with time range returns the start time. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 14 0)) - (timestamp (format-time-string "<%Y-%m-%d %a %H:%M-15:30>" time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-timestamp-with-date-range-returns-start-date () + (let* ((time (test-time-tomorrow-at 14 0)) + (timestamp (format-time-string "<%Y-%m-%d %a %H:%M-15:30>" time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-timestamp-with-date-range-returns-start-date () "Test that a timestamp with date range returns start date time. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time1 (test-time-tomorrow-at 10 0)) - (time2 (test-time-days-from-now 2)) - (timestamp (concat (test-timestamp-string time1) "--" - (format-time-string "<%Y-%m-%d %a %H:%M>" time2))) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) + (let* ((time1 (test-time-tomorrow-at 10 0)) + (time2 (test-time-days-from-now 2)) + (timestamp (concat (test-timestamp-string time1) "--" + (format-time-string "<%Y-%m-%d %a %H:%M>" time2))) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) ;;; Boundary Cases -(ert-deftest test-chime-timestamp-parse-midnight-timestamp-returns-time-list () +(chime-deftest test-chime-timestamp-parse-midnight-timestamp-returns-time-list () "Test that midnight (00:00) timestamp parses correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 0 0)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-last-minute-of-day-returns-time-list () + (let* ((time (test-time-tomorrow-at 0 0)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-last-minute-of-day-returns-time-list () "Test that last minute of day (23:59) timestamp parses correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 23 59)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-year-boundary-new-years-eve-returns-time-list () + (let* ((time (test-time-tomorrow-at 23 59)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-year-boundary-new-years-eve-returns-time-list () "Test that New Year's Eve timestamp parses correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((now (test-time-now)) - (decoded (decode-time now)) - (year (nth 5 decoded)) - ;; Create Dec 31 at 23:30 for current test year - (time (encode-time 0 30 23 31 12 year)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-year-boundary-new-years-day-returns-time-list () + (let* ((now (test-time-now)) + (decoded (decode-time now)) + (year (nth 5 decoded)) + ;; Create Dec 31 at 23:30 for current test year + (time (encode-time 0 30 23 31 12 year)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-year-boundary-new-years-day-returns-time-list () "Test that New Year's Day timestamp parses correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((now (test-time-now)) - (decoded (decode-time now)) - (year (1+ (nth 5 decoded))) ; Next year - ;; Create Jan 1 at 00:30 for next test year - (time (encode-time 0 30 0 1 1 year)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-single-digit-time-returns-time-list () + (let* ((now (test-time-now)) + (decoded (decode-time now)) + (year (1+ (nth 5 decoded))) ; Next year + ;; Create Jan 1 at 00:30 for next test year + (time (encode-time 0 30 0 1 1 year)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-single-digit-time-returns-time-list () "Test that single-digit hours and minutes parse correctly. REFACTORED: Uses dynamic timestamps" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((time (test-time-tomorrow-at 1 5)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-leap-year-feb-29-returns-time-list () + (let* ((time (test-time-tomorrow-at 1 5)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-leap-year-feb-29-returns-time-list () "Test that Feb 29 in leap year parses correctly. REFACTORED: Uses dynamic timestamps (2024 leap year)" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* (;; Use 2024 as a known leap year - (time (encode-time 0 0 14 29 2 2024)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-month-boundary-end-of-month-returns-time-list () + (let* (;; Use 2024 as a known leap year + (time (encode-time 0 0 14 29 2 2024)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) + +(chime-deftest test-chime-timestamp-parse-month-boundary-end-of-month-returns-time-list () "Test that end of month timestamp parses correctly. REFACTORED: Uses dynamic timestamps (Oct 31)" - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((now (test-time-now)) - (decoded (decode-time now)) - (year (nth 5 decoded)) - ;; Create Oct 31 at 14:00 for current test year - (time (encode-time 0 0 14 31 10 year)) - (timestamp (test-timestamp-string time)) - (result (chime--timestamp-parse timestamp))) - (should (listp result)) - (should (= (length result) 2)) - (should result)) - (test-chime-timestamp-parse-teardown))) + (let* ((now (test-time-now)) + (decoded (decode-time now)) + (year (nth 5 decoded)) + ;; Create Oct 31 at 14:00 for current test year + (time (encode-time 0 0 14 31 10 year)) + (timestamp (test-timestamp-string time)) + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result))) ;;; Bug Reproduction Tests -(ert-deftest test-chime-timestamp-parse-tomorrow-timestamp-returns-correct-date () +(chime-deftest test-chime-timestamp-parse-tomorrow-timestamp-returns-correct-date () "Test that a tomorrow timestamp is parsed as tomorrow, not today. This reproduces the bug where timestamps like <2025-11-03 Mon 10:00-10:30> on Nov 02 are incorrectly grouped as 'Today' instead of 'Tomorrow'." - (test-chime-timestamp-parse-setup) - (unwind-protect - (with-test-time (encode-time 0 23 11 2 11 2025) ; Nov 02, 2025 11:23:00 AM - (let* ((tomorrow-timestamp "<2025-11-03 Mon 10:00-10:30>") - (parsed (chime--timestamp-parse tomorrow-timestamp)) - (now (current-time))) - ;; Should parse successfully - (should parsed) - ;; Convert parsed time (HIGH LOW) to full time by appending (0 0) - (let* ((parsed-time (append parsed '(0 0))) - (parsed-decoded (decode-time parsed-time)) - (time-diff-seconds (- (time-to-seconds parsed-time) - (time-to-seconds now)))) - ;; Verify the parsed date is Nov 03, 2025 (not Nov 02!) - (should (= 3 (decoded-time-day parsed-decoded))) - (should (= 11 (decoded-time-month parsed-decoded))) - (should (= 2025 (decoded-time-year parsed-decoded))) - ;; Verify the parsed time is 10:00 - (should (= 10 (decoded-time-hour parsed-decoded))) - (should (= 0 (decoded-time-minute parsed-decoded))) - ;; Time difference should be ~22h 37m (81420 seconds) - (should (> time-diff-seconds 81360)) ; At least 22h 36m - (should (< time-diff-seconds 81480))))) ; At most 22h 38m - (test-chime-timestamp-parse-teardown))) + (with-test-time (encode-time 0 23 11 2 11 2025) ; Nov 02, 2025 11:23:00 AM + (let* ((tomorrow-timestamp "<2025-11-03 Mon 10:00-10:30>") + (parsed (chime--timestamp-parse tomorrow-timestamp)) + (now (current-time))) + ;; Should parse successfully + (should parsed) + ;; Convert parsed time (HIGH LOW) to full time by appending (0 0) + (let* ((parsed-time (append parsed '(0 0))) + (parsed-decoded (decode-time parsed-time)) + (time-diff-seconds (- (time-to-seconds parsed-time) + (time-to-seconds now)))) + ;; Verify the parsed date is Nov 03, 2025 (not Nov 02!) + (should (= 3 (decoded-time-day parsed-decoded))) + (should (= 11 (decoded-time-month parsed-decoded))) + (should (= 2025 (decoded-time-year parsed-decoded))) + ;; Verify the parsed time is 10:00 + (should (= 10 (decoded-time-hour parsed-decoded))) + (should (= 0 (decoded-time-minute parsed-decoded))) + ;; Time difference should be ~22h 37m (81420 seconds) + (should (> time-diff-seconds 81360)) ; At least 22h 36m + (should (< time-diff-seconds 81480)))))) ;;; Error Cases -(ert-deftest test-chime-timestamp-parse-empty-string-returns-nil () +(chime-deftest test-chime-timestamp-parse-empty-string-returns-nil () "Test that empty string returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp "") - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-nil-input-returns-nil () + (let* ((timestamp "") + (result (chime--timestamp-parse timestamp))) + (should (null result)))) + +(chime-deftest test-chime-timestamp-parse-nil-input-returns-nil () "Test that nil input returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp nil) - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-missing-opening-bracket-returns-nil () + (let* ((timestamp nil) + (result (chime--timestamp-parse timestamp))) + (should (null result)))) + +(chime-deftest test-chime-timestamp-parse-missing-opening-bracket-returns-nil () "Test that timestamp missing opening bracket returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp "2025-10-24 Fri 14:00>") - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-missing-closing-bracket-returns-nil () + (let* ((timestamp "2025-10-24 Fri 14:00>") + (result (chime--timestamp-parse timestamp))) + (should (null result)))) + +(chime-deftest test-chime-timestamp-parse-missing-closing-bracket-returns-nil () "Test that timestamp missing closing bracket returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp "<2025-10-24 Fri 14:00") - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-invalid-date-format-returns-nil () + (let* ((timestamp "<2025-10-24 Fri 14:00") + (result (chime--timestamp-parse timestamp))) + (should (null result)))) + +(chime-deftest test-chime-timestamp-parse-invalid-date-format-returns-nil () "Test that invalid date format returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp "<10-24-2025 Fri 14:00>") - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-invalid-month-returns-nil () + (let* ((timestamp "<10-24-2025 Fri 14:00>") + (result (chime--timestamp-parse timestamp))) + (should (null result)))) + +(chime-deftest test-chime-timestamp-parse-invalid-month-returns-nil () "Test that invalid month value returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp "<2025-13-24 Fri 14:00>") - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-invalid-day-returns-nil () + (let* ((timestamp "<2025-13-24 Fri 14:00>") + (result (chime--timestamp-parse timestamp))) + (should (null result)))) + +(chime-deftest test-chime-timestamp-parse-invalid-day-returns-nil () "Test that invalid day value returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp "<2025-10-32 Fri 14:00>") - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-invalid-time-hour-returns-nil () + (let* ((timestamp "<2025-10-32 Fri 14:00>") + (result (chime--timestamp-parse timestamp))) + (should (null result)))) + +(chime-deftest test-chime-timestamp-parse-invalid-time-hour-returns-nil () "Test that invalid hour value returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp "<2025-10-24 Fri 25:00>") - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-invalid-time-minute-returns-nil () + (let* ((timestamp "<2025-10-24 Fri 25:00>") + (result (chime--timestamp-parse timestamp))) + (should (null result)))) + +(chime-deftest test-chime-timestamp-parse-invalid-time-minute-returns-nil () "Test that invalid minute value returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp "<2025-10-24 Fri 14:60>") - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) - -(ert-deftest test-chime-timestamp-parse-date-only-no-time-returns-nil () + (let* ((timestamp "<2025-10-24 Fri 14:60>") + (result (chime--timestamp-parse timestamp))) + (should (null result)))) + +(chime-deftest test-chime-timestamp-parse-date-only-no-time-returns-nil () "Test that day-wide timestamp without time returns nil." - (test-chime-timestamp-parse-setup) - (unwind-protect - (let* ((timestamp "<2025-10-24 Fri>") - (result (chime--timestamp-parse timestamp))) - (should (null result))) - (test-chime-timestamp-parse-teardown))) + (let* ((timestamp "<2025-10-24 Fri>") + (result (chime--timestamp-parse timestamp))) + (should (null result)))) (provide 'test-chime-timestamp-parse) ;;; test-chime-timestamp-parse.el ends here diff --git a/tests/test-chime-timestamp-within-interval-p.el b/tests/test-chime-timestamp-within-interval-p.el index 2cb6439..5857463 100644 --- a/tests/test-chime-timestamp-within-interval-p.el +++ b/tests/test-chime-timestamp-within-interval-p.el @@ -26,287 +26,222 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - -;;; Setup and Teardown - -(defun test-chime-timestamp-within-interval-p-setup () - "Setup function run before each test." - (chime-create-test-base-dir)) - -(defun test-chime-timestamp-within-interval-p-teardown () - "Teardown function run after each test." - (chime-delete-test-base-dir)) - ;;; Normal Cases -(ert-deftest test-chime-timestamp-within-interval-p-exactly-at-interval-returns-t () +(chime-deftest test-chime-timestamp-within-interval-p-exactly-at-interval-returns-t () "Test that timestamp exactly at interval returns t. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Timestamp at 14:10 (10 minutes from 14:00) - (timestamp (test-time-today-at 14 10)) - (interval 10)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-zero-interval-returns-t () + (let* ((now (test-time-today-at 14 0)) + ;; Timestamp at 14:10 (10 minutes from 14:00) + (timestamp (test-time-today-at 14 10)) + (interval 10)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-zero-interval-returns-t () "Test that zero interval (notify now) returns t for current time. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 30)) - ;; Timestamp at exactly current time (14:30) - (timestamp (test-time-today-at 14 30)) - (interval 0)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-five-minutes-returns-t () + (let* ((now (test-time-today-at 14 30)) + ;; Timestamp at exactly current time (14:30) + (timestamp (test-time-today-at 14 30)) + (interval 0)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-five-minutes-returns-t () "Test that 5-minute interval works correctly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 25)) - ;; Timestamp at 14:30 (5 minutes from 14:25) - (timestamp (test-time-today-at 14 30)) - (interval 5)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-sixty-minutes-returns-t () + (let* ((now (test-time-today-at 14 25)) + ;; Timestamp at 14:30 (5 minutes from 14:25) + (timestamp (test-time-today-at 14 30)) + (interval 5)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-sixty-minutes-returns-t () "Test that 60-minute (1 hour) interval works correctly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Timestamp at 15:00 (60 minutes from 14:00) - (timestamp (test-time-today-at 15 0)) - (interval 60)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-large-interval-returns-t () + (let* ((now (test-time-today-at 14 0)) + ;; Timestamp at 15:00 (60 minutes from 14:00) + (timestamp (test-time-today-at 15 0)) + (interval 60)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-large-interval-returns-t () "Test that large interval (1 day = 1440 minutes) works correctly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Timestamp at 14:00 next day (1440 minutes from now) - ;; Add 86400 seconds (1440 minutes = 1 day) to now - ;; Convert to list format for compatibility - (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 86400))))) - (interval 1440)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-thirty-minutes-returns-t () + (let* ((now (test-time-today-at 14 0)) + ;; Timestamp at 14:00 next day (1440 minutes from now) + ;; Add 86400 seconds (1440 minutes = 1 day) to now + ;; Convert to list format for compatibility + (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 86400))))) + (interval 1440)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-thirty-minutes-returns-t () "Test that 30-minute interval works correctly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 15)) - ;; Timestamp at 14:45 (30 minutes from 14:15) - (timestamp (test-time-today-at 14 45)) - (interval 30)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) + (let* ((now (test-time-today-at 14 15)) + ;; Timestamp at 14:45 (30 minutes from 14:15) + (timestamp (test-time-today-at 14 45)) + (interval 30)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) ;;; Boundary Cases -(ert-deftest test-chime-timestamp-within-interval-p-one-minute-before-returns-nil () +(chime-deftest test-chime-timestamp-within-interval-p-one-minute-before-returns-nil () "Test that timestamp 1 minute before interval returns nil. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Timestamp at 14:09 (9 minutes from 14:00, not 10) - (timestamp (test-time-today-at 14 9)) - (interval 10)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should-not result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-one-minute-after-returns-nil () + (let* ((now (test-time-today-at 14 0)) + ;; Timestamp at 14:09 (9 minutes from 14:00, not 10) + (timestamp (test-time-today-at 14 9)) + (interval 10)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should-not result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-one-minute-after-returns-nil () "Test that timestamp 1 minute after interval returns nil. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Timestamp at 14:11 (11 minutes from 14:00, not 10) - (timestamp (test-time-today-at 14 11)) - (interval 10)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should-not result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-crossing-midnight-returns-t () + (let* ((now (test-time-today-at 14 0)) + ;; Timestamp at 14:11 (11 minutes from 14:00, not 10) + (timestamp (test-time-today-at 14 11)) + (interval 10)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should-not result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-crossing-midnight-returns-t () "Test that interval crossing midnight works correctly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 23 50)) - ;; Timestamp at 00:00 next day (10 minutes from 23:50) - ;; Add 600 seconds (10 minutes) to 23:50 to get 00:00 next day - ;; Convert to list format for compatibility - (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 600))))) - (interval 10)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-crossing-day-boundary-returns-t () + (let* ((now (test-time-today-at 23 50)) + ;; Timestamp at 00:00 next day (10 minutes from 23:50) + ;; Add 600 seconds (10 minutes) to 23:50 to get 00:00 next day + ;; Convert to list format for compatibility + (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 600))))) + (interval 10)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-crossing-day-boundary-returns-t () "Test that interval crossing to next day works correctly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 23 30)) - ;; Timestamp at 00:30 next day (60 minutes from 23:30) - ;; Add 3600 seconds (60 minutes) to 23:30 to get 00:30 next day - ;; Convert to list format for compatibility - (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 3600))))) - (interval 60)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-week-interval-returns-t () + (let* ((now (test-time-today-at 23 30)) + ;; Timestamp at 00:30 next day (60 minutes from 23:30) + ;; Add 3600 seconds (60 minutes) to 23:30 to get 00:30 next day + ;; Convert to list format for compatibility + (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 3600))))) + (interval 60)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-week-interval-returns-t () "Test that very large interval (1 week = 10080 minutes) works. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Timestamp at 14:00 one week later (10080 minutes = 7 days from now) - ;; Add 604800 seconds (10080 minutes = 7 days) to now - ;; Convert to list format for compatibility - (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 604800))))) - (interval 10080)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-at-midnight-returns-t () + (let* ((now (test-time-today-at 14 0)) + ;; Timestamp at 14:00 one week later (10080 minutes = 7 days from now) + ;; Add 604800 seconds (10080 minutes = 7 days) to now + ;; Convert to list format for compatibility + (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 604800))))) + (interval 10080)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-at-midnight-returns-t () "Test that timestamp at exact midnight works correctly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 23 50)) - ;; Timestamp at midnight (10 minutes from 23:50) - ;; Add 600 seconds (10 minutes) to 23:50 to get 00:00 next day - ;; Convert to list format for compatibility - (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 600))))) - (interval 10)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) + (let* ((now (test-time-today-at 23 50)) + ;; Timestamp at midnight (10 minutes from 23:50) + ;; Add 600 seconds (10 minutes) to 23:50 to get 00:00 next day + ;; Convert to list format for compatibility + (timestamp (apply #'encode-time (decode-time (time-add now (seconds-to-time 600))))) + (interval 10)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) ;;; Error Cases -(ert-deftest test-chime-timestamp-within-interval-p-nil-timestamp-returns-nil () +(chime-deftest test-chime-timestamp-within-interval-p-nil-timestamp-returns-nil () "Test that nil timestamp returns nil. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (timestamp nil) - (interval 10)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should-not result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-nil-interval-returns-nil () + (let* ((now (test-time-today-at 14 0)) + (timestamp nil) + (interval 10)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should-not result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-nil-interval-returns-nil () "Test that nil interval returns nil. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (timestamp (test-time-today-at 14 10)) - (interval nil)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should-not result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-negative-interval-returns-nil () + (let* ((now (test-time-today-at 14 0)) + (timestamp (test-time-today-at 14 10)) + (interval nil)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should-not result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-negative-interval-returns-nil () "Test that negative interval returns nil (past timestamps). REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Timestamp 10 minutes in the past (13:50) - (timestamp (test-time-today-at 13 50)) - (interval -10)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-invalid-timestamp-returns-nil () + (let* ((now (test-time-today-at 14 0)) + ;; Timestamp 10 minutes in the past (13:50) + (timestamp (test-time-today-at 13 50)) + (interval -10)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-invalid-timestamp-returns-nil () "Test that invalid timestamp format returns nil. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - (timestamp "not-a-timestamp") - (interval 10)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should-not result)))) - (test-chime-timestamp-within-interval-p-teardown))) - -(ert-deftest test-chime-timestamp-within-interval-p-float-interval-works () + (let* ((now (test-time-today-at 14 0)) + (timestamp "not-a-timestamp") + (interval 10)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should-not result))))) + +(chime-deftest test-chime-timestamp-within-interval-p-float-interval-works () "Test that float interval gets converted properly. REFACTORED: Uses dynamic timestamps and with-test-time" - (test-chime-timestamp-within-interval-p-setup) - (unwind-protect - (let* ((now (test-time-today-at 14 0)) - ;; Timestamp at 14:10 (10 minutes from 14:00) - (timestamp (test-time-today-at 14 10)) - (interval 10.5)) - (with-test-time now - (let ((result (chime--timestamp-within-interval-p timestamp interval))) - (should result)))) - (test-chime-timestamp-within-interval-p-teardown))) + (let* ((now (test-time-today-at 14 0)) + ;; Timestamp at 14:10 (10 minutes from 14:00) + (timestamp (test-time-today-at 14 10)) + (interval 10.5)) + (with-test-time now + (let ((result (chime--timestamp-within-interval-p timestamp interval))) + (should result))))) (provide 'test-chime-timestamp-within-interval-p) ;;; test-chime-timestamp-within-interval-p.el ends here diff --git a/tests/test-chime-tooltip-bugs.el b/tests/test-chime-tooltip-bugs.el index 8a11317..0d141b9 100644 --- a/tests/test-chime-tooltip-bugs.el +++ b/tests/test-chime-tooltip-bugs.el @@ -8,8 +8,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) ;;; Setup and Teardown diff --git a/tests/test-chime-tooltip-day-calculation.el b/tests/test-chime-tooltip-day-calculation.el index 6e4f778..724ff9d 100644 --- a/tests/test-chime-tooltip-day-calculation.el +++ b/tests/test-chime-tooltip-day-calculation.el @@ -13,8 +13,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) (defmacro test-chime-tooltip-day-calculation--with-tooltip (now content &rest body) diff --git a/tests/test-chime-update-modeline-helpers.el b/tests/test-chime-update-modeline-helpers.el index 429265c..f28ad81 100644 --- a/tests/test-chime-update-modeline-helpers.el +++ b/tests/test-chime-update-modeline-helpers.el @@ -10,8 +10,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;;; Tests for chime--find-soonest-time-in-window diff --git a/tests/test-chime-update-modeline.el b/tests/test-chime-update-modeline.el index 525e39e..7b1a809 100644 --- a/tests/test-chime-update-modeline.el +++ b/tests/test-chime-update-modeline.el @@ -26,10 +26,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) - ;;; Setup and Teardown (defun test-chime-update-modeline-setup () diff --git a/tests/test-chime-validation-retry.el b/tests/test-chime-validation-retry.el index a2765f4..ec5e2fe 100644 --- a/tests/test-chime-validation-retry.el +++ b/tests/test-chime-validation-retry.el @@ -20,8 +20,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;; Setup and Teardown diff --git a/tests/test-chime-whitelist-blacklist-conflicts.el b/tests/test-chime-whitelist-blacklist-conflicts.el index 5becfe0..b2465c5 100644 --- a/tests/test-chime-whitelist-blacklist-conflicts.el +++ b/tests/test-chime-whitelist-blacklist-conflicts.el @@ -30,9 +30,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) - ;;; Setup and Teardown (defun test-chime-conflicts-setup () @@ -45,8 +42,11 @@ (defun test-chime-conflicts-teardown () "Teardown function run after each test." (chime-delete-test-base-dir) - (setq chime-include-filters nil) - (setq chime-exclude-filters nil)) + ;; Restore the defcustom defaults rather than leaving the globals clobbered. + ;; Each test let-binds both filter vars, so the globals only need to return + ;; to their standard values to keep cross-file test isolation. + (custom-reevaluate-setting 'chime-include-filters) + (custom-reevaluate-setting 'chime-exclude-filters)) ;;; Keyword Conflict Tests diff --git a/tests/test-integration-chime-mode.el b/tests/test-integration-chime-mode.el index 40436fb..229d877 100644 --- a/tests/test-integration-chime-mode.el +++ b/tests/test-integration-chime-mode.el @@ -28,8 +28,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) ;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;; Tests diff --git a/tests/test-integration-per-event-override.el b/tests/test-integration-per-event-override.el index 8455590..119e345 100644 --- a/tests/test-integration-per-event-override.el +++ b/tests/test-integration-per-event-override.el @@ -32,7 +32,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) (require 'cl-lib) (defun test-integration-per-event-override--events-from-agenda () diff --git a/tests/test-integration-recurring-events-tooltip.el b/tests/test-integration-recurring-events-tooltip.el index 810e3ee..c904d37 100644 --- a/tests/test-integration-recurring-events-tooltip.el +++ b/tests/test-integration-recurring-events-tooltip.el @@ -42,10 +42,6 @@ (setq chime-debug t) (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -;; 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-recurring--orig-agenda-files nil diff --git a/tests/test-integration-startup.el b/tests/test-integration-startup.el index 1f60b54..952a975 100644 --- a/tests/test-integration-startup.el +++ b/tests/test-integration-startup.el @@ -45,8 +45,6 @@ (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) ;; Load test utilities -(require 'testutil-general (expand-file-name "testutil-general.el")) -(require 'testutil-time (expand-file-name "testutil-time.el")) (require 'testutil-events (expand-file-name "testutil-events.el")) ;;; Helper Functions diff --git a/tests/test-integration-watchdog-signal.el b/tests/test-integration-watchdog-signal.el new file mode 100644 index 0000000..f786ff9 --- /dev/null +++ b/tests/test-integration-watchdog-signal.el @@ -0,0 +1,151 @@ +;;; test-integration-watchdog-signal.el --- Real-signal watchdog tests -*- lexical-binding: t; -*- + +;; Copyright (C) 2024-2026 Craig Jennings + +;; Author: Craig Jennings <c@cjennings.net> + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Integration tests for the watchdog against a genuinely stuck child. +;; +;; The unit suite (test-chime-async-watchdog.el, test-chime-async-lifecycle.el) +;; stubs the whole process layer, so the signal semantics the watchdog rests +;; on were unverified anywhere. These tests spawn real processes and let the +;; real kernel deliver the real signals. +;; +;; Components integrated: +;; - chime--kill-async-process (real) +;; - chime--interrupt-stale-process (real) +;; - Emacs process primitives: start-process, delete-process, process-live-p (real) +;; - chime--record-async-failure (MOCKED — the failure machinery has its own tests) +;; +;; Validates: +;; - A child that ignores SIGINT is still killed +;; - The child's process buffer is reaped, which async.el would leak +;; - The watchdog abandons the stuck child and lets the next tick spawn +;; +;; Tagged :slow: each test spawns processes and waits on the kernel. Run +;; with `make test-all'. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) + +;;; Helpers + +(defun test-chime-signal--spawn-sigint-immune-child () + "Spawn a real child that traps SIGINT and refuses to die from it. +This is the shape of the bug: a batch Emacs blocked on an interactive +prompt ignores the interrupt and lives on as a zombie." + (let ((process (start-process + "chime-sigint-immune" (generate-new-buffer " *chime-sig*") + "sh" "-c" "trap '' INT; sleep 60"))) + (set-process-query-on-exit-flag process nil) + ;; Give the shell a moment to install the trap. + (sleep-for 0.3) + process)) + +(defun test-chime-signal--wait-for-death (process seconds) + "Wait up to SECONDS for PROCESS to die. Return non-nil if it did." + (let ((deadline (time-add (current-time) (seconds-to-time seconds)))) + (while (and (process-live-p process) + (time-less-p (current-time) deadline)) + (accept-process-output process 0 50)) + (not (process-live-p process)))) + +;;; Integration: the kill path against a real signal-immune child + +(ert-deftest test-integration-watchdog-sigint-immune-child-is-still-killed () + "A child that traps SIGINT survives an interrupt but not `chime--kill-async-process'. +This is the whole reason the watchdog escalates: `interrupt-process' is a +request, and a stuck child is entitled to ignore it." + :tags '(:slow) + (let ((process (test-chime-signal--spawn-sigint-immune-child))) + (unwind-protect + (progn + (should (process-live-p process)) + ;; SIGINT: politely ignored. + (interrupt-process process) + (should-not (test-chime-signal--wait-for-death process 1.5)) + (should (process-live-p process)) + ;; The kill path does not ask. + (chime--kill-async-process process) + (should-not (process-live-p process))) + (when (process-live-p process) (delete-process process))))) + +(ert-deftest test-integration-watchdog-killed-child-leaves-no-buffer () + "The killed child's process buffer is reaped rather than leaked. +async.el kills a child's buffer only on a zero exit, so every signalled +child would otherwise leak one, once per `chime-async-timeout' for as long +as the hang persists." + :tags '(:slow) + (let* ((process (test-chime-signal--spawn-sigint-immune-child)) + (buffer (process-buffer process))) + (unwind-protect + (progn + (should (buffer-live-p buffer)) + (chime--kill-async-process process) + (should-not (process-live-p process)) + (should-not (buffer-live-p buffer))) + (when (process-live-p process) (delete-process process)) + (when (buffer-live-p buffer) (kill-buffer buffer))))) + +(ert-deftest test-integration-watchdog-abandons-a-real-stuck-child () + "The watchdog kills a real over-age child, records the failure, and clears state. +Only `chime--record-async-failure' is mocked; the process, the signal, and +the age check are all real." + :tags '(:slow) + (let* ((process (test-chime-signal--spawn-sigint-immune-child)) + (recorded nil) + (chime-async-timeout 1) + (chime--process process) + (chime--process-generation 0) + ;; Backdate the spawn so the child is over-age immediately. + (chime--process-start-time (time-subtract (current-time) + (seconds-to-time 30)))) + (unwind-protect + (progn + (cl-letf (((symbol-function 'chime--record-async-failure) + (lambda (err prefix) (setq recorded (cons prefix err))))) + (chime--interrupt-stale-process)) + (should-not (process-live-p process)) + (should-not chime--process) + (should-not chime--process-start-time) + ;; The straggler's callback is orphaned. + (should (= chime--process-generation 1)) + (should (equal "Async watchdog" (car recorded)))) + (when (process-live-p process) (delete-process process))))) + +(ert-deftest test-integration-watchdog-leaves-a-young-real-child-alone () + "A child inside its timeout is not killed, even though it is real and stuck." + :tags '(:slow) + (let* ((process (test-chime-signal--spawn-sigint-immune-child)) + (chime-async-timeout 60) + (chime--process process) + (chime--process-generation 0) + (chime--process-start-time (current-time))) + (unwind-protect + (progn + (chime--interrupt-stale-process) + (should (process-live-p process)) + (should (eq chime--process process)) + (should (= chime--process-generation 0))) + (when (process-live-p process) (delete-process process)) + (let ((buffer (process-buffer process))) + (when (buffer-live-p buffer) (kill-buffer buffer)))))) + +(provide 'test-integration-watchdog-signal) +;;; test-integration-watchdog-signal.el ends here diff --git a/tests/test-testutil-general.el b/tests/test-testutil-general.el index 7b4e445..c3c89f2 100644 --- a/tests/test-testutil-general.el +++ b/tests/test-testutil-general.el @@ -9,7 +9,6 @@ ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) -(require 'testutil-general (expand-file-name "testutil-general.el")) (ert-deftest test-chime-test-default-base-dir-uses-env-override () "Normal: CHIME_TEST_TMPDIR selects an explicit test root." |
