summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-10-24 22:41:32 -0500
committerCraig Jennings <c@cjennings.net>2025-10-24 22:41:32 -0500
commit11d54d0b985db98ecdfce838a3e5dabb59f0e95e (patch)
tree778d1cc0c4924185367a05401ce094a727a21767 /tests
moving back to github
Diffstat (limited to 'tests')
-rw-r--r--tests/test-chime-apply-blacklist.el292
-rw-r--r--tests/test-chime-apply-whitelist.el283
-rw-r--r--tests/test-chime-check-event.el185
-rw-r--r--tests/test-chime-extract-time.el388
-rw-r--r--tests/test-chime-has-timestamp.el227
-rw-r--r--tests/test-chime-notification-text.el204
-rw-r--r--tests/test-chime-notifications.el227
-rw-r--r--tests/test-chime-notify.el244
-rw-r--r--tests/test-chime-time-left.el222
-rw-r--r--tests/test-chime-timestamp-parse.el321
-rw-r--r--tests/test-chime-timestamp-within-interval-p.el280
-rw-r--r--tests/test-chime-update-modeline.el204
-rw-r--r--tests/testing-strategy.org319
-rw-r--r--tests/testutil-general.el184
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.