diff options
| author | Craig Jennings <c@cjennings.net> | 2025-10-24 22:41:32 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-10-24 22:41:32 -0500 |
| commit | 11d54d0b985db98ecdfce838a3e5dabb59f0e95e (patch) | |
| tree | 778d1cc0c4924185367a05401ce094a727a21767 /tests | |
moving back to github
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-chime-apply-blacklist.el | 292 | ||||
| -rw-r--r-- | tests/test-chime-apply-whitelist.el | 283 | ||||
| -rw-r--r-- | tests/test-chime-check-event.el | 185 | ||||
| -rw-r--r-- | tests/test-chime-extract-time.el | 388 | ||||
| -rw-r--r-- | tests/test-chime-has-timestamp.el | 227 | ||||
| -rw-r--r-- | tests/test-chime-notification-text.el | 204 | ||||
| -rw-r--r-- | tests/test-chime-notifications.el | 227 | ||||
| -rw-r--r-- | tests/test-chime-notify.el | 244 | ||||
| -rw-r--r-- | tests/test-chime-time-left.el | 222 | ||||
| -rw-r--r-- | tests/test-chime-timestamp-parse.el | 321 | ||||
| -rw-r--r-- | tests/test-chime-timestamp-within-interval-p.el | 280 | ||||
| -rw-r--r-- | tests/test-chime-update-modeline.el | 204 | ||||
| -rw-r--r-- | tests/testing-strategy.org | 319 | ||||
| -rw-r--r-- | tests/testutil-general.el | 184 |
14 files changed, 3580 insertions, 0 deletions
diff --git a/tests/test-chime-apply-blacklist.el b/tests/test-chime-apply-blacklist.el new file mode 100644 index 0000000..6f785ef --- /dev/null +++ b/tests/test-chime-apply-blacklist.el @@ -0,0 +1,292 @@ +;;; test-chime-apply-blacklist.el --- Tests for chime--apply-blacklist -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--apply-blacklist function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) + +;;; Setup and Teardown + +(defun test-chime-apply-blacklist-setup () + "Setup function run before each test." + (chime-create-test-base-dir) + ;; Reset blacklist settings + (setq chime-keyword-blacklist nil) + (setq chime-tags-blacklist nil) + (setq chime-predicate-blacklist nil)) + +(defun test-chime-apply-blacklist-teardown () + "Teardown function run after each test." + (chime-delete-test-base-dir) + (setq chime-keyword-blacklist nil) + (setq chime-tags-blacklist nil) + (setq chime-predicate-blacklist nil)) + +;;; Normal Cases + +(ert-deftest test-chime-apply-blacklist-nil-blacklist-returns-all-markers () + "Test that nil blacklist returns all markers unchanged." + (test-chime-apply-blacklist-setup) + (unwind-protect + (let* ((chime-keyword-blacklist nil) + (chime-tags-blacklist nil) + (markers (list (make-marker) (make-marker) (make-marker))) + (result (chime--apply-blacklist markers))) + ;; Should return all markers when blacklist is nil + (should (equal (length result) 3))) + (test-chime-apply-blacklist-teardown))) + +(ert-deftest test-chime-apply-blacklist-keyword-blacklist-filters-correctly () + "Test that keyword blacklist filters out markers correctly." + (test-chime-apply-blacklist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* DONE Task 2\n") + (let ((marker2 (copy-marker (point)))) + (insert "* TODO Task 3\n") + (let ((marker3 (copy-marker (point))) + (chime-keyword-blacklist '("DONE"))) + ;; Mock org-entry-get to return appropriate keywords + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (equal property "TODO") + (cond + ((equal pom marker1) "TODO") + ((equal pom marker2) "DONE") + ((equal pom marker3) "TODO") + (t nil)))))) + (let ((result (chime--apply-blacklist (list marker1 marker2 marker3)))) + ;; Should filter out DONE marker + (should (= (length result) 2)) + (should (member marker1 result)) + (should-not (member marker2 result)) + (should (member marker3 result)))))))) + (test-chime-apply-blacklist-teardown))) + +(ert-deftest test-chime-apply-blacklist-tags-blacklist-filters-correctly () + "Test that tags blacklist filters out markers correctly." + (test-chime-apply-blacklist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* Task 2\n") + (let ((marker2 (copy-marker (point)))) + (insert "* Task 3\n") + (let ((marker3 (copy-marker (point))) + (chime-tags-blacklist '("personal"))) + ;; Mock chime--get-tags to return appropriate tags + (cl-letf (((symbol-function 'chime--get-tags) + (lambda (pom) + (cond + ((equal pom marker1) '("work" "urgent")) + ((equal pom marker2) '("personal")) + ((equal pom marker3) '("work")) + (t nil))))) + (let ((result (chime--apply-blacklist (list marker1 marker2 marker3)))) + ;; Should filter out marker with "personal" tag + (should (= (length result) 2)) + (should (member marker1 result)) + (should-not (member marker2 result)) + (should (member marker3 result)))))))) + (test-chime-apply-blacklist-teardown))) + +(ert-deftest test-chime-apply-blacklist-keyword-and-tags-blacklist-uses-or-logic () + "Test that both keyword and tags blacklists use OR logic." + (test-chime-apply-blacklist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* DONE Task 2\n") + (let ((marker2 (copy-marker (point)))) + (insert "* NEXT Task 3\n") + (let ((marker3 (copy-marker (point))) + (chime-keyword-blacklist '("DONE")) + (chime-tags-blacklist '("archive"))) + ;; Mock functions + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (equal property "TODO") + (cond + ((equal pom marker1) "TODO") + ((equal pom marker2) "DONE") + ((equal pom marker3) "NEXT") + (t nil))))) + ((symbol-function 'chime--get-tags) + (lambda (pom) + (cond + ((equal pom marker1) nil) + ((equal pom marker2) nil) + ((equal pom marker3) '("archive")) + (t nil))))) + (let ((result (chime--apply-blacklist (list marker1 marker2 marker3)))) + ;; Should filter out marker2 (DONE) and marker3 (archive tag) + (should (= (length result) 1)) + (should (member marker1 result)) + (should-not (member marker2 result)) + (should-not (member marker3 result)))))))) + (test-chime-apply-blacklist-teardown))) + +(ert-deftest test-chime-apply-blacklist-multiple-keywords-filters-all () + "Test that multiple keywords in blacklist filters all matching." + (test-chime-apply-blacklist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* DONE Task 2\n") + (let ((marker2 (copy-marker (point)))) + (insert "* CANCELLED Task 3\n") + (let ((marker3 (copy-marker (point))) + (chime-keyword-blacklist '("DONE" "CANCELLED"))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (equal property "TODO") + (cond + ((equal pom marker1) "TODO") + ((equal pom marker2) "DONE") + ((equal pom marker3) "CANCELLED") + (t nil)))))) + (let ((result (chime--apply-blacklist (list marker1 marker2 marker3)))) + ;; Should only keep TODO marker + (should (= (length result) 1)) + (should (member marker1 result)))))))) + (test-chime-apply-blacklist-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-apply-blacklist-empty-markers-list-returns-empty () + "Test that empty markers list returns empty." + (test-chime-apply-blacklist-setup) + (unwind-protect + (let ((chime-keyword-blacklist '("DONE")) + (result (chime--apply-blacklist '()))) + (should (equal result '()))) + (test-chime-apply-blacklist-teardown))) + +(ert-deftest test-chime-apply-blacklist-single-item-blacklist-works () + "Test that single-item blacklist works correctly." + (test-chime-apply-blacklist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* DONE Task 2\n") + (let ((marker2 (copy-marker (point))) + (chime-keyword-blacklist '("DONE"))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (equal property "TODO") + (cond + ((equal pom marker1) "TODO") + ((equal pom marker2) "DONE") + (t nil)))))) + (let ((result (chime--apply-blacklist (list marker1 marker2)))) + (should (= (length result) 1)) + (should (member marker1 result))))))) + (test-chime-apply-blacklist-teardown))) + +(ert-deftest test-chime-apply-blacklist-all-markers-blacklisted-returns-empty () + "Test that blacklisting all markers returns empty list." + (test-chime-apply-blacklist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* DONE Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* DONE Task 2\n") + (let ((marker2 (copy-marker (point))) + (chime-keyword-blacklist '("DONE"))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (equal property "TODO") + (cond + ((equal pom marker1) "DONE") + ((equal pom marker2) "DONE") + (t nil)))))) + (let ((result (chime--apply-blacklist (list marker1 marker2)))) + (should (equal result '()))))))) + (test-chime-apply-blacklist-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-apply-blacklist-handles-nil-keyword-gracefully () + "Test that nil keyword in marker is handled gracefully." + (test-chime-apply-blacklist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* Entry without TODO keyword\n") + (let ((marker1 (copy-marker (point))) + (chime-keyword-blacklist '("DONE"))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + nil))) + (let ((result (chime--apply-blacklist (list marker1)))) + ;; Should keep marker with nil keyword (not in blacklist) + (should (= (length result) 1)))))) + (test-chime-apply-blacklist-teardown))) + +(ert-deftest test-chime-apply-blacklist-handles-nil-tags-gracefully () + "Test that nil tags in marker is handled gracefully." + (test-chime-apply-blacklist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* Entry without tags\n") + (let ((marker1 (copy-marker (point))) + (chime-tags-blacklist '("archive"))) + (cl-letf (((symbol-function 'chime--get-tags) + (lambda (pom) nil))) + (let ((result (chime--apply-blacklist (list marker1)))) + ;; Should keep marker with nil tags (not in blacklist) + (should (= (length result) 1)))))) + (test-chime-apply-blacklist-teardown))) + +(provide 'test-chime-apply-blacklist) +;;; test-chime-apply-blacklist.el ends here diff --git a/tests/test-chime-apply-whitelist.el b/tests/test-chime-apply-whitelist.el new file mode 100644 index 0000000..d89f1b5 --- /dev/null +++ b/tests/test-chime-apply-whitelist.el @@ -0,0 +1,283 @@ +;;; test-chime-apply-whitelist.el --- Tests for chime--apply-whitelist -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--apply-whitelist function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) + +;;; Setup and Teardown + +(defun test-chime-apply-whitelist-setup () + "Setup function run before each test." + (chime-create-test-base-dir) + ;; Reset whitelist settings + (setq chime-keyword-whitelist nil) + (setq chime-tags-whitelist nil) + (setq chime-predicate-whitelist nil)) + +(defun test-chime-apply-whitelist-teardown () + "Teardown function run after each test." + (chime-delete-test-base-dir) + (setq chime-keyword-whitelist nil) + (setq chime-tags-whitelist nil) + (setq chime-predicate-whitelist nil)) + +(defun test-chime-create-org-marker (keyword tags) + "Create a marker pointing to an org entry with KEYWORD and TAGS. +Returns a marker with mocked org-entry-get and chime--get-tags." + (with-temp-buffer + (org-mode) + (insert (format "* %s Test Entry\n" (or keyword ""))) + (let ((marker (point-marker))) + ;; Mock org-entry-get to return the keyword + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (and (equal pom marker) (equal property "TODO")) + keyword))) + ((symbol-function 'chime--get-tags) + (lambda (pom) + (when (equal pom marker) + tags)))) + marker)))) + +;;; Normal Cases + +(ert-deftest test-chime-apply-whitelist-nil-whitelist-returns-all-markers () + "Test that nil whitelist returns all markers unchanged." + (test-chime-apply-whitelist-setup) + (unwind-protect + (let* ((chime-keyword-whitelist nil) + (chime-tags-whitelist nil) + (markers (list (make-marker) (make-marker) (make-marker))) + (result (chime--apply-whitelist markers))) + ;; Should return all markers when whitelist is nil + (should (equal (length result) 3))) + (test-chime-apply-whitelist-teardown))) + +(ert-deftest test-chime-apply-whitelist-keyword-whitelist-filters-correctly () + "Test that keyword whitelist filters markers correctly." + (test-chime-apply-whitelist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* DONE Task 2\n") + (let ((marker2 (copy-marker (point)))) + (insert "* TODO Task 3\n") + (let ((marker3 (copy-marker (point))) + (chime-keyword-whitelist '("TODO"))) + ;; Mock org-entry-get to return appropriate keywords + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (equal property "TODO") + (cond + ((equal pom marker1) "TODO") + ((equal pom marker2) "DONE") + ((equal pom marker3) "TODO") + (t nil)))))) + (let ((result (chime--apply-whitelist (list marker1 marker2 marker3)))) + ;; Should only return TODO markers + (should (= (length result) 2)) + (should (member marker1 result)) + (should-not (member marker2 result)) + (should (member marker3 result)))))))) + (test-chime-apply-whitelist-teardown))) + +(ert-deftest test-chime-apply-whitelist-tags-whitelist-filters-correctly () + "Test that tags whitelist filters markers correctly." + (test-chime-apply-whitelist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* Task 2\n") + (let ((marker2 (copy-marker (point)))) + (insert "* Task 3\n") + (let ((marker3 (copy-marker (point))) + (chime-tags-whitelist '("work"))) + ;; Mock chime--get-tags to return appropriate tags + (cl-letf (((symbol-function 'chime--get-tags) + (lambda (pom) + (cond + ((equal pom marker1) '("work" "urgent")) + ((equal pom marker2) '("personal")) + ((equal pom marker3) '("work")) + (t nil))))) + (let ((result (chime--apply-whitelist (list marker1 marker2 marker3)))) + ;; Should only return markers with "work" tag + (should (= (length result) 2)) + (should (member marker1 result)) + (should-not (member marker2 result)) + (should (member marker3 result)))))))) + (test-chime-apply-whitelist-teardown))) + +(ert-deftest test-chime-apply-whitelist-keyword-and-tags-whitelist-uses-or-logic () + "Test that both keyword and tags whitelists use OR logic." + (test-chime-apply-whitelist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* DONE Task 2\n") + (let ((marker2 (copy-marker (point)))) + (insert "* NEXT Task 3\n") + (let ((marker3 (copy-marker (point))) + (chime-keyword-whitelist '("TODO")) + (chime-tags-whitelist '("urgent"))) + ;; Mock functions + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (equal property "TODO") + (cond + ((equal pom marker1) "TODO") + ((equal pom marker2) "DONE") + ((equal pom marker3) "NEXT") + (t nil))))) + ((symbol-function 'chime--get-tags) + (lambda (pom) + (cond + ((equal pom marker1) nil) + ((equal pom marker2) '("urgent")) + ((equal pom marker3) nil) + (t nil))))) + (let ((result (chime--apply-whitelist (list marker1 marker2 marker3)))) + ;; Should return marker1 (TODO keyword) and marker2 (urgent tag) + (should (= (length result) 2)) + (should (member marker1 result)) + (should (member marker2 result)) + (should-not (member marker3 result)))))))) + (test-chime-apply-whitelist-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-apply-whitelist-empty-markers-list-returns-empty () + "Test that empty markers list returns empty." + (test-chime-apply-whitelist-setup) + (unwind-protect + (let ((chime-keyword-whitelist '("TODO")) + (result (chime--apply-whitelist '()))) + (should (equal result '()))) + (test-chime-apply-whitelist-teardown))) + +(ert-deftest test-chime-apply-whitelist-single-item-whitelist-works () + "Test that single-item whitelist works correctly." + (test-chime-apply-whitelist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* DONE Task 2\n") + (let ((marker2 (copy-marker (point))) + (chime-keyword-whitelist '("TODO"))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (equal property "TODO") + (cond + ((equal pom marker1) "TODO") + ((equal pom marker2) "DONE") + (t nil)))))) + (let ((result (chime--apply-whitelist (list marker1 marker2)))) + (should (= (length result) 1)) + (should (member marker1 result))))))) + (test-chime-apply-whitelist-teardown))) + +(ert-deftest test-chime-apply-whitelist-no-matching-markers-returns-empty () + "Test that no matching markers returns empty list." + (test-chime-apply-whitelist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* DONE Task 1\n") + (let ((marker1 (copy-marker (point)))) + (insert "* CANCELLED Task 2\n") + (let ((marker2 (copy-marker (point))) + (chime-keyword-whitelist '("TODO"))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (when (equal property "TODO") + (cond + ((equal pom marker1) "DONE") + ((equal pom marker2) "CANCELLED") + (t nil)))))) + (let ((result (chime--apply-whitelist (list marker1 marker2)))) + (should (equal result '()))))))) + (test-chime-apply-whitelist-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-apply-whitelist-handles-nil-keyword-gracefully () + "Test that nil keyword in marker is handled gracefully." + (test-chime-apply-whitelist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* Entry without TODO keyword\n") + (let ((marker1 (copy-marker (point))) + (chime-keyword-whitelist '("TODO"))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + nil))) + (let ((result (chime--apply-whitelist (list marker1)))) + ;; Should filter out marker with nil keyword + (should (equal result '())))))) + (test-chime-apply-whitelist-teardown))) + +(ert-deftest test-chime-apply-whitelist-handles-nil-tags-gracefully () + "Test that nil tags in marker is handled gracefully." + (test-chime-apply-whitelist-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* Entry without tags\n") + (let ((marker1 (copy-marker (point))) + (chime-tags-whitelist '("work"))) + (cl-letf (((symbol-function 'chime--get-tags) + (lambda (pom) nil))) + (let ((result (chime--apply-whitelist (list marker1)))) + ;; Should filter out marker with nil tags + (should (equal result '())))))) + (test-chime-apply-whitelist-teardown))) + +(provide 'test-chime-apply-whitelist) +;;; test-chime-apply-whitelist.el ends here diff --git a/tests/test-chime-check-event.el b/tests/test-chime-check-event.el new file mode 100644 index 0000000..e6d645f --- /dev/null +++ b/tests/test-chime-check-event.el @@ -0,0 +1,185 @@ +;;; test-chime-check-event.el --- Tests for chime--check-event -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--check-event function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) + +;;; Setup and Teardown + +(defun test-chime-check-event-setup () + "Setup function run before each test." + (chime-create-test-base-dir)) + +(defun test-chime-check-event-teardown () + "Teardown function run after each test." + (chime-delete-test-base-dir)) + +;;; Normal Cases + +(ert-deftest test-chime-check-event-single-notification-returns-message () + "Test that single matching notification returns formatted message." + (test-chime-check-event-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Event at 14:10 (10 minutes from now) + (event-time (encode-time 0 10 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time)))) + (title . "Team Meeting") + (intervals . (10)))) + (result (chime--check-event event))) + ;; Should return list with one formatted message + (should (listp result)) + (should (= 1 (length result))) + (should (stringp (car result))) + ;; Message should contain title and time information + (should (string-match-p "Team Meeting" (car result))) + (should (string-match-p "02:10 PM" (car result))) + (should (string-match-p "in 10 minutes" (car result)))) + (test-chime-check-event-teardown))) + +(ert-deftest test-chime-check-event-multiple-notifications-returns-multiple-messages () + "Test that multiple matching notifications return multiple formatted messages." + (test-chime-check-event-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Two events: 14:10 and 14:05 + (event-time-1 (encode-time 0 10 14 24 10 2025)) + (event-time-2 (encode-time 0 5 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time-1) + ("<2025-10-24 Fri 14:05>" . ,event-time-2)))) + (title . "Important Call") + (intervals . (10 5)))) ; Both match + (result (chime--check-event event))) + ;; Should return two formatted messages + (should (listp result)) + (should (= 2 (length result))) + (should (cl-every #'stringp result)) + ;; Both should mention the title + (should (string-match-p "Important Call" (car result))) + (should (string-match-p "Important Call" (cadr result)))) + (test-chime-check-event-teardown))) + +(ert-deftest test-chime-check-event-zero-interval-returns-right-now-message () + "Test that zero interval produces 'right now' message." + (test-chime-check-event-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Event at exactly now + (event-time (encode-time 0 0 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:00>" . ,event-time)))) + (title . "Daily Standup") + (intervals . (0)))) + (result (chime--check-event event))) + (should (listp result)) + (should (= 1 (length result))) + (should (string-match-p "Daily Standup" (car result))) + (should (string-match-p "right now" (car result)))) + (test-chime-check-event-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-check-event-no-matching-notifications-returns-empty-list () + "Test that event with no matching times returns empty list." + (test-chime-check-event-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Event at 14:20 (doesn't match 10 minute interval) + (event-time (encode-time 0 20 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:20>" . ,event-time)))) + (title . "Future Event") + (intervals . (10)))) + (result (chime--check-event event))) + (should (listp result)) + (should (= 0 (length result)))) + (test-chime-check-event-teardown))) + +(ert-deftest test-chime-check-event-day-wide-event-returns-empty-list () + "Test that day-wide event (no time) returns empty list." + (test-chime-check-event-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (event-time (encode-time 0 0 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri>" . ,event-time)))) + (title . "All Day Event") + (intervals . (10)))) + (result (chime--check-event event))) + (should (listp result)) + (should (= 0 (length result)))) + (test-chime-check-event-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-check-event-empty-times-returns-empty-list () + "Test that event with no times returns empty list." + (test-chime-check-event-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (event `((times . (())) + (title . "No Times Event") + (intervals . (10)))) + (result (chime--check-event event))) + (should (listp result)) + (should (= 0 (length result)))) + (test-chime-check-event-teardown))) + +(ert-deftest test-chime-check-event-empty-intervals-returns-empty-list () + "Test that event with no intervals returns empty list." + (test-chime-check-event-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (event-time (encode-time 0 10 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time)))) + (title . "No Intervals Event") + (intervals . ()))) + (result (chime--check-event event))) + (should (listp result)) + (should (= 0 (length result)))) + (test-chime-check-event-teardown))) + +(provide 'test-chime-check-event) +;;; test-chime-check-event.el ends here diff --git a/tests/test-chime-extract-time.el b/tests/test-chime-extract-time.el new file mode 100644 index 0000000..4041713 --- /dev/null +++ b/tests/test-chime-extract-time.el @@ -0,0 +1,388 @@ +;;; test-chime-extract-time.el --- Tests for chime--extract-time -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--extract-time function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.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)) + +;;; Normal Cases + +(ert-deftest test-chime-extract-time-scheduled-timestamp-extracted () + "Test that SCHEDULED timestamp is extracted correctly." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "SCHEDULED")) + "<2025-10-24 Fri 14:30>") + (t nil))))) + (let ((result (chime--extract-time marker))) + (should (listp result)) + (should (= (length result) 1)) + (should (equal (caar result) "<2025-10-24 Fri 14:30>")) + (should (listp (cdar result))))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-deadline-timestamp-extracted () + "Test that DEADLINE timestamp is extracted correctly." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "DEADLINE")) + "<2025-10-24 Fri 16:00>") + (t nil))))) + (let ((result (chime--extract-time marker))) + (should (listp result)) + (should (= (length result) 1)) + (should (equal (caar result) "<2025-10-24 Fri 16:00>")) + (should (listp (cdar result))))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-plain-timestamp-extracted () + "Test that plain TIMESTAMP is extracted correctly." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* Test Event\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "TIMESTAMP")) + "<2025-10-24 Fri 10:00>") + (t nil))))) + (let ((result (chime--extract-time marker))) + (should (listp result)) + (should (= (length result) 1)) + (should (equal (caar result) "<2025-10-24 Fri 10:00>")) + (should (listp (cdar result))))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-multiple-timestamps-all-extracted () + "Test that multiple timestamps are all extracted." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "SCHEDULED")) + "<2025-10-24 Fri 14:30>") + ((and (equal pom marker) (equal property "DEADLINE")) + "<2025-10-24 Fri 16:00>") + (t nil))))) + (let ((result (chime--extract-time marker))) + (should (listp result)) + (should (= (length result) 2)) + ;; Check both timestamps are present + (should (--some (equal (car it) "<2025-10-24 Fri 14:30>") result)) + (should (--some (equal (car it) "<2025-10-24 Fri 16:00>") result)))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-all-three-timestamp-types-extracted () + "Test that all three timestamp types can be extracted together." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Complex Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "SCHEDULED")) + "<2025-10-24 Fri 09:00>") + ((and (equal pom marker) (equal property "DEADLINE")) + "<2025-10-24 Fri 17:00>") + ((and (equal pom marker) (equal property "TIMESTAMP")) + "<2025-10-24 Fri 12:00>") + (t nil))))) + (let ((result (chime--extract-time marker))) + (should (listp result)) + (should (= (length result) 3)))))) + (test-chime-extract-time-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-extract-time-no-timestamps-returns-empty () + "Test that entry with no timestamps returns empty list." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + nil))) + (let ((result (chime--extract-time marker))) + (should (listp result)) + (should (= (length result) 0)))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-only-scheduled-extracted () + "Test that only SCHEDULED is extracted when others are missing." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "SCHEDULED")) + "<2025-10-24 Fri 14:30>") + (t nil))))) + (let ((result (chime--extract-time marker))) + (should (= (length result) 1)) + (should (equal (caar result) "<2025-10-24 Fri 14:30>")))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-only-deadline-extracted () + "Test that only DEADLINE is extracted when others are missing." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "DEADLINE")) + "<2025-10-24 Fri 16:00>") + (t nil))))) + (let ((result (chime--extract-time marker))) + (should (= (length result) 1)) + (should (equal (caar result) "<2025-10-24 Fri 16:00>")))))) + (test-chime-extract-time-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-extract-time-malformed-timestamp-returns-nil-cdr () + "Test that malformed timestamps return cons with nil cdr." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "SCHEDULED")) + "not-a-valid-timestamp") + ((and (equal pom marker) (equal property "DEADLINE")) + "<2025-10-24 Fri 16:00>") + (t nil))))) + (let ((result (chime--extract-time marker))) + ;; Should return both, but malformed one has nil cdr + (should (= (length result) 2)) + ;; Find the malformed timestamp result + (let ((malformed (--find (equal (car it) "not-a-valid-timestamp") result))) + (should malformed) + (should-not (cdr malformed))))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-day-wide-timestamp-returns-nil-cdr () + "Test that day-wide timestamps (no time) return cons with nil cdr." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "SCHEDULED")) + "<2025-10-24 Fri>") ; Day-wide, no time + ((and (equal pom marker) (equal property "DEADLINE")) + "<2025-10-24 Fri 16:00>") ; Has time + (t nil))))) + (let ((result (chime--extract-time marker))) + ;; Should return both timestamps + (should (= (length result) 2)) + ;; Day-wide timestamp has nil cdr + (let ((day-wide (--find (equal (car it) "<2025-10-24 Fri>") result))) + (should day-wide) + (should-not (cdr day-wide))) + ;; Timed timestamp has valid cdr + (let ((timed (--find (equal (car it) "<2025-10-24 Fri 16:00>") result))) + (should timed) + (should (cdr timed))))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-empty-timestamp-string-returns-nil-cdr () + "Test that empty timestamp strings return cons with nil cdr." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "SCHEDULED")) + "") + ((and (equal pom marker) (equal property "DEADLINE")) + "<2025-10-24 Fri 16:00>") + (t nil))))) + (let ((result (chime--extract-time marker))) + ;; Should return both entries + (should (= (length result) 2)) + ;; Empty string has nil cdr + (let ((empty (--find (equal (car it) "") result))) + (should empty) + (should-not (cdr empty))))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-all-malformed-returns-cons-with-nil-cdrs () + "Test that all malformed timestamps return cons with nil cdrs." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* TODO Test Task\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "SCHEDULED")) + "not-valid") + ((and (equal pom marker) (equal property "DEADLINE")) + "also-not-valid") + ((and (equal pom marker) (equal property "TIMESTAMP")) + "<2025-10-24 Fri>") ; Day-wide + (t nil))))) + (let ((result (chime--extract-time marker))) + ;; Should return 3 entries, all with nil cdr + (should (= (length result) 3)) + (should (--every (not (cdr it)) result)))))) + (test-chime-extract-time-teardown))) + +;;; org-gcal Integration Tests + +(ert-deftest test-chime-extract-time-org-gcal-time-range-format () + "Test extraction of org-gcal style timestamp with time range. +org-gcal uses format like <2025-10-24 Fri 17:30-18:00> with HH:MM-HH:MM range." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* Testing Round Trip\n") + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "TIMESTAMP")) + "<2025-10-24 Fri 17:30-18:00>") + (t nil))))) + (let ((result (chime--extract-time marker))) + (should (listp result)) + (should (>= (length result) 1)) + ;; Should extract the timestamp string + (should (equal (caar result) "<2025-10-24 Fri 17:30-18:00>")) + ;; Should have parsed time value (not nil) + (should (listp (cdar result))) + (should (cdar result)))))) + (test-chime-extract-time-teardown))) + +(ert-deftest test-chime-extract-time-org-gcal-in-drawer () + "Test extraction of timestamp inside org-gcal drawer. +org-gcal stores timestamps in :org-gcal: drawers which should still be detected." + (test-chime-extract-time-setup) + (unwind-protect + (with-temp-buffer + (org-mode) + (insert "* Testing Chime!\n") + (insert ":PROPERTIES:\n") + (insert ":ETag: \"3522688202987102\"\n") + (insert ":calendar-id: user@example.com\n") + (insert ":entry-id: abc123/user@example.com\n") + (insert ":org-gcal-managed: gcal\n") + (insert ":END:\n") + (insert ":org-gcal:\n") + (insert "<2025-10-24 Fri 17:30-18:00>\n") + (insert ":END:\n") + (goto-char (point-min)) + (let ((marker (copy-marker (point)))) + (cl-letf (((symbol-function 'org-entry-get) + (lambda (pom property &optional inherit literal-nil) + (cond + ((and (equal pom marker) (equal property "TIMESTAMP")) + "<2025-10-24 Fri 17:30-18:00>") + (t nil))))) + (let ((result (chime--extract-time marker))) + (should (listp result)) + (should (>= (length result) 1)) + (should (equal (caar result) "<2025-10-24 Fri 17:30-18:00>")) + (should (cdar result)))))) + (test-chime-extract-time-teardown))) + +(provide 'test-chime-extract-time) +;;; test-chime-extract-time.el ends here diff --git a/tests/test-chime-has-timestamp.el b/tests/test-chime-has-timestamp.el new file mode 100644 index 0000000..7cc7831 --- /dev/null +++ b/tests/test-chime-has-timestamp.el @@ -0,0 +1,227 @@ +;;; test-chime-has-timestamp.el --- Tests for chime--has-timestamp -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--has-timestamp function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.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 () + "Test that standard timestamp with time returns non-nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 14:30>") + (result (chime--has-timestamp timestamp))) + (should result)) + (test-chime-has-timestamp-teardown))) + +(ert-deftest test-chime-has-timestamp-timestamp-without-brackets-returns-non-nil () + "Test that timestamp without brackets but with time returns non-nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "2025-10-24 Fri 14:30") + (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 () + "Test that timestamp with time range returns non-nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 14:00-15:30>") + (result (chime--has-timestamp timestamp))) + (should result)) + (test-chime-has-timestamp-teardown))) + +(ert-deftest test-chime-has-timestamp-scheduled-with-time-returns-non-nil () + "Test that SCHEDULED timestamp with time returns non-nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "SCHEDULED: <2025-10-24 Fri 09:00>") + (result (chime--has-timestamp timestamp))) + (should result)) + (test-chime-has-timestamp-teardown))) + +(ert-deftest test-chime-has-timestamp-deadline-with-time-returns-non-nil () + "Test that DEADLINE timestamp with time returns non-nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "DEADLINE: <2025-10-24 Fri 17:00>") + (result (chime--has-timestamp timestamp))) + (should result)) + (test-chime-has-timestamp-teardown))) + +(ert-deftest test-chime-has-timestamp-repeater-with-time-returns-non-nil () + "Test that timestamp with repeater and time returns non-nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 14:00 +1w>") + (result (chime--has-timestamp timestamp))) + (should result)) + (test-chime-has-timestamp-teardown))) + +(ert-deftest test-chime-has-timestamp-midnight-timestamp-returns-non-nil () + "Test that midnight timestamp (00:00) returns non-nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 00:00>") + (result (chime--has-timestamp timestamp))) + (should result)) + (test-chime-has-timestamp-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-has-timestamp-day-wide-timestamp-returns-nil () + "Test that day-wide timestamp without time returns nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri>") + (result (chime--has-timestamp timestamp))) + (should-not result)) + (test-chime-has-timestamp-teardown))) + +(ert-deftest test-chime-has-timestamp-date-only-returns-nil () + "Test that date-only timestamp without day name returns nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24>") + (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 () + "Test that timestamp with single-digit hour returns non-nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 9:00>") + (result (chime--has-timestamp timestamp))) + (should result)) + (test-chime-has-timestamp-teardown))) + +(ert-deftest test-chime-has-timestamp-embedded-in-text-returns-non-nil () + "Test that timestamp embedded in text returns non-nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "Meeting scheduled for <2025-10-24 Fri 14:00> 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 () + "Test that string with multiple timestamps returns non-nil for first match." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 14:00> and <2025-10-25 Sat 15:00>") + (result (chime--has-timestamp timestamp))) + (should result)) + (test-chime-has-timestamp-teardown))) + +;;; Error Cases + +(ert-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 () + "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 () + "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 () + "Test that invalid timestamp format returns nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<10-24-2025 Fri 14:00>") + (result (chime--has-timestamp timestamp))) + (should-not result)) + (test-chime-has-timestamp-teardown))) + +(ert-deftest test-chime-has-timestamp-partial-timestamp-returns-nil () + "Test that partial timestamp returns nil." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24") + (result (chime--has-timestamp timestamp))) + (should-not result)) + (test-chime-has-timestamp-teardown))) + +;;; org-gcal Integration Tests + +(ert-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." + (test-chime-has-timestamp-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 17:30-18:00>") + (result (chime--has-timestamp timestamp))) + (should result)) + (test-chime-has-timestamp-teardown))) + +(provide 'test-chime-has-timestamp) +;;; test-chime-has-timestamp.el ends here diff --git a/tests/test-chime-notification-text.el b/tests/test-chime-notification-text.el new file mode 100644 index 0000000..7d7d192 --- /dev/null +++ b/tests/test-chime-notification-text.el @@ -0,0 +1,204 @@ +;;; test-chime-notification-text.el --- Tests for chime--notification-text -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--notification-text function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) + +;;; Setup and Teardown + +(defun test-chime-notification-text-setup () + "Setup function run before each test." + (chime-create-test-base-dir) + ;; Reset display format to default + (setq chime-display-time-format-string "%I:%M %p")) + +(defun test-chime-notification-text-teardown () + "Teardown function run after each test." + (chime-delete-test-base-dir)) + +;;; Normal Cases + +(ert-deftest test-chime-notification-text-standard-event-formats-correctly () + "Test that standard event formats correctly." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 14:30>" . 10)) + (event '((title . "Team Meeting"))) + (result (chime--notification-text str-interval event))) + ;; Should format: "Team Meeting at 02:30 PM (in X minutes)" + (should (stringp result)) + (should (string-match-p "Team Meeting" result)) + (should (string-match-p "02:30 PM" result)) + (should (string-match-p "in 10 minutes" result))) + (test-chime-notification-text-teardown))) + +(ert-deftest test-chime-notification-text-morning-time-formats-with-am () + "Test that morning time uses AM." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 09:15>" . 5)) + (event '((title . "Standup"))) + (result (chime--notification-text str-interval event))) + (should (string-match-p "Standup" result)) + (should (string-match-p "09:15 AM" result)) + (should (string-match-p "in 5 minutes" result))) + (test-chime-notification-text-teardown))) + +(ert-deftest test-chime-notification-text-midnight-formats-correctly () + "Test that midnight time formats correctly." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 00:00>" . 30)) + (event '((title . "Midnight Event"))) + (result (chime--notification-text str-interval event))) + (should (string-match-p "Midnight Event" result)) + (should (string-match-p "12:00 AM" result)) + (should (string-match-p "in 30 minutes" result))) + (test-chime-notification-text-teardown))) + +(ert-deftest test-chime-notification-text-noon-formats-correctly () + "Test that noon time formats correctly." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 12:00>" . 15)) + (event '((title . "Lunch"))) + (result (chime--notification-text str-interval event))) + (should (string-match-p "Lunch" result)) + (should (string-match-p "12:00 PM" result)) + (should (string-match-p "in 15 minutes" result))) + (test-chime-notification-text-teardown))) + +(ert-deftest test-chime-notification-text-zero-minutes-shows-right-now () + "Test that zero minutes shows 'right now'." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 14:00>" . 0)) + (event '((title . "Current Event"))) + (result (chime--notification-text str-interval event))) + (should (string-match-p "Current Event" result)) + (should (string-match-p "02:00 PM" result)) + (should (string-match-p "right now" result))) + (test-chime-notification-text-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-notification-text-very-long-title-included () + "Test that very long titles are included in full." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 15:45>" . 20)) + (long-title "This is a very long event title that contains many words and might wrap in the notification display") + (event `((title . ,long-title))) + (result (chime--notification-text str-interval event))) + ;; Should include the full title + (should (string-match-p long-title result)) + (should (string-match-p "03:45 PM" result)) + (should (string-match-p "in 20 minutes" result))) + (test-chime-notification-text-teardown))) + +(ert-deftest test-chime-notification-text-title-with-special-characters () + "Test that titles with special characters work correctly." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 16:30>" . 5)) + (event '((title . "Review: Alice's PR #123 (urgent!)"))) + (result (chime--notification-text str-interval event))) + (should (string-match-p "Review: Alice's PR #123 (urgent!)" result)) + (should (string-match-p "04:30 PM" result))) + (test-chime-notification-text-teardown))) + +(ert-deftest test-chime-notification-text-custom-time-format () + "Test that custom time format string is respected." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 14:30>" . 10)) + (event '((title . "Meeting"))) + (chime-display-time-format-string "%H:%M") ; 24-hour format + (result (chime--notification-text str-interval event))) + ;; Should use 24-hour format + (should (string-match-p "Meeting" result)) + (should (string-match-p "14:30" result)) + (should-not (string-match-p "PM" result))) + (test-chime-notification-text-teardown))) + +(ert-deftest test-chime-notification-text-large-interval-shows-hours () + "Test that large intervals show hours." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 18:00>" . 120)) ; 2 hours + (event '((title . "Evening Event"))) + (result (chime--notification-text str-interval event))) + (should (string-match-p "Evening Event" result)) + (should (string-match-p "06:00 PM" result)) + ;; Should show hours format + (should (string-match-p "in 2 hours" result))) + (test-chime-notification-text-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-notification-text-empty-title-shows-empty () + "Test that empty title still generates output." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 14:30>" . 10)) + (event '((title . ""))) + (result (chime--notification-text str-interval event))) + ;; Should still format, even with empty title + (should (stringp result)) + (should (string-match-p "02:30 PM" result)) + (should (string-match-p "in 10 minutes" result))) + (test-chime-notification-text-teardown))) + +(ert-deftest test-chime-notification-text-missing-title-shows-nil () + "Test that missing title shows nil in output." + (test-chime-notification-text-setup) + (unwind-protect + (let* ((str-interval '("<2025-10-24 Fri 14:30>" . 10)) + (event '()) ; No title + (result (chime--notification-text str-interval event))) + ;; Should still generate output with nil title + (should (stringp result)) + (should (string-match-p "02:30 PM" result)) + (should (string-match-p "in 10 minutes" result))) + (test-chime-notification-text-teardown))) + +(provide 'test-chime-notification-text) +;;; test-chime-notification-text.el ends here diff --git a/tests/test-chime-notifications.el b/tests/test-chime-notifications.el new file mode 100644 index 0000000..f3aa5d7 --- /dev/null +++ b/tests/test-chime-notifications.el @@ -0,0 +1,227 @@ +;;; test-chime-notifications.el --- Tests for chime--notifications -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--notifications function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.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 () + "Test that single time with single interval returns one notification pair." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Event at 14:10 (10 minutes from now) + (event-time (encode-time 0 10 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time)))) + (title . "Test Event") + (intervals . (10)))) + (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 () + "Test that single time with multiple intervals returns multiple notification pairs." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Event at 14:10 + (event-time (encode-time 0 10 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time)))) + (title . "Test Event") + (intervals . (10 5)))) ; 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 () + "Test that multiple times with single interval returns matching notifications." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Two events: one at 14:10, one at 14:05 + (event-time-1 (encode-time 0 10 14 24 10 2025)) + (event-time-2 (encode-time 0 5 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time-1) + ("<2025-10-24 Fri 14:05>" . ,event-time-2)))) + (title . "Test Event") + (intervals . (10)))) ; 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 () + "Test that multiple times and intervals return all matching combinations." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Event at 14:10 and 14:05 + (event-time-1 (encode-time 0 10 14 24 10 2025)) + (event-time-2 (encode-time 0 5 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time-1) + ("<2025-10-24 Fri 14:05>" . ,event-time-2)))) + (title . "Test Event") + (intervals . (10 5)))) ; 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 () + "Test that zero interval (notify now) works correctly." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Event at exactly current time + (event-time (encode-time 0 0 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:00>" . ,event-time)))) + (title . "Test Event") + (intervals . (0)))) + (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 () + "Test that day-wide events (without time) are filtered out." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Mix of day-wide and timed events + (event-time (encode-time 0 10 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri>" . ,event-time) ; Day-wide + ("<2025-10-24 Fri 14:10>" . ,event-time)))) ; Timed + (title . "Test Event") + (intervals . (10)))) + (result (chime--notifications event))) + ;; Should return only timed event + (should (listp result)) + (should (= 1 (length result)))) + (test-chime-notifications-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-notifications-empty-times-returns-empty-list () + "Test that event with no times returns empty list." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (event `((times . (())) + (title . "Test Event") + (intervals . (10)))) + (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 () + "Test that event with no intervals returns empty list." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (event-time (encode-time 0 10 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time)))) + (title . "Test Event") + (intervals . ()))) + (result (chime--notifications event))) + (should (listp result)) + (should (= 0 (length result)))) + (test-chime-notifications-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-notifications-nil-times-returns-empty-list () + "Test that event with nil times returns empty list." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (event `((times . (nil)) + (title . "Test Event") + (intervals . (10)))) + (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 () + "Test that event with nil intervals returns empty list." + (test-chime-notifications-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (event-time (encode-time 0 10 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time)))) + (title . "Test Event") + (intervals . nil))) + (result (chime--notifications event))) + (should (listp result)) + (should (= 0 (length result)))) + (test-chime-notifications-teardown))) + +(provide 'test-chime-notifications) +;;; test-chime-notifications.el ends here diff --git a/tests/test-chime-notify.el b/tests/test-chime-notify.el new file mode 100644 index 0000000..451508d --- /dev/null +++ b/tests/test-chime-notify.el @@ -0,0 +1,244 @@ +;;; test-chime-notify.el --- Tests for chime--notify -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--notify function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) + +;;; Setup and Teardown + +(defun test-chime-notify-setup () + "Setup function run before each test." + (chime-create-test-base-dir) + ;; Reset notification settings + (setq chime-notification-title "Agenda") + (setq chime-notification-icon nil) + (setq chime-alert-severity 'medium) + (setq chime-extra-alert-plist nil) + (setq chime-play-sound t) + ;; Use a simple test path for sound file + (setq chime-sound-file "/tmp/test-chime.wav")) + +(defun test-chime-notify-teardown () + "Teardown function run after each test." + (chime-delete-test-base-dir)) + +;;; Normal Cases + +(ert-deftest test-chime-notify-plays-sound-when-enabled-and-file-exists () + "Test that sound is played when enabled and file exists." + (test-chime-notify-setup) + (unwind-protect + (let ((sound-played nil) + (alert-called nil) + (alert-message nil)) + (cl-letf* ((chime-play-sound t) + (chime-sound-file (expand-file-name "test-sound.wav" chime-test-base-dir)) + ;; Mock file-exists-p to return t + ((symbol-function 'file-exists-p) (lambda (file) t)) + ;; Mock play-sound-file to track if called + ((symbol-function 'play-sound-file) + (lambda (file) + (setq sound-played t))) + ;; Mock alert to track if called + ((symbol-function 'alert) + (lambda (msg &rest args) + (setq alert-called t) + (setq alert-message msg)))) + (chime--notify "Team Meeting at 02:10 PM") + ;; Should play sound + (should sound-played) + ;; Should show alert + (should alert-called) + (should (equal alert-message "Team Meeting at 02:10 PM")))) + (test-chime-notify-teardown))) + +(ert-deftest test-chime-notify-uses-beep-when-no-sound-file-specified () + "Test that beep is used when chime-sound-file is nil." + (test-chime-notify-setup) + (unwind-protect + (let ((beep-called nil) + (alert-called nil)) + (cl-letf* ((chime-play-sound t) + (chime-sound-file nil) + ;; Mock beep to track if called + ((symbol-function 'beep) + (lambda () (setq beep-called t))) + ;; Mock alert + ((symbol-function 'alert) + (lambda (msg &rest args) (setq alert-called t)))) + (chime--notify "Standup in 5 minutes") + ;; Should call beep + (should beep-called) + ;; Should show alert + (should alert-called))) + (test-chime-notify-teardown))) + +(ert-deftest test-chime-notify-no-sound-when-disabled () + "Test that no sound is played when chime-play-sound is nil." + (test-chime-notify-setup) + (unwind-protect + (let ((sound-played nil) + (beep-called nil) + (alert-called nil)) + (cl-letf* ((chime-play-sound nil) + ((symbol-function 'play-sound-file) + (lambda (file) (setq sound-played t))) + ((symbol-function 'beep) + (lambda () (setq beep-called t))) + ((symbol-function 'alert) + (lambda (msg &rest args) (setq alert-called t)))) + (chime--notify "Daily Standup") + ;; Should NOT play sound or beep + (should-not sound-played) + (should-not beep-called) + ;; Should still show alert + (should alert-called))) + (test-chime-notify-teardown))) + +(ert-deftest test-chime-notify-passes-correct-parameters-to-alert () + "Test that alert is called with correct parameters." + (test-chime-notify-setup) + (unwind-protect + (let ((alert-params nil)) + (cl-letf* ((chime-play-sound nil) + (chime-notification-title "Custom Title") + (chime-notification-icon "/path/to/icon.png") + (chime-alert-severity 'high) + (chime-extra-alert-plist '(:persistent t)) + ;; Mock alert to capture parameters + ((symbol-function 'alert) + (lambda (msg &rest args) (setq alert-params args)))) + (chime--notify "Test Event") + ;; Verify alert was called with correct parameters + (should (equal (plist-get alert-params :title) "Custom Title")) + (should (equal (plist-get alert-params :icon) "/path/to/icon.png")) + (should (equal (plist-get alert-params :severity) 'high)) + (should (equal (plist-get alert-params :category) 'chime)) + ;; Extra plist should be merged in + (should (equal (plist-get alert-params :persistent) t)))) + (test-chime-notify-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-notify-empty-message-still-notifies () + "Test that empty message still triggers notification." + (test-chime-notify-setup) + (unwind-protect + (let ((alert-called nil) + (alert-message nil)) + (cl-letf* ((chime-play-sound nil) + ((symbol-function 'alert) + (lambda (msg &rest args) + (setq alert-called t) + (setq alert-message msg)))) + (chime--notify "") + ;; Should still call alert, even with empty message + (should alert-called) + (should (equal alert-message "")))) + (test-chime-notify-teardown))) + +(ert-deftest test-chime-notify-no-sound-file-when-file-doesnt-exist () + "Test that no sound is played when file doesn't exist." + (test-chime-notify-setup) + (unwind-protect + (let ((sound-played nil) + (alert-called nil)) + (cl-letf* ((chime-play-sound t) + (chime-sound-file "/nonexistent/path/sound.wav") + ;; 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))) + ((symbol-function 'alert) + (lambda (msg &rest args) (setq alert-called t)))) + (chime--notify "Test Event") + ;; Should NOT play sound + (should-not sound-played) + ;; Should still show alert + (should alert-called))) + (test-chime-notify-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-notify-handles-sound-playback-error-gracefully () + "Test that errors in sound playback don't prevent notification." + (test-chime-notify-setup) + (unwind-protect + (let ((alert-called nil)) + (cl-letf* ((chime-play-sound t) + (chime-sound-file "/some/file.wav") + ;; Mock file-exists-p to return t + ((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"))) + ((symbol-function 'alert) + (lambda (msg &rest args) (setq alert-called t)))) + ;; Should not throw error + (should-not (condition-case nil + (progn (chime--notify "Test Event") nil) + (error t))) + ;; Should still show alert despite sound error + (should alert-called))) + (test-chime-notify-teardown))) + +(ert-deftest test-chime-notify-handles-beep-error-gracefully () + "Test that errors in beep don't prevent notification." + (test-chime-notify-setup) + (unwind-protect + (let ((alert-called nil)) + (cl-letf* ((chime-play-sound t) + (chime-sound-file nil) + ;; Mock beep to throw error + ((symbol-function 'beep) + (lambda () (error "Beep failed"))) + ((symbol-function 'alert) + (lambda (msg &rest args) (setq alert-called t)))) + ;; Should not throw error + (should-not (condition-case nil + (progn (chime--notify "Test Event") nil) + (error t))) + ;; Should still show alert despite beep error + (should alert-called))) + (test-chime-notify-teardown))) + +(provide 'test-chime-notify) +;;; test-chime-notify.el ends here diff --git a/tests/test-chime-time-left.el b/tests/test-chime-time-left.el new file mode 100644 index 0000000..c94801a --- /dev/null +++ b/tests/test-chime-time-left.el @@ -0,0 +1,222 @@ +;;; test-chime-time-left.el --- Tests for chime--time-left -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--time-left function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) + +;;; Setup and Teardown + +(defun test-chime-time-left-setup () + "Setup function run before each test." + (chime-create-test-base-dir)) + +(defun test-chime-time-left-teardown () + "Teardown function run after each test." + (chime-delete-test-base-dir)) + +;;; Normal Cases + +(ert-deftest test-chime-time-left-one-minute-formats-correctly () + "Test that 1 minute formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 60))) + (should (stringp result)) + (should (string-match-p "in 1 minute" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-five-minutes-formats-correctly () + "Test that 5 minutes formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 300))) + (should (stringp result)) + (should (string-match-p "in 5 minutes" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-ten-minutes-formats-correctly () + "Test that 10 minutes formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 600))) + (should (stringp result)) + (should (string-match-p "in 10 minutes" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-thirty-minutes-formats-correctly () + "Test that 30 minutes formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 1800))) + (should (stringp result)) + (should (string-match-p "in 30 minutes" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-one-hour-formats-correctly () + "Test that 1 hour formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 3600))) + (should (stringp result)) + ;; At exactly 1 hour (3600s), still shows minutes format + (should (string-match-p "in 60 minutes" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-two-hours-formats-correctly () + "Test that 2 hours formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 7200))) + (should (stringp result)) + (should (string-match-p "in 2 hours" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-one-hour-thirty-minutes-formats-correctly () + "Test that 1 hour 30 minutes formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 5400))) + (should (stringp result)) + ;; Should show both hours and minutes + (should (string-match-p "in 1 hour 30 minutes" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-three-hours-fifteen-minutes-formats-correctly () + "Test that 3 hours 15 minutes formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 11700))) + (should (stringp result)) + (should (string-match-p "in 3 hours 15 minutes" result))) + (test-chime-time-left-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-time-left-zero-seconds-returns-right-now () + "Test that 0 seconds returns 'right now'." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 0))) + (should (stringp result)) + (should (string-equal "right now" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-one-second-shows-right-now () + "Test that 1 second shows 'right now'." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 1))) + (should (stringp result)) + ;; Less than a minute, but format-seconds might show "in 0 minutes" + ;; or the implementation might handle this specially + (should result)) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-fifty-nine-seconds-shows-minutes () + "Test that 59 seconds shows in minutes format." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 59))) + (should (stringp result)) + ;; Should use minutes format (< 1 hour) + (should (string-match-p "in" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-exactly-one-hour-shows-minutes-format () + "Test that exactly 1 hour shows minutes format." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 3600))) + (should (stringp result)) + ;; At exactly 3600s, still uses minutes format (boundary case) + (should (string-match-p "in 60 minutes" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-fifty-nine-minutes-shows-minutes-only () + "Test that 59 minutes shows minutes format only." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 3540))) ; 59 minutes + (should (stringp result)) + (should (string-match-p "in 59 minutes" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-twenty-four-hours-formats-correctly () + "Test that 24 hours formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 86400))) + (should (stringp result)) + (should (string-match-p "in 24 hours" result))) + (test-chime-time-left-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-time-left-negative-value-returns-right-now () + "Test that negative value returns 'right now'." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left -60))) + (should (stringp result)) + (should (string-equal "right now" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-large-negative-returns-right-now () + "Test that large negative value returns 'right now'." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left -3600))) + (should (stringp result)) + (should (string-equal "right now" result))) + (test-chime-time-left-teardown))) + +(ert-deftest test-chime-time-left-very-large-value-formats-correctly () + "Test that very large value (1 week) formats correctly." + (test-chime-time-left-setup) + (unwind-protect + (let ((result (chime--time-left 604800))) ; 1 week + (should (stringp result)) + ;; Should format with days/hours + (should (string-match-p "in" result))) + (test-chime-time-left-teardown))) + +(provide 'test-chime-time-left) +;;; test-chime-time-left.el ends here diff --git a/tests/test-chime-timestamp-parse.el b/tests/test-chime-timestamp-parse.el new file mode 100644 index 0000000..c97806b --- /dev/null +++ b/tests/test-chime-timestamp-parse.el @@ -0,0 +1,321 @@ +;;; test-chime-timestamp-parse.el --- Tests for chime--timestamp-parse -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--timestamp-parse function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.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 () + "Test that a standard timestamp with time component returns a time list." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 14:30>") + (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 () + "Test that a SCHEDULED timestamp parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 09:00>") + (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 () + "Test that a DEADLINE timestamp parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 17:00>") + (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 () + "Test that a timestamp with +1w repeater parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 14:00 +1w>") + (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 () + "Test that a timestamp with .+1d repeater parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 08:00 .+1d>") + (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 () + "Test that a timestamp with ++1w repeater parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 10:30 ++1w>") + (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 () + "Test that a timestamp with time range returns the start time." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 14:00-15:30>") + (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 () + "Test that a timestamp with date range returns start date time." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 10:00>--<2025-10-25 Sat 10:00>") + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result)) + (test-chime-timestamp-parse-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-timestamp-parse-midnight-timestamp-returns-time-list () + "Test that midnight (00:00) timestamp parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 00:00>") + (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 () + "Test that last minute of day (23:59) timestamp parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 23:59>") + (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 () + "Test that New Year's Eve timestamp parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-12-31 Wed 23:30>") + (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 () + "Test that New Year's Day timestamp parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2026-01-01 Thu 00:30>") + (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 () + "Test that single-digit hours and minutes parse correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-24 Fri 01:05>") + (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 () + "Test that Feb 29 in leap year parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2024-02-29 Thu 14:00>") + (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 () + "Test that end of month timestamp parses correctly." + (test-chime-timestamp-parse-setup) + (unwind-protect + (let* ((timestamp "<2025-10-31 Fri 14:00>") + (result (chime--timestamp-parse timestamp))) + (should (listp result)) + (should (= (length result) 2)) + (should result)) + (test-chime-timestamp-parse-teardown))) + +;;; Error Cases + +(ert-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 () + "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 () + "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 () + "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 () + "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 () + "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 () + "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 () + "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 () + "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 () + "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))) + +(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 new file mode 100644 index 0000000..89b8056 --- /dev/null +++ b/tests/test-chime-timestamp-within-interval-p.el @@ -0,0 +1,280 @@ +;;; test-chime-timestamp-within-interval-p.el --- Tests for chime--timestamp-within-interval-p -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--timestamp-within-interval-p function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.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 () + "Test that timestamp exactly at interval returns t." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 14:10 (10 minutes from 14:00) + (timestamp (encode-time 0 10 14 24 10 2025)) + (interval 10) + (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 () + "Test that zero interval (notify now) returns t for current time." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 30 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at exactly current time (14:30) + (timestamp (encode-time 0 30 14 24 10 2025)) + (interval 0) + (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 () + "Test that 5-minute interval works correctly." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 25 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 14:30 (5 minutes from 14:25) + (timestamp (encode-time 0 30 14 24 10 2025)) + (interval 5) + (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 () + "Test that 60-minute (1 hour) interval works correctly." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 15:00 (60 minutes from 14:00) + (timestamp (encode-time 0 0 15 24 10 2025)) + (interval 60) + (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 () + "Test that large interval (1 day = 1440 minutes) works correctly." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 14:00 next day (1440 minutes from now) + (timestamp (encode-time 0 0 14 25 10 2025)) + (interval 1440) + (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 () + "Test that 30-minute interval works correctly." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 15 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 14:45 (30 minutes from 14:15) + (timestamp (encode-time 0 45 14 24 10 2025)) + (interval 30) + (result (chime--timestamp-within-interval-p timestamp interval))) + (should result)) + (test-chime-timestamp-within-interval-p-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-timestamp-within-interval-p-one-minute-before-returns-nil () + "Test that timestamp 1 minute before interval returns nil." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 14:09 (9 minutes from 14:00, not 10) + (timestamp (encode-time 0 9 14 24 10 2025)) + (interval 10) + (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 () + "Test that timestamp 1 minute after interval returns nil." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 14:11 (11 minutes from 14:00, not 10) + (timestamp (encode-time 0 11 14 24 10 2025)) + (interval 10) + (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 () + "Test that interval crossing midnight works correctly." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 50 23 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 00:00 next day (10 minutes from 23:50) + (timestamp (encode-time 0 0 0 25 10 2025)) + (interval 10) + (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 () + "Test that interval crossing to next day works correctly." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 30 23 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 00:30 next day (60 minutes from 23:30) + (timestamp (encode-time 0 30 0 25 10 2025)) + (interval 60) + (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 () + "Test that very large interval (1 week = 10080 minutes) works." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 14:00 one week later + (timestamp (encode-time 0 0 14 31 10 2025)) + (interval 10080) + (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 () + "Test that timestamp at exact midnight works correctly." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 50 23 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at midnight (10 minutes from 23:50) + (timestamp (encode-time 0 0 0 25 10 2025)) + (interval 10) + (result (chime--timestamp-within-interval-p timestamp interval))) + (should result)) + (test-chime-timestamp-within-interval-p-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-timestamp-within-interval-p-nil-timestamp-returns-nil () + "Test that nil timestamp returns nil." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (timestamp nil) + (interval 10) + (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 () + "Test that nil interval returns nil." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (timestamp (encode-time 0 10 14 24 10 2025)) + (interval nil) + (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 () + "Test that negative interval returns nil (past timestamps)." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp 10 minutes in the past (13:50) + (timestamp (encode-time 0 50 13 24 10 2025)) + (interval -10) + (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 () + "Test that invalid timestamp format returns nil." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + (timestamp "not-a-timestamp") + (interval 10) + (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 () + "Test that float interval gets converted properly." + (test-chime-timestamp-within-interval-p-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ;; Timestamp at 14:10 (10 minutes from 14:00) + (timestamp (encode-time 0 10 14 24 10 2025)) + (interval 10.5) + (result (chime--timestamp-within-interval-p timestamp interval))) + (should result)) + (test-chime-timestamp-within-interval-p-teardown))) + +(provide 'test-chime-timestamp-within-interval-p) +;;; test-chime-timestamp-within-interval-p.el ends here diff --git a/tests/test-chime-update-modeline.el b/tests/test-chime-update-modeline.el new file mode 100644 index 0000000..c2a5f54 --- /dev/null +++ b/tests/test-chime-update-modeline.el @@ -0,0 +1,204 @@ +;;; test-chime-update-modeline.el --- Tests for chime--update-modeline -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 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--update-modeline function. +;; Tests cover normal cases, boundary cases, and error cases. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) + +;;; Setup and Teardown + +(defun test-chime-update-modeline-setup () + "Setup function run before each test." + (chime-create-test-base-dir) + ;; Reset modeline settings + (setq chime-modeline-string nil) + (setq chime-modeline-lookahead 30) + (setq chime-modeline-format " ⏰ %s")) + +(defun test-chime-update-modeline-teardown () + "Teardown function run after each test." + (chime-delete-test-base-dir) + (setq chime-modeline-string nil)) + +;;; Normal Cases + +(ert-deftest test-chime-update-modeline-single-event-within-window-updates-modeline () + "Test that single event within lookahead window updates modeline." + (test-chime-update-modeline-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ((symbol-function 'force-mode-line-update) (lambda ())) + ;; Event at 14:10 (10 minutes from now) + (event-time (encode-time 0 10 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time)))) + (title . "Team Meeting"))) + (events (list event))) + (chime--update-modeline events) + ;; Should set modeline string + (should chime-modeline-string) + (should (stringp chime-modeline-string)) + (should (string-match-p "Team Meeting" chime-modeline-string)) + (should (string-match-p "10 minutes" chime-modeline-string))) + (test-chime-update-modeline-teardown))) + +(ert-deftest test-chime-update-modeline-multiple-events-picks-soonest () + "Test that with multiple events, soonest one is shown." + (test-chime-update-modeline-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ((symbol-function 'force-mode-line-update) (lambda ())) + ;; Event 1 at 14:05 (5 minutes - soonest) + (event-time-1 (encode-time 0 5 14 24 10 2025)) + (event1 `((times . ((("<2025-10-24 Fri 14:05>" . ,event-time-1)))) + (title . "Standup"))) + ;; Event 2 at 14:25 (25 minutes) + (event-time-2 (encode-time 0 25 14 24 10 2025)) + (event2 `((times . ((("<2025-10-24 Fri 14:25>" . ,event-time-2)))) + (title . "Code Review"))) + (events (list event1 event2))) + (chime--update-modeline events) + ;; Should show the soonest event + (should chime-modeline-string) + (should (string-match-p "Standup" chime-modeline-string)) + (should-not (string-match-p "Code Review" chime-modeline-string))) + (test-chime-update-modeline-teardown))) + +(ert-deftest test-chime-update-modeline-event-outside-window-no-update () + "Test that event outside lookahead window doesn't update modeline." + (test-chime-update-modeline-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ((symbol-function 'force-mode-line-update) (lambda ())) + ;; Event at 15:10 (70 minutes from now, outside 30 minute window) + (event-time (encode-time 0 10 15 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 15:10>" . ,event-time)))) + (title . "Far Future Event"))) + (events (list event))) + (chime--update-modeline events) + ;; Should NOT set modeline string + (should-not chime-modeline-string)) + (test-chime-update-modeline-teardown))) + +(ert-deftest test-chime-update-modeline-zero-lookahead-clears-modeline () + "Test that zero lookahead clears modeline." + (test-chime-update-modeline-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ((symbol-function 'force-mode-line-update) (lambda ())) + (event-time (encode-time 0 10 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:10>" . ,event-time)))) + (title . "Team Meeting"))) + (events (list event))) + (setq chime-modeline-lookahead 0) + (chime--update-modeline events) + ;; Should clear modeline + (should-not chime-modeline-string)) + (test-chime-update-modeline-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-update-modeline-no-events-clears-modeline () + "Test that no events clears modeline." + (test-chime-update-modeline-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ((symbol-function 'force-mode-line-update) (lambda ())) + (events '())) + (chime--update-modeline events) + (should-not chime-modeline-string)) + (test-chime-update-modeline-teardown))) + +(ert-deftest test-chime-update-modeline-day-wide-events-filtered-out () + "Test that day-wide events are filtered out." + (test-chime-update-modeline-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ((symbol-function 'force-mode-line-update) (lambda ())) + (event-time (encode-time 0 0 0 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri>" . ,event-time)))) + (title . "All Day Event"))) + (events (list event))) + (chime--update-modeline events) + (should-not chime-modeline-string)) + (test-chime-update-modeline-teardown))) + +(ert-deftest test-chime-update-modeline-event-at-exact-boundary-included () + "Test that event at exact lookahead boundary is included." + (test-chime-update-modeline-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ((symbol-function 'force-mode-line-update) (lambda ())) + ;; Event at 14:30 (exactly 30 minutes, at boundary) + (event-time (encode-time 0 30 14 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 14:30>" . ,event-time)))) + (title . "Boundary Event"))) + (events (list event))) + (chime--update-modeline events) + (should chime-modeline-string) + (should (string-match-p "Boundary Event" chime-modeline-string))) + (test-chime-update-modeline-teardown))) + +;;; Error Cases + +(ert-deftest test-chime-update-modeline-past-events-not-shown () + "Test that past events are not shown in modeline." + (test-chime-update-modeline-setup) + (unwind-protect + (cl-letf* ((mock-time (encode-time 0 0 14 24 10 2025)) + ((symbol-function 'current-time) (lambda () mock-time)) + ((symbol-function 'force-mode-line-update) (lambda ())) + ;; Event at 13:50 (10 minutes ago) + (event-time (encode-time 0 50 13 24 10 2025)) + (event `((times . ((("<2025-10-24 Fri 13:50>" . ,event-time)))) + (title . "Past Event"))) + (events (list event))) + (chime--update-modeline events) + (should-not chime-modeline-string)) + (test-chime-update-modeline-teardown))) + +(provide 'test-chime-update-modeline) +;;; test-chime-update-modeline.el ends here diff --git a/tests/testing-strategy.org b/tests/testing-strategy.org new file mode 100644 index 0000000..d7b25a9 --- /dev/null +++ b/tests/testing-strategy.org @@ -0,0 +1,319 @@ +#+TITLE: CHIME Testing Strategy +#+AUTHOR: Craig Jennings +#+DATE: 2024-10-24 + +* Overview + +This document outlines the ERT test strategy for the CHIME package, prioritizing critical functionality and documenting what should be tested and why. + +* Test Guidelines + +** Requirements +- All tests in =chime.el/tests/= directory +- File naming: =test-chime-<methodname>.el= +- Use =testutil-general.el= utilities for test data management +- Tests must be independent, deterministic, and isolated + +** Testing Approach +- ERT framework +- Each test verifies one specific behavior +- Mock external dependencies (file I/O, time, network) +- Descriptive naming: =test-chime-<function>-<scenario>-<expected-result>= + +** CHIME-Specific Considerations +- Mock: =alert=, =play-sound-file=, =current-time=, async operations, org-agenda functions +- Focus on critical paths: notification logic, modeline updates, sound playback, time calculations + +* Priority Tiers + +** TIER 1: Core Logic (Must Test) + +*** DONE Test chime--timestamp-parse +:PROPERTIES: +:PRIORITY: A +:TIER: 1 +:END: + +*Why:* Fixed bugs here. Converts org timestamps to Emacs time. Foundation of all time calculations. + +*Risk:* +- Malformed timestamps +- Timezone issues +- Repeating timestamps (=+1w=, =.+1d=, =++1w=) +- Edge cases around midnight +- Invalid date/time strings + +*Test Coverage:* +- Normal: Valid timestamps with various formats +- Boundary: Midnight, year boundaries, DST transitions +- Error: Malformed strings, missing components, invalid dates + +*Status:* ✅ Complete - 25 tests, all passing + +*** DONE Test chime--has-timestamp +:PROPERTIES: +:PRIORITY: A +:TIER: 1 +:END: + +*Why:* Fixed bugs here. Determines if event has valid time component. Gates whether events get processed. + +*Risk:* +- Misclassifying day-wide vs timed events +- False positives/negatives on timestamp detection +- Not handling all org timestamp formats + +*Test Coverage:* +- Normal: Standard timestamps with time components +- Boundary: Timestamps at boundaries, with/without time +- Error: Invalid formats, missing brackets, partial timestamps + +*Status:* ✅ Complete - 17 tests, all passing + +*** DONE Test chime--timestamp-within-interval-p +:PROPERTIES: +:PRIORITY: A +:TIER: 1 +:END: + +*Why:* The "should we notify NOW?" decision. Critical timing logic. + +*Risk:* +- Off-by-one errors +- Boundary conditions around notification times +- Incorrect interval calculations +- Missing notifications at exact times + +*Test Coverage:* +- Normal: Events at various intervals (5 min, 1 hour, etc.) +- Boundary: Exactly at notification time, 0 minutes, very large intervals +- Error: Negative intervals, past events, invalid timestamps + +*Status:* ✅ Complete - 17 tests, all passing + +*** DONE Test chime--notifications +:PROPERTIES: +:PRIORITY: A +:TIER: 1 +:END: + +*Why:* Calculates which notifications should fire for an event. Combines alert times with event times. + +*Risk:* +- Missing notifications +- Duplicate notifications +- Incorrect intervals +- Not respecting per-event CHIME_NOTIFY_BEFORE property +- Wrong interaction between global and per-event settings + +*Test Coverage:* +- Normal: Single alert time, multiple alert times, per-event overrides +- Boundary: Alert at event time (0 minutes), very early alerts +- Error: Invalid alert times, missing properties, conflicting settings + +*Status:* ✅ Complete - 10 tests, all passing, no bugs found + +*** DONE Test chime--check-event +:PROPERTIES: +:PRIORITY: A +:TIER: 1 +:END: + +*Why:* Processes single event and returns notification list. Main business logic. + +*Risk:* +- Logic errors combining filtering and time checks +- Missing edge cases in event processing +- Incorrect event data extraction +- Filtering interactions + +*Test Coverage:* +- Normal: Valid events with various configurations +- Boundary: Events at exact notification time, multiple timestamps +- Error: Malformed events, missing required fields + +*Status:* ✅ Complete - 7 tests, all passing, no bugs found + +** TIER 2: Key Features (Should Test) + +*** DONE Test chime--update-modeline +:PROPERTIES: +:PRIORITY: B +:TIER: 2 +:END: + +*Why:* New feature. Calculates soonest event within lookahead window. + +*Risk:* +- Wrong event shown +- Returns nil when should show event +- Not updating when events change +- Lookahead window calculation errors +- Format string issues + +*Test Coverage:* +- Normal: Single event, multiple events, events outside window +- Boundary: Event exactly at lookahead boundary, 0 lookahead +- Error: No events, all events outside window, malformed events + +*Status:* ✅ Complete - 8 tests, all passing, no bugs found + +*** DONE Test chime--notify +:PROPERTIES: +:PRIORITY: B +:TIER: 2 +:END: + +*Why:* Actually sends notifications and plays sound. User-facing. + +*Risk:* +- Sound file not found +- Sound file corrupted +- Notification failures +- Error handling doesn't prevent notification display + +*Test Coverage:* +- Normal: Valid sound file, notification with sound enabled +- Boundary: Sound disabled, custom sound file +- Error: Missing sound file, invalid file path, permission errors + +*Status:* ✅ Complete - 8 tests, all passing, no bugs found + +*** DONE Test chime--notification-text +:PROPERTIES: +:PRIORITY: B +:TIER: 2 +:END: + +*Why:* Formats notification messages. User sees this directly. + +*Risk:* +- Formatting errors +- Edge cases in time display +- Unicode/special character handling +- Very long event titles + +*Test Coverage:* +- Normal: Standard event with title and time +- Boundary: Very long titles, titles with special chars, minimal data +- Error: Missing event data, malformed time intervals + +*Status:* ✅ Complete - 11 tests, all passing, no bugs found + +*** DONE Test chime--time-left +:PROPERTIES: +:PRIORITY: B +:TIER: 2 +:END: + +*Why:* Formats "in X minutes/hours" text. User-visible. + +*Risk:* +- Plural/singular errors +- Formatting edge cases (0 minutes, "right now") +- Very large time values +- Negative times + +*Test Coverage:* +- Normal: Various time intervals (seconds, minutes, hours, days) +- Boundary: 0 seconds, 1 minute, 60 minutes, 24 hours +- Error: Negative times, extremely large values + +*Status:* ✅ Complete - 17 tests, all passing, no bugs found + +** TIER 3: Filtering (Good to Test) + +*** DONE Test chime--apply-whitelist +:PROPERTIES: +:PRIORITY: C +:TIER: 3 +:END: + +*Why:* Filters events by keyword/tag whitelist. + +*Risk:* +- Filtering out valid events +- Letting through invalid events +- Interaction between keyword and tag filters + +*Test Coverage:* +- Normal: Valid whitelist with matching/non-matching events +- Boundary: Empty whitelist (nil), single item, many items +- Error: Invalid whitelist format, malformed events + +*Status:* ✅ Complete - 9 tests, all passing, no bugs found + +*** DONE Test chime--apply-blacklist +:PROPERTIES: +:PRIORITY: C +:TIER: 3 +:END: + +*Why:* Filters events by keyword/tag blacklist. + +*Risk:* +- Filtering out valid events +- Letting through invalid events +- Interaction with whitelist + +*Test Coverage:* +- Normal: Valid blacklist with matching/non-matching events +- Boundary: Empty blacklist, single item, many items +- Error: Invalid blacklist format, malformed events + +*Status:* ✅ Complete - 10 tests, all passing, no bugs found + +*** DONE Test chime--extract-time +:PROPERTIES: +:PRIORITY: C +:TIER: 3 +:END: + +*Why:* Extracts SCHEDULED/DEADLINE/timestamp from org entry. + +*Risk:* +- Missing timestamps +- Wrong priority (SCHEDULED vs DEADLINE vs plain) +- Not handling multiple timestamps correctly + +*Test Coverage:* +- Normal: Events with SCHEDULED, DEADLINE, plain timestamps +- Boundary: Multiple timestamps, conflicting times +- Error: No timestamps, malformed org entries + +*Status:* ✅ Complete - 12 tests, all passing, no bugs found + +* Methods Skipped + +These methods are either trivial, covered by higher-level tests, or difficult to unit test meaningfully. + +** SKIP chime--extract-title +*Reason:* Simple text extraction from org entry. Covered by integration tests. Low risk. + +** SKIP chime--gather-info +*Reason:* Simple aggregator function that calls other functions. Testing the individual functions provides sufficient coverage. + +** SKIP chime--get-tags +*Reason:* Trivial property getter. One-liner wrapper around org-entry-get. + +** SKIP chime--time= +*Reason:* Trivial time comparison helper. Low complexity, low risk. + +** SKIP chime--today +*Reason:* Simple wrapper around time-to-days. Trivial. + +** SKIP chime--retrieve-events +*Reason:* Complex org-agenda integration using async. Very difficult to unit test. Better tested via integration tests. + +** SKIP chime--check-events +*Reason:* Already well-covered by existing integration tests (chime-tests.el). Orchestration function that calls testable components. + +** SKIP Day-wide event functions +*Functions:* =chime-current-time-is-day-wide-time=, =chime-day-wide-notifications=, =chime-display-as-day-wide-event=, =chime-event-has-any-day-wide-timestamp=, =chime-event-has-any-passed-time=, =chime--day-wide-notification-text= + +*Reason:* Edge case functionality with lower priority. Existing integration tests provide some coverage. + +** SKIP Mode management functions +*Functions:* =chime-mode=, =chime--start=, =chime--stop=, =chime-check= + +*Reason:* Integration test territory. Requires full Emacs environment. Difficult to meaningfully unit test. diff --git a/tests/testutil-general.el b/tests/testutil-general.el new file mode 100644 index 0000000..556e520 --- /dev/null +++ b/tests/testutil-general.el @@ -0,0 +1,184 @@ +;;; testutil-general.el --- -*- coding: utf-8; lexical-binding: t; -*- +;; +;; Author: Craig Jennings <c@cjennings.net> +;; +;;; Commentary: +;; This library provides general helper functions and constants for managing +;; test directories and files across test suites. +;; +;; It establishes a user-local hidden directory as the root for all test assets, +;; provides utilities to create this directory safely, create temporary files +;; and subdirectories within it, and clean up after tests. +;; +;; This library should be required by test suites to ensure consistent, +;; reliable, and isolated file-system resources. +;; +;;; Code: + +(defconst chime-test-base-dir + (expand-file-name "~/.temp-chime-tests/") + "Base directory for all CHIME test files and directories. +All test file-system artifacts should be created under this hidden +directory in the user's home. This avoids relying on ephemeral system +directories like /tmp and reduces flaky test failures caused by external +cleanup.") + +(defun chime-create-test-base-dir () + "Create the test base directory `chime-test-base-dir' if it does not exist. +Returns the absolute path to the test base directory. +Signals an error if creation fails." + (let ((dir (file-name-as-directory chime-test-base-dir))) + (unless (file-directory-p dir) + (make-directory dir t)) + (if (file-directory-p dir) dir + (error "Failed to create test base directory %s" dir)))) + +(defun chime-create--directory-ensuring-parents (dirpath) + "Create nested directories specified by DIRPATH. +Error if DIRPATH exists already. +Ensure DIRPATH is within `chime-test-base-dir`." + (let* ((base (file-name-as-directory chime-test-base-dir)) + (fullpath (expand-file-name dirpath base))) + (unless (string-prefix-p base fullpath) + (error "Directory path %s is outside base test directory %s" fullpath base)) + (when (file-exists-p fullpath) + (error "Directory path already exists: %s" fullpath)) + (make-directory fullpath t) + fullpath)) + +(defun chime-create--file-ensuring-parents (filepath content &optional executable) + "Create file at FILEPATH (relative to `chime-test-base-dir`) with CONTENT. +Error if file exists already. +Create parent directories as needed. +If EXECUTABLE is non-nil, set execute permissions on file. +Ensure FILEPATH is within `chime-test-base-dir`." + (let* ((base (file-name-as-directory chime-test-base-dir)) + (fullpath (expand-file-name filepath base)) + (parent-dir (file-name-directory fullpath))) + (unless (string-prefix-p base fullpath) + (error "File path %s is outside base test directory %s" fullpath base)) + (when (file-exists-p fullpath) + (error "File already exists: %s" fullpath)) + (unless (file-directory-p parent-dir) + (make-directory parent-dir t)) + (with-temp-buffer + (when content + (insert content)) + (write-file fullpath)) + (when executable + (chmod fullpath #o755)) + fullpath)) + +(defun chime-create-directory-or-file-ensuring-parents (path &optional content executable) + "Create a directory or file specified by PATH relative to `chime-test-base-dir`. +If PATH ends with a slash, create nested directories. +Else create a file with optional CONTENT. +If EXECUTABLE is non-nil and creating a file, set executable permissions. +Error if the target path already exists. +Return the full created path." + (let ((is-dir (string-suffix-p "/" path))) + (if is-dir + (chime-create--directory-ensuring-parents path) + (chime-create--file-ensuring-parents path content executable)))) + + +;; (defun chime-create-file-with-content-ensuring-parents (filepath content &optional executable) +;; "Create a file at FILEPATH with CONTENT, ensuring parent directories exist. +;; FILEPATH will be relative to `chime-test-base-dir'. +;; Signals an error if the file already exists. +;; If EXECUTABLE is non-nil, set executable permission on the file. +;; Errors if the resulting path is outside `chime-test-base-dir`." +;; (let* ((base (file-name-as-directory chime-test-base-dir)) +;; (fullpath (if (file-name-absolute-p filepath) +;; (expand-file-name filepath) +;; (expand-file-name filepath base)))) +;; (unless (string-prefix-p base fullpath) +;; (error "File path %s is outside base test directory %s" fullpath base)) +;; (let ((parent-dir (file-name-directory fullpath))) +;; (when (file-exists-p fullpath) +;; (error "File already exists: %s" fullpath)) +;; (unless (file-directory-p parent-dir) +;; (make-directory parent-dir t)) +;; (with-temp-buffer +;; (insert content) +;; (write-file fullpath)) +;; (when executable +;; (chmod fullpath #o755)) +;; fullpath))) + +(defun chime-fix-permissions-recursively (dir) + "Recursively set read/write permissions for user under DIR. +Directories get user read, write, and execute permissions to allow recursive +operations." + (when (file-directory-p dir) + (dolist (entry (directory-files-recursively dir ".*" t)) + (when (file-exists-p entry) + (let* ((attrs (file-attributes entry)) + (is-dir (car attrs)) + (mode (file-modes entry)) + (user-r (logand #o400 mode)) + (user-w (logand #o200 mode)) + (user-x (logand #o100 mode)) + new-mode) + (setq new-mode mode) + (unless user-r + (setq new-mode (logior new-mode #o400))) + (unless user-w + (setq new-mode (logior new-mode #o200))) + (when is-dir + ;; Ensure user-execute for directories + (unless user-x + (setq new-mode (logior new-mode #o100)))) + (unless (= mode new-mode) + (set-file-modes entry new-mode))))))) + +(defun chime-delete-test-base-dir () + "Recursively delete test base directory `chime-test-base-dir' and contents. +Ensures all contained files and directories have user read/write permissions +so deletion is not blocked by permissions. +After deletion, verifies that the directory no longer exists. +Signals an error if the directory still exists after deletion attempt." + (let ((dir (file-name-as-directory chime-test-base-dir))) + (when (file-directory-p dir) + (chime-fix-permissions-recursively dir) + (delete-directory dir t)) + (when (file-directory-p dir) + (error "Test base directory %s still exists after deletion" dir)))) + +(defun chime-create-temp-test-file (&optional prefix) + "Create a uniquely named temporary file under `chime-test-base-dir'. +Optional argument PREFIX is a string to prefix the filename, defaults +to \"tempfile-\". Returns the absolute path to the newly created empty file. +Errors if base test directory cannot be created or file creation fails." + (let ((base (chime-create-test-base-dir)) + (file nil)) + (setq file (make-temp-file (expand-file-name (or prefix "tempfile-") base))) + (unless (file-exists-p file) + (error "Failed to create temporary test file under %s" base)) + file)) + +(defun chime-create-test-subdirectory (subdir) + "Ensure subdirectory SUBDIR (relative to `chime-test-base-dir') exists. +Creates parent directories as needed. +Returns the absolute path to the subdirectory. +Signals an error if creation fails. +SUBDIR must be a relative path string." + (let* ((base (chime-create-test-base-dir)) + (fullpath (expand-file-name subdir base))) + (unless (file-directory-p fullpath) + (make-directory fullpath t)) + (if (file-directory-p fullpath) fullpath + (error "Failed to create test subdirectory %s" subdir)))) + +(defun chime-create-temp-test-file-with-content (content &optional prefix) + "Create uniquely named temp file in =chime-test-base-dir= and write CONTENT to it. +Optional PREFIX is a filename prefix string, default \"tempfile-\". +Returns absolute path to the created file." + (let ((file (chime-create-temp-test-file prefix))) + (with-temp-buffer + (insert content) + (write-file file)) + file)) + +(provide 'testutil-general) +;;; testutil-general.el ends here. |
