diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-05 04:19:24 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-05 04:19:24 -0500 |
| commit | 39a465d54964873980b1b3fe147e7e640bf74985 (patch) | |
| tree | e25ed25c389096e8f5893d2398390852044f62b9 | |
| parent | 8a83694d8882a3a80328b7ffd8ddbc2c2c73f3ae (diff) | |
| download | org-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.el | 9 | ||||
| -rw-r--r-- | tests/test-org-drill-entry-status.el | 212 |
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 |
