aboutsummaryrefslogtreecommitdiff

Quick reference for running and writing tests in the Chime project.

Quick start | Running tests | Writing tests | Test infrastructure | Key patterns | Important notes | Dependencies | Test inventory

Quick start

# From the project root:
make test                              # Run all tests (unit + integration)
make test-unit                         # Run unit tests only
make test-integration                  # Run integration tests only

# From the tests/ directory (more options):
make test-file FILE=validate           # Run tests in one file (fuzzy match)
make test-one TEST=pilot               # Run a single test (fuzzy match)
make count                             # Count tests per file

Running tests

All test logic lives in tests/Makefile. The root Makefile delegates to it, so all commands work from either the project root or the tests/ directory.

Command Purpose
make test Run all tests (unit + integration)
make test-unit Run unit tests only
make test-integration Run integration tests only (test-integration-*.el)
make test-file FILE=overdue Run tests in one file (fuzzy match)
make test-one TEST=pilot Run a single test (fuzzy match)
make test-name TEST=pattern Run tests matching an ERT name pattern
make count Count tests per file, sorted by count
make list List all test names
make validate Check parentheses balance in all files
make lint Run elisp-lint on all files
make check-deps Verify all dependencies are installed
make clean Remove byte-compiled files and logs
make help Show all available commands

Each test file runs in its own Emacs process for isolation. Integration tests are identified by the test-integration- prefix. Dependencies are auto-detected from ~/.emacs.d/elpa/. Override with EMACS or ELPA_DIR environment variables.

Examples

# Run one file by fuzzy match
make test-file FILE=validate-configuration
# Finds: test-chime-validate-configuration.el

# Run one test by fuzzy match
make test-one TEST=pilot
# Finds: test-chime-validate-configuration-normal-valid-config-returns-nil

# Run tests matching an ERT name pattern
make test-name TEST="test-chime-check-*"

# Use a specific Emacs version
make EMACS=emacs29 test

Writing tests

File structure

Every test file loads test-bootstrap.el, which sets up packages, dependencies, and chime itself:

;;; test-chime-FEATURE.el --- Tests for FEATURE -*- lexical-binding: t; -*-

;; Copyright (C) 2026 Craig Jennings
;; Author: Craig Jennings <c@cjennings.net>
;; License: GPL-3.0-or-later

;;; Commentary:
;; Unit tests for FEATURE.

;;; Code:

(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
(require 'testutil-time)     ; if using time utilities
(require 'testutil-events)   ; if using event utilities

;;; Normal Cases

(ert-deftest test-chime-FEATURE-normal-description ()
  "Descriptive docstring."
  (should (equal expected (function-under-test input))))

;;; Boundary Cases
;;; Error Cases

(provide 'test-chime-FEATURE)
;;; test-chime-FEATURE.el ends here

Naming convention

test-chime-FEATURE-CATEGORY-description
             |        |
             |        +-- normal, boundary, error
             +----------- function or module name

Examples:

  • test-chime-get-tags-normal-single-tag
  • test-chime-extract-time-boundary-midnight
  • test-chime-validate-configuration-error-missing-deps

Test categories

Tests are split into three categories:

  • Normal: Standard inputs, expected use cases
  • Boundary: Empty inputs, nil values, single-element lists, unicode, max values
  • Error: Invalid inputs, missing dependencies, malformed data

Test infrastructure

test-bootstrap.el

Loads ert, dash, alert, async, org-agenda, and chime.el so test files don't repeat that boilerplate. For debug tests, set chime-debug before requiring:

(setq chime-debug t)
(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))

testutil-time.el — Dynamic timestamps

Generates timestamps relative to "now" so tests never expire. The base time is always current-time + 30 days at 10:00 AM for consistency.

Core functions

Function Purpose Example
test-time-now Base time (today+30 at 10:00) (test-time-now)
test-time-at Relative to now (days, hours, min) (test-time-at 0 2 0) -> 2h from now
test-time-today-at Today at specific time (test-time-today-at 14 30) -> 2:30 PM
test-time-tomorrow-at Tomorrow at specific time (test-time-tomorrow-at 9 0)
test-time-yesterday-at Yesterday at specific time (test-time-yesterday-at 17 0)
test-time-days-from-now N days in future (test-time-days-from-now 3 14 0)
test-time-days-ago N days in past (test-time-days-ago 7)

Timestamp strings

Function Purpose Output
test-timestamp-string Org timestamp <2026-05-04 Mon 14:00>
test-timestamp-string TIME t All-day timestamp <2026-05-04 Mon>
test-timestamp-range-string Date range <2026-05-04 Mon>--<2026-05-07 Thu>
test-timestamp-repeating Repeating event <2026-05-04 Mon +1w>

Mock macro

(with-test-time (test-time-now)
  ;; current-time is mocked to return the test base time
  (should (equal (current-time) (test-time-now))))

testutil-events.el — Event creation

Builds org events and event data structures for tests.

Creating org content

;; Single timed event
(test-create-org-event "Meeting" (test-time-now) t)
;; => "* TODO Meeting\nSCHEDULED: <2026-05-04 Mon 10:00>\n"

;; All-day event
(test-create-org-event "Birthday" (test-time-now) nil t)
;; => "* Birthday\n<2026-05-04 Mon>\n"

;; Multiple events
(test-create-org-events
 `(("Meeting" ,(test-time-at 0 2 0) t)
   ("Call" ,(test-time-at 0 4 0) t)))

Gathering events from content

;; Gather all events from org content string
(let ((events (test-gather-events-from-content content)))
  (should (= 2 (length events))))

;; Gather exactly one event (errors if != 1)
(let ((event (test-gather-single-event-from-content content)))
  (should (string= "Meeting" (cdr (assoc 'title event)))))

Creating event data directly

;; Simple event (title, time, optional interval/severity)
(test-make-simple-event "Call" (test-time-now) 5 'high)

;; Full control over data structure
(test-make-event-data "Meeting"
                      (list (cons (test-timestamp-string time) time))
                      '((10 . medium) (0 . high)))

Convenience macros

;; Temp file with auto-cleanup
(with-test-event-file (test-create-org-event "Meeting" (test-time-now))
  (with-current-buffer test-buffer
    (should (search-forward "Meeting" nil t))))

;; Gather events with auto-cleanup
(with-gathered-events (test-create-org-event "Call" (test-time-now))
                      events
  (should (= 1 (length events))))

;; Standard setup/teardown (test base dir lifecycle)
(with-test-setup
  (let ((file (chime-create-temp-test-file)))
    (should (file-exists-p file))))

;; Override chime config for a test
(with-chime-config
  chime-modeline-lookahead-minutes 1440
  chime-tooltip-lookahead-hours 24
  (should (= chime-modeline-lookahead-minutes 1440)))

;; Create org file from event specs
(with-org-event-file
    (("Meeting" event-time t)
     ("Birthday" bday-time nil t))
    org-file
  (setq org-agenda-files (list org-file)))

testutil-general.el — File system utilities

Manages test directories and temp files under ~/.temp-chime-tests/.

Function Purpose
chime-create-test-base-dir Create test root (idempotent)
chime-delete-test-base-dir Recursively delete test root
chime-create-temp-test-file Create uniquely named temp file
chime-create-temp-test-file-with-content Temp file with content
chime-create-test-subdirectory Create a subdirectory under test root
chime-create-directory-or-file-ensuring-parents Create dir (trailing /) or file

All paths are sandboxed under chime-test-base-dir – attempts to escape are rejected with an error.

Key patterns

Mocking external dependencies

(cl-letf (((symbol-function 'file-exists-p) (lambda (_) t))
          ((symbol-function 'require) (lambda (_ &optional _ _) t)))
  ;; these functions are mocked only within this scope
  )

Mocking time

(with-test-time (test-time-today-at 9 55)
  ;; current-time now returns 9:55 AM
  (should (chime--timestamp-within-interval-p timestamp 10)))

Overriding config

(with-chime-config
  chime-sound-file nil
  chime-alert-intervals '((10 . medium))
  (chime--process-notifications events))

Important notes

  1. Load path: The Makefile automatically finds dependencies in ~/.emacs.d/elpa/
  2. Fuzzy matching: test-file and test-one support partial names
  3. Test logs: Output saved to test-output.log, test-file-output.log, etc. in tests/
  4. Mock warnings: "Redefining 'file-exists-p' might break native compilation" is normal and expected
  5. Dynamic timestamps: Never hardcode dates – use testutil-time.el functions
  6. Test isolation: Each test file runs in its own Emacs process

Dependencies

Required packages (auto-detected by Makefile):

  • dash (list manipulation)
  • alert (notifications)
  • async (async processes)
  • org-agenda (built-in, events source)

Use make check-deps (from tests/) to verify all dependencies are installed.

Test inventory

645 tests across 53 files (as of 2026-04-04).

Top files by count:

File Tests
test-convert-org-contacts-birthdays.el 35
test-chime-sanitize-title.el 30
test-chime-notification-text.el 30
test-chime-timestamp-parse.el 26
test-chime-12hour-format.el 26
test-chime-time-left.el 24
test-chime-modeline.el 20
test-chime-time-utilities.el 19
test-chime-all-day-events.el 19
test-chime-update-modeline.el 18
test-chime-has-timestamp.el 18

Run make count from tests/ for the full breakdown.