diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-27 20:47:35 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-27 20:47:35 -0500 |
| commit | 39d01b75ac679410821ce5e16c09ec6b7799f791 (patch) | |
| tree | d825389532e98a9bcfc3fddd87b787c7879c5dcc | |
| parent | 198e772efbb17cc8dbd514b0c0487d780c5e3eaa (diff) | |
| download | org-drill-39d01b75ac679410821ce5e16c09ec6b7799f791.tar.gz org-drill-39d01b75ac679410821ce5e16c09ec6b7799f791.zip | |
refactor: thread card-state struct through the item-data round-trip
Second step of #147. get-item-data now returns an org-drill-card-state and store-item-data takes one, so the six recall fields move as named slots instead of a positional list. The three call sites (smart-reschedule, hypothetical-next-review-date, copy-scheduling-to-marker) read scheduler inputs through accessors and build a struct for the store, which removes the hand re-ordering between the get-shape and the store-shape.
Behavior is unchanged. The legacy LEARN_DATA read path and the virgin-item sentinel are preserved field-for-field, and store takes just the struct because its last-interval slot already holds the interval to persist. The schedulers still take positional args; they adopt the struct in the following commits.
I updated the round-trip, integration, and setup-helper tests to build and read the struct via a small list-view helper, so the existing expected-value assertions stay readable.
| -rw-r--r-- | org-drill.el | 222 | ||||
| -rw-r--r-- | tests/test-integration-drill-session-simple-workflow-integration-test.el | 61 | ||||
| -rw-r--r-- | tests/test-org-drill-additional-coverage.el | 4 | ||||
| -rw-r--r-- | tests/test-org-drill-cloze-and-scheduling-helpers.el | 4 | ||||
| -rw-r--r-- | tests/test-org-drill-item-data-roundtrip.el | 52 | ||||
| -rw-r--r-- | tests/test-org-drill-orchestration-helpers.el | 4 | ||||
| -rw-r--r-- | tests/test-org-drill-prompt-and-misc.el | 4 | ||||
| -rw-r--r-- | tests/test-org-drill-queue-and-misc.el | 4 |
8 files changed, 216 insertions, 139 deletions
diff --git a/org-drill.el b/org-drill.el index 270a19f..9e38666 100644 --- a/org-drill.el +++ b/org-drill.el @@ -1145,64 +1145,77 @@ Returns the parsed list or nil if invalid or unsafe." last-interval repetitions ease failures meanq total-repeats) (defun org-drill-get-item-data () - "Return alist of 6 items, containing all the stored recall - data for the item at point: + "Return an `org-drill-card-state' with the stored recall data for the item at point. + +Slots: - LAST-INTERVAL is the interval in days that was used to schedule the item's current review date. -- REPEATS is the number of items the item has been successfully recalled without +- REPETITIONS is the number of times the item has been successfully recalled without any failures. It is reset to 0 upon failure to recall the item. - FAILURES is the total number of times the user has failed to recall the item. - TOTAL-REPEATS includes both successful and unsuccessful repetitions. -- AVERAGE-QUALITY is the mean quality of recall of the item over - all its repetitions, successful and unsuccessful. -- EASE is a number reflecting how easy the item is to learn. Higher is easier. -" +- MEANQ is the mean quality of recall of the item over all its repetitions, + successful and unsuccessful. +- EASE is a number reflecting how easy the item is to learn. Higher is easier." (let ((learn-str (org-entry-get (point) "LEARN_DATA")) (repeats (org-drill-entry-total-repeats :missing))) (cond (learn-str (let ((learn-data (org-drill--safe-read-learn-data learn-str))) (if learn-data - (list (nth 0 learn-data) ; last interval - (nth 1 learn-data) ; repetitions - (org-drill-entry-failure-count) - (nth 1 learn-data) - (org-drill-entry-last-quality) - (nth 2 learn-data)) ; EF + (make-org-drill-card-state + :last-interval (nth 0 learn-data) + :repetitions (nth 1 learn-data) + :failures (org-drill-entry-failure-count) + :total-repeats (nth 1 learn-data) + :meanq (org-drill-entry-last-quality) + :ease (nth 2 learn-data)) ; EF ;; If LEARN_DATA is invalid/unsafe, fall through to next case (if (not (eql :missing repeats)) - (list (org-drill-entry-last-interval) - (org-drill-entry-repeats-since-fail) - (org-drill-entry-failure-count) - (org-drill-entry-total-repeats) - (org-drill-entry-average-quality) - (org-drill-entry-ease)) + (make-org-drill-card-state + :last-interval (org-drill-entry-last-interval) + :repetitions (org-drill-entry-repeats-since-fail) + :failures (org-drill-entry-failure-count) + :total-repeats (org-drill-entry-total-repeats) + :meanq (org-drill-entry-average-quality) + :ease (org-drill-entry-ease)) ;; Virgin item - (list 0 0 0 0 nil nil))))) + (make-org-drill-card-state + :last-interval 0 :repetitions 0 :failures 0 + :total-repeats 0 :meanq nil :ease nil))))) ((not (eql :missing repeats)) - (list (org-drill-entry-last-interval) - (org-drill-entry-repeats-since-fail) - (org-drill-entry-failure-count) - (org-drill-entry-total-repeats) - (org-drill-entry-average-quality) - (org-drill-entry-ease))) + (make-org-drill-card-state + :last-interval (org-drill-entry-last-interval) + :repetitions (org-drill-entry-repeats-since-fail) + :failures (org-drill-entry-failure-count) + :total-repeats (org-drill-entry-total-repeats) + :meanq (org-drill-entry-average-quality) + :ease (org-drill-entry-ease))) (t ; virgin item - (list 0 0 0 0 nil nil))))) - -(defun org-drill-store-item-data (last-interval repeats failures - total-repeats meanq - ease) - "Stores the given data in the item at point." + (make-org-drill-card-state + :last-interval 0 :repetitions 0 :failures 0 + :total-repeats 0 :meanq nil :ease nil))))) + +(defun org-drill-store-item-data (state) + "Store the recall data in STATE, an `org-drill-card-state', into the item at point. +STATE's LAST-INTERVAL slot holds the interval to schedule going forward (the +caller passes the scheduler's next-interval there)." (org-entry-delete (point) "LEARN_DATA") (org-set-property "DRILL_LAST_INTERVAL" - (number-to-string (org-drill-round-float last-interval 4))) - (org-set-property "DRILL_REPEATS_SINCE_FAIL" (number-to-string repeats)) - (org-set-property "DRILL_TOTAL_REPEATS" (number-to-string total-repeats)) - (org-set-property "DRILL_FAILURE_COUNT" (number-to-string failures)) + (number-to-string + (org-drill-round-float (org-drill-card-state-last-interval state) 4))) + (org-set-property "DRILL_REPEATS_SINCE_FAIL" + (number-to-string (org-drill-card-state-repetitions state))) + (org-set-property "DRILL_TOTAL_REPEATS" + (number-to-string (org-drill-card-state-total-repeats state))) + (org-set-property "DRILL_FAILURE_COUNT" + (number-to-string (org-drill-card-state-failures state))) (org-set-property "DRILL_AVERAGE_QUALITY" - (number-to-string (org-drill-round-float meanq 3))) + (number-to-string + (org-drill-round-float (org-drill-card-state-meanq state) 3))) (org-set-property "DRILL_EASE" - (number-to-string (org-drill-round-float ease 3)))) + (number-to-string + (org-drill-round-float (org-drill-card-state-ease state) 3)))) ;;; SM2 Algorithm ============================================================= (defun org-drill-determine-next-interval-sm2 (last-interval n ef quality @@ -1481,23 +1494,33 @@ item will be scheduled exactly this many days into the future." (weight (org-entry-get (point) "DRILL_CARD_WEIGHT"))) (if (stringp weight) (setq weight (string-to-number weight))) - (cl-destructuring-bind (last-interval repetitions failures - total-repeats meanq ease) - (org-drill-get-item-data) + (let ((state (org-drill-get-item-data))) (cl-destructuring-bind (next-interval repetitions ease failures meanq total-repeats &optional new-ofmatrix) (cl-case org-drill-spaced-repetition-algorithm - (sm5 (org-drill-determine-next-interval-sm5 last-interval repetitions - ease quality failures - meanq total-repeats ofmatrix)) - (sm2 (org-drill-determine-next-interval-sm2 last-interval repetitions - ease quality failures - meanq total-repeats)) - (simple8 (org-drill-determine-next-interval-simple8 last-interval repetitions - quality failures meanq - total-repeats - delta-days))) + (sm5 (org-drill-determine-next-interval-sm5 + (org-drill-card-state-last-interval state) + (org-drill-card-state-repetitions state) + (org-drill-card-state-ease state) quality + (org-drill-card-state-failures state) + (org-drill-card-state-meanq state) + (org-drill-card-state-total-repeats state) ofmatrix)) + (sm2 (org-drill-determine-next-interval-sm2 + (org-drill-card-state-last-interval state) + (org-drill-card-state-repetitions state) + (org-drill-card-state-ease state) quality + (org-drill-card-state-failures state) + (org-drill-card-state-meanq state) + (org-drill-card-state-total-repeats state))) + (simple8 (org-drill-determine-next-interval-simple8 + (org-drill-card-state-last-interval state) + (org-drill-card-state-repetitions state) + quality + (org-drill-card-state-failures state) + (org-drill-card-state-meanq state) + (org-drill-card-state-total-repeats state) + delta-days))) (if (numberp days-ahead) (setq next-interval days-ahead)) @@ -1505,11 +1528,15 @@ item will be scheduled exactly this many days into the future." (numberp weight) (cl-plusp weight) (not (cl-minusp next-interval))) (setq next-interval - (max 1.0 (+ last-interval - (/ (- next-interval last-interval) weight))))) + (max 1.0 (+ (org-drill-card-state-last-interval state) + (/ (- next-interval + (org-drill-card-state-last-interval state)) + weight))))) - (org-drill-store-item-data next-interval repetitions failures - total-repeats meanq ease) + (org-drill-store-item-data + (make-org-drill-card-state + :last-interval next-interval :repetitions repetitions :ease ease + :failures failures :meanq meanq :total-repeats total-repeats)) (if (eql 'sm5 org-drill-spaced-repetition-algorithm) (setq org-drill-sm5-optimal-factor-matrix new-ofmatrix)) @@ -1533,34 +1560,46 @@ item will be scheduled exactly this many days into the future." "Return aninteger representing the number of days into the future that the current item would be scheduled, based on a recall quality of QUALITY." - (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT"))) - (cl-destructuring-bind (last-interval repetitions failures - total-repeats meanq ease) - (org-drill-get-item-data) + (let ((weight (org-entry-get (point) "DRILL_CARD_WEIGHT")) + (state (org-drill-get-item-data))) (if (stringp weight) (setq weight (string-to-number weight))) (cl-destructuring-bind (next-interval _repetitions _ease _failures _meanq _total-repeats &optional _ofmatrix) (cl-case org-drill-spaced-repetition-algorithm - (sm5 (org-drill-determine-next-interval-sm5 last-interval repetitions - ease quality failures - meanq total-repeats - org-drill-sm5-optimal-factor-matrix)) - (sm2 (org-drill-determine-next-interval-sm2 last-interval repetitions - ease quality failures - meanq total-repeats)) - (simple8 (org-drill-determine-next-interval-simple8 last-interval repetitions - quality failures meanq - total-repeats))) + (sm5 (org-drill-determine-next-interval-sm5 + (org-drill-card-state-last-interval state) + (org-drill-card-state-repetitions state) + (org-drill-card-state-ease state) quality + (org-drill-card-state-failures state) + (org-drill-card-state-meanq state) + (org-drill-card-state-total-repeats state) + org-drill-sm5-optimal-factor-matrix)) + (sm2 (org-drill-determine-next-interval-sm2 + (org-drill-card-state-last-interval state) + (org-drill-card-state-repetitions state) + (org-drill-card-state-ease state) quality + (org-drill-card-state-failures state) + (org-drill-card-state-meanq state) + (org-drill-card-state-total-repeats state))) + (simple8 (org-drill-determine-next-interval-simple8 + (org-drill-card-state-last-interval state) + (org-drill-card-state-repetitions state) + quality + (org-drill-card-state-failures state) + (org-drill-card-state-meanq state) + (org-drill-card-state-total-repeats state)))) (cond ((not (cl-plusp next-interval)) 0) ((and (numberp weight) (cl-plusp weight)) - (+ last-interval - (max 1.0 (/ (- next-interval last-interval) weight)))) + (+ (org-drill-card-state-last-interval state) + (max 1.0 (/ (- next-interval + (org-drill-card-state-last-interval state)) + weight)))) (t - next-interval)))))) + next-interval))))) (defun org-drill-hypothetical-next-review-dates () "Return hypothetical next review dates." @@ -3649,27 +3688,24 @@ the tag \\='imported\\='." The destination's previous data is stripped first. Skips zero- total-repeats items (i.e., never-rated cards) — those have no scheduling information worth migrating." - (cl-destructuring-bind (last-interval repetitions failures - total-repeats meanq ease) - (org-drill-get-item-data) - (let ((last-reviewed (org-entry-get (point) "DRILL_LAST_REVIEWED")) - (last-quality (org-entry-get (point) "DRILL_LAST_QUALITY")) - (scheduled-time (org-get-scheduled-time (point)))) - (with-current-buffer (marker-buffer marker) - (save-excursion - (goto-char marker) - (org-drill-strip-entry-data) - (unless (zerop total-repeats) - (org-drill-store-item-data last-interval repetitions failures - total-repeats meanq ease) - (if last-quality - (org-set-property "DRILL_LAST_QUALITY" last-quality) - (org-delete-property "DRILL_LAST_QUALITY")) - (if last-reviewed - (org-set-property "DRILL_LAST_REVIEWED" last-reviewed) - (org-delete-property "DRILL_LAST_REVIEWED")) - (when scheduled-time - (org-schedule nil scheduled-time)))))))) + (let ((state (org-drill-get-item-data)) + (last-reviewed (org-entry-get (point) "DRILL_LAST_REVIEWED")) + (last-quality (org-entry-get (point) "DRILL_LAST_QUALITY")) + (scheduled-time (org-get-scheduled-time (point)))) + (with-current-buffer (marker-buffer marker) + (save-excursion + (goto-char marker) + (org-drill-strip-entry-data) + (unless (zerop (org-drill-card-state-total-repeats state)) + (org-drill-store-item-data state) + (if last-quality + (org-set-property "DRILL_LAST_QUALITY" last-quality) + (org-delete-property "DRILL_LAST_QUALITY")) + (if last-reviewed + (org-set-property "DRILL_LAST_REVIEWED" last-reviewed) + (org-delete-property "DRILL_LAST_REVIEWED")) + (when scheduled-time + (org-schedule nil scheduled-time))))))) (defun org-drill--migrate-from-source (src dest ignore-new-items-p) "Walk SRC's entries, migrating scheduling data into matching DEST entries. diff --git a/tests/test-integration-drill-session-simple-workflow-integration-test.el b/tests/test-integration-drill-session-simple-workflow-integration-test.el index 7cc72c3..e6f0aae 100644 --- a/tests/test-integration-drill-session-simple-workflow-integration-test.el +++ b/tests/test-integration-drill-session-simple-workflow-integration-test.el @@ -6,7 +6,7 @@ ;; ;; 1. Entry detection (org-drill-entry-p) ;; 2. Entry enumeration (org-drill-map-entries) -;; 3. Data retrieval (org-drill-get-item-data) +;; 3. Data retrieval (test-int--state-as-list (org-drill-get-item-data)) ;; 4. Scheduling algorithm (org-drill-determine-next-interval-sm2) ;; 5. Data storage (org-drill-store-item-data) ;; @@ -19,6 +19,19 @@ (require 'assess) (require 'org-drill) +;;; Helpers + +(defun test-int--state-as-list (state) + "View STATE (an `org-drill-card-state') as the canonical field list +(LAST-INTERVAL REPETITIONS FAILURES TOTAL-REPEATS MEANQ EASE), so existing +list-style assertions in this file keep working after the struct refactor." + (list (org-drill-card-state-last-interval state) + (org-drill-card-state-repetitions state) + (org-drill-card-state-failures state) + (org-drill-card-state-total-repeats state) + (org-drill-card-state-meanq state) + (org-drill-card-state-ease state))) + ;;; Test Data (defconst test-integration-simple-workflow-basic-entries @@ -132,7 +145,7 @@ Should correctly parse all DRILL_* properties." ;; Find first drill entry (re-search-forward "^\\* First Card :drill:") (beginning-of-line) - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) ;; Verify structure: (last-interval repeats failures total-repeats meanq ease) (should (listp data)) (should (= (length data) 6)) @@ -154,7 +167,7 @@ Should return default values for new items." ;; Find first drill entry (re-search-forward "^\\* Brand New Card :drill:") (beginning-of-line) - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) ;; Verify structure exists (should (listp data)) (should (= (length data) 6)) @@ -179,7 +192,7 @@ Verifies integration between data retrieval and SM2 algorithm." (re-search-forward "^\\* Second Card :drill:") (beginning-of-line) (cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease) - (org-drill-get-item-data) + (test-int--state-as-list (org-drill-get-item-data)) ;; Simulate quality rating of 4 (good recall) (let* ((quality 4) (result (org-drill-determine-next-interval-sm2 @@ -204,7 +217,7 @@ Verifies that failure handling works correctly in integrated workflow." (re-search-forward "^\\* Third Card :drill:") (beginning-of-line) (cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease) - (org-drill-get-item-data) + (test-int--state-as-list (org-drill-get-item-data)) ;; Simulate complete failure (quality 0) (let* ((quality 0) (result (org-drill-determine-next-interval-sm2 @@ -232,7 +245,7 @@ Verifies org-drill-store-item-data updates properties correctly." ;; Get original data (cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease) - (org-drill-get-item-data) + (test-int--state-as-list (org-drill-get-item-data)) ;; Calculate new scheduling data (let* ((quality 5) ; Perfect recall @@ -247,8 +260,11 @@ Verifies org-drill-store-item-data updates properties correctly." (new-total (nth 5 result))) ;; Store new data - (org-drill-store-item-data next-interval new-repeats new-failures - new-total new-meanq new-ease) + (org-drill-store-item-data + (make-org-drill-card-state + :last-interval next-interval :repetitions new-repeats + :failures new-failures :total-repeats new-total + :meanq new-meanq :ease new-ease)) ;; Verify data was stored (properties exist and are valid) (should (org-entry-get (point) "DRILL_LAST_INTERVAL")) @@ -317,7 +333,7 @@ Simulates reviewing a card and verifies all components work together." ;; Step 3: Retrieve current data (cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease) - (org-drill-get-item-data) + (test-int--state-as-list (org-drill-get-item-data)) (should (numberp last-interval)) (should (numberp repeats)) @@ -334,11 +350,14 @@ Simulates reviewing a card and verifies all components work together." (new-total (nth 5 result))) ;; Step 5: Store results - (org-drill-store-item-data next-interval new-repeats new-failures - new-total new-meanq new-ease) + (org-drill-store-item-data + (make-org-drill-card-state + :last-interval next-interval :repetitions new-repeats + :failures new-failures :total-repeats new-total + :meanq new-meanq :ease new-ease)) ;; Step 6: Verify data persisted - (let ((retrieved-data (org-drill-get-item-data))) + (let ((retrieved-data (test-int--state-as-list (org-drill-get-item-data)))) (should (= (nth 0 retrieved-data) (floor next-interval))) (should (= (nth 1 retrieved-data) new-repeats)) (should (= (nth 2 retrieved-data) new-failures)) @@ -367,7 +386,7 @@ Question content. (re-search-forward "^\\* Corrupted Card :drill:") (beginning-of-line) ;; Should not error when retrieving data with invalid values - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) (should (listp data)) ;; Verify function handles corruption gracefully (should (= (length data) 6))))))) @@ -385,7 +404,7 @@ Question: Test question? (re-search-forward "^\\* Card Without Properties :drill:") (beginning-of-line) ;; Should not error, provide defaults - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) (should (listp data)) (should (= (length data) 6))))))) @@ -409,7 +428,7 @@ Question content. (lambda () (re-search-forward "^\\* Negative Values Card :drill:") (beginning-of-line) - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) (should (listp data)) ;; Verify system doesn't crash with negative values (should (= (length data) 6))))))) @@ -434,7 +453,7 @@ Question content. (lambda () (re-search-forward "^\\* Large Values Card :drill:") (beginning-of-line) - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) (should (listp data)) (should (= (length data) 6)) ;; Verify scheduling can handle extreme values @@ -457,7 +476,7 @@ Should handle gracefully without crashing." (re-search-forward "Question: What is 2\\+2\\?") (beginning-of-line) ;; Should not crash, may return nil or defaults - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) ;; Just verify it doesn't crash (should (or (null data) (listp data))))))) @@ -472,7 +491,7 @@ Should handle non-drill headings gracefully." (beginning-of-line) (should-not (org-drill-entry-p)) ;; Retrieving data from non-drill entry - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) ;; Should not crash (should (or (null data) (listp data))))))) @@ -545,7 +564,7 @@ Question with Unicode: 日本語 Café. (re-search-forward "^\\* Unicode Property Card :drill:") (beginning-of-line) ;; Should handle Unicode in content without issues - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) (should (listp data)) (should (= (length data) 6))))))) @@ -581,7 +600,7 @@ Question. (lambda () (re-search-forward "^\\* Empty Drawer Card :drill:") (beginning-of-line) - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) ;; Should provide defaults for missing properties (should (listp data)) (should (= (length data) 6))))))) @@ -606,7 +625,7 @@ Question. (lambda () (re-search-forward "^\\* Float Values Card :drill:") (beginning-of-line) - (let ((data (org-drill-get-item-data))) + (let ((data (test-int--state-as-list (org-drill-get-item-data)))) (should (listp data)) ;; Verify system handles float->int conversion (should (numberp (nth 0 data))) diff --git a/tests/test-org-drill-additional-coverage.el b/tests/test-org-drill-additional-coverage.el index f8f68dd..6d97614 100644 --- a/tests/test-org-drill-additional-coverage.el +++ b/tests/test-org-drill-additional-coverage.el @@ -137,7 +137,7 @@ new-entries." "DRILL_CARD_WEIGHT > 1 stretches the next-interval delta — the resulting SCHEDULED date is closer to today than without weight." (with-fresh-drill-entry - (org-drill-store-item-data 10 3 0 3 4.5 2.5) + (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)) (with-fixed-now (let ((sched-no-weight nil) (sched-with-weight nil)) @@ -145,7 +145,7 @@ SCHEDULED date is closer to today than without weight." (org-drill-smart-reschedule 5) (setq sched-no-weight (org-entry-get (point) "SCHEDULED")) ;; Reset and reschedule with weight=2. - (org-drill-store-item-data 10 3 0 3 4.5 2.5) + (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)) (org-set-property "DRILL_CARD_WEIGHT" "2") (org-drill-smart-reschedule 5) (setq sched-with-weight (org-entry-get (point) "SCHEDULED")) diff --git a/tests/test-org-drill-cloze-and-scheduling-helpers.el b/tests/test-org-drill-cloze-and-scheduling-helpers.el index 1ee4c61..39573aa 100644 --- a/tests/test-org-drill-cloze-and-scheduling-helpers.el +++ b/tests/test-org-drill-cloze-and-scheduling-helpers.el @@ -148,7 +148,7 @@ 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) + (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))) @@ -180,7 +180,7 @@ This is what users see in the prompt: `1 / 2 / 3 / 4 / 6 / 9' style hints." (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) + (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) diff --git a/tests/test-org-drill-item-data-roundtrip.el b/tests/test-org-drill-item-data-roundtrip.el index 126c8ac..dc445a1 100644 --- a/tests/test-org-drill-item-data-roundtrip.el +++ b/tests/test-org-drill-item-data-roundtrip.el @@ -9,6 +9,10 @@ ;; "When I rate a card, my progress is saved. When I open the file ;; tomorrow, my progress is still there." ;; +;; get-item-data returns an `org-drill-card-state' struct; store-item-data +;; takes one. The tests below view the struct as a field list (via +;; `test-roundtrip--state-as-list') so the expected values read at a glance. +;; ;; The contract has three branches: ;; ;; 1. *Modern format*: store writes six DRILL_* properties; get reads @@ -19,7 +23,7 @@ ;; transparently reads either format. Backward compat — a 2015-era ;; deck should still drill. ;; -;; 3. *Virgin item*: a card that's never been rated returns +;; 3. *Virgin item*: a card that's never been rated returns a state of ;; `(0 0 0 0 nil nil)' so the scheduler knows to treat it as new. ;;; Code: @@ -30,6 +34,17 @@ ;;;; Helpers +(defun test-roundtrip--state-as-list (state) + "Return STATE's fields as a list, in the canonical +(LAST-INTERVAL REPETITIONS FAILURES TOTAL-REPEATS MEANQ EASE) order, so the +struct can be compared against a literal expected list." + (list (org-drill-card-state-last-interval state) + (org-drill-card-state-repetitions state) + (org-drill-card-state-failures state) + (org-drill-card-state-total-repeats state) + (org-drill-card-state-meanq state) + (org-drill-card-state-ease state))) + (defmacro with-fresh-drill-entry (&rest body) "Run BODY at point on a fresh drill entry with no DRILL_* or LEARN_DATA properties." (declare (indent 0)) @@ -58,11 +73,12 @@ PROPS is a list of (NAME . STRING-VALUE) cons cells." (ert-deftest test-org-drill-get-item-data-virgin-returns-zero-list () "A drill entry with no persisted state returns the virgin sentinel. -Six elements: zero interval, zero repeats, zero failures, zero total, +Six fields: zero interval, zero repeats, zero failures, zero total, nil meanq, nil ease. This is the value the scheduler reads on a never-rated card." (with-fresh-drill-entry - (should (equal '(0 0 0 0 nil nil) (org-drill-get-item-data))))) + (should (equal '(0 0 0 0 nil nil) + (test-roundtrip--state-as-list (org-drill-get-item-data)))))) ;;;; Modern format — read-side @@ -74,13 +90,14 @@ never-rated card." ("DRILL_TOTAL_REPEATS" . "5") ("DRILL_AVERAGE_QUALITY" . "3.8") ("DRILL_EASE" . "2.4")) - (should (equal '(10 3 1 5 3.8 2.4) (org-drill-get-item-data))))) + (should (equal '(10 3 1 5 3.8 2.4) + (test-roundtrip--state-as-list (org-drill-get-item-data)))))) (ert-deftest test-org-drill-get-item-data-modern-partial-properties-fall-back-to-defaults () "Missing DRILL_* properties take their per-field defaults — but the presence of any DRILL_* property still puts the function in modern mode." (with-modern-drill-entry '(("DRILL_TOTAL_REPEATS" . "3")) - (let ((result (org-drill-get-item-data))) + (let ((result (test-roundtrip--state-as-list (org-drill-get-item-data)))) ;; total-repeats → 3 (set), others take their defaults (should (equal 0 (nth 0 result))) ; last-interval default 0 (should (equal 0 (nth 1 result))) ; repeats default 0 @@ -94,7 +111,7 @@ presence of any DRILL_* property still puts the function in modern mode." (ert-deftest test-org-drill-store-item-data-writes-all-six-properties () "store-item-data sets all six DRILL_* properties on the entry at point." (with-fresh-drill-entry - (org-drill-store-item-data 10 3 1 5 3.8 2.4) + (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)) (should (equal "10.0" (org-entry-get (point) "DRILL_LAST_INTERVAL"))) (should (equal "3" (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL"))) (should (equal "1" (org-entry-get (point) "DRILL_FAILURE_COUNT"))) @@ -106,7 +123,7 @@ presence of any DRILL_* property still puts the function in modern mode." "Floating-point fields are rounded — interval to 4dp, meanq/ease to 3dp. Keeps the buffer tidy and avoids stray precision noise like 2.4999999998." (with-fresh-drill-entry - (org-drill-store-item-data 10.123456789 3 1 5 3.8765432 2.4567899) + (org-drill-store-item-data (make-org-drill-card-state :last-interval 10.123456789 :repetitions 3 :failures 1 :total-repeats 5 :meanq 3.8765432 :ease 2.4567899)) (should (equal "10.1235" (org-entry-get (point) "DRILL_LAST_INTERVAL"))) (should (equal "3.877" (org-entry-get (point) "DRILL_AVERAGE_QUALITY"))) (should (equal "2.457" (org-entry-get (point) "DRILL_EASE"))))) @@ -115,7 +132,7 @@ Keeps the buffer tidy and avoids stray precision noise like 2.4999999998." "Storing modern format wipes any legacy LEARN_DATA on the entry. Otherwise get-item-data would still read the stale legacy value first." (with-modern-drill-entry '(("LEARN_DATA" . "(2.5 1 0.5)")) - (org-drill-store-item-data 10 3 1 5 3.8 2.4) + (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)) (should (null (org-entry-get (point) "LEARN_DATA"))))) ;;;; Round-trip — the core user-facing assertion @@ -130,16 +147,18 @@ floats because `org-drill-round-float' returns float; counters (REPEATS, FAILURES, TOTAL-REPEATS) come back as ints. Numerically the round-trip is lossless; the scheduler accepts both." (with-fresh-drill-entry - (org-drill-store-item-data 10 3 1 5 3.8 2.4) - (should (equal '(10.0 3 1 5 3.8 2.4) (org-drill-get-item-data))))) + (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)) + (should (equal '(10.0 3 1 5 3.8 2.4) + (test-roundtrip--state-as-list (org-drill-get-item-data)))))) (ert-deftest test-org-drill-item-data-roundtrip-preserves-zero-values () "A first-rating round-trip with mostly zeros survives intact. Same type-mixing pattern as the non-zero round-trip — see that test's note for why LAST-INTERVAL is 0.0 and the counters are integer 0." (with-fresh-drill-entry - (org-drill-store-item-data 0 0 0 1 5.0 2.5) - (should (equal '(0.0 0 0 1 5.0 2.5) (org-drill-get-item-data))))) + (org-drill-store-item-data (make-org-drill-card-state :last-interval 0 :repetitions 0 :failures 0 :total-repeats 1 :meanq 5.0 :ease 2.5)) + (should (equal '(0.0 0 0 1 5.0 2.5) + (test-roundtrip--state-as-list (org-drill-get-item-data)))))) ;;;; Legacy LEARN_DATA — backward compat @@ -151,7 +170,8 @@ from their separate DRILL_* properties for the legacy path." ("DRILL_FAILURE_COUNT" . "0") ("DRILL_LAST_QUALITY" . "4")) ;; Returned: (interval, repeats, failures, repeats-again, last-quality, ease) - (should (equal '(7 2 0 2 4 2.6) (org-drill-get-item-data))))) + (should (equal '(7 2 0 2 4 2.6) + (test-roundtrip--state-as-list (org-drill-get-item-data)))))) (ert-deftest test-org-drill-get-item-data-invalid-LEARN_DATA-falls-through-to-modern () "If LEARN_DATA is malformed, fall through to the modern DRILL_* fields. @@ -163,14 +183,16 @@ Defends against a corrupted legacy entry from breaking a session." ("DRILL_TOTAL_REPEATS" . "6") ("DRILL_AVERAGE_QUALITY" . "3.5") ("DRILL_EASE" . "2.1")) - (should (equal '(12 4 2 6 3.5 2.1) (org-drill-get-item-data))))) + (should (equal '(12 4 2 6 3.5 2.1) + (test-roundtrip--state-as-list (org-drill-get-item-data)))))) (ert-deftest test-org-drill-get-item-data-invalid-LEARN_DATA-and-no-modern-returns-virgin () "Malformed LEARN_DATA on an entry with no DRILL_* fallback returns virgin sentinel. This matters: a corrupted-only legacy entry shouldn't crash the session, just be treated as never rated." (with-modern-drill-entry '(("LEARN_DATA" . "garbage")) - (should (equal '(0 0 0 0 nil nil) (org-drill-get-item-data))))) + (should (equal '(0 0 0 0 nil nil) + (test-roundtrip--state-as-list (org-drill-get-item-data)))))) (provide 'test-org-drill-item-data-roundtrip) diff --git a/tests/test-org-drill-orchestration-helpers.el b/tests/test-org-drill-orchestration-helpers.el index 5589fa7..774f664 100644 --- a/tests/test-org-drill-orchestration-helpers.el +++ b/tests/test-org-drill-orchestration-helpers.el @@ -211,7 +211,7 @@ the resume hint." (insert "* Question :drill:\n") (org-mode) (goto-char (point-min)) - (org-drill-store-item-data 10 3 1 5 3.8 2.4) + (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)) (org-drill--copy-scheduling-to-marker dst-marker))) (with-current-buffer (find-file-noselect dst-file) (goto-char (point-min)) @@ -254,7 +254,7 @@ the resume hint." (ert-deftest test-strip-unmatched-dest-entries-clears-properties () "Every entry left in the table has its scheduling props stripped." (with-org-tempfile "* Question :drill:\n" - (org-drill-store-item-data 10 3 1 5 3.8 2.4) + (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)) (clrhash org-drill-dest-id-table) (puthash "stale-id" (point-marker) org-drill-dest-id-table) (org-drill--strip-unmatched-dest-entries) diff --git a/tests/test-org-drill-prompt-and-misc.el b/tests/test-org-drill-prompt-and-misc.el index d684c9a..968ad57 100644 --- a/tests/test-org-drill-prompt-and-misc.el +++ b/tests/test-org-drill-prompt-and-misc.el @@ -95,7 +95,7 @@ (ert-deftest test-org-drill-relearn-item-resets-interval-to-zero () "Relearning sets DRILL_LAST_INTERVAL back to 0 — the card behaves as new." (with-fresh-drill-entry - (org-drill-store-item-data 30 5 0 5 4.0 2.5) + (org-drill-store-item-data (make-org-drill-card-state :last-interval 30 :repetitions 5 :failures 0 :total-repeats 5 :meanq 4.0 :ease 2.5)) (with-fixed-now (org-drill-relearn-item)) ;; After relearn: last-interval should be 0 @@ -105,7 +105,7 @@ (ert-deftest test-org-drill-relearn-item-removes-scheduled-stamp () "Relearning unschedules the entry (days-ahead = 0 path)." (with-fresh-drill-entry - (org-drill-store-item-data 30 5 0 5 4.0 2.5) + (org-drill-store-item-data (make-org-drill-card-state :last-interval 30 :repetitions 5 :failures 0 :total-repeats 5 :meanq 4.0 :ease 2.5)) (org-schedule nil "2026-06-01") (with-fixed-now (org-drill-relearn-item)) diff --git a/tests/test-org-drill-queue-and-misc.el b/tests/test-org-drill-queue-and-misc.el index 81a7844..057e6f8 100644 --- a/tests/test-org-drill-queue-and-misc.el +++ b/tests/test-org-drill-queue-and-misc.el @@ -228,7 +228,7 @@ actions require explicit consent." (insert "* Question :drill:\n") (org-mode) (goto-char (point-min)) - (org-drill-store-item-data 10 3 1 5 3.8 2.4) + (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)) (cl-letf (((symbol-function 'yes-or-no-p) (lambda (_) nil))) (org-drill-strip-all-data) ;; Properties survive — user said no. @@ -241,7 +241,7 @@ actions require explicit consent." (insert "* Question :drill:\n") (org-mode) (goto-char (point-min)) - (org-drill-store-item-data 10 3 1 5 3.8 2.4) + (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)) (cl-letf (((symbol-function 'yes-or-no-p) (lambda (_) t))) (org-drill-strip-all-data) (dolist (prop org-drill-scheduling-properties) |
