aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-05 03:43:25 -0500
committerCraig Jennings <c@cjennings.net>2026-05-05 03:43:25 -0500
commit77eb4f046cc90d535ba30ec8a408f52cb85da63f (patch)
tree33d89e4480c996ec9b583c9c97e5e978cfa8797a /tests
parent49459e2d74ca3b7d11917e314cd13c6cd8f3f039 (diff)
downloadorg-drill-77eb4f046cc90d535ba30ec8a408f52cb85da63f.tar.gz
org-drill-77eb4f046cc90d535ba30ec8a408f52cb85da63f.zip
fix: include child subtree in entry-empty-p search bound (upstream #13)
kqr (2019-07-22) reported that drill entries whose answer lives inside a child sub-heading were silently skipped. Their example: a question in the heading text and the answer under `** The Answer`. The function returned t (empty) for such entries, so they never got presented during drill sessions. The cause is `(outline-next-heading)` in `org-drill-entry-empty-p`. That primitive lands on the first heading at any level, including children. So the search range was metadata-end up to the child's heading line, which excluded the child's body. Bodies that lived in child sub-headings never got searched. I switched the bound to `(org-end-of-subtree t t)`, which covers the whole subtree of the current heading and degrades gracefully at the last heading in the buffer. The reporter suggested `outline-forward-same-level`, but that primitive errors at the last sibling, which would be its own regression. `org-end-of-subtree` is the canonical Emacs idiom for this kind of bound and handles end-of-buffer correctly. I added `tests/test-org-drill-entry-empty-p.el` with 6 ERT tests across Normal, Boundary (kqr's exact fixture), and edge categories. The two regression tests fail at HEAD before the fix and pass after. One semantic note worth flagging: any subtree content now counts as non-empty, including bare child headings with no body of their own. The bug report is silent on that case and I expect it to be rare in practice. If anyone reports the new behavior as a regression, the fix would be to filter heading lines out of the graphical-character search.
Diffstat (limited to 'tests')
-rw-r--r--tests/test-org-drill-entry-empty-p.el135
1 files changed, 135 insertions, 0 deletions
diff --git a/tests/test-org-drill-entry-empty-p.el b/tests/test-org-drill-entry-empty-p.el
new file mode 100644
index 0000000..047c36f
--- /dev/null
+++ b/tests/test-org-drill-entry-empty-p.el
@@ -0,0 +1,135 @@
+;;; test-org-drill-entry-empty-p.el --- Tests for org-drill-entry-empty-p -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Regression tests for `org-drill-entry-empty-p'.
+;;
+;; Upstream issue #13 (kqr, 2019-07-22) reported that drill entries
+;; whose body lives inside a child sub-heading were being skipped as
+;; empty. Example: a question stored in the heading text, with the
+;; answer inside `** The Answer'.
+;;
+;; Root cause: the function used `(outline-next-heading)' to compute
+;; the search-end bound. `outline-next-heading' lands on the next
+;; heading at any level, including a child — so the search range was
+;; metadata-end up to the child heading's start, which excluded the
+;; child's body text. The function returned t (empty) on entries
+;; that clearly had content.
+;;
+;; Fix: use `org-end-of-subtree' for the bound. That covers the
+;; whole subtree (including children) and degrades gracefully at the
+;; last heading in the buffer (where `outline-forward-same-level'
+;; would have errored).
+
+;;; Code:
+
+(require 'ert)
+(require 'org)
+(require 'org-drill)
+
+;;;; Helpers
+
+(defmacro with-org-fixture (content &rest body)
+ "Run BODY in a temp org-mode buffer containing CONTENT, point at start."
+ (declare (indent 1))
+ `(with-temp-buffer
+ (let ((org-startup-folded nil))
+ (insert ,content)
+ (org-mode)
+ (goto-char (point-min))
+ ,@body)))
+
+;;;; Normal cases
+
+(ert-deftest test-org-drill-entry-empty-p-normal-empty-entry-returns-t ()
+ "An entry with only metadata and no body is empty."
+ (with-org-fixture "* Question :drill:
+:PROPERTIES:
+:ID: abc
+:END:
+"
+ (should (org-drill-entry-empty-p))))
+
+(ert-deftest test-org-drill-entry-empty-p-normal-direct-body-returns-nil ()
+ "An entry with body text directly under the heading is not empty."
+ (with-org-fixture "* Question :drill:
+:PROPERTIES:
+:ID: abc
+:END:
+
+The answer lives right here.
+"
+ (should-not (org-drill-entry-empty-p))))
+
+;;;; Boundary / regression — issue #13
+
+(ert-deftest test-org-drill-entry-empty-p-regression-body-in-child-not-empty ()
+ "Issue #13: an entry with its answer inside a child sub-heading is not empty.
+
+This is the exact shape kqr reported. The question lives in the
+heading text and the answer lives inside `** The Answer'. Pre-fix,
+this returned t because `outline-next-heading' set the search bound
+to the child's start, excluding the child's body."
+ (with-org-fixture "* Entry question? :drill:
+SCHEDULED: <2019-04-05 Fri>
+:PROPERTIES:
+:ID: def
+:END:
+
+** The Answer
+
+Some text
+"
+ (should-not (org-drill-entry-empty-p))))
+
+(ert-deftest test-org-drill-entry-empty-p-boundary-last-entry-in-buffer ()
+ "An entry that's the last in the buffer is handled without error.
+
+`outline-forward-same-level' would error at the last heading; the fix
+uses `org-end-of-subtree' which handles end-of-buffer gracefully."
+ (with-org-fixture "* Final question :drill:
+:PROPERTIES:
+:ID: jkl
+:END:
+
+The answer.
+"
+ (should-not (org-drill-entry-empty-p))))
+
+(ert-deftest test-org-drill-entry-empty-p-boundary-followed-by-sibling ()
+ "An empty entry followed by a non-empty sibling stays empty.
+
+Sanity check that the wider search bound doesn't bleed into siblings."
+ (with-org-fixture "* First :drill:
+:PROPERTIES:
+:ID: mno
+:END:
+
+* Second :drill:
+:PROPERTIES:
+:ID: pqr
+:END:
+
+This one has body.
+"
+ (should (org-drill-entry-empty-p))))
+
+;;;; Error cases
+
+(ert-deftest test-org-drill-entry-empty-p-normal-deeply-nested-content ()
+ "Content several levels deep under the entry is still found."
+ (with-org-fixture "* Question :drill:
+:PROPERTIES:
+:ID: stu
+:END:
+
+** Section A
+*** Subsection
+**** Sub-subsection
+
+Deeply nested answer.
+"
+ (should-not (org-drill-entry-empty-p))))
+
+(provide 'test-org-drill-entry-empty-p)
+
+;;; test-org-drill-entry-empty-p.el ends here