> > > >>Now doing a refactor properly and ensuring that the code does exactly > >>what it used to do before is pretty difficult. In fact, I'm pretty sure > >>that with the patch I sent applied, that the code likely acts slightly > >>different then before. I can't find any specific examples, but I would > >>bet that they do exist. > > > > I understand this. With any change there is obviously a risk that a user's > > configuration will be broken. The best way to minimize that risk is testing. > > That is why I wrote new tests for org-habit. I actually wrote the tests > > that > > included my patch before I started refactoring the code in order to lock in > > the > > current behavior. You told me to remove those tests. I'm ok with that. What > > I > > don't understand is how are we supposed to ensure that our code doesn't > > break > > the user experience if we aren't allowed to test internal functions or > > undocumented behaviors? That's not a question for you obviously, that's a > > general question for the list. > > Your tests did not test anything new. Yes your tests are very helpful during > development as they move the error closer to where it appears, but the tests > are not novel as far as I can tell. >
> In your refactor you are doing two things: > > 1. Switching the functions to use the org-element API > > 2. Moving everything around > > > Both of these individually can create unwanted changes. Many of these > unwanted > changes can be caught by static analysis (just reading the diff). However, to > make this approach efficient, we need focused and small diffs. This is why I > have been asking for smaller and smaller commits. > > I do want to make something clear: I want all your changes. I want org-habit > to look radically different. I want all the features you want. However, I > want each step to be small and easy to understand. > > Now testing is useful, but it does not replace static analysis. I mean I > suppose in theory it could but in this instance it clearly does not. > > There are no tests for month/year repeaters and there are no tests for > malformed data. > I added week, month and years tests. I also added more tests for malformed data. In the future, it would be good to actually define errors for malformed data so that we can actually assert the correct errors are thrown for each type of malformed data. Unless I'm missing something, I don't know another way to confirm the correct errors are being thrown for the malformed data tests. Beyond that, I really do think we need more tests for org-habit. I will focus on expanding the current tests in the future instead of just adding new ones. > What data is accepted is where I suspect the switch to the org-element API > will > yield different results from the existing code. See below for some cases I > found > > This is not to say that we need those tests as we can still catch problems > with > static analysis. > > > > This is not what you said in your original email. You said: > > > >> 1. A code refactor and cleanup that does not add any extension machinery > > > >> 2. Add the extension machinery > > > >> 3. Add another habit style > > You're right. You gave me exactly what I asked for and now I'm asking for > something different. I'm really sorry and that must be really frustrating. > What I meant, but did not say, is that I would like small focused changes. > > > > I have attached a patch to this email that addresses all of your critiques. > > I > > combined everything back into a single function. > > I am new to review and I aplogize for the confusion. I'm going to need to > learn to express myself more clearly and concisly. My issue is not about how > many functions there are but is about the size of the diff. > > > I added back the original state change notes regex. > > I aplogize if I mislead you with the suggestion of `org-agenda-span-to-ndays' > and `org--log-note-format-regexp'. My intention was not to offer solutions > but > to point towards something I thought might be relevant for you to analyze > further. > > > Finally, I removed > > `org-habit--repeater-unit-to-days'. I am willing to keep working with you on > > this. I am willing to take critiques. However, I don't think it makes sense > > for > > me to review your patch on my thread. I started this thread fully expecting > > a > > code review. If you would like to do this refactor yourself, please let me > > know. I will cancel this thread so that you can start another one. > > I don't understand the issue with offering a patch for you to review. I > wanted > to clearly express the type of patch I wanted to see and figured the best way > was to show you exactly what I was looking for. I aplogize if this offended > you as that was not the intention. Although I'm still not sure exactly what > the issue with doing so is. > > If you'd like to request a different reviewer we can make that happen. > I apologize if my tone was too harsh. My point was that typically when you do a code review, you just review the code. I've never done a code review where the reviewer just sends me the version they want. I feel like that defeats the purpose of the whole endeavour. > Also I was wondering if using the Emacs builtin "range.el" library would be > useful for managing the list of done dates? It seems like a nifty library. > I'm honestly not a huge fan of "range.el". It's a good idea, but the execution leaves much to be desired in my opinion. Really, org-mode needs its own solution for date ranges. In any case, I won't be touching that code until we start working on the extension mechanism. > > +This list represents a \"habit\" for the rest of this module. > > At a future point it might be nice to make a "habit-data" cl-defstruct. > Maybe. Just converting the habit-data structure into an alist with symbol keys, e.g. (list (cons 'scheduled-date 739785) (cons 'repeater-style 1) ...), would already mean we could drop the defsubst and just use alist-get or map-elt. > > + (org-habit--repeater-unit-to-days (repeater-unit) > > + "Convert REPEATER-UNIT into a number of days." > > + (pcase repeater-unit > > + (`day 1) > > + (`week 7) > > + (`month 30.4) > > + (`year 365.25))) > > `cl-case' would do here. Doesn't handle 'hour value (explained more > below). > I updated this function so that it errors on hours or when there is no repeater unit at all. Is there any reason specifically that I should use `pcase' over `cl-case'? > > > + (org-habit--get-repeater-and-deadline-data (timestamp-element) > > + "Extract repeater and deadline data from TIMESTAMP-ELEMENT. > > + Returns a list with the following elements: > > + > > + 0: Scheduled date for the habit (may be in the past) > > + 1: \".+\"-style repeater for the schedule, in days > > + 2: Optional deadline (nil if not present) > > + 3: If deadline, the repeater for the deadline, otherwise nil." > > + (let* ((scheduled-date-in-days > > + (org-habit--convert-timestamp-to-days timestamp-element)) > > + (repeater-unit > > + (org-element-property :repeater-unit timestamp-element)) > > + (repeater-value > > + (org-element-property :repeater-value timestamp-element)) > > + (repeater-value-in-days > > + (* repeater-value (org-habit--repeater-unit-to-days > > repeater-unit))) > > + (deadline-unit > > + (org-element-property :repeater-deadline-unit > > timestamp-element)) > > + (deadline-value > > + (org-element-property :repeater-deadline-value > > timestamp-element)) > > + (deadline-value-in-days > > + (when deadline-value (* deadline-value > > (org-habit--repeater-unit-to-days deadline-unit)))) > > The existing code searches for repeaters with a regex of "dwmy" whereas > an org-element repeater can also include a "h" for hour. I believe this > code will multiply by 'nil' for hour repeaters/deadlines. > Yeah good catch. I mixed up the error handling. I made sure to be more consistent this time. > Also the previous code call `floor' on the value-in-days variables to > account for floats caused by month and year repeaters. Is that no > longer necessary? > That was a mistake. I added that back. > > + (save-excursion > > + (if pom (goto-char pom)) > > + (cl-assert (org-is-habit-p (point))) > > + (let* ((headline-element (org-element-at-point)) > > + (scheduled-timestamp > > + (org-element-property :scheduled headline-element)) > > + (repeater-type > > + (org-element-property :repeater-type scheduled-timestamp)) > > I imagine this will give an uninformative error if `scheduled-timestamp' > is nil > I made sure `org-habit--convert-timestamp-to-days' throws a good error when there is no timestamp. > > + (repeater-and-deadline-data (if repeater-type > > + > > (org-habit--get-repeater-and-deadline-data scheduled-timestamp) > > + (error "Habit `%s' has no > > scheduled repeat period or has an incorrect one" > > + (org-element-property > > :title headline-element)))) > > Excessively long lines > > Sorry, I've gotten in the bad habit of writing long lines. Le mar. 9 juin 2026 à 13:50, Morgan Smith <[email protected]> a écrit : > > Earl Chase <[email protected]> writes: > > > > >>Now doing a refactor properly and ensuring that the code does exactly > >>what it used to do before is pretty difficult. In fact, I'm pretty sure > >>that with the patch I sent applied, that the code likely acts slightly > >>different then before. I can't find any specific examples, but I would > >>bet that they do exist. > > > > I understand this. With any change there is obviously a risk that a user's > > configuration will be broken. The best way to minimize that risk is testing. > > That is why I wrote new tests for org-habit. I actually wrote the tests > > that > > included my patch before I started refactoring the code in order to lock in > > the > > current behavior. You told me to remove those tests. I'm ok with that. What > > I > > don't understand is how are we supposed to ensure that our code doesn't > > break > > the user experience if we aren't allowed to test internal functions or > > undocumented behaviors? That's not a question for you obviously, that's a > > general question for the list. > > Your tests did not test anything new. Yes your tests are very helpful during > development as they move the error closer to where it appears, but the tests > are not novel as far as I can tell. > > In your refactor you are doing two things: > > 1. Switching the functions to use the org-element API > > 2. Moving everything around > > > Both of these individually can create unwanted changes. Many of these > unwanted > changes can be caught by static analysis (just reading the diff). However, to > make this approach efficient, we need focused and small diffs. This is why I > have been asking for smaller and smaller commits. > > I do want to make something clear: I want all your changes. I want org-habit > to look radically different. I want all the features you want. However, I > want each step to be small and easy to understand. > > Now testing is useful, but it does not replace static analysis. I mean I > suppose in theory it could but in this instance it clearly does not. > > There are no tests for month/year repeaters and there are no tests for > malformed data. > > What data is accepted is where I suspect the switch to the org-element API > will > yield different results from the existing code. See below for some cases I > found > > This is not to say that we need those tests as we can still catch problems > with > static analysis. > > > > This is not what you said in your original email. You said: > > > >> 1. A code refactor and cleanup that does not add any extension machinery > > > >> 2. Add the extension machinery > > > >> 3. Add another habit style > > You're right. You gave me exactly what I asked for and now I'm asking for > something different. I'm really sorry and that must be really frustrating. > What I meant, but did not say, is that I would like small focused changes. > > > > I have attached a patch to this email that addresses all of your critiques. > > I > > combined everything back into a single function. > > I am new to review and I aplogize for the confusion. I'm going to need to > learn to express myself more clearly and concisly. My issue is not about how > many functions there are but is about the size of the diff. > > > I added back the original state change notes regex. > > I aplogize if I mislead you with the suggestion of `org-agenda-span-to-ndays' > and `org--log-note-format-regexp'. My intention was not to offer solutions > but > to point towards something I thought might be relevant for you to analyze > further. > > > Finally, I removed > > `org-habit--repeater-unit-to-days'. I am willing to keep working with you on > > this. I am willing to take critiques. However, I don't think it makes sense > > for > > me to review your patch on my thread. I started this thread fully expecting > > a > > code review. If you would like to do this refactor yourself, please let me > > know. I will cancel this thread so that you can start another one. > > I don't understand the issue with offering a patch for you to review. I > wanted > to clearly express the type of patch I wanted to see and figured the best way > was to show you exactly what I was looking for. I aplogize if this offended > you as that was not the intention. Although I'm still not sure exactly what > the issue with doing so is. > > If you'd like to request a different reviewer we can make that happen. > > Also I was wondering if using the Emacs builtin "range.el" library would be > useful for managing the list of done dates? It seems like a nifty library. > > > +This list represents a \"habit\" for the rest of this module. > > At a future point it might be nice to make a "habit-data" cl-defstruct. > > > + (org-habit--repeater-unit-to-days (repeater-unit) > > + "Convert REPEATER-UNIT into a number of days." > > + (pcase repeater-unit > > + (`day 1) > > + (`week 7) > > + (`month 30.4) > > + (`year 365.25))) > > `cl-case' would do here. Doesn't handle 'hour value (explained more > below). > > > > + (org-habit--get-repeater-and-deadline-data (timestamp-element) > > + "Extract repeater and deadline data from TIMESTAMP-ELEMENT. > > + Returns a list with the following elements: > > + > > + 0: Scheduled date for the habit (may be in the past) > > + 1: \".+\"-style repeater for the schedule, in days > > + 2: Optional deadline (nil if not present) > > + 3: If deadline, the repeater for the deadline, otherwise nil." > > + (let* ((scheduled-date-in-days > > + (org-habit--convert-timestamp-to-days timestamp-element)) > > + (repeater-unit > > + (org-element-property :repeater-unit timestamp-element)) > > + (repeater-value > > + (org-element-property :repeater-value timestamp-element)) > > + (repeater-value-in-days > > + (* repeater-value (org-habit--repeater-unit-to-days > > repeater-unit))) > > + (deadline-unit > > + (org-element-property :repeater-deadline-unit > > timestamp-element)) > > + (deadline-value > > + (org-element-property :repeater-deadline-value > > timestamp-element)) > > + (deadline-value-in-days > > + (when deadline-value (* deadline-value > > (org-habit--repeater-unit-to-days deadline-unit)))) > > The existing code searches for repeaters with a regex of "dwmy" whereas > an org-element repeater can also include a "h" for hour. I believe this > code will multiply by 'nil' for hour repeaters/deadlines. > > Also the previous code call `floor' on the value-in-days variables to > account for floats caused by month and year repeaters. Is that no > longer necessary? > > > + (save-excursion > > + (if pom (goto-char pom)) > > + (cl-assert (org-is-habit-p (point))) > > + (let* ((headline-element (org-element-at-point)) > > + (scheduled-timestamp > > + (org-element-property :scheduled headline-element)) > > + (repeater-type > > + (org-element-property :repeater-type scheduled-timestamp)) > > I imagine this will give an uninformative error if `scheduled-timestamp' > is nil > > > + (repeater-and-deadline-data (if repeater-type > > + > > (org-habit--get-repeater-and-deadline-data scheduled-timestamp) > > + (error "Habit `%s' has no > > scheduled repeat period or has an incorrect one" > > + (org-element-property > > :title headline-element)))) > > Excessively long lines
From ca55c06ea2edcfa5cc0ce64122398ab1cfae77f8 Mon Sep 17 00:00:00 2001 From: ApollonDeParnasse <[email protected]> Date: Sun, 31 May 2026 13:10:27 -0500 Subject: [PATCH] org-habit.el: `org-habit-parse-todo' refactor * lisp/org-habit.el (org-habit-parse-todo): Use the org-element API to get habit data. (org-habit-build-graph): Use repeater types instead of repeater values. * testing/lisp/test-org-habit.el (test-org-habit/simple-habit/week): New tests for `org-habit-parse-todo'. (test-org-habit/simple-habit/month): New tests for `org-habit-parse-todo'. (test-org-habit/bad-habit-no-repeater-type): New tests for `org-habit-parse-todo'. (test-org-habit/bad-habit-no-repeater-unit): New tests for `org-habit-parse-todo'. (test-org-habit/bad-habit-short-repeater): New tests for `org-habit-parse-todo'. (test-org-habit/bad-habit-no-scheduled): New tests for `org-habit-parse-todo'. --- lisp/org-habit.el | 236 ++++++++++++++++++++++++--------- testing/lisp/test-org-habit.el | 156 ++++++++++++++++++++++ 2 files changed, 327 insertions(+), 65 deletions(-) diff --git a/lisp/org-habit.el b/lisp/org-habit.el index 8d0108639..c8ff0cea4 100644 --- a/lisp/org-habit.el +++ b/lisp/org-habit.el @@ -35,6 +35,9 @@ (require 'org) (require 'org-agenda) +(declare-function org-element-property "org-element-ast" (property node)) +(declare-function org-element-end "org-element" (node)) + (defgroup org-habit nil "Options concerning habit tracking in Org mode." :tag "Org Habit" @@ -160,6 +163,7 @@ means of creating calendar-based reminders." :group 'org-faces) (defun org-habit-duration-to-days (ts) + (declare (obsolete nil "10.0")) (if (string-match "\\([0-9]+\\)\\([dwmy]\\)" ts) ;; lead time is specified. (floor (* (string-to-number (match-string 1 ts)) @@ -181,69 +185,171 @@ Returns a list with the following elements: 1: \".+\"-style repeater for the schedule, in days 2: Optional deadline (nil if not present) 3: If deadline, the repeater for the deadline, otherwise nil - 4: A list of all the past dates this todo was mark closed - 5: Repeater type as a string - -This list represents a \"habit\" for the rest of this module." - (save-excursion - (if pom (goto-char pom)) - (cl-assert (org-is-habit-p (point))) - (let* ((scheduled (org-get-scheduled-time (point))) - (scheduled-repeat (org-get-repeat (org-entry-get (point) "SCHEDULED"))) - (end (org-entry-end-position)) - (habit-entry (org-no-properties (nth 4 (org-heading-components)))) - closed-dates deadline dr-days sr-days sr-type) - (if scheduled - (setq scheduled (time-to-days scheduled)) - (error "Habit %s has no scheduled date" habit-entry)) - (unless scheduled-repeat - (error - "Habit `%s' has no scheduled repeat period or has an incorrect one" - habit-entry)) - (setq sr-days (org-habit-duration-to-days scheduled-repeat) - sr-type (progn (string-match "[\\.+]?\\+" scheduled-repeat) - (match-string-no-properties 0 scheduled-repeat))) - (unless (> sr-days 0) - (error "Habit %s scheduled repeat period is less than 1d" habit-entry)) - (when (string-match "/\\([0-9]+[dwmy]\\)" scheduled-repeat) - (setq dr-days (org-habit-duration-to-days - (match-string-no-properties 1 scheduled-repeat))) - (if (<= dr-days sr-days) - (error "Habit %s deadline repeat period is less than or equal to scheduled (%s)" - habit-entry scheduled-repeat)) - (setq deadline (+ scheduled (- dr-days sr-days)))) - (org-back-to-heading t) - (let* ((maxdays (+ org-habit-preceding-days org-habit-following-days)) - (reversed org-log-states-order-reversed) - (search (if reversed 're-search-forward 're-search-backward)) - (limit (if reversed end (point))) - (count 0) - (re (format - "^[ \t]*-[ \t]+\\(?:State \"%s\".*%s%s\\)" - (regexp-opt org-done-keywords) - org-ts-regexp-inactive - (let ((value (cdr (assq 'done org-log-note-headings)))) - (if (not value) "" - (concat "\\|" - (org-replace-escapes - (regexp-quote value) - `(("%d" . ,org-ts-regexp-inactive) - ("%D" . ,org-ts-regexp) - ("%s" . "\"\\S-+\"") - ("%S" . "\"\\S-+\"") - ("%t" . ,org-ts-regexp-inactive) - ("%T" . ,org-ts-regexp) - ("%u" . ".*?") - ("%U" . ".*?"))))))))) - (unless reversed (goto-char end)) - (while (and (< count maxdays) (funcall search re limit t)) - (push (time-to-days - (org-time-string-to-time - (or (match-string-no-properties 1) - (match-string-no-properties 2)))) - closed-dates) - (setq count (1+ count)))) - (list scheduled sr-days deadline dr-days closed-dates sr-type)))) + 4: A list of all the past dates this todo was mark done + 5: Symbol of the repeater type + +This list represents a \"habit\" for the rest of this module. +When POM is non-nil, it should be a marker or point." + (cl-flet* + ((org-habit--convert-timestamp-to-days (headline-element) + "Convert timestamp of HEADLINE-ELEMENT into a number of days since the epoch." + (if-let* ((time-string + (org-element-property :raw-value + (org-element-property + :scheduled + headline-element)))) + (time-to-days (org-time-string-to-time time-string)) + (error "Habit %s has no scheduled timestamp" + (org-element-property + :title + headline-element)))) + + (org-habit--repeater-unit-to-days (repeater-unit headline-element) + "Convert REPEATER-UNIT into a number of days. + HEADLINE-ELEMENT must be an org headline element." + (pcase-exhaustive repeater-unit + (`hour (error "Habit %s has an hourly repeater" + (org-element-property + :title + headline-element))) + (`day 1) + (`week 7) + (`month 30.4) + (`year 365.25) + (_ (error "Habit %s repeater is missing a unit" + (org-element-property + :title + headline-element))))) + + (org-habit--get-done-dates-for-todo (headline-element) + "Get the dates a todo with a repeater was marked done. + HEADLINE-ELEMENT should be a headline element with a + TODO and state changes notes." + (org-back-to-heading t) + (let* ((maxdays + (+ org-habit-preceding-days org-habit-following-days)) + (reversed org-log-states-order-reversed) + (search + (if reversed 're-search-forward 're-search-backward)) + (end (org-element-end headline-element)) + (limit (if reversed end (point))) + (count 0) + (done-dates) + (re (format + "^[ \t]*-[ \t]+\\(?:State \"%s\".*%s%s\\)" + (regexp-opt org-done-keywords) + org-ts-regexp-inactive + (let ((value (cdr (assq 'done org-log-note-headings)))) + (if (not value) "" + (concat "\\|" + (org-replace-escapes + (regexp-quote value) + `(("%d" . ,org-ts-regexp-inactive) + ("%D" . ,org-ts-regexp) + ("%s" . "\"\\S-+\"") + ("%S" . "\"\\S-+\"") + ("%t" . ,org-ts-regexp-inactive) + ("%T" . ,org-ts-regexp) + ("%u" . ".*?") + ("%U" . ".*?"))))))))) + (unless reversed (goto-char end)) + (while (and (< count maxdays) (funcall search re limit t)) + (push (time-to-days + (org-time-string-to-time + (or (match-string-no-properties 1) + (match-string-no-properties 2)))) + done-dates) + (setq count (1+ count))) + done-dates)) + + (org-habit--get-repeater-and-deadline-data (headline-element repeater-type) + "Extract repeater and deadline data from TIMESTAMP-ELEMENT. + Returns a list with the following elements: + + 0: Scheduled date for the habit (may be in the past) + 1: \".+\"-style repeater for the schedule, in days + 2: Optional deadline (nil if not present) + 3: If deadline, the repeater for the deadline, otherwise nil." + (let* ((timestamp-element + (org-element-property + :scheduled + headline-element)) + (scheduled-date-in-days + (org-habit--convert-timestamp-to-days + headline-element)) + (repeater-unit + (org-element-property + :repeater-unit + timestamp-element)) + (repeater-value + (org-element-property + :repeater-value + timestamp-element)) + (repeater-value-in-days + (floor (* repeater-value + (org-habit--repeater-unit-to-days + repeater-unit headline-element)))) + (deadline-unit + (org-element-property + :repeater-deadline-unit + timestamp-element)) + (deadline-value + (org-element-property + :repeater-deadline-value + timestamp-element)) + (deadline-value-in-days + (when deadline-value + (floor (* deadline-value + (org-habit--repeater-unit-to-days + deadline-unit headline-element))))) + (deadline-date-in-days + (when (and deadline-value + deadline-value-in-days) + (+ scheduled-date-in-days + (- deadline-value-in-days + repeater-value-in-days))))) + (cond + ((not repeater-type) (error + "Habit %s repeater does not have a type" + (org-element-property + :title + headline-element))) + ((< repeater-value-in-days 1) (error + "Habit %s repeater value must be at least one day" + (org-element-property + :title + headline-element))) + ((and deadline-value-in-days + (<= deadline-value-in-days + repeater-value-in-days)) + (error + "Habit %s deadline repeat period is less than or equal to scheduled (%s)" + (org-element-property + :title + headline-element) + repeater-value-in-days)) + (t (list scheduled-date-in-days + repeater-value-in-days + deadline-date-in-days + deadline-value-in-days)))))) + (save-excursion + (if pom (goto-char pom)) + (cl-assert (org-is-habit-p (point))) + (let* ((headline-element (org-element-at-point)) + (repeater-type (org-element-property + :repeater-type + (org-element-property + :scheduled + headline-element))) + (repeater-and-deadline-data + (org-habit--get-repeater-and-deadline-data + headline-element + repeater-type)) + (done-dates + (org-habit--get-done-dates-for-todo + headline-element))) + (append repeater-and-deadline-data + (list done-dates repeater-type)))))) (defsubst org-habit-scheduled (habit) (nth 0 habit)) @@ -363,8 +469,8 @@ current time." ;; At the last done date, use current ;; scheduling in all cases. ((null done-dates) scheduled) - ((equal type ".+") (+ last-done-date s-repeat)) - ((equal type "+") + ((equal type 'restart) (+ last-done-date s-repeat)) + ((equal type 'completed) ;; Since LAST-DONE-DATE, each done mark ;; shifted scheduled date by S-REPEAT. (- scheduled (* (length done-dates) s-repeat))) diff --git a/testing/lisp/test-org-habit.el b/testing/lisp/test-org-habit.el index b79bdf068..aa28b1b6f 100644 --- a/testing/lisp/test-org-habit.el +++ b/testing/lisp/test-org-habit.el @@ -82,6 +82,7 @@ SCHEDULED: <2009-10-17 Sat " repeater-type-string "2d" - 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 @@ -122,6 +123,69 @@ SCHEDULED: <2009-10-21 Sat ++2d> (org-agenda nil "f") (buffer-string))))))))) + +(ert-deftest test-org-habit/simple-habit/week () + "Test the agenda view for a simple habit." + (org-test-with-timezone "UTC0" + (org-test-at-time "2026-06-16" + (let ((org-agenda-custom-commands + org-test-habit-no-fluff-agenda) + (org-habit-graph-column 5)) + (org-test-agenda-with-agenda + "* TODO habit +SCHEDULED: <2026-06-16 Tue +1w> +:PROPERTIES: +:STYLE: habit +:END: +- State \"DONE\" from \"TODO\" [2026-06-16 Tue] +- State \"DONE\" from \"TODO\" [2026-06-09 Tue] +- State \"DONE\" from \"TODO\" [2026-06-02 Tue] +- State \"DONE\" from \"TODO\" [2026-05-26 Tue] +- State \"DONE\" from \"TODO\" [2026-05-12 Tue] +- State \"DONE\" from \"TODO\" [2026-05-05 Tue] +- State \"DONE\" from \"TODO\" [2026-04-28 Tue] +- State \"DONE\" from \"TODO\" [2026-04-21 Tue] +- State \"DONE\" from \"TODO\" [2026-04-14 Tue] +- State \"DONE\" from \"TODO\" [2026-04-07 Tue] +- State \"DONE\" from \"TODO\" [2026-03-31 Tue]" + (should + (string-equal + "\nhabit* * * * \n" + (progn + (org-agenda nil "f") + (buffer-string))))))))) + +(ert-deftest test-org-habit/simple-habit/month () + "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 "2026-06-17" + (let ((org-agenda-custom-commands + org-test-habit-no-fluff-agenda) + (org-habit-graph-column 5)) + (org-test-agenda-with-agenda + "* TODO habit +SCHEDULED: <2026-06-17 Wed .+1m/3m> +:PROPERTIES: +:STYLE: habit +:LAST_REPEAT: [2026-05-17 Mon 00:36] +:END: +- State \"DONE\" from \"TODO\" [2026-05-17 Sun] +- State \"DONE\" from \"TODO\" [2026-04-16 Thu] +- State \"DONE\" from \"TODO\" [2026-03-17 Tue] +- State \"DONE\" from \"TODO\" [2026-02-14 Sat] +- State \"DONE\" from \"TODO\" [2025-12-15 Mon] +- State \"DONE\" from \"TODO\" [2025-11-15 Sat] +- State \"DONE\" from \"TODO\" [2025-09-15 Mon] +- State \"DONE\" from \"TODO\" [2025-08-16 Sat] +- State \"DONE\" from \"TODO\" [2025-07-16 Wed]" + (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 @@ -376,6 +440,64 @@ SCHEDULED: <2009-10-17 Sat> (should-error (org-agenda nil "a")))) +(ert-deftest test-org-habit/bad-habit-no-repeater-type () + "Test a habit without a repeater type." + (org-test-agenda-with-agenda + "* TODO no repeater +SCHEDULED: <2009-10-17 Sat 7d> +:PROPERTIES: +:STYLE: habit +:END:" + (should-error + (org-agenda nil "a"))) + + (org-test-agenda-with-agenda + "* TODO no repeater +SCHEDULED: <2026-06-17 Wed 12w> +:PROPERTIES: +:STYLE: habit +:END:" + (should-error + (org-agenda nil "a"))) + + (org-test-agenda-with-agenda + "* TODO no repeater +SCHEDULED: <2026-03-05 Thu 3m> +:PROPERTIES: +:STYLE: habit +:END:" + (should-error + (org-agenda nil "a")))) + +(ert-deftest test-org-habit/bad-habit-no-repeater-unit () + "Test a habit without a repeater type." + (org-test-agenda-with-agenda + "* TODO no repeater unit +SCHEDULED: <2026-01-21 Wed .+7> +:PROPERTIES: +:STYLE: habit +:END:" + (should-error + (org-agenda nil "a"))) + + (org-test-agenda-with-agenda + "* TODO unitless +SCHEDULED: <2026-03-09 Mon +7> +:PROPERTIES: +:STYLE: habit +:END:" + (should-error + (org-agenda nil "a"))) + (org-test-agenda-with-agenda + "* TODO still no repeater unit +SCHEDULED: <2026-02-08 Sun ++2> +: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 @@ -383,6 +505,32 @@ SCHEDULED: <2009-10-17 Sat> SCHEDULED: <2009-10-17 Sat +0d> :PROPERTIES: :STYLE: habit +:END:" + (should-error + (org-agenda nil "a"))) + + (org-test-agenda-with-agenda + "* TODO repeat period less then 1 day +SCHEDULED: <2026-06-17 Wed +0w> +:PROPERTIES: +:STYLE: habit +:END:" + (should-error + (org-agenda nil "a"))) + + (org-test-agenda-with-agenda + "* TODO repeat period less then 1 day +SCHEDULED: <2026-06-12 Fri +1h> +:PROPERTIES: +:STYLE: habit +:END:" + (should-error + (org-agenda nil "a"))) + (org-test-agenda-with-agenda + "* TODO repeat period less then 1 day +SCHEDULED: <2026-06-08 Mon +23h> +:PROPERTIES: +:STYLE: habit :END:" (should-error (org-agenda nil "a")))) @@ -393,6 +541,14 @@ SCHEDULED: <2009-10-17 Sat +0d> "* TODO no scheduled <2009-10-17 Sat +1d> :PROPERTIES: :STYLE: habit +:END:" + (should-error + (org-agenda nil "a"))) + + (org-test-agenda-with-agenda + "* TODO no scheduled <2026-06-17 Wed 11:59 +1d> +:PROPERTIES: +:STYLE: habit :END:" (should-error (org-agenda nil "a")))) -- 2.54.0
