aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--chime.el355
-rw-r--r--docs/CONFIGURATION.org45
-rw-r--r--docs/TROUBLESHOOTING.org51
-rw-r--r--tests/Makefile31
-rw-r--r--tests/test-bootstrap.el23
-rw-r--r--tests/test-chime--deduplicate-events-by-title.el4
-rw-r--r--tests/test-chime-12hour-format.el4
-rw-r--r--tests/test-chime-all-day-events.el2
-rw-r--r--tests/test-chime-apply-blacklist.el8
-rw-r--r--tests/test-chime-apply-whitelist.el3
-rw-r--r--tests/test-chime-async-helpers.el1
-rw-r--r--tests/test-chime-async-injection.el101
-rw-r--r--tests/test-chime-async-lifecycle.el184
-rw-r--r--tests/test-chime-async-watchdog.el32
-rw-r--r--tests/test-chime-check-event.el2
-rw-r--r--tests/test-chime-check-interval.el3
-rw-r--r--tests/test-chime-day-wide-notifications.el1
-rw-r--r--tests/test-chime-day-wide-time-matching.el4
-rw-r--r--tests/test-chime-days-until-event.el131
-rw-r--r--tests/test-chime-debug-functions.el4
-rw-r--r--tests/test-chime-edge-coverage.el1
-rw-r--r--tests/test-chime-event-contract.el2
-rw-r--r--tests/test-chime-event-is-today.el4
-rw-r--r--tests/test-chime-extract-time.el361
-rw-r--r--tests/test-chime-extract-title.el3
-rw-r--r--tests/test-chime-filter-day-wide-events.el4
-rw-r--r--tests/test-chime-format-event-for-tooltip.el303
-rw-r--r--tests/test-chime-format-refresh.el4
-rw-r--r--tests/test-chime-gather-info.el2
-rw-r--r--tests/test-chime-get-tags.el3
-rw-r--r--tests/test-chime-group-events-by-day.el4
-rw-r--r--tests/test-chime-has-timestamp.el278
-rw-r--r--tests/test-chime-intervals-for-marker.el1
-rw-r--r--tests/test-chime-jump-to-event.el1
-rw-r--r--tests/test-chime-lifecycle.el31
-rw-r--r--tests/test-chime-make-tooltip.el2
-rw-r--r--tests/test-chime-modeline-faces.el2
-rw-r--r--tests/test-chime-modeline-no-events-text.el4
-rw-r--r--tests/test-chime-modeline.el2
-rw-r--r--tests/test-chime-notification-boundaries.el435
-rw-r--r--tests/test-chime-notification-text.el4
-rw-r--r--tests/test-chime-notifications.el310
-rw-r--r--tests/test-chime-notify.el59
-rw-r--r--tests/test-chime-overdue-todos.el377
-rw-r--r--tests/test-chime-process-notifications.el567
-rw-r--r--tests/test-chime-propertize-modeline.el2
-rw-r--r--tests/test-chime-sanitize-title.el466
-rw-r--r--tests/test-chime-sound.el334
-rw-r--r--tests/test-chime-time-left.el3
-rw-r--r--tests/test-chime-time-utilities.el4
-rw-r--r--tests/test-chime-timestamp-parse.el508
-rw-r--r--tests/test-chime-timestamp-within-interval-p.el379
-rw-r--r--tests/test-chime-tooltip-bugs.el2
-rw-r--r--tests/test-chime-tooltip-day-calculation.el2
-rw-r--r--tests/test-chime-update-modeline-helpers.el2
-rw-r--r--tests/test-chime-update-modeline.el4
-rw-r--r--tests/test-chime-validation-retry.el2
-rw-r--r--tests/test-chime-whitelist-blacklist-conflicts.el10
-rw-r--r--tests/test-integration-chime-mode.el2
-rw-r--r--tests/test-integration-per-event-override.el1
-rw-r--r--tests/test-integration-recurring-events-tooltip.el4
-rw-r--r--tests/test-integration-startup.el2
-rw-r--r--tests/test-integration-watchdog-signal.el151
-rw-r--r--tests/test-testutil-general.el1
65 files changed, 3083 insertions, 2552 deletions
diff --git a/.gitignore b/.gitignore
index d08a1bc..c631550 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,6 @@
CLAUDE.md
githooks/
/coverage-makefile.txt
+
+# Task archive — follows todo.org's privacy
+/archive/task-archive.org
diff --git a/chime.el b/chime.el
index 3988f35..52ed569 100644
--- a/chime.el
+++ b/chime.el
@@ -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."