aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-05 04:19:24 -0500
committerCraig Jennings <c@cjennings.net>2026-05-05 04:19:24 -0500
commit39a465d54964873980b1b3fe147e7e640bf74985 (patch)
treee25ed25c389096e8f5893d2398390852044f62b9
parent8a83694d8882a3a80328b7ffd8ddbc2c2c73f3ae (diff)
downloadorg-drill-39a465d54964873980b1b3fe147e7e640bf74985.tar.gz
org-drill-39a465d54964873980b1b3fe147e7e640bf74985.zip
test: add entry-status, days-since-creation, and overdue ordering coverage
Plus a docs fix to org-drill-order-overdue-entries' header comment. 16 ERT tests covering: - org-drill-entry-status: non-drill nil, empty entry nil, virgin :new, future :future, low-quality :failed, due+short-interval :young, due+long-interval :old, very-overdue :overdue, skipped-leech :unscheduled, three-element return shape - org-drill-entry-days-since-creation: with DATE_ADDED, missing without flag (nil), missing with use-last-interval-p flag (overdue+interval) - org-drill-order-overdue-entries: empty stays empty, non-lapsed sorted by DUE desc, lapsed split (by DUE crossing threshold, not AGE) appearing after sorted by AGE desc Fixed misleading header comment at line 2888 — it claimed the lapse split was by AGE, but the code uses DUE (cl-second). This matches the semantic gate in org-drill--entry-lapsed-p, so the code was right and the comment was stale. Updated the comment to state the actual three-step sort.
-rw-r--r--org-drill.el9
-rw-r--r--tests/test-org-drill-entry-status.el212
2 files changed, 219 insertions, 2 deletions
diff --git a/org-drill.el b/org-drill.el
index e2234e5..dd210a2 100644
--- a/org-drill.el
+++ b/org-drill.el
@@ -2889,8 +2889,13 @@ all the markers used by Org-Drill will be freed."
;;; where POS is a marker pointing to the start of the entry, and
;;; DUE is a number indicating how many days ago the entry was due.
;;; AGE is the number of days elapsed since item creation (nil if unknown).
-;;; if age > lapse threshold (default 90), sort by age (oldest first)
-;;; if age < lapse threshold, sort by due (biggest first)
+;;;
+;;; Sort order:
+;;; 1. Split by DUE: entries with DUE > org-drill-lapse-threshold-days
+;;; are "lapsed", the rest are "not-lapsed". This matches the gate
+;;; in `org-drill--entry-lapsed-p'.
+;;; 2. Not-lapsed are sorted by DUE descending (most-overdue first).
+;;; 3. Lapsed are appended after, sorted by AGE descending (oldest first).
(defun org-drill-order-overdue-entries (session)
(let* ((lapsed-days (if org-drill--lapse-very-overdue-entries-p
org-drill-lapse-threshold-days
diff --git a/tests/test-org-drill-entry-status.el b/tests/test-org-drill-entry-status.el
new file mode 100644
index 0000000..239bcac
--- /dev/null
+++ b/tests/test-org-drill-entry-status.el
@@ -0,0 +1,212 @@
+;;; test-org-drill-entry-status.el --- Tests for entry classification & creation-age & overdue ordering -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the dispatcher that decides which queue a card belongs in:
+;;
+;; - `org-drill-entry-status': returns (STATUS DUE AGE) where STATUS is
+;; one of nil / :unscheduled / :future / :new / :failed / :overdue /
+;; :young / :old. Every drill iteration runs this on every candidate.
+;; - `org-drill-entry-days-since-creation': how old is the card? Used
+;; for the :unscheduled / :failed / :overdue / :young / :old call.
+;; - `org-drill-order-overdue-entries': sort the overdue queue with
+;; recently-overdue first, lapsed-very-old behind.
+;;
+;; The user-facing contract: every card lands in exactly one bucket per
+;; iteration, and bucket choice is deterministic given the card's state.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'org)
+(require 'org-drill)
+
+;;;; Helpers
+
+(defmacro with-org-buffer (content &rest body)
+ (declare (indent 1))
+ `(with-temp-buffer
+ (let ((org-startup-folded nil))
+ (insert ,content)
+ (org-mode)
+ (goto-char (point-min))
+ ,@body)))
+
+(defmacro with-fixed-now (&rest body)
+ `(cl-letf (((symbol-function 'current-time)
+ (lambda () (encode-time 0 0 12 5 5 2026))))
+ ,@body))
+
+(defun status-of (session)
+ "Return the STATUS element of `(org-drill-entry-status session)' at point."
+ (car (org-drill-entry-status session)))
+
+(defun make-marker-at (pos)
+ (let ((m (make-marker))) (set-marker m pos) m))
+
+;;;; org-drill-entry-status
+
+(ert-deftest test-org-drill-entry-status-non-drill-entry-returns-nil ()
+ (with-org-buffer "* Plain heading\nbody\n"
+ (with-fixed-now
+ (should (null (status-of (org-drill-session)))))))
+
+(ert-deftest test-org-drill-entry-status-empty-drill-entry-default-card-type-returns-nil ()
+ "An entry tagged drill but with no body and no card-type that allows
+empty bodies is silently skipped — status nil."
+ (with-org-buffer "* Question :drill:\n:PROPERTIES:\n:ID: x\n:END:\n"
+ (with-fixed-now
+ (should (null (status-of (org-drill-session)))))))
+
+(ert-deftest test-org-drill-entry-status-virgin-unscheduled-with-body-returns-new ()
+ "A drill entry with body and no schedule is :new."
+ (with-org-buffer "* Question :drill:\nThis is the body of the question.\n"
+ (with-fixed-now
+ (should (eq :new (status-of (org-drill-session)))))))
+
+(ert-deftest test-org-drill-entry-status-future-scheduled-returns-future ()
+ (with-org-buffer "* Question :drill:\nbody\n"
+ (org-schedule nil "2026-05-10")
+ (with-fixed-now
+ (should (eq :future (status-of (org-drill-session)))))))
+
+(ert-deftest test-org-drill-entry-status-past-with-failed-quality-returns-failed ()
+ "An entry with last-quality below the failure threshold is :failed —
+this status overrides young/old/overdue."
+ (with-org-buffer "* Question :drill:\nbody\n"
+ (org-schedule nil "2026-04-30")
+ (org-set-property "DRILL_LAST_QUALITY" "1") ; below default failure-quality
+ (org-set-property "DRILL_LAST_INTERVAL" "5")
+ (org-set-property "DRILL_TOTAL_REPEATS" "3") ; not virgin
+ (with-fixed-now
+ (should (eq :failed (status-of (org-drill-session)))))))
+
+(ert-deftest test-org-drill-entry-status-due-and-young-returns-young ()
+ "Past-scheduled entry, good last-quality, short last-interval → :young."
+ (with-org-buffer "* Question :drill:\nbody\n"
+ (org-schedule nil "2026-05-04") ; one day overdue
+ (org-set-property "DRILL_LAST_QUALITY" "5")
+ (org-set-property "DRILL_LAST_INTERVAL" "3")
+ (org-set-property "DRILL_TOTAL_REPEATS" "2")
+ (with-fixed-now
+ ;; 1 day overdue out of 3 last-interval → not overdue (factor < 1.5)
+ (should (eq :young (status-of (org-drill-session)))))))
+
+(ert-deftest test-org-drill-entry-status-due-and-old-returns-old ()
+ "Long last-interval (>= org-drill-days-before-old) makes a non-overdue
+due card :old."
+ (with-org-buffer "* Question :drill:\nbody\n"
+ (org-schedule nil "2026-05-04")
+ (org-set-property "DRILL_LAST_QUALITY" "5")
+ (org-set-property "DRILL_LAST_INTERVAL" "30")
+ (org-set-property "DRILL_TOTAL_REPEATS" "10")
+ (let ((org-drill-days-before-old 10)) ; default is 10 in org-drill
+ (with-fixed-now
+ (should (eq :old (status-of (org-drill-session))))))))
+
+(ert-deftest test-org-drill-entry-status-very-overdue-returns-overdue ()
+ "Past-scheduled by much more than the last-interval factor → :overdue."
+ (with-org-buffer "* Question :drill:\nbody\n"
+ (org-schedule nil "2026-04-15") ; 20 days overdue
+ (org-set-property "DRILL_LAST_QUALITY" "5")
+ (org-set-property "DRILL_LAST_INTERVAL" "5")
+ (org-set-property "DRILL_TOTAL_REPEATS" "3")
+ (with-fixed-now
+ (should (eq :overdue (status-of (org-drill-session)))))))
+
+(ert-deftest test-org-drill-entry-status-skipped-leech-returns-unscheduled ()
+ "When `org-drill-leech-method' is `skip', a leech card returns :unscheduled
+because its days-overdue computes nil."
+ (with-org-buffer "* Hard one :drill:leech:\nbody\n"
+ (org-schedule nil "2026-04-30")
+ (let ((org-drill-leech-method 'skip))
+ (with-fixed-now
+ (should (eq :unscheduled (status-of (org-drill-session))))))))
+
+(ert-deftest test-org-drill-entry-status-returns-three-element-list ()
+ "Return shape is always (STATUS DUE AGE) — three elements."
+ (with-org-buffer "* Question :drill:\nbody\n"
+ (with-fixed-now
+ (let ((result (org-drill-entry-status (org-drill-session))))
+ (should (= 3 (length result)))))))
+
+;;;; org-drill-entry-days-since-creation
+
+(ert-deftest test-org-drill-entry-days-since-creation-with-date-added ()
+ "DATE_ADDED set → returns days since that date."
+ (with-org-buffer "* Question :drill:\n"
+ (org-set-property "DATE_ADDED" "<2026-04-01 Wed>")
+ (with-fixed-now
+ (let ((days (org-drill-entry-days-since-creation (org-drill-session))))
+ (should (numberp days))
+ (should (>= days 33)) ; ~34 days from 4-01 to 5-05
+ (should (<= days 35))))))
+
+(ert-deftest test-org-drill-entry-days-since-creation-no-date-no-flag-returns-nil ()
+ "DATE_ADDED missing, USE-LAST-INTERVAL-P nil → returns nil."
+ (with-org-buffer "* Question :drill:\n"
+ (with-fixed-now
+ (should (null (org-drill-entry-days-since-creation (org-drill-session)))))))
+
+(ert-deftest test-org-drill-entry-days-since-creation-no-date-with-flag-uses-interval ()
+ "DATE_ADDED missing, USE-LAST-INTERVAL-P t → days-overdue + last-interval."
+ (with-org-buffer "* Question :drill:\nbody\n"
+ (org-schedule nil "2026-05-02") ; 3 days overdue
+ (org-set-property "DRILL_LAST_INTERVAL" "5")
+ (with-fixed-now
+ ;; Expected: 3 (overdue) + 5 (last-interval) = 8
+ (should (= 8 (org-drill-entry-days-since-creation (org-drill-session) t))))))
+
+;;;; org-drill-order-overdue-entries
+
+(ert-deftest test-org-drill-order-overdue-entries-empty-stays-empty ()
+ (let ((session (org-drill-session)))
+ (oset session overdue-data nil)
+ (org-drill-order-overdue-entries session)
+ (should (null (oref session overdue-entries)))))
+
+(ert-deftest test-org-drill-order-overdue-entries-non-lapsed-sort-by-due-descending ()
+ "Recently-overdue cards: most-overdue-first.
+Each overdue-data entry is (POS DUE AGE). Sorting puts higher DUE first."
+ (with-temp-buffer
+ (insert "abc\ndef\nghi\n")
+ (let* ((m1 (make-marker-at 1))
+ (m2 (make-marker-at 5))
+ (m3 (make-marker-at 9))
+ (session (org-drill-session))
+ (org-drill--lapse-very-overdue-entries-p nil))
+ ;; 3 entries, due 5/2/8 days, all young (low age)
+ (oset session overdue-data
+ `((,m1 5 1) (,m2 2 1) (,m3 8 1)))
+ (org-drill-order-overdue-entries session)
+ ;; Expected order: m3 (due=8), m1 (due=5), m2 (due=2)
+ (should (equal (list m3 m1 m2) (oref session overdue-entries))))))
+
+(ert-deftest test-org-drill-order-overdue-entries-lapsed-go-after-non-lapsed ()
+ "Lapsed (very-overdue) entries form a separate group sorted by AGE,
+appearing after the non-lapsed ones.
+
+Note: lapse split is by DUE (days overdue), not AGE — matches the
+semantic of `org-drill--entry-lapsed-p' which gates on days-overdue
+crossing the threshold. Inside the lapsed group, secondary sort is
+by AGE descending (oldest cards first)."
+ (with-temp-buffer
+ (insert "abc\ndef\nghi\njkl\n")
+ (let* ((m-fresh (make-marker-at 1))
+ (m-lapsed-old (make-marker-at 5))
+ (m-lapsed-older (make-marker-at 9))
+ (session (org-drill-session))
+ (org-drill--lapse-very-overdue-entries-p t)
+ (org-drill-lapse-threshold-days 90))
+ (oset session overdue-data
+ `((,m-fresh 5 30) ; non-lapsed (DUE 5 < 90)
+ (,m-lapsed-old 100 50) ; lapsed (DUE 100 > 90)
+ (,m-lapsed-older 100 200))) ; lapsed too, but older
+ (org-drill-order-overdue-entries session)
+ ;; Non-lapsed first, then lapsed sorted by AGE descending.
+ (should (equal (list m-fresh m-lapsed-older m-lapsed-old)
+ (oref session overdue-entries))))))
+
+(provide 'test-org-drill-entry-status)
+
+;;; test-org-drill-entry-status.el ends here