Hello!

TLDR: I wrote a test suite for org habit.  See attached.

I am interested in adding some features to org habit.  However, it would be
much easier if org habit used the org-element API so I am interested in
re-writing org habit.  However, re-writing org habit would be much easier if
there was a comprehensive test suite.

I have now written such a test suite.  I have used testcover to ensure my test
suite covers nearly every single code path at least once (100% coverage is
always hard to achieve and I believe impossible in this case without a couple
code changes).

I ran the test suite using this guix command which used emacs version 30.2 in a
container.

=guix shell -C emacs git coreutils texinfo make glibc tzdata -- make test=

I also tried it with the emacs-next package which is built from the development
branch at commit 9663c959c73 (2025-04-07).

Please let me know what you think.

Thanks,

Morgan

>From a7472679650688670f11f574dea6102afea31cb1 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 | 340 +++++++++++++++++++++++++++++++++
 1 file changed, 340 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..652a2ca70
--- /dev/null
+++ b/testing/lisp/test-org-habit.el
@@ -0,0 +1,340 @@
+;;; 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."
+  (org-test-at-time "2009-10-22"
+    (let ((org-agenda-custom-commands
+           org-test-habit-no-fluff-agenda))
+      (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/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 "\nShave                                   !       \n")
+            (1 "\nShave                                    !       \n")
+            (2 "\nShave                                   * !       \n")
+            (3 "\nShave                                    * !       \n")
+            (10 "\nShave                                      * *  * !       \n")
+            (20 "\nShave                                     *  * *     * *  * !       \n")
+            ((40 100) (concat "\nShave"
+                              (make-string org-habit-preceding-days 32)
+                              "*   *  *     *   *  * *     * *  * !       \n"))
+            (t (cl-assert nil nil "Missing case!")))
+          (progn
+            (org-agenda nil "f")
+            (buffer-string))))))))
+
+(ert-deftest test-org-habit/following-days ()
+  "Test how modifiying `org-habit-following-days' affects habits in the agenda."
+  (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   "\nShave                                      *  * *     * *  *  \n")
+            ((1 2 3 10 20 40 100)
+             (concat "\nShave                                      *  * *     * *  * !"
+                     (make-string org-habit-following-days 32)
+                     "\n"))
+            (t (cl-assert nil nil "Missing case!")))
+          (progn
+            (org-agenda nil "f")
+            (buffer-string))))))))
+
+(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'."
+  (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
+                   (cl-case test-time
+                    (2009-10-15
+                     "\nShave                                    *   *  * *     * *  *       \n")
+                    (2009-10-16
+                     "\nShave                                   *   *  * *     * *  *!       \n")
+                    (2009-10-17
+                     "\nShave                                      *  * *     * *  * !       \n")
+                    (2009-10-18
+                     "\nShave                                     *  * *     * *  *  !       \n")
+                    (2009-10-19
+                     "\nShave                                    *  * *     * *  *   !       \n")
+                    (2009-10-20
+                     "\nShave                                   *  * *     * *  *    !       \n")
+                    (2009-10-21
+                     "\nShave                                     * *     * *  *     !       \n")
+                    (2009-10-22
+                     "\nShave                                    * *     * *  *      !       \n")
+                    (t (cl-assert nil t "Missing case for: %S!" (symbol-name test-time))))))
+              (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")
+                    (buffer-string))))))))))))
+
+(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: 76ce3fcc573e79d186e4ab6b8d6c06c9d078e498
-- 
2.51.0

Reply via email to