1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
;;; testutil-time.el --- Time utilities for dynamic test timestamps -*- 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:
;; Utilities for generating dynamic timestamps in tests.
;; Tests should use relative time relationships (TODAY, TOMORROW, etc.)
;; instead of hardcoded dates to avoid test expiration.
;;; Code:
(require 'org)
;;; Core Time Generation
(defun test-time-now ()
"Return a base 'now' time that's always valid.
Uses actual current time + 30 days to ensure tests remain valid.
Always returns 10:00 AM on that day for consistency."
(let* ((now (current-time))
(decoded (decode-time now))
(future-time (time-add now (days-to-time 30))))
;; Set to 10:00 AM for consistency
(encode-time 0 0 10
(decoded-time-day (decode-time future-time))
(decoded-time-month (decode-time future-time))
(decoded-time-year (decode-time future-time)))))
(defun test-time-at (days hours minutes)
"Return time relative to test-time-now.
DAYS, HOURS, MINUTES can be positive (future) or negative (past).
Examples:
(test-time-at 0 0 0) ; NOW
(test-time-at 0 2 0) ; 2 hours from now
(test-time-at -1 0 0) ; Yesterday at same time
(test-time-at 1 0 0) ; Tomorrow at same time"
(let* ((base (test-time-now))
(seconds (+ (* days 86400)
(* hours 3600)
(* minutes 60))))
(time-add base (seconds-to-time seconds))))
;;; Convenience Functions
(defun test-time-today-at (hour minute)
"Return time for TODAY at HOUR:MINUTE.
Example: (test-time-today-at 14 30) ; Today at 2:30 PM"
(let* ((base (test-time-now))
(decoded (decode-time base)))
(encode-time 0 minute hour
(decoded-time-day decoded)
(decoded-time-month decoded)
(decoded-time-year decoded))))
(defun test-time-yesterday-at (hour minute)
"Return time for YESTERDAY at HOUR:MINUTE."
(test-time-at -1 (- hour 10) minute))
(defun test-time-tomorrow-at (hour minute)
"Return time for TOMORROW at HOUR:MINUTE."
(test-time-at 1 (- hour 10) minute))
(defun test-time-days-ago (days &optional hour minute)
"Return time for DAYS ago, optionally at HOUR:MINUTE.
If HOUR/MINUTE not provided, uses 10:00 AM."
(let ((h (or hour 10))
(m (or minute 0)))
(test-time-at (- days) (- h 10) m)))
(defun test-time-days-from-now (days &optional hour minute)
"Return time for DAYS from now, optionally at HOUR:MINUTE.
If HOUR/MINUTE not provided, uses 10:00 AM."
(let ((h (or hour 10))
(m (or minute 0)))
(test-time-at days (- h 10) m)))
;;; Timestamp String Generation
(defun test-timestamp-string (time &optional all-day-p)
"Convert Emacs TIME to org timestamp string.
If ALL-DAY-P is non-nil, omit time component: <2025-10-24 Thu>
Otherwise include time: <2025-10-24 Thu 14:00>
Correctly calculates day-of-week name to match the date."
(let* ((decoded (decode-time time))
(year (decoded-time-year decoded))
(month (decoded-time-month decoded))
(day (decoded-time-day decoded))
(hour (decoded-time-hour decoded))
(minute (decoded-time-minute decoded))
(dow (decoded-time-weekday decoded))
(day-names ["Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat"])
(day-name (aref day-names dow)))
(if all-day-p
(format "<%04d-%02d-%02d %s>" year month day day-name)
(format "<%04d-%02d-%02d %s %02d:%02d>" year month day day-name hour minute))))
(defun test-timestamp-range-string (start-time end-time)
"Create range timestamp from START-TIME to END-TIME.
Example: <2025-10-24 Thu>--<2025-10-27 Sun>"
(format "%s--%s"
(test-timestamp-string start-time t)
(test-timestamp-string end-time t)))
(defun test-timestamp-repeating (time repeater &optional all-day-p)
"Add REPEATER to timestamp for TIME.
REPEATER should be like '+1w', '.+1d', '++1m'
Example: <2025-10-24 Thu +1w>"
(let ((base-ts (test-timestamp-string time all-day-p)))
;; Remove closing > and add repeater
(concat (substring base-ts 0 -1) " " repeater ">")))
;;; Mock Helpers
(defmacro with-test-time (base-time &rest body)
"Execute BODY with mocked current-time returning BASE-TIME.
BASE-TIME can be generated with test-time-* functions.
Example:
(with-test-time (test-time-now)
(do-something-that-uses-current-time))"
`(cl-letf (((symbol-function 'current-time)
(lambda () ,base-time)))
,@body))
(provide 'testutil-time)
;;; testutil-time.el ends here
|