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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
|
;;; test-org-drill-determine-next-interval-simple8.el --- Tests for Simple8 algorithm -*- lexical-binding: t; -*-
;;; Commentary:
;; Unit tests for org-drill-determine-next-interval-simple8 and its three
;; pure-math helpers (simple8-first-interval, simple8-interval-factor,
;; simple8-quality->ease).
;;
;; Simple8 differs from SM2 and SM5 in three main ways:
;; - It does not take an EF parameter. The ease is recomputed from meanq
;; each call via the polynomial in `org-drill-simple8-quality->ease'.
;; - It returns a 6-element list (not 7). No optimal-factor matrix.
;; - It supports an optional delta-days parameter for adjusting intervals
;; on both early and late reviews when org-drill-adjust-intervals-for-
;; early-and-late-repetitions-p is enabled.
;;
;; Function signature:
;; (test-scheduler--call-simple8 last-interval repeats quality
;; failures meanq totaln
;; &optional delta-days)
;;
;; Returns: (NEXT-INTERVAL REPEATS EASE FAILURES AVERAGE-QUALITY TOTAL-REPEATS)
;;; Code:
(require 'ert)
(require 'assess)
(require 'org-drill)
(add-to-list 'load-path
(file-name-directory (or load-file-name buffer-file-name)))
(require 'testutil-scheduler)
;;; Normal Cases - Successful Reviews
(ert-deftest test-org-drill-determine-next-interval-simple8-normal-first-review-quality-4 ()
"Normal: first review (repeats=0, last-interval=0) uses first-interval branch."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result (test-scheduler--call-simple8 0 0 4 0 nil 0 nil))
(interval (test-scheduler--extract-interval result))
(repeats (test-scheduler--extract-repeats result)))
(should (> interval 0))
(should (= repeats 1)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-normal-second-review-quality-4 ()
"Normal: subsequent review uses interval-factor branch and grows the interval."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result (test-scheduler--call-simple8 4 1 4 0 nil 1 nil))
(interval (test-scheduler--extract-interval result))
(repeats (test-scheduler--extract-repeats result)))
(should (> interval 0))
(should (= repeats 2)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-normal-third-review-quality-4 ()
"Normal: third review continues the interval-factor branch."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result (test-scheduler--call-simple8 10 2 4 0 4.0 2 nil))
(interval (test-scheduler--extract-interval result))
(repeats (test-scheduler--extract-repeats result)))
(should (> interval 0))
(should (= repeats 3)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-normal-quality-5-perfect-recall ()
"Normal: quality 5 produces a higher ease than the same call with quality 3."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result-q5 (test-scheduler--call-simple8 10 2 5 0 5.0 2 nil))
(result-q3 (test-scheduler--call-simple8 10 2 3 0 3.0 2 nil))
(ease-q5 (test-scheduler--extract-ease result-q5))
(ease-q3 (test-scheduler--extract-ease result-q3)))
(should (> ease-q5 ease-q3)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-normal-quality-3-adequate-recall ()
"Normal: quality 3 passes (above failure threshold) and increments repeats."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result (test-scheduler--call-simple8 10 2 3 0 3.0 2 nil))
(repeats (test-scheduler--extract-repeats result)))
(should (= repeats 3)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-normal-failure-quality-0 ()
"Normal: failed review with quality 0 resets interval to -1 and repeats to 0."
(let* ((result (test-scheduler--call-simple8 10 3 0 0 nil 3 nil))
(interval (test-scheduler--extract-interval result))
(repeats (test-scheduler--extract-repeats result))
(failures (test-scheduler--extract-failures result)))
(should (= interval -1))
(should (= repeats 0))
(should (= failures 1))))
(ert-deftest test-org-drill-determine-next-interval-simple8-normal-failure-quality-1 ()
"Normal: failed review with quality 1 resets interval and increments failures."
(let* ((result (test-scheduler--call-simple8 15 5 1 2 3.5 5 nil))
(interval (test-scheduler--extract-interval result))
(failures (test-scheduler--extract-failures result)))
(should (= interval -1))
(should (= failures 3))))
(ert-deftest test-org-drill-determine-next-interval-simple8-normal-failure-quality-2 ()
"Normal: quality 2 (= default org-drill-failure-quality) triggers failure path."
(let* ((result (test-scheduler--call-simple8 10 3 2 0 nil 3 nil))
(interval (test-scheduler--extract-interval result)))
(should (= interval -1))))
;;; Boundary Cases
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-nil-meanq-uses-quality ()
"Boundary: nil meanq initializes to current quality."
(let* ((quality 4)
(result (test-scheduler--call-simple8 0 0 quality 0 nil 0 nil))
(meanq (test-scheduler--extract-meanq result)))
(should (= meanq quality))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-zero-repeats-forces-first-interval ()
"Boundary: zero repeats forces first-interval branch (last-interval > 0)."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result (test-scheduler--call-simple8 100 0 4 0 nil 0 nil))
(interval (test-scheduler--extract-interval result)))
;; First-interval is small (around 2.4849), not last-interval * factor.
(should (> interval 0))
(should (< interval 10)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-zero-last-interval-forces-first-interval ()
"Boundary: zero last-interval forces first-interval branch (repeats > 0)."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result (test-scheduler--call-simple8 0 5 4 0 4.0 5 nil))
(interval (test-scheduler--extract-interval result)))
(should (> interval 0)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-quality-5-maximum ()
"Boundary: maximum quality 5 is accepted and produces a positive interval."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result (test-scheduler--call-simple8 4 1 5 0 nil 1 nil))
(interval (test-scheduler--extract-interval result)))
(should (> interval 0)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-quality-0-minimum ()
"Boundary: minimum quality 0 triggers the failure path with interval -1."
(let* ((result (test-scheduler--call-simple8 10 3 0 0 nil 3 nil))
(interval (test-scheduler--extract-interval result)))
(should (= interval -1))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-meanq-weighted-average ()
"Boundary: meanq is correctly calculated as a weighted average over totaln."
(let* ((quality 4)
(meanq 3.0)
(totaln 10)
(result (test-scheduler--call-simple8 10 3 quality 0 meanq totaln nil))
(new-meanq (test-scheduler--extract-meanq result))
(expected (/ (+ quality (* meanq totaln 1.0)) (1+ totaln))))
(should (< (abs (- new-meanq expected)) 0.0001))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-delta-days-nil ()
"Boundary: nil delta-days (default) skips both early and late adjustments."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result (test-scheduler--call-simple8 4 1 4 0 nil 1 nil))
(interval (test-scheduler--extract-interval result)))
(should (numberp interval))
(should (> interval 0)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-delta-days-positive-flag-disabled ()
"Boundary: positive delta-days with flag disabled does not change the interval."
(let ((org-drill-add-random-noise-to-intervals-p nil)
(org-drill-adjust-intervals-for-early-and-late-repetitions-p nil))
(let* ((result-no-adjust (test-scheduler--call-simple8 4 1 4 0 nil 1 5))
(result-no-delta (test-scheduler--call-simple8 4 1 4 0 nil 1 nil))
(interval-no-adjust (test-scheduler--extract-interval result-no-adjust))
(interval-no-delta (test-scheduler--extract-interval result-no-delta)))
(should (= interval-no-adjust interval-no-delta)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-delta-days-positive-flag-enabled ()
"Boundary: positive delta-days with flag enabled runs late-review adjustment."
(let ((org-drill-add-random-noise-to-intervals-p nil)
(org-drill-adjust-intervals-for-early-and-late-repetitions-p t))
(let* ((result (test-scheduler--call-simple8 4 1 4 0 nil 1 5))
(interval (test-scheduler--extract-interval result)))
(should (numberp interval))
(should (> interval 0)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-delta-days-negative-flag-disabled ()
"Boundary: negative delta-days with flag disabled does not change the interval."
(let ((org-drill-add-random-noise-to-intervals-p nil)
(org-drill-adjust-intervals-for-early-and-late-repetitions-p nil))
(let* ((result-no-adjust (test-scheduler--call-simple8 4 1 4 0 nil 1 -3))
(result-no-delta (test-scheduler--call-simple8 4 1 4 0 nil 1 nil))
(interval-no-adjust (test-scheduler--extract-interval result-no-adjust))
(interval-no-delta (test-scheduler--extract-interval result-no-delta)))
(should (= interval-no-adjust interval-no-delta)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-delta-days-negative-flag-enabled ()
"Boundary: negative delta-days with flag enabled runs early-interval-factor."
(let ((org-drill-add-random-noise-to-intervals-p nil)
(org-drill-adjust-intervals-for-early-and-late-repetitions-p t))
(let* ((result (test-scheduler--call-simple8 4 1 4 0 nil 1 -3))
(interval (test-scheduler--extract-interval result)))
(should (numberp interval))
(should (> interval 0)))))
(ert-deftest test-org-drill-determine-next-interval-simple8-boundary-totaln-increments-on-both-paths ()
"Boundary: totaln increments on both success and failure paths.
This matches the SM2 and SM5 behavior so DRILL_TOTAL_REPEATS counts every
review attempt regardless of which scheduling algorithm produced it."
(let* ((totaln 42)
(result-success (test-scheduler--call-simple8 0 0 4 0 nil totaln nil))
(result-failure (test-scheduler--call-simple8 10 3 0 0 nil totaln nil)))
(should (= (test-scheduler--extract-total-repeats result-success) (1+ totaln)))
(should (= (test-scheduler--extract-total-repeats result-failure) (1+ totaln)))))
;;; Error Cases - cl-assert violations
(defmacro test-scheduler--should-cl-assert (&rest body)
"Assert BODY signals a cl-assertion-failed.
ERT in Emacs 29 installs an aggressive `signal-hook-function' that
intercepts every signal in a test body — not just inside `should'
forms — and shadowing the hook locally doesn't help. The eight
tests that exercise cl-assert preconditions therefore can't pass
cleanly on 29.x and are skipped there via `skip-unless'.
This still catches the right signal on Emacs 30+ (and locally),
where ERT's hook leaves inner condition-case alone."
`(progn
(skip-unless (>= emacs-major-version 30))
(let ((caught
(condition-case _err
(progn ,@body 'no-error)
(cl-assertion-failed 'caught))))
(unless (eq caught 'caught)
(ert-fail "expected cl-assertion-failed signal, got none")))))
(ert-deftest test-org-drill-determine-next-interval-simple8-error-negative-repeats ()
"Error: repeats=-1 violates the (cl-assert (>= repeats 0)) precondition."
(test-scheduler--should-cl-assert
(test-scheduler--call-simple8 0 -1 4 0 nil 0 nil)))
(ert-deftest test-org-drill-determine-next-interval-simple8-error-quality-below-zero ()
"Error: quality=-1 violates the cl-assert quality range."
(test-scheduler--should-cl-assert
(test-scheduler--call-simple8 0 0 -1 0 nil 0 nil)))
(ert-deftest test-org-drill-determine-next-interval-simple8-error-quality-above-five ()
"Error: quality=6 violates the cl-assert quality range."
(test-scheduler--should-cl-assert
(test-scheduler--call-simple8 0 0 6 0 nil 0 nil)))
(ert-deftest test-org-drill-determine-next-interval-simple8-error-meanq-above-five ()
"Error: meanq=10 violates the meanq cl-assert (Simple8-specific check)."
(test-scheduler--should-cl-assert
(test-scheduler--call-simple8 10 3 4 0 10.0 3 nil)))
(ert-deftest test-org-drill-determine-next-interval-simple8-error-meanq-below-zero ()
"Error: meanq=-1 violates the meanq cl-assert (Simple8-specific check)."
(test-scheduler--should-cl-assert
(test-scheduler--call-simple8 10 3 4 0 -1.0 3 nil)))
;;; Algorithm Verification
(ert-deftest test-org-drill-determine-next-interval-simple8-algorithm-return-value-structure ()
"Algorithm: return value is a 6-element list (not 7 like SM2/SM5)."
(let ((result (test-scheduler--call-simple8 10 3 4 0 3.5 10 nil)))
(should (listp result))
(should (= (length result) 6))
(should (numberp (nth 0 result))) ; interval
(should (numberp (nth 1 result))) ; repeats
(should (numberp (nth 2 result))) ; ease
(should (numberp (nth 3 result))) ; failures
(should (numberp (nth 4 result))) ; meanq
(should (numberp (nth 5 result))))) ; totaln
(ert-deftest test-org-drill-determine-next-interval-simple8-algorithm-failure-resets-repeats-to-zero ()
"Algorithm: failure path resets repeats to 0, regardless of input value."
(let* ((result (test-scheduler--call-simple8 10 7 0 0 nil 7 nil))
(repeats (test-scheduler--extract-repeats result)))
(should (= repeats 0))))
(ert-deftest test-org-drill-determine-next-interval-simple8-algorithm-ease-derived-from-meanq ()
"Algorithm: returned ease = simple8-quality->ease(returned-meanq)."
(let* ((result (test-scheduler--call-simple8 10 2 4 0 4.0 2 nil))
(returned-ease (test-scheduler--extract-ease result))
(returned-meanq (test-scheduler--extract-meanq result))
(expected-ease (org-drill-simple8-quality->ease returned-meanq)))
(should (< (abs (- returned-ease expected-ease)) 0.0001))))
(ert-deftest test-org-drill-determine-next-interval-simple8-algorithm-interval-grows-over-successive-reviews ()
"Algorithm: successive successful quality-4 reviews produce growing intervals."
(let ((org-drill-add-random-noise-to-intervals-p nil))
(let* ((result-1 (test-scheduler--call-simple8 0 0 4 0 nil 0 nil))
(interval-1 (test-scheduler--extract-interval result-1))
(meanq-1 (test-scheduler--extract-meanq result-1))
(result-2 (test-scheduler--call-simple8 interval-1 1 4 0 meanq-1 1 nil))
(interval-2 (test-scheduler--extract-interval result-2)))
(should (> interval-2 interval-1)))))
;;; Helper Functions
(ert-deftest test-org-drill-simple8-quality->ease-zero ()
"Helper: simple8-quality->ease(0) returns the polynomial constant term (1.4515)."
(let ((ease (org-drill-simple8-quality->ease 0)))
(should (< (abs (- ease 1.4515)) 0.0001))))
(ert-deftest test-org-drill-simple8-quality->ease-monotonic ()
"Helper: simple8-quality->ease is monotonically increasing on [0, 5]."
(let ((ease-0 (org-drill-simple8-quality->ease 0))
(ease-5 (org-drill-simple8-quality->ease 5)))
(should (> ease-5 ease-0))))
(ert-deftest test-org-drill-simple8-first-interval-zero-failures ()
"Helper: simple8-first-interval(0) returns the leading coefficient (2.4849)."
(let ((interval (org-drill-simple8-first-interval 0)))
(should (< (abs (- interval 2.4849)) 0.0001))))
(ert-deftest test-org-drill-simple8-first-interval-decreases-with-failures ()
"Helper: simple8-first-interval is monotonically decreasing in failures."
(let ((interval-0 (org-drill-simple8-first-interval 0))
(interval-10 (org-drill-simple8-first-interval 10)))
(should (< interval-10 interval-0))))
(ert-deftest test-org-drill-simple8-interval-factor-repetition-one ()
"Helper: simple8-interval-factor returns input ease at repetition=1.
At repetition=1, learn-fraction^log(1,2) = learn-fraction^0 = 1, so the
factor reduces to 1.2 + (ease - 1.2) * 1 = ease."
(let* ((ease 1.5)
(factor (org-drill-simple8-interval-factor ease 1)))
(should (< (abs (- factor ease)) 0.0001))))
(provide 'test-org-drill-determine-next-interval-simple8)
;;; test-org-drill-determine-next-interval-simple8.el ends here
|