See attached 2 patches.  The "fixup" patch is just to see what changed from
last time.  Apply the other one.

Tests pass on emacs 28, 29, 30.2 with TZ=UTC.

Tests pass on emacs 30.2 with TZ="Europe/Istanbul" and TZ="America/New_York".

Tests pass on emacs 30.2 with TZ="Asia/Kathmandu" and TZ="Canada/Newfoundland"
(except for `test-ox-icalendar/todo-repeater-until-utc')


Ihor Radchenko <[email protected]> writes:

> Morgan Smith <[email protected]> writes:
>
>> Tests do not pass on Emacs 28 because Ihor used `string-equal-ignore-case' in
>> the latest commits.
>
> Fixed
> https://git.savannah.gnu.org/cgit/emacs/org-mode.git/commit/?id=99ee1bcd6
>

Thanks!

>
> Time zones are crazy indeed. There are many more bugs in this area.
> There are also locales...
>

Oh dear.  My todo list includes an item to run all the tests with the
system time set near DST changes (using guix stuff again :P).  I'm
adding the locales to the todo list as well now.  I'm sure I'll find
some fun stuff there.

>> Although personally I'm a fan of tests that show exactly what behavior the
>> software does, even if it's wrong.  If the behavior starts being wrong in a
>> different way I'd like to know and the ":expected-result :failed" won't
>> tell me.
>
> Because not every single aspect of Org behavior is documented, tests are
> sometimes used as kind of reference about how the code *should*
> behave. So, having a test for something implies that one or other
> behavior (even if awkward) is intentional. In some cases, Org mode's
> behavior is weird only on a first glance. However, that behavior may be
> thoughtfully discussed on the mailing list in the past and then codified
> in the tests. Having tests that simply test for knowingly erroneous
> behavior will break such assumptions.
>

Ah I see.  I like the tests as a way to rigorously document a bug but
others might interpret that as rigorous documentation of org-mode
itself.  I would appreciate it if we could come up with a way to
rigorously document a bug but I suppose if I just fix the bugs then it
won't be needed.

>> That being said, I did just realize that grepping the
>> code for "todo" items is not very easy.  Should I be using "XXX" comments
>> instead?
>
> We use FIXME. It is more greppable in Org's context.
> See https://orgmode.org/worg/org-maintenance.html#minor-major-releases
>

Noted!  I should really read that document at some point :P

>> +(ert-deftest test-org-habit/org-extend-today-until ()
>> +  "Test habit graph with `org-extend-today-until' set."
>> +  :expected-result :failed
>> ...
>> +              ;; TODO: actual result is
>> +              ;; (t "\nhabit   * *     \n")
>> +              (1 "\nhabit  ** !   \n")
>
> Is this todo still relevant?
>

Yes that is the current behavior.  I haven't fixed any bugs yet.  Just
trying to document them right now.

>> +           (dolist (test-time '(2009-10-15
>> +                                2009-10-16
>> ...
>> +             (let ((expected-output-string
>> +                    (cl-case test-time
>> +                      (2009-10-15
>> +                       " *   *  * *     * *  *       \n")
>> +                      (2009-10-16
>> +                       "*   *  * *     * *  *!       \n")
>> ...
>
> I believe that you can simplify this by using an alist
> '((2009-10-15 . "...")
>   (2009-10-16 . "...) ...)

Done.

>From 3cef4b41fcdb6ff4d15e08061c923a8bc8b368af Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Sat, 25 Oct 2025 11:29:01 -0400
Subject: [PATCH] fixup! 8527f2b6f2bc9b4e7afb2ee9f4cadff90c9c9ccb

---
 testing/lisp/test-org-habit.el | 32 ++++++++++++--------------------
 1 file changed, 12 insertions(+), 20 deletions(-)

diff --git a/testing/lisp/test-org-habit.el b/testing/lisp/test-org-habit.el
index 8add6322f..cfbd25dc3 100644
--- a/testing/lisp/test-org-habit.el
+++ b/testing/lisp/test-org-habit.el
@@ -142,7 +142,7 @@ test-org-habit/org-extend-today-until
            (string-equal
             (cl-case org-extend-today-until
               (0 "\nhabit  * *!     \n")
-              ;; TODO: actual result is
+              ;; Current behavior is:
               ;; (t "\nhabit   * *     \n")
               (1 "\nhabit  ** !   \n")
               (2 "\nhabit * * !   \n"))
@@ -192,7 +192,7 @@ test-org-habit/dst
 - State \"DONE\"       from \"TODO\"       [2009-10-25 Sun]"
           (should
            (string-equal
-            ;; TODO: we lost a day in the transition! actual result:
+            ;; we lost a day in the transition! actual result:
             ;; "\nhabit  * *!    \n"
             "\nhabit  * *!     \n"
             (progn
@@ -290,24 +290,16 @@ test-org-habit/show-habits
                                 2009-10-21
                                 2009-10-22))
              (let ((expected-output-string
-                    (cl-case test-time
-                      (2009-10-15
-                       " *   *  * *     * *  *       \n")
-                      (2009-10-16
-                       "*   *  * *     * *  *!       \n")
-                      (2009-10-17
-                       "   *  * *     * *  * !       \n")
-                      (2009-10-18
-                       "  *  * *     * *  *  !       \n")
-                      (2009-10-19
-                       " *  * *     * *  *   !       \n")
-                      (2009-10-20
-                       "*  * *     * *  *    !       \n")
-                      (2009-10-21
-                       "  * *     * *  *     !       \n")
-                      (2009-10-22
-                       " * *     * *  *      !       \n")
-                      (t (cl-assert nil t "Missing case for: %S!" (symbol-name test-time))))))
+                    (alist-get
+                     test-time
+                     '((2009-10-15 . " *   *  * *     * *  *       \n")
+                       (2009-10-16 . "*   *  * *     * *  *!       \n")
+                       (2009-10-17 . "   *  * *     * *  * !       \n")
+                       (2009-10-18 . "  *  * *     * *  *  !       \n")
+                       (2009-10-19 . " *  * *     * *  *   !       \n")
+                       (2009-10-20 . "*  * *     * *  *    !       \n")
+                       (2009-10-21 . "  * *     * *  *     !       \n")
+                       (2009-10-22 . " * *     * *  *      !       \n")))))
                (org-test-at-time (symbol-name test-time)
                  (should
                   (string-equal
-- 
2.51.0

>From f51bb047e87e3ad59da8c369571e3d35cc19b470 Mon Sep 17 00:00:00 2001
From: Morgan Smith <[email protected]>
Date: Thu, 23 Jan 2025 15:35:50 -0500
Subject: [PATCH] testing/lisp/test-org-habit.el: Add org habit tests

* testing/lisp/test-org-habit.el: New file full of tests.
---
 testing/lisp/test-org-habit.el | 421 +++++++++++++++++++++++++++++++++
 1 file changed, 421 insertions(+)
 create mode 100644 testing/lisp/test-org-habit.el

diff --git a/testing/lisp/test-org-habit.el b/testing/lisp/test-org-habit.el
new file mode 100644
index 000000000..beb6ee0d4
--- /dev/null
+++ b/testing/lisp/test-org-habit.el
@@ -0,0 +1,421 @@
+;;; test-org-habit.el --- Tests for org-habit.el -*- lexical-binding: t ; -*-
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Unit tests for Org Habits.
+
+;;; Code:
+
+(require 'org-test "../testing/org-test")
+(require 'org-agenda)
+(require 'org-habit)
+(require 'test-org-agenda)
+
+
+;; Tests
+
+(defvar org-test-habit-no-fluff-agenda
+  '(("f" "no fluff" agenda ""
+     ((org-agenda-overriding-header "")
+      (org-agenda-format-date "")
+      (org-agenda-span 'day)
+      (org-agenda-show-all-dates nil)
+      (org-agenda-todo-keyword-format "")
+      (org-agenda-prefix-format "")))))
+
+(defun org-test-habit-agenda-string (repeater-type-string repeater-deadline?)
+  "Return an org habit test string.
+REPEATER-TYPE-STRING is used as the repeater type (ex.  \".+\").
+When REPEATER-DEADLINE? is non-nil, add a repeater deadline.
+Order is determined by `org-log-states-order-reversed'."
+  (concat
+   "* TODO Shave
+SCHEDULED: <2009-10-17 Sat " repeater-type-string "2d"
+   (if repeater-deadline?
+       "/4d"
+     "")
+   ">
+:PROPERTIES:
+:STYLE:    habit
+:LAST_REPEAT: [2009-10-19 Mon 00:36]
+:END:
+"
+
+   (if org-log-states-order-reversed
+       "- State \"DONE\"       from \"TODO\"       [2009-10-15 Thu]
+- State \"DONE\"       from \"TODO\"       [2009-10-12 Mon]
+- CLOSING NOTE [2009-10-10 Sat] \\
+  this style occurs when `org-log-done' is `note'.
+- State \"DONE\"       from \"TODO\"       [2009-10-04 Sun]
+- State \"DONE\"       from \"TODO\"       [2009-10-02 Fri]
+- State \"DONE\"       from \"TODO\"       [2009-09-29 Tue]
+- State \"DONE\"       from \"TODO\"       [2009-09-25 Fri]
+- State \"DONE\"       from \"TODO\"       [2009-09-19 Sat]
+- State \"DONE\"       from \"TODO\"       [2009-09-16 Wed]
+- State \"DONE\"       from \"TODO\"       [2009-09-12 Sat]"
+
+     "- State \"DONE\"       from \"TODO\"       [2009-09-12 Sat]
+- State \"DONE\"       from \"TODO\"       [2009-09-16 Wed]
+- State \"DONE\"       from \"TODO\"       [2009-09-19 Sat]
+- State \"DONE\"       from \"TODO\"       [2009-09-25 Fri]
+- State \"DONE\"       from \"TODO\"       [2009-09-29 Tue]
+- State \"DONE\"       from \"TODO\"       [2009-10-02 Fri]
+- State \"DONE\"       from \"TODO\"       [2009-10-04 Sun]
+- CLOSING NOTE [2009-10-10 Sat] \\
+  this style occurs when `org-log-done' is `note'.
+- State \"DONE\"       from \"TODO\"       [2009-10-12 Mon]
+- State \"DONE\"       from \"TODO\"       [2009-10-15 Thu]")))
+
+(defmacro org-test-habit (&rest body)
+  "Run BODY multiple times for testing habits.
+Add agenda from `org-test-habit-no-fluff-agenda' to
+`org-agenda-custom-commands'.
+
+Use habit data from `org-test-habit-agenda-string' both with and without
+a repeater deadline and the the log data reversed and not-reversed."
+  (declare (indent 0))
+  `(let ((org-agenda-custom-commands
+          org-test-habit-no-fluff-agenda))
+     (dolist (org-log-states-order-reversed '(t nil))
+       (dolist (repeater-deadline? '(nil t))
+         (dolist (repeater-type-string '(".+" "+" "++"))
+           (org-test-agenda-with-agenda
+               (org-test-habit-agenda-string repeater-type-string repeater-deadline?)
+             ,@body))))))
+
+(ert-deftest test-org-habit/simple-habit ()
+  "Test the agenda view for a simple habit."
+  ;; Avoid DST when TZ="Europe/Istanbul".  See `test-org-habit/dst'.
+  (org-test-with-timezone "UTC0"
+    (org-test-at-time "2009-10-22"
+      (let ((org-agenda-custom-commands
+             org-test-habit-no-fluff-agenda)
+            (org-habit-graph-column 5))
+        (org-test-agenda-with-agenda
+            "* TODO habit
+SCHEDULED: <2009-10-21 Sat ++2d>
+:PROPERTIES:
+:STYLE:    habit
+:END:
+- State \"DONE\"       from \"TODO\"       [2009-10-19 Sun]
+- State \"DONE\"       from \"TODO\"       [2009-10-17 Sun]"
+          (should
+           (string-equal
+            "\nhabit                * *  !       \n"
+            (progn
+              (org-agenda nil "f")
+              (buffer-string)))))))))
+
+(ert-deftest test-org-habit/org-extend-today-until ()
+  "Test habit graph with `org-extend-today-until' set."
+  :expected-result :failed
+  (org-test-at-time "2009-10-20"
+    (let ((org-agenda-custom-commands
+           org-test-habit-no-fluff-agenda)
+          (org-habit-preceding-days 5)
+          (org-habit-following-days 5)
+          (org-habit-graph-column 5)
+          (org-habit-show-all-today t))
+      (dolist (org-extend-today-until '(0 1 2))
+        (org-test-agenda-with-agenda
+            "* TODO habit
+SCHEDULED: <2009-10-20 Sat ++1d>
+:PROPERTIES:
+:STYLE:    habit
+:END:
+- State \"DONE\"       from \"TODO\"       [2009-10-19 Sun 00:20]
+- State \"DONE\"       from \"TODO\"       [2009-10-17 Sun 01:20]"
+          (should
+           (string-equal
+            (cl-case org-extend-today-until
+              (0 "\nhabit  * *!     \n")
+              ;; Current behavior is:
+              ;; (t "\nhabit   * *     \n")
+              (1 "\nhabit  ** !   \n")
+              (2 "\nhabit * * !   \n"))
+            (progn
+              (org-agenda nil "f")
+              (buffer-string)))))))))
+
+(ert-deftest test-org-habit/dst ()
+  "Test the habit graph traversing a daylight savings time transition."
+  :expected-result :failed
+  (org-test-with-timezone "America/New_York"
+    ;; DST transition (spring forward) [2009-03-08 01:59] -> [2009-03-08 03:00]
+    (org-test-at-time "2009-03-05"
+      (let ((org-agenda-custom-commands
+             org-test-habit-no-fluff-agenda)
+            (org-habit-preceding-days 5)
+            (org-habit-following-days 5)
+            (org-habit-graph-column 5))
+        (org-test-agenda-with-agenda
+            "* TODO habit
+SCHEDULED: <2009-03-05 Sat ++1d>
+:PROPERTIES:
+:STYLE:    habit
+:END:
+- State \"DONE\"       from \"TODO\"       [2009-03-04 Wed]
+- State \"DONE\"       from \"TODO\"       [2009-03-02 Mon]"
+          (should
+           (string-equal
+            "\nhabit  * *!     \n"
+            (progn
+              (org-agenda nil "f")
+              (buffer-string)))))))
+    ;; DST transition (fall back) [2009-11-01 01:59] -> [2009-11-01 01:00]
+    (org-test-at-time "2009-10-28"
+      (let ((org-agenda-custom-commands
+             org-test-habit-no-fluff-agenda)
+            (org-habit-preceding-days 5)
+            (org-habit-following-days 5)
+            (org-habit-graph-column 5))
+        (org-test-agenda-with-agenda
+            "* TODO habit
+SCHEDULED: <2009-10-28 Sat ++1d>
+:PROPERTIES:
+:STYLE:    habit
+:END:
+- State \"DONE\"       from \"TODO\"       [2009-10-27 Sun]
+- State \"DONE\"       from \"TODO\"       [2009-10-25 Sun]"
+          (should
+           (string-equal
+            ;; we lost a day in the transition! actual result:
+            ;; "\nhabit  * *!    \n"
+            "\nhabit  * *!     \n"
+            (progn
+              (org-agenda nil "f")
+              (buffer-string)))))))))
+
+(ert-deftest test-org-habit/habit ()
+  "Test the agenda view for a habit."
+  (org-test-at-time "2009-10-17"
+    (org-test-habit
+     (should
+      (string-equal
+       "\nShave                                      *  * *     * *  * !       \n"
+       (progn
+         (org-agenda nil "f")
+         (buffer-string)))))))
+
+(ert-deftest test-org-habit/graph-column ()
+  "Test how modifiying `org-habit-graph-column' affects habits in the agenda."
+  (org-test-at-time "2009-10-17"
+    (org-test-habit
+     (dolist (org-habit-graph-column '(0 1 2 3 10 20 40 100))
+       (should
+        (string-equal
+         (cl-case org-habit-graph-column
+           (0 "\n   *  * *     * *  * !       \n")
+           (1 "\nS   *  * *     * *  * !       \n")
+           (2 "\nSh   *  * *     * *  * !       \n")
+           (3 "\nSha   *  * *     * *  * !       \n")
+           ((10 20 40 100) (concat "\nShave"
+                                   (make-string (- org-habit-graph-column 2) 32)
+                                   "*  * *     * *  * !       \n"))
+           (t (cl-assert nil nil "Missing case!")))
+         (progn
+           (org-agenda nil "f")
+           (buffer-string))))))))
+
+(ert-deftest test-org-habit/preceding-days ()
+  "Test how modifiying `org-habit-preceding-days' affects habits in the agenda."
+  (org-test-at-time "2009-10-17"
+    (org-test-habit
+     (dolist (org-habit-preceding-days '(0 1 2 3 10 20 40 100))
+       (should
+        (string-equal
+         (cl-case org-habit-preceding-days
+           (0 " !       \n")
+           (1 "  !       \n")
+           (2 " * !       \n")
+           (3 "  * !       \n")
+           (10 "    * *  * !       \n")
+           (20 "   *  * *     * *  * !       \n")
+           ((40 100) (concat (make-string (- org-habit-preceding-days 34) 32)
+                             "*   *  *     *   *  * *     * *  * !       \n"))
+           (t (cl-assert nil nil "Missing case!")))
+         (progn
+           (org-agenda nil "f")
+           (buffer-substring (+ 1 org-habit-graph-column) (point-max)))))))))
+
+(ert-deftest test-org-habit/following-days ()
+  "Test how modifiying `org-habit-following-days' affects habits in the agenda."
+  ;; Avoid DST when TZ="America/New_York".  See `test-org-habit/dst'.
+  (org-test-with-timezone "UTC0"
+    (org-test-at-time "2009-10-17"
+      (org-test-habit
+       (dolist (org-habit-following-days '(0 1 2 3 10 20 40 100))
+         (should
+          (string-equal
+           (cl-case org-habit-following-days
+             (0   "    *  * *     * *  *  \n")
+             ((1 2 3 10 20 40 100)
+              (concat "    *  * *     * *  * !"
+                      (make-string org-habit-following-days 32)
+                      "\n"))
+             (t (cl-assert nil nil "Missing case!")))
+           (progn
+             (org-agenda nil "f")
+             (buffer-substring (+ 1 org-habit-graph-column) (point-max))))))))))
+
+(ert-deftest test-org-habit/show-habits ()
+  "Test displaying habits in the agenda at various points in time.
+Also test modifying the variables `org-habit-show-habits',
+`org-habit-show-habits-only-for-today', and `org-habit-show-all-today'."
+  ;; Avoid DST when TZ="Europe/Istanbul".  See `test-org-habit/dst'.
+  (org-test-with-timezone "UTC0"
+    (org-test-habit
+     (dolist (org-habit-show-habits '(nil t))
+       (dolist (org-habit-show-habits-only-for-today '(nil t))
+         (dolist (org-habit-show-all-today '(nil t))
+           (dolist (test-time '(2009-10-15
+                                2009-10-16
+                                2009-10-17
+                                2009-10-18
+                                2009-10-19
+                                2009-10-20
+                                2009-10-21
+                                2009-10-22))
+             (let ((expected-output-string
+                    (alist-get
+                     test-time
+                     '((2009-10-15 . " *   *  * *     * *  *       \n")
+                       (2009-10-16 . "*   *  * *     * *  *!       \n")
+                       (2009-10-17 . "   *  * *     * *  * !       \n")
+                       (2009-10-18 . "  *  * *     * *  *  !       \n")
+                       (2009-10-19 . " *  * *     * *  *   !       \n")
+                       (2009-10-20 . "*  * *     * *  *    !       \n")
+                       (2009-10-21 . "  * *     * *  *     !       \n")
+                       (2009-10-22 . " * *     * *  *      !       \n")))))
+               (org-test-at-time (symbol-name test-time)
+                 (should
+                  (string-equal
+                   (if org-habit-show-habits
+                       (cl-case test-time
+                         ((2009-10-15 2009-10-16)
+                          (if org-habit-show-all-today
+                              expected-output-string
+                            ""))
+                         ((2009-10-17 2009-10-18 2009-10-19 2009-10-20 2009-10-21 2009-10-22)
+                          expected-output-string)
+                         (t (cl-assert nil t "Missing case for: %S!" (symbol-name test-time))))
+                     "")
+                   (progn
+                     (org-agenda nil "f")
+                     (let ((result (buffer-string)))
+                       (if (string-empty-p result)
+                           result
+                         (substring result (+ 1 org-habit-graph-column))))))))))))))))
+
+(ert-deftest test-org-habit/toggle-display-in-agenda ()
+  "Test the agenda view for a habit."
+  (let ((org-agenda-custom-commands
+         '(("f" "no fluff" agenda ""
+            ;; This differs from `org-test-habit-no-fluff-agenda' by
+            ;; adding this header.  Without this we have cases where
+            ;; the agenda buffer is completly empty and that causes
+            ;; funny things to happen
+            ((org-agenda-overriding-header "h")
+             (org-agenda-format-date "")
+             (org-agenda-span 'day)
+             (org-agenda-show-all-dates nil)
+             (org-agenda-todo-keyword-format "")
+             (org-agenda-prefix-format "")))))
+        (org-habit-graph-column 7)
+        (org-habit-following-days 1)
+        (org-habit-preceding-days 5))
+    ;; (test-time . expected-string)
+    (dolist (test-data '(("2009-10-15" . "h\n\nShave  * *  * \n")
+                         ("2009-10-17" . "h\n\nShave  *  * ! \n")))
+      (org-test-at-time (car test-data)
+        (org-test-agenda-with-agenda
+            (org-test-habit-agenda-string "++" nil)
+          (org-agenda nil "f")
+          (should
+           (string-equal
+            (if (string-equal (car test-data) "2009-10-17")
+                (cdr test-data)
+              "h\n")
+            (buffer-string)))
+          (org-habit-toggle-display-in-agenda nil)
+          (should
+           (string-equal
+            "h\n"
+            (buffer-string)))
+          (org-habit-toggle-display-in-agenda nil)
+          (should
+           (string-equal
+            (if (string-equal (car test-data) "2009-10-17")
+                (cdr test-data)
+              "h\n")
+            (buffer-string)))
+          (org-habit-toggle-display-in-agenda t)
+          (should
+           (string-equal
+            (cdr test-data)
+            (buffer-string))))))))
+
+;;; Bad habits
+
+(ert-deftest test-org-habit/bad-habit-no-repeater ()
+  "Test a habit without a repeater."
+  (org-test-agenda-with-agenda
+      "* TODO no repeater
+SCHEDULED: <2009-10-17 Sat>
+:PROPERTIES:
+:STYLE:    habit
+:END:"
+    (should-error
+     (org-agenda nil "a"))))
+
+(ert-deftest test-org-habit/bad-habit-short-repeater ()
+  "Test a habit with a period of less then 1 day."
+  (org-test-agenda-with-agenda
+      "* TODO repeat period less then 1 day
+SCHEDULED: <2009-10-17 Sat +0d>
+:PROPERTIES:
+:STYLE:    habit
+:END:"
+    (should-error
+     (org-agenda nil "a"))))
+
+(ert-deftest test-org-habit/bad-habit-no-scheduled ()
+  "Test a habit that is not scheduled."
+  (org-test-agenda-with-agenda
+      "* TODO no scheduled <2009-10-17 Sat +1d>
+:PROPERTIES:
+:STYLE:    habit
+:END:"
+    (should-error
+     (org-agenda nil "a"))))
+
+(ert-deftest test-org-habit/bad-habit-deadline-less-scheduled ()
+  "Test a habit where the deadline is less then or equal to the scheduled."
+  (dolist (deadline '("1d" "2d"))
+    (org-test-agenda-with-agenda
+        (concat
+         "* TODO deadline < or = to scheduled
+SCHEDULED: <2009-10-17 Sat +2d/" deadline ">
+:PROPERTIES:
+:STYLE:    habit
+:END:")
+      (should-error
+       (org-agenda nil "a")))))
+
+
+(provide 'test-org-habit)
+
+;;; test-org-habit.el ends here

base-commit: ea38f257d65d1dad02fa58da06b74682e40db0f3
-- 
2.51.0

Reply via email to