aboutsummaryrefslogtreecommitdiff
path: root/tests/test-org-drill-session-state.el
blob: 3f99a8e402b77ee832c9d939f77801cdfd46ac53 (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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
;;; test-org-drill-session-state.el --- Tests for session queue predicates  -*- lexical-binding: t; -*-

;;; Commentary:
;; Tests for the predicates and accessors that drive the drill loop's
;; main control flow:
;;
;; - `org-drill-entries-pending-p': are there cards left to drill?
;; - `org-drill-pending-entry-count': how many?
;; - `org-drill-maximum-duration-reached-p': did we hit the time limit?
;; - `org-drill-maximum-item-count-reached-p': did we hit the count limit?
;; - `org-drill--entry-lapsed-p': has this entry crossed the lapse
;;   threshold (very-old, very-overdue)?
;; - `org-drill-free-markers': clean up markers at session end.
;;
;; The user-facing contract: when I start a session, drill until I hit
;; my configured limits or run out of cards, then stop cleanly.

;;; Code:

(require 'ert)
(require 'cl-lib)
(require 'org)
(require 'org-drill)

;;;; Helpers

(defun make-marker-at (point-or-pos)
  "Make a marker pointing at the given position."
  (let ((m (make-marker)))
    (set-marker m point-or-pos)
    m))

(defmacro with-fixed-now (&rest body)
  `(cl-letf (((symbol-function 'current-time)
              (lambda () (encode-time 0 0 12 5 5 2026))))
     ,@body))

;;;; org-drill-entries-pending-p

(ert-deftest test-org-drill-entries-pending-p-empty-session-returns-nil ()
  "A fresh session with no entries in any queue is not pending."
  (let ((session (org-drill-session)))
    (should-not (org-drill-entries-pending-p session))))

(ert-deftest test-org-drill-entries-pending-p-current-item-counts ()
  "If there's a current-item being drilled, the session is still pending."
  (let ((session (org-drill-session)))
    (oset session current-item (make-marker-at 1))
    (should (org-drill-entries-pending-p session))))

(ert-deftest test-org-drill-entries-pending-p-again-entries-bypasses-limits ()
  "Items in `again-entries' (failed earlier this session) keep the session
pending even when item-count limits are hit — re-drilling those is mandatory."
  (let ((session (org-drill-session))
        (org-drill-maximum-items-per-session 1))
    (oset session done-entries (list (make-marker-at 1) (make-marker-at 2)))
    (oset session again-entries (list (make-marker-at 3)))
    (should (org-drill-entries-pending-p session))))

(ert-deftest test-org-drill-entries-pending-p-respects-item-count-limit ()
  "When max-items is reached and only new-entries remain, no longer pending."
  (let ((session (org-drill-session))
        (org-drill-maximum-items-per-session 2))
    (oset session done-entries (list (make-marker-at 1) (make-marker-at 2)))
    (oset session new-entries (list (make-marker-at 3)))
    (should-not (org-drill-entries-pending-p session))))

(ert-deftest test-org-drill-entries-pending-p-non-empty-new-queue ()
  "Cards in any of the prioritized queues count as pending.
The fresh session needs a real start-time — the default initform of
0.0 (epoch) plus the default 20-minute duration limit makes the
session look long-expired."
  (dolist (slot '(new-entries failed-entries young-mature-entries
                  old-mature-entries overdue-entries))
    (let ((session (org-drill-session)))
      (oset session start-time (float-time (current-time)))
      (eieio-oset session slot (list (make-marker-at 1)))
      (should (org-drill-entries-pending-p session)))))

;;;; org-drill-on-timeout-action — discard-current

(ert-deftest test-org-drill-entries-pending-p-discard-current-times-out-drops-again ()
  "With `discard-current' and the duration reached, the again-queue no longer
keeps the session pending — time is up, so the re-drill items are dropped."
  (let ((session (org-drill-session))
        (org-drill-on-timeout-action 'discard-current)
        (org-drill-maximum-duration 1))                       ; 1-minute limit
    (oset session start-time (- (float-time (current-time)) 3600)) ; started an hour ago
    (oset session again-entries (list (make-marker-at 1)))
    (should-not (org-drill-entries-pending-p session))))

(ert-deftest test-org-drill-entries-pending-p-discard-current-times-out-drops-current-item ()
  "With `discard-current' and the duration reached, a leftover current-item is
dropped rather than forcing the session to continue."
  (let ((session (org-drill-session))
        (org-drill-on-timeout-action 'discard-current)
        (org-drill-maximum-duration 1))
    (oset session start-time (- (float-time (current-time)) 3600))
    (oset session current-item (make-marker-at 1))
    (should-not (org-drill-entries-pending-p session))))

(ert-deftest test-org-drill-entries-pending-p-finish-current-times-out-keeps-again ()
  "Default `finish-current' preserves the old behavior: the again-queue keeps
the session pending even past the duration limit."
  (let ((session (org-drill-session))
        (org-drill-on-timeout-action 'finish-current)
        (org-drill-maximum-duration 1))
    (oset session start-time (- (float-time (current-time)) 3600))
    (oset session again-entries (list (make-marker-at 1)))
    (should (org-drill-entries-pending-p session))))

(ert-deftest test-org-drill-entries-pending-p-discard-current-before-timeout-is-normal ()
  "Before the duration is reached, `discard-current' behaves normally — queued
items still count as pending."
  (let ((session (org-drill-session))
        (org-drill-on-timeout-action 'discard-current)
        (org-drill-maximum-duration 20))
    (oset session start-time (float-time (current-time)))     ; just started
    (oset session again-entries (list (make-marker-at 1)))
    (should (org-drill-entries-pending-p session))))

;;;; org-drill-pending-entry-count

(ert-deftest test-org-drill-pending-entry-count-empty-session-zero ()
  (let ((session (org-drill-session)))
    (should (= 0 (org-drill-pending-entry-count session)))))

(ert-deftest test-org-drill-pending-entry-count-sums-all-queues ()
  "Count includes current-item plus every queue."
  (let ((session (org-drill-session)))
    (oset session current-item (make-marker-at 1))
    (oset session new-entries (list (make-marker-at 2) (make-marker-at 3)))
    (oset session failed-entries (list (make-marker-at 4)))
    (oset session young-mature-entries (list (make-marker-at 5)))
    (oset session old-mature-entries (list (make-marker-at 6) (make-marker-at 7)))
    (oset session overdue-entries (list (make-marker-at 8)))
    (oset session again-entries (list (make-marker-at 9)))
    (should (= 9 (org-drill-pending-entry-count session)))))

(ert-deftest test-org-drill-pending-entry-count-current-item-only-when-marker ()
  "Current-item only contributes 1 when it's a marker (not nil, not other)."
  (let ((session (org-drill-session)))
    (oset session current-item nil)
    (should (= 0 (org-drill-pending-entry-count session)))
    (oset session current-item (make-marker-at 1))
    (should (= 1 (org-drill-pending-entry-count session)))))

;;;; org-drill-maximum-duration-reached-p

(ert-deftest test-org-drill-maximum-duration-reached-p-not-set-returns-nil ()
  "When `org-drill-maximum-duration' is nil, never time out."
  (let ((session (org-drill-session))
        (org-drill-maximum-duration nil))
    (oset session start-time 0.0)            ; epoch start, very long ago
    (should-not (org-drill-maximum-duration-reached-p session))))

(ert-deftest test-org-drill-maximum-duration-reached-p-cram-mode-bypassed ()
  "Cram mode ignores the duration limit — cram all you want."
  (with-fixed-now
    (let ((session (org-drill-session))
          (org-drill-maximum-duration 1))    ; 1 minute
      (oset session start-time 0.0)
      (oset session cram-mode t)
      (should-not (org-drill-maximum-duration-reached-p session)))))

(ert-deftest test-org-drill-maximum-duration-reached-p-fresh-session-not-reached ()
  "A session that just started hasn't hit the duration limit."
  (with-fixed-now
    (let ((session (org-drill-session))
          (org-drill-maximum-duration 30))   ; 30 minutes
      (oset session start-time (float-time (current-time)))
      (should-not (org-drill-maximum-duration-reached-p session)))))

(ert-deftest test-org-drill-maximum-duration-reached-p-old-session-reached ()
  "A session that started far in the past has hit the duration limit."
  (with-fixed-now
    (let ((session (org-drill-session))
          (org-drill-maximum-duration 1))    ; 1 minute
      (oset session start-time 0.0)         ; epoch — way more than 1 min ago
      (should (org-drill-maximum-duration-reached-p session)))))

;;;; org-drill-maximum-item-count-reached-p

(ert-deftest test-org-drill-maximum-item-count-reached-p-not-set-returns-nil ()
  (let ((session (org-drill-session))
        (org-drill-maximum-items-per-session nil))
    (oset session done-entries (list (make-marker-at 1) (make-marker-at 2)))
    (should-not (org-drill-maximum-item-count-reached-p session))))

(ert-deftest test-org-drill-maximum-item-count-reached-p-cram-mode-bypassed ()
  (let ((session (org-drill-session))
        (org-drill-maximum-items-per-session 1))
    (oset session done-entries (list (make-marker-at 1) (make-marker-at 2)))
    (oset session cram-mode t)
    (should-not (org-drill-maximum-item-count-reached-p session))))

(ert-deftest test-org-drill-maximum-item-count-reached-p-under-limit ()
  (let ((session (org-drill-session))
        (org-drill-maximum-items-per-session 5))
    (oset session done-entries (list (make-marker-at 1) (make-marker-at 2)))
    (should-not (org-drill-maximum-item-count-reached-p session))))

(ert-deftest test-org-drill-maximum-item-count-reached-p-at-limit ()
  (let ((session (org-drill-session))
        (org-drill-maximum-items-per-session 2))
    (oset session done-entries (list (make-marker-at 1) (make-marker-at 2)))
    (should (org-drill-maximum-item-count-reached-p session))))

(ert-deftest test-org-drill-maximum-item-count-reached-p-includes-failed-when-flag-set ()
  "When `org-drill-item-count-includes-failed-items-p' is t, again-entries
count toward the limit."
  (let ((session (org-drill-session))
        (org-drill-maximum-items-per-session 3)
        (org-drill-item-count-includes-failed-items-p t))
    (oset session done-entries (list (make-marker-at 1) (make-marker-at 2)))
    (oset session again-entries (list (make-marker-at 3)))
    (should (org-drill-maximum-item-count-reached-p session))))

(ert-deftest test-org-drill-maximum-item-count-reached-p-excludes-failed-when-flag-clear ()
  "When the flag is nil, again-entries don't count toward the limit."
  (let ((session (org-drill-session))
        (org-drill-maximum-items-per-session 3)
        (org-drill-item-count-includes-failed-items-p nil))
    (oset session done-entries (list (make-marker-at 1) (make-marker-at 2)))
    (oset session again-entries (list (make-marker-at 3)))
    (should-not (org-drill-maximum-item-count-reached-p session))))

;;;; org-drill--entry-lapsed-p

(ert-deftest test-org-drill--entry-lapsed-p-feature-disabled-returns-nil ()
  "When the lapse feature flag is off, no entry is ever lapsed."
  (with-temp-buffer
    (let ((org-startup-folded nil))
      (insert "* Question :drill:\n")
      (org-mode)
      (goto-char (point-min))
      (org-schedule nil "2020-01-01")  ; far past
      (let ((session (org-drill-session))
            (org-drill--lapse-very-overdue-entries-p nil))
        (with-fixed-now
          (should-not (org-drill--entry-lapsed-p session)))))))

(ert-deftest test-org-drill--entry-lapsed-p-old-overdue-entry-flagged ()
  "With the flag on, an entry overdue past the threshold is lapsed."
  (with-temp-buffer
    (let ((org-startup-folded nil))
      (insert "* Question :drill:\n")
      (org-mode)
      (goto-char (point-min))
      (org-schedule nil "2020-01-01")  ; ~6 years overdue
      (let ((session (org-drill-session))
            (org-drill--lapse-very-overdue-entries-p t)
            (org-drill-lapse-threshold-days 90))
        (with-fixed-now
          (should (org-drill--entry-lapsed-p session)))))))

(ert-deftest test-org-drill--entry-lapsed-p-recent-entry-not-flagged ()
  "An entry only days overdue isn't lapsed."
  (with-temp-buffer
    (let ((org-startup-folded nil))
      (insert "* Question :drill:\n")
      (org-mode)
      (goto-char (point-min))
      (org-schedule nil "2026-05-01")  ; 4 days ago vs threshold 90
      (let ((session (org-drill-session))
            (org-drill--lapse-very-overdue-entries-p t)
            (org-drill-lapse-threshold-days 90))
        (with-fixed-now
          (should-not (org-drill--entry-lapsed-p session)))))))

;;;; org-drill-free-markers

(ert-deftest test-org-drill-free-markers-frees-explicit-list ()
  (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-free-markers session (list m1 m2))
      (should (null (marker-position m1)))
      (should (null (marker-position m2)))
      ;; m3 was not in the list — still alive
      (should (numberp (marker-position m3))))))

(ert-deftest test-org-drill-free-markers-t-frees-all-session-markers ()
  "When called with t, every marker across all session queues is freed."
  (with-temp-buffer
    (insert "abc\ndef\nghi\njkl\n")
    (let* ((m-done (make-marker-at 1))
           (m-new (make-marker-at 5))
           (m-failed (make-marker-at 9))
           (m-overdue (make-marker-at 13))
           (session (org-drill-session)))
      (oset session done-entries (list m-done))
      (oset session new-entries (list m-new))
      (oset session failed-entries (list m-failed))
      (oset session overdue-entries (list m-overdue))
      (org-drill-free-markers session t)
      (should (null (marker-position m-done)))
      (should (null (marker-position m-new)))
      (should (null (marker-position m-failed)))
      (should (null (marker-position m-overdue))))))

(provide 'test-org-drill-session-state)

;;; test-org-drill-session-state.el ends here