diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-chime-day-wide-notifications.el | 80 | ||||
| -rw-r--r-- | tests/test-chime-edge-coverage.el | 174 | ||||
| -rw-r--r-- | tests/test-chime-fetch-and-process.el | 83 | ||||
| -rw-r--r-- | tests/test-chime-jump-to-event.el | 125 | ||||
| -rw-r--r-- | tests/test-chime-lifecycle.el | 87 |
5 files changed, 549 insertions, 0 deletions
diff --git a/tests/test-chime-day-wide-notifications.el b/tests/test-chime-day-wide-notifications.el new file mode 100644 index 0000000..260abfc --- /dev/null +++ b/tests/test-chime-day-wide-notifications.el @@ -0,0 +1,80 @@ +;;; test-chime-day-wide-notifications.el --- Tests for chime--day-wide-notifications -*- lexical-binding: t; -*- + +;; Copyright (C) 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. + +;;; Commentary: + +;; Direct unit tests for `chime--day-wide-notifications', which combines the +;; day-wide filter, the notification-text builder, dedup, and severity +;; wrapping into a single pipeline. +;; +;; Mock-clock note: `with-test-time' replaces `current-time' with a lambda +;; that returns the captured base time. The base must be computed BEFORE +;; entering the macro, because `test-time-now' itself calls `current-time' +;; — passing `(test-time-now)' inside the macro causes infinite recursion. + +;;; 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)." + (let* ((base (test-time-now)) + (today (test-time-today-at 0 0)) + (ts (test-timestamp-string today t)) + (event (chime--make-event (list (cons ts nil)) + "Birthday" + '((0 . medium)))) + (chime-day-wide-advance-notice nil) + (chime-show-any-overdue-with-day-wide-alerts t)) + (with-test-time base + (let ((result (chime--day-wide-notifications (list event)))) + (should (= 1 (length result))) + (should (eq 'medium (cdr (car result)))) + (should (stringp (car (car result)))) + (should (string-match-p "Birthday" (car (car result)))))))) + +(ert-deftest test-chime-day-wide-notifications-boundary-empty-events () + "Boundary: empty events list yields empty notification list." + (should (null (chime--day-wide-notifications '())))) + +(ert-deftest test-chime-day-wide-notifications-boundary-deduplicates-identical-text () + "Boundary: two events producing identical notification text collapse to one." + (let* ((base (test-time-now)) + (today (test-time-today-at 0 0)) + (ts (test-timestamp-string today t)) + (event1 (chime--make-event (list (cons ts nil)) + "Birthday" + '((0 . medium)))) + (event2 (chime--make-event (list (cons ts nil)) + "Birthday" + '((0 . medium)))) + (chime-day-wide-advance-notice nil) + (chime-show-any-overdue-with-day-wide-alerts t)) + (with-test-time base + (let ((result (chime--day-wide-notifications (list event1 event2)))) + (should (= 1 (length result))))))) + +(ert-deftest test-chime-day-wide-notifications-boundary-filters-non-day-wide-events () + "Boundary: events that don't pass the day-wide filter contribute nothing." + (let* ((base (test-time-now)) + (future (test-time-tomorrow-at 9 0)) + (ts (test-timestamp-string future)) + (event (chime--make-event (list (cons ts future)) + "Future Timed" + '((10 . medium)))) + (chime-day-wide-advance-notice nil) + (chime-show-any-overdue-with-day-wide-alerts t)) + (with-test-time base + (should (null (chime--day-wide-notifications (list event))))))) + +(provide 'test-chime-day-wide-notifications) +;;; test-chime-day-wide-notifications.el ends here diff --git a/tests/test-chime-edge-coverage.el b/tests/test-chime-edge-coverage.el new file mode 100644 index 0000000..1325e77 --- /dev/null +++ b/tests/test-chime-edge-coverage.el @@ -0,0 +1,174 @@ +;;; test-chime-edge-coverage.el --- Cover edge branches surfaced by coverage gaps -*- lexical-binding: t; -*- + +;; Copyright (C) 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: + +;; Small focused tests that exercise specific branches missed by the +;; per-function suites. Each test points at the line range it covers +;; so future readers can correlate quickly. + +;;; 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") + +(ert-deftest test-chime-day-wide-notification-text-future-event-uses-today-fallback () + "Boundary: a future event reaches the cond's `t' fallback when no advance notice is configured." + (let* ((future-time (test-time-tomorrow-at 10 0)) + (timestamp (test-timestamp-string future-time t)) + (event (chime--make-event (list (cons timestamp nil)) + "Future Event" + '((0 . medium)))) + (chime-day-wide-advance-notice nil)) + (should (string= "Future Event is due or scheduled today" + (chime--day-wide-notification-text event))))) + +;;;; chime--format-event-for-tooltip pcase fallback + +(ert-deftest test-chime-format-event-for-tooltip-unknown-placeholder-passes-through () + "Boundary: unknown %X placeholders fall through the pcase and are kept as-is." + (let* ((time (test-time-today-at 14 30)) + (timestamp (test-timestamp-string time)) + (chime-tooltip-event-format "%t %T %u %X %Z") + (result (chime--format-event-for-tooltip timestamp 30 "Meeting"))) + (should (string-match-p "%X" result)) + (should (string-match-p "%Z" result)))) + +;;;; chime--build-upcoming-events-list show-all-day-p=nil branch + +(ert-deftest test-chime-build-upcoming-events-list-filters-day-wide-when-not-shown () + "Normal: when SHOW-ALL-DAY-P is nil, the call routes through `chime--filter-day-wide-events'." + (let* ((now (test-time-now)) + (timed-time (test-time-at 0 1 0)) + (timed-ts (test-timestamp-string timed-time)) + (all-day-ts (test-timestamp-string now t)) + (event (chime--make-event + (list (cons timed-ts timed-time) + (cons all-day-ts nil)) + "Mixed Event" + '((0 . medium)))) + (lookahead 1440)) + (let ((with-all (chime--build-upcoming-events-list (list event) now lookahead t)) + (without-all (chime--build-upcoming-events-list (list event) now lookahead nil))) + (should (= 1 (length with-all))) + (should (= 1 (length without-all))) + ;; Both pick the timed entry as soonest; the branch coverage is the + ;; point of this test. + (should (equal (nth 1 (car with-all)) + (nth 1 (car without-all))))))) + +;;;; chime--timestamp-parse error path without context arg + +(ert-deftest test-chime-timestamp-parse-error-without-context-omits-context-suffix () + "Error: when CONTEXT is nil, the failure message omits the in '...' suffix." + (let ((captured nil)) + (cl-letf (((symbol-function 'org-parse-time-string) + (lambda (&rest _) (error "synthetic parse failure"))) + ((symbol-function 'message) + (lambda (fmt &rest args) + (push (apply #'format fmt args) captured)))) + (should (null (chime--timestamp-parse "<2026-05-11 Mon 09:30>")))) + (let ((joined (mapconcat #'identity captured "\n"))) + (should (string-match-p "Failed to parse timestamp" joined)) + ;; No "in '...': " suffix when context is nil. + (should-not (string-match-p " in '" joined))))) + +(ert-deftest test-chime-timestamp-parse-error-with-context-includes-context-suffix () + "Error: when CONTEXT is non-nil, the failure message includes the in '...' suffix." + (let ((captured nil)) + (cl-letf (((symbol-function 'org-parse-time-string) + (lambda (&rest _) (error "synthetic parse failure"))) + ((symbol-function 'message) + (lambda (fmt &rest args) + (push (apply #'format fmt args) captured)))) + (should (null (chime--timestamp-parse "<2026-05-11 Mon 09:30>" + "ContextEvent")))) + (let ((joined (mapconcat #'identity captured "\n"))) + (should (string-match-p "in 'ContextEvent'" joined))))) + +;;;; chime--extract-gcal-timestamps drawer without :END: + +(ert-deftest test-chime-extract-gcal-timestamps-drawer-without-end-returns-empty () + "Boundary: drawer without a closing :END: yields no timestamps and does not error. +Exercises the `(point)' fallback branch in the drawer-end computation." + (let* ((future (test-time-tomorrow-at 9 30)) + (ts (test-timestamp-string future)) + (content (format "* Meeting +:org-gcal: +%s +" ts))) + (with-temp-buffer + (org-mode) + (insert content) + (goto-char (point-min)) + (should (null (chime--extract-gcal-timestamps "Meeting")))))) + +;;;; chime--display-validation-results :error branch + +(ert-deftest test-chime-display-validation-results-counts-error-results () + "Normal: results with :error severity get counted into the summary." + (let ((messages nil) + (chime-validation-summary-format "errs=%d%s warns=%d%s")) + (cl-letf (((symbol-function 'message) + (lambda (fmt &rest args) + (push (apply #'format fmt args) messages)))) + (chime--display-validation-results + '((:error "first failed" "first detail") + (:error "second failed" "second detail") + (:warning "third soft" "third detail") + (:ok "fourth ok" nil)))) + (should (member "errs=2s warns=1" (nreverse messages))))) + +;;;; chime--record-async-failure with chime-debug feature loaded + +(ert-deftest test-chime-record-async-failure-calls-debug-logger-when-feature-loaded () + "Normal: when the chime-debug feature is loaded, the debug logger is invoked." + (let ((logged nil) + (chime--consecutive-async-failures 0) + (chime-max-consecutive-failures 0) + (chime-modeline-no-events-text " ⏰") + (chime-modeline-string nil)) + (cl-letf (((symbol-function 'featurep) + (lambda (feat &rest _) (eq feat 'chime-debug))) + ((symbol-function 'chime--debug-log-async-error) + (lambda (err) (setq logged err))) + ((symbol-function 'force-mode-line-update) (lambda (&optional _)))) + (chime--record-async-failure '(error "boom") "Async error")) + (should (equal '(error "boom") logged)))) + +;;;; chime--log-silently mid-line branch (point not at BOL) + +(ert-deftest test-chime-log-silently-inserts-leading-newline-when-not-at-bol () + "Boundary: when *Messages* tail is mid-line, log-silently inserts a leading newline." + (with-current-buffer (get-buffer-create "*Messages*") + (let ((inhibit-read-only t)) + (goto-char (point-max)) + (insert "no-newline-prefix") + (let ((pos-before (point-max))) + (chime--log-silently "edge-coverage") + (goto-char pos-before) + ;; The inserted newline separates the two strings. + (should (looking-at "\n")) + (should (search-forward "edge-coverage" nil t)))))) + +(provide 'test-chime-edge-coverage) +;;; test-chime-edge-coverage.el ends here diff --git a/tests/test-chime-fetch-and-process.el b/tests/test-chime-fetch-and-process.el new file mode 100644 index 0000000..6e2917c --- /dev/null +++ b/tests/test-chime-fetch-and-process.el @@ -0,0 +1,83 @@ +;;; test-chime-fetch-and-process.el --- Tests for chime--fetch-and-process branches -*- lexical-binding: t; -*- + +;; Copyright (C) 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. + +;;; Commentary: + +;; Cover the two error branches inside the `chime--fetch-and-process' +;; async callback: +;; +;; 1. The async subprocess returned an `(async-signal . ERR)' tuple. +;; 2. The user-supplied callback raised an error during processing. +;; +;; Both are caught by the surrounding `condition-case' and routed through +;; `chime--record-async-failure' with a distinct prefix. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) +(require 'cl-lib) + +(ert-deftest test-chime-fetch-and-process-async-signal-records-failure () + "Error: when async returns an `async-signal' tuple, failure is recorded with the async prefix." + (let ((recorded nil) + (chime--process nil) + (chime--last-check-time '(0 0)) + (chime-modeline-no-events-text " ⏰") + (chime-modeline-string nil) + (chime--consecutive-async-failures 0) + (chime-max-consecutive-failures 0)) + (cl-letf (((symbol-function 'async-start) + (lambda (_start-form finish-func) + (funcall finish-func '(async-signal error "boom")) + 'fake-process)) + ((symbol-function 'chime--record-async-failure) + (lambda (err prefix) (setq recorded (cons prefix err)))) + ((symbol-function 'force-mode-line-update) (lambda (&optional _)))) + (chime--fetch-and-process (lambda (_events) nil))) + (should (equal "Async error" (car recorded))) + (should (equal '(error "boom") (cdr recorded))))) + +(ert-deftest test-chime-fetch-and-process-callback-error-records-failure () + "Error: when the callback raises during processing, failure is recorded with the processing prefix." + (let ((recorded nil) + (chime--process nil) + (chime--last-check-time '(0 0)) + (chime-modeline-no-events-text " ⏰") + (chime-modeline-string nil) + (chime--consecutive-async-failures 0) + (chime-max-consecutive-failures 0)) + (cl-letf (((symbol-function 'async-start) + (lambda (_start-form finish-func) + (funcall finish-func '(((title . "Event")))) + 'fake-process)) + ((symbol-function 'chime--record-async-failure) + (lambda (err prefix) (setq recorded (cons prefix err)))) + ((symbol-function 'force-mode-line-update) (lambda (&optional _)))) + (chime--fetch-and-process (lambda (_events) (error "callback boom")))) + (should (equal "Error processing events" (car recorded))) + (should (string-match-p "callback boom" + (error-message-string (cdr recorded)))))) + +(ert-deftest test-chime-fetch-and-process-skips-when-process-live () + "Boundary: an active live process blocks a fresh fetch." + (let ((fetched nil) + (chime--process 'fake-live-process)) + (cl-letf (((symbol-function 'process-live-p) + (lambda (proc) (eq proc 'fake-live-process))) + ((symbol-function 'async-start) + (lambda (&rest _) + (setq fetched t) + 'unused))) + (chime--fetch-and-process (lambda (_events) nil))) + (should-not fetched))) + +(provide 'test-chime-fetch-and-process) +;;; test-chime-fetch-and-process.el ends here diff --git a/tests/test-chime-jump-to-event.el b/tests/test-chime-jump-to-event.el new file mode 100644 index 0000000..6db885b --- /dev/null +++ b/tests/test-chime-jump-to-event.el @@ -0,0 +1,125 @@ +;;; test-chime-jump-to-event.el --- Tests for chime--jump-to-event navigation -*- lexical-binding: t; -*- + +;; Copyright (C) 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. + +;;; Commentary: + +;; Unit tests for `chime--jump-to-event'. The function reconstructs an +;; org buffer position from the serialized marker-file + marker-pos pair +;; that survives the async boundary, opens the file, moves point, and +;; reveals the entry. + +;;; 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." + (let* ((base (chime-create-test-base-dir)) + (path (expand-file-name + (concat (make-temp-name "jump-to-event-") ".org") + base))) + (with-temp-file path + (insert content)) + path)) + +(ert-deftest test-chime-jump-to-event-normal-opens-file-and-moves-point () + "Normal: opens the event's file and moves point to the recorded position." + (unwind-protect + (let* ((content "* First heading +some body + +* Target heading +target body +") + (file (test-chime-jump-to-event--make-temp-org-file content)) + (target-pos (with-temp-buffer + (insert-file-contents file) + (goto-char (point-min)) + (search-forward "* Target heading") + (line-beginning-position))) + (event (chime--make-event + '(("<2026-05-10 Sun 09:30>" . (26760 32460))) + "Target heading" + '((0 . medium)) + file + target-pos)) + (buffer-before-jump nil)) + (unwind-protect + (progn + (setq buffer-before-jump (current-buffer)) + (chime--jump-to-event event) + (should (string= file (buffer-file-name))) + (should (= target-pos (point)))) + (when (and (get-file-buffer file)) + (kill-buffer (get-file-buffer file))) + (when (buffer-live-p buffer-before-jump) + (switch-to-buffer buffer-before-jump)))) + (chime-delete-test-base-dir))) + +(ert-deftest test-chime-jump-to-event-boundary-nil-marker-file-no-op () + "Boundary: an event with nil marker-file does nothing (no error, no jump)." + (let* ((event (chime--make-event + '(("<2026-05-10 Sun 09:30>" . (26760 32460))) + "No File" + '((0 . medium)) + nil + 123)) + (buffer-before (current-buffer))) + (chime--jump-to-event event) + (should (eq buffer-before (current-buffer))))) + +(ert-deftest test-chime-jump-to-event-boundary-missing-file-no-op () + "Boundary: when the recorded file no longer exists, the jump is a no-op." + (let* ((event (chime--make-event + '(("<2026-05-10 Sun 09:30>" . (26760 32460))) + "Missing File" + '((0 . medium)) + "/tmp/chime-this-path-does-not-exist-xyz.org" + 42)) + (buffer-before (current-buffer))) + (chime--jump-to-event event) + (should (eq buffer-before (current-buffer))))) + +(ert-deftest test-chime-jump-to-event-boundary-uses-org-show-entry-fallback () + "Boundary: when `org-fold-show-entry' is unbound, falls back to `org-show-entry'." + (unwind-protect + (let* ((content "* Heading +body +") + (file (test-chime-jump-to-event--make-temp-org-file content)) + (event (chime--make-event + '(("<2026-05-10 Sun 09:30>" . (26760 32460))) + "Heading" + '((0 . medium)) + file + 1)) + (fallback-called nil) + (orig-fboundp (symbol-function 'fboundp)) + (buffer-before-jump (current-buffer))) + (unwind-protect + (cl-letf (((symbol-function 'fboundp) + (lambda (sym) + (and (not (eq sym 'org-fold-show-entry)) + (funcall orig-fboundp sym)))) + ((symbol-function 'org-show-entry) + (lambda () (setq fallback-called t)))) + (chime--jump-to-event event) + (should fallback-called) + (should (string= file (buffer-file-name)))) + (when (get-file-buffer file) + (kill-buffer (get-file-buffer file))) + (when (buffer-live-p buffer-before-jump) + (switch-to-buffer buffer-before-jump)))) + (chime-delete-test-base-dir))) + +(provide 'test-chime-jump-to-event) +;;; test-chime-jump-to-event.el ends here diff --git a/tests/test-chime-lifecycle.el b/tests/test-chime-lifecycle.el new file mode 100644 index 0000000..8ce9c13 --- /dev/null +++ b/tests/test-chime-lifecycle.el @@ -0,0 +1,87 @@ +;;; test-chime-lifecycle.el --- Tests for chime--stop and chime--start -*- lexical-binding: t; -*- + +;; Copyright (C) 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. + +;;; Commentary: + +;; Unit tests for the internal lifecycle entry points `chime--stop' and +;; `chime--start'. Tests cover branches not exercised by higher-level +;; mode-toggle tests: in-progress process cleanup and the debug-only +;; scheduling log line. + +;;; Code: + +(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) + (chime--timer nil) + (chime--process 'fake-process) + (chime--validation-done t) + (chime--validation-retry-count 5)) + (cl-letf (((symbol-function 'interrupt-process) + (lambda (proc) (setq interrupted proc)))) + (chime--stop)) + (should (eq 'fake-process interrupted)) + (should (null chime--process)) + (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) + (chime--timer nil) + (chime--process nil) + (chime--validation-done t) + (chime--validation-retry-count 5)) + (cl-letf (((symbol-function 'interrupt-process) + (lambda (proc) (setq interrupted proc)))) + (chime--stop)) + (should (null interrupted)) + (should-not chime--validation-done) + (should (= 0 chime--validation-retry-count)))) + +(ert-deftest test-chime-start-logs-debug-message-when-feature-loaded () + "Normal: with the chime-debug feature loaded, start emits a scheduling log line." + (let ((logged nil) + (chime--timer nil) + (chime--process nil)) + (cl-letf (((symbol-function 'featurep) + (lambda (feat &rest _) (eq feat 'chime-debug))) + ((symbol-function 'run-at-time) + (lambda (&rest _) 'fake-timer)) + ((symbol-function 'chime--log-silently) + (lambda (fmt &rest args) + (push (apply #'format fmt args) logged)))) + (chime--start)) + (should (cl-some (lambda (m) (string-match-p "Scheduling first check" m)) + logged)) + (should (eq 'fake-timer chime--timer)))) + +(ert-deftest test-chime-start-skips-debug-log-when-feature-absent () + "Boundary: without the chime-debug feature loaded, no log line is emitted." + (let ((logged nil) + (chime--timer nil) + (chime--process nil)) + (cl-letf (((symbol-function 'featurep) + (lambda (feat &rest _) (not (eq feat 'chime-debug)))) + ((symbol-function 'run-at-time) + (lambda (&rest _) 'fake-timer)) + ((symbol-function 'chime--log-silently) + (lambda (fmt &rest args) + (push (apply #'format fmt args) logged)))) + (chime--start)) + (should (null logged)) + (should (eq 'fake-timer chime--timer)))) + +(provide 'test-chime-lifecycle) +;;; test-chime-lifecycle.el ends here |
