aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-05 04:07:49 -0500
committerCraig Jennings <c@cjennings.net>2026-05-05 04:07:49 -0500
commit47976dbe601215cbe0bfea92c246d23190821b07 (patch)
tree71430a7331e0554a1de0f49da1c3d6a41f79fae5
parent784c831352ec2a60bba5199503fef7d80070a315 (diff)
downloadorg-drill-47976dbe601215cbe0bfea92c246d23190821b07.tar.gz
org-drill-47976dbe601215cbe0bfea92c246d23190821b07.zip
test: add tests for cloze regex, hypothetical scheduling, and entry stripping
15 ERT tests covering: - org-drill--compute-cloze-regexp: match default and custom delimiters, hint separator, three-capture-group structure for fontification - org-drill--compute-cloze-keywords: font-lock spec shape - org-drill-hypothetical-next-review-date: virgin-card scheduling, quality-monotonic next-interval, DRILL_CARD_WEIGHT damping - org-drill-hypothetical-next-review-dates: 6-element non-decreasing list driving the rating-prompt preview - org-drill-strip-entry-data: scheduling-property cleanup, no-op on virgin entry
-rw-r--r--tests/test-org-drill-cloze-and-scheduling-helpers.el185
1 files changed, 185 insertions, 0 deletions
diff --git a/tests/test-org-drill-cloze-and-scheduling-helpers.el b/tests/test-org-drill-cloze-and-scheduling-helpers.el
new file mode 100644
index 0000000..ba6503c
--- /dev/null
+++ b/tests/test-org-drill-cloze-and-scheduling-helpers.el
@@ -0,0 +1,185 @@
+;;; test-org-drill-cloze-and-scheduling-helpers.el --- Tests for cloze regex and small scheduler helpers -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for several small helpers users encounter indirectly:
+;;
+;; - `org-drill--compute-cloze-regexp' / `--compute-cloze-keywords':
+;; build the regex that detects cloze syntax in card text. Users
+;; write `[hidden text]' or `[hidden||hint]' and expect those to be
+;; recognised — regardless of whether they've customized the cloze
+;; delimiters.
+;;
+;; - `org-drill-hypothetical-next-review-date' /
+;; `org-drill-hypothetical-next-review-dates': drive the "if you rate
+;; this Q, you'll see this card again in N days" preview the prompt
+;; shows. The function reads the card's stored state and runs the
+;; active scheduler.
+;;
+;; - `org-drill-strip-entry-data': removes scheduling state from an
+;; entry — what happens when a user shares their deck with someone
+;; else.
+
+;;; Code:
+
+(require 'ert)
+(require 'org)
+(require 'org-drill)
+
+;;;; Helpers
+
+(defmacro with-fresh-drill-entry (&rest body)
+ (declare (indent 0))
+ `(with-temp-buffer
+ (let ((org-startup-folded nil))
+ (insert "* Question :drill:\n")
+ (org-mode)
+ (goto-char (point-min))
+ ,@body)))
+
+;;;; org-drill--compute-cloze-regexp
+
+(ert-deftest test-org-drill--compute-cloze-regexp-matches-default-cloze ()
+ "With default `[' and `]' delimiters, `[hidden]' is recognised as cloze."
+ (let ((re (org-drill--compute-cloze-regexp)))
+ (should (string-match-p re "Capital of France: [Paris]"))))
+
+(ert-deftest test-org-drill--compute-cloze-regexp-matches-cloze-with-hint ()
+ "`[hidden||hint]' is recognised — the hint separator is the default `||'."
+ (let ((re (org-drill--compute-cloze-regexp)))
+ (should (string-match-p re "Capital of France: [Paris||a city of light]"))))
+
+(ert-deftest test-org-drill--compute-cloze-regexp-rejects-empty-cloze ()
+ "An empty `[]' doesn't match — the regex requires at least one char inside."
+ (let ((re (org-drill--compute-cloze-regexp)))
+ (should-not (string-match-p re "Empty: []"))))
+
+(ert-deftest test-org-drill--compute-cloze-regexp-rejects-bare-text ()
+ "Plain prose with no cloze delimiters doesn't match."
+ (let ((re (org-drill--compute-cloze-regexp)))
+ (should-not (string-match-p re "No clozes here at all."))))
+
+(ert-deftest test-org-drill--compute-cloze-regexp-honors-custom-delimiters ()
+ "Custom `{{` / `}}' delimiters work — the regex rebuilds from current customs."
+ (let ((org-drill-left-cloze-delimiter "{{")
+ (org-drill-right-cloze-delimiter "}}"))
+ (let ((re (org-drill--compute-cloze-regexp)))
+ (should (string-match-p re "Capital of France: {{Paris}}"))
+ ;; And it shouldn't accidentally match the old bracket form.
+ (should-not (string-match-p re "Capital of France: [Paris]")))))
+
+(ert-deftest test-org-drill--compute-cloze-regexp-captures-three-groups ()
+ "The regex captures left-bracket+text, hint-or-empty, right-bracket as three groups.
+Cloze fontification depends on this — regression check."
+ (let* ((re (org-drill--compute-cloze-regexp))
+ (s "[hidden||hint]"))
+ (should (string-match re s))
+ (should (equal "[hidden" (match-string 1 s)))
+ (should (equal "||hint" (match-string 2 s)))
+ (should (equal "]" (match-string 3 s)))))
+
+;;;; org-drill--compute-cloze-keywords
+
+(ert-deftest test-org-drill--compute-cloze-keywords-returns-fontification-spec ()
+ "Returns a one-element list whose only entry is a font-lock matcher
+with the cloze regex and three per-group face specs.
+
+Note on shape: font-lock face specs use a quoted face-name form, e.g.
+`(1 'org-drill-visible-cloze-face nil)' — the cadr at runtime is the
+quoted-symbol list `(quote org-drill-visible-cloze-face)', not the
+symbol itself."
+ (let ((kw (org-drill--compute-cloze-keywords)))
+ (should (= 1 (length kw)))
+ (let ((spec (car kw)))
+ (should (stringp (car spec))) ; the regex
+ (should (= 4 (length spec))) ; regex + 3 face specs
+ ;; Each per-group entry is (group-num 'face flag). Pull the face
+ ;; symbol out of its quoted form before comparing.
+ (cl-flet ((face-of (entry) (cadr (cadr entry))))
+ (should (eq 'org-drill-visible-cloze-face (face-of (nth 1 spec))))
+ (should (eq 'org-drill-visible-cloze-hint-face (face-of (nth 2 spec))))
+ (should (eq 'org-drill-visible-cloze-face (face-of (nth 3 spec))))))))
+
+;;;; org-drill-hypothetical-next-review-date
+
+(ert-deftest test-org-drill-hypothetical-next-review-date-virgin-quality-5 ()
+ "On a virgin card with default SM5, quality-5 returns the SM5 first interval."
+ (with-fresh-drill-entry
+ (let ((days (org-drill-hypothetical-next-review-date 5)))
+ (should (numberp days))
+ (should (> days 0)))))
+
+(ert-deftest test-org-drill-hypothetical-next-review-date-quality-2-or-below-returns-zero ()
+ "Quality below 3 (failure) means the card resets — zero days, drill again today."
+ (with-fresh-drill-entry
+ (should (equal 0 (org-drill-hypothetical-next-review-date 0)))
+ (should (equal 0 (org-drill-hypothetical-next-review-date 1)))
+ (should (equal 0 (org-drill-hypothetical-next-review-date 2)))))
+
+(ert-deftest test-org-drill-hypothetical-next-review-date-quality-monotonic ()
+ "Higher quality means longer next-interval — the curve should be monotonic
+non-decreasing across q=3 → q=5 on a virgin card."
+ (with-fresh-drill-entry
+ (let ((q3 (org-drill-hypothetical-next-review-date 3))
+ (q4 (org-drill-hypothetical-next-review-date 4))
+ (q5 (org-drill-hypothetical-next-review-date 5)))
+ (should (<= q3 q4))
+ (should (<= q4 q5)))))
+
+(ert-deftest test-org-drill-hypothetical-next-review-date-respects-card-weight ()
+ "DRILL_CARD_WEIGHT > 1 stretches the next-interval delta.
+The contract: weight=2 with old-interval 0 and computed next of N gives
+roughly old + max(1, (next-old)/2) days."
+ (with-fresh-drill-entry
+ ;; Set up a card that's been reviewed before, with weight = 2.
+ (org-set-property "DRILL_CARD_WEIGHT" "2")
+ (org-drill-store-item-data 10 3 0 3 4.5 2.5)
+ (let ((q5-no-weight (progn
+ (org-delete-property "DRILL_CARD_WEIGHT")
+ (org-drill-hypothetical-next-review-date 5)))
+ (q5-with-weight (progn
+ (org-set-property "DRILL_CARD_WEIGHT" "2")
+ (org-drill-hypothetical-next-review-date 5))))
+ ;; Weight should *reduce* the gain compared to no weight.
+ (should (<= q5-with-weight q5-no-weight)))))
+
+;;;; org-drill-hypothetical-next-review-dates
+
+(ert-deftest test-org-drill-hypothetical-next-review-dates-returns-six-values ()
+ "Returns one value per quality level (0..5) — six total."
+ (with-fresh-drill-entry
+ (let ((dates (org-drill-hypothetical-next-review-dates)))
+ (should (= 6 (length dates))))))
+
+(ert-deftest test-org-drill-hypothetical-next-review-dates-non-decreasing ()
+ "Each entry is at least as large as the previous — the function clamps to monotonic.
+This is what users see in the prompt: `1 / 2 / 3 / 4 / 6 / 9' style hints."
+ (with-fresh-drill-entry
+ (let* ((dates (org-drill-hypothetical-next-review-dates))
+ (pairs (cl-mapcar #'list dates (cdr dates))))
+ (dolist (pair pairs)
+ (should (<= (car pair) (cadr pair)))))))
+
+;;;; org-drill-strip-entry-data
+
+(ert-deftest test-org-drill-strip-entry-data-removes-scheduling-properties ()
+ "Stripping wipes every property listed in `org-drill-scheduling-properties'."
+ (with-fresh-drill-entry
+ (org-drill-store-item-data 10 3 1 5 3.8 2.4)
+ ;; sanity: the props are there
+ (should (org-entry-get (point) "DRILL_LAST_INTERVAL"))
+ (org-drill-strip-entry-data)
+ ;; every scheduling property is gone
+ (dolist (prop org-drill-scheduling-properties)
+ (should (null (org-entry-get (point) prop))))))
+
+(ert-deftest test-org-drill-strip-entry-data-on-virgin-entry-is-a-no-op ()
+ "Stripping a card that has no data succeeds quietly."
+ (with-fresh-drill-entry
+ ;; should not error
+ (org-drill-strip-entry-data)
+ (dolist (prop org-drill-scheduling-properties)
+ (should (null (org-entry-get (point) prop))))))
+
+(provide 'test-org-drill-cloze-and-scheduling-helpers)
+
+;;; test-org-drill-cloze-and-scheduling-helpers.el ends here