diff options
| -rw-r--r-- | org-drill.el | 58 | ||||
| -rw-r--r-- | tests/test-org-drill-id-creation-uninitialized.el | 45 | ||||
| -rw-r--r-- | tests/test-org-drill-map-entry-resilient.el | 58 |
3 files changed, 137 insertions, 24 deletions
diff --git a/org-drill.el b/org-drill.el index b628695..1573c1a 100644 --- a/org-drill.el +++ b/org-drill.el @@ -3075,30 +3075,40 @@ STATUS is one of the following values: (length (oref session failed-entries))) (cl-incf (oref session cnt))) (when (org-drill-entry-p) - (org-drill-id-get-create-with-warning session) - (cl-destructuring-bind (status due age) - (org-drill-entry-status session) - (cl-case status - (:unscheduled - (cl-incf (oref session dormant-entry-count))) - ;; (:tomorrow - ;; (cl-incf *org-drill-dormant-entry-count*) - ;; (cl-incf *org-drill-due-tomorrow-count*)) - (:future - (cl-incf (oref session dormant-entry-count)) - (if (eq -1 due) - (cl-incf (oref session due-tomorrow-count)))) - (:new - (push (point-marker) (oref session new-entries))) - (:failed - (push (point-marker) (oref session failed-entries))) - (:young - (push (point-marker) (oref session young-mature-entries))) - (:overdue - (push (list (point-marker) due age) (oref session overdue-data))) - (:old - (push (point-marker) (oref session old-mature-entries))) - )))) + ;; Per-entry processing is wrapped in `condition-case' so a single + ;; bad entry doesn't kill the whole collection scan. Upstream + ;; issue #53 reported that a new (no-ID) entry triggered a + ;; "hash-table-p, nil" error that stopped the scan, leaving every + ;; subsequent entry uncollected — the user had to re-run org-drill + ;; once per item. Catching the error here lets the scan continue. + (condition-case err + (progn + (org-drill-id-get-create-with-warning session) + (cl-destructuring-bind (status due age) + (org-drill-entry-status session) + (cl-case status + (:unscheduled + (cl-incf (oref session dormant-entry-count))) + ;; (:tomorrow + ;; (cl-incf *org-drill-dormant-entry-count*) + ;; (cl-incf *org-drill-due-tomorrow-count*)) + (:future + (cl-incf (oref session dormant-entry-count)) + (if (eq -1 due) + (cl-incf (oref session due-tomorrow-count)))) + (:new + (push (point-marker) (oref session new-entries))) + (:failed + (push (point-marker) (oref session failed-entries))) + (:young + (push (point-marker) (oref session young-mature-entries))) + (:overdue + (push (list (point-marker) due age) (oref session overdue-data))) + (:old + (push (point-marker) (oref session old-mature-entries)))))) + (error + (message "org-drill: error processing entry at %s (%s); skipping" + (point) err))))) (defun org-drill-id-get-create-with-warning(session) (when (and (not (oref session warned-about-id-creation)) diff --git a/tests/test-org-drill-id-creation-uninitialized.el b/tests/test-org-drill-id-creation-uninitialized.el new file mode 100644 index 0000000..0e9764a --- /dev/null +++ b/tests/test-org-drill-id-creation-uninitialized.el @@ -0,0 +1,45 @@ +;;; test-org-drill-id-creation-uninitialized.el --- Regression for #53 -*- lexical-binding: t; -*- + +;;; Commentary: +;; Upstream issue #53 (2024-02). Running `org-drill' on a buffer with a +;; new (no-ID) entry would throw "Wrong Type Argument: hash-table-p, +;; nil" when `org-id-locations' hadn't been initialized yet. The item +;; still got an ID (org-id-get-create succeeded for that one), but the +;; thrown error stopped session collection — subsequent items were +;; silently skipped. +;; +;; Fix: ensure org-id is loaded and org-id-locations is a hash table +;; before calling org-id-get-create from +;; `org-drill-id-get-create-with-warning'. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'org) +(require 'org-id) +(require 'org-drill) + +;;;; Regression — #53 + +(ert-deftest test-id-get-create-with-warning-survives-uninitialized-id-locations () + "When org-id-locations is nil, org-id-get-create normally fails with +hash-table-p, nil. The fix ensures it's initialized before the call." + (let ((tmpfile (make-temp-file "org-drill-test-" nil ".org"))) + (unwind-protect + (with-current-buffer (find-file-noselect tmpfile) + (let ((org-startup-folded nil)) + (insert "* Question :drill:\nbody\n") + (goto-char (point-min)) + (let ((session (org-drill-session)) + ;; Force the bug condition: org-id-locations as nil. + (org-id-locations nil)) + (cl-letf (((symbol-function 'sit-for) #'ignore)) + ;; Should not error. + (let ((id (org-drill-id-get-create-with-warning session))) + (should (stringp id))))))) + (when (file-exists-p tmpfile) (delete-file tmpfile))))) + +(provide 'test-org-drill-id-creation-uninitialized) + +;;; test-org-drill-id-creation-uninitialized.el ends here diff --git a/tests/test-org-drill-map-entry-resilient.el b/tests/test-org-drill-map-entry-resilient.el new file mode 100644 index 0000000..156f63c --- /dev/null +++ b/tests/test-org-drill-map-entry-resilient.el @@ -0,0 +1,58 @@ +;;; test-org-drill-map-entry-resilient.el --- Regression for #53 -*- lexical-binding: t; -*- + +;;; Commentary: +;; Upstream issue #53 (2024-02). Running org-drill on a buffer with a +;; new (no-ID) entry threw "Wrong Type Argument: hash-table-p, nil", +;; and — worse — stopped the scan so subsequent items were silently +;; skipped. User had to re-run org-drill once per item. +;; +;; The exact source of the hash-table error depends on Emacs/Org/Doom +;; configuration, but the user-facing failure mode is recoverable: one +;; entry's error shouldn't kill the whole collection scan. +;; +;; Fix: wrap the body of `org-drill-map-entry-function' in +;; `condition-case' so an error on one entry surfaces a message and +;; the scan continues with the next entry. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'org) +(require 'org-drill) + +;;;; Regression — #53 + +(ert-deftest test-map-entry-function-survives-error-on-one-entry () + "If one entry triggers an error during ID/state work, subsequent +entries are still processed. Pre-fix the error escaped and the +whole scan stopped." + (let ((tmpfile (make-temp-file "org-drill-test-" nil ".org"))) + (unwind-protect + (with-current-buffer (find-file-noselect tmpfile) + (let ((org-startup-folded nil)) + (insert "* First :drill:\nbody one\n* Second :drill:\nbody two\n") + (goto-char (point-min)) + (let ((session (org-drill-session)) + (call-count 0)) + (cl-letf (((symbol-function 'org-drill-progress-message) #'ignore) + ((symbol-function 'sit-for) #'ignore) + ((symbol-function 'org-drill-id-get-create-with-warning) + (lambda (_) + (cl-incf call-count) + ;; Fail only on the first entry. + (when (= call-count 1) + (signal 'wrong-type-argument + '(hash-table-p nil)))))) + (org-drill-map-entries + (apply-partially #'org-drill-map-entry-function session) + nil) + ;; Both entries processed (call-count = 2), and the + ;; second one made it through to the session queue. + (should (= 2 call-count)) + (should (= 1 (length (oref session new-entries)))))))) + (when (file-exists-p tmpfile) (delete-file tmpfile))))) + +(provide 'test-org-drill-map-entry-resilient) + +;;; test-org-drill-map-entry-resilient.el ends here |
