aboutsummaryrefslogtreecommitdiff
path: root/tests/test-org-drill-cloze-and-scheduling-helpers.el
blob: 39573aaf2bf9688a3ec863434166a46fb756d719 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
;;; 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-sm2-algorithm ()
  "With sm2 selected, hypothetical-next-review-date returns a positive number."
  (with-fresh-drill-entry
    (let ((org-drill-spaced-repetition-algorithm 'sm2))
      (let ((days (org-drill-hypothetical-next-review-date 5)))
        (should (numberp days))
        (should (>= days 0))))))

(ert-deftest test-org-drill-hypothetical-next-review-date-simple8-algorithm ()
  "With simple8 selected, hypothetical-next-review-date returns a positive number."
  (with-fresh-drill-entry
    (let ((org-drill-spaced-repetition-algorithm 'simple8))
      (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-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 (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 0 :total-repeats 3 :meanq 4.5 :ease 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 (make-org-drill-card-state :last-interval 10 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8 :ease 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