diff options
| author | Craig Jennings <c@cjennings.net> | 2025-11-13 11:36:06 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-11-13 11:36:06 -0600 |
| commit | cf182d001f66164232b8735bad43eb07121109c6 (patch) | |
| tree | a70cc6f6d16a2c4cbb67c6850e59728158f3f383 | |
| parent | 87e74a3a6ccf5b05b760e9f8beec9a78886ab076 (diff) | |
| download | org-drill-cf182d001f66164232b8735bad43eb07121109c6.tar.gz org-drill-cf182d001f66164232b8735bad43eb07121109c6.zip | |
test: Add Phase 1 foundation tests for critical functions
- Add unit tests for org-drill-entry-p (14 tests)
- Add unit tests for org-drill-part-of-drill-entry-p (14 tests)
- Add unit tests for SM2 scheduling algorithm (23 tests)
- Add integration test for basic drill workflow (11 tests)
- Update Makefile to support test-*.el naming pattern
- Rename org-drill-test.el to test-org-drill.el for consistency
Total: 65 tests, all passing
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | tests/test-integration-drill-session-simple-workflow-integration-test.el | 348 | ||||
| -rw-r--r-- | tests/test-org-drill-determine-next-interval-sm2.el | 307 | ||||
| -rw-r--r-- | tests/test-org-drill-entry-p.el | 182 | ||||
| -rw-r--r-- | tests/test-org-drill-part-of-drill-entry-p.el | 231 | ||||
| -rw-r--r-- | tests/test-org-drill.el (renamed from tests/org-drill-test.el) | 0 |
6 files changed, 1071 insertions, 1 deletions
@@ -26,7 +26,9 @@ endif # Test directories and files TEST_DIR = tests -UNIT_TESTS = $(filter-out $(TEST_DIR)/%-integration-test.el, $(wildcard $(TEST_DIR)/*-test.el)) +# Match test-*.el pattern, excluding integration tests +UNIT_TESTS = $(filter-out $(TEST_DIR)/%integration-test.el, \ + $(wildcard $(TEST_DIR)/test-*.el)) INTEGRATION_TESTS = $(wildcard $(TEST_DIR)/*-integration-test.el) ALL_TESTS = $(UNIT_TESTS) $(INTEGRATION_TESTS) diff --git a/tests/test-integration-drill-session-simple-workflow-integration-test.el b/tests/test-integration-drill-session-simple-workflow-integration-test.el new file mode 100644 index 0000000..9e839a2 --- /dev/null +++ b/tests/test-integration-drill-session-simple-workflow-integration-test.el @@ -0,0 +1,348 @@ +;;; test-integration-drill-session-simple-workflow-integration-test.el --- Integration test for basic drill workflow + +;;; Commentary: +;; Integration test for the basic org-drill workflow, testing how +;; components work together: +;; +;; 1. Entry detection (org-drill-entry-p) +;; 2. Entry enumeration (org-drill-map-entries) +;; 3. Data retrieval (org-drill-get-item-data) +;; 4. Scheduling algorithm (org-drill-determine-next-interval-sm2) +;; 5. Data storage (org-drill-store-item-data) +;; +;; This test uses actual org-mode buffers with drill entries to verify +;; the complete workflow from entry detection through scheduling. + +;;; Code: + +(require 'ert) +(require 'assess) +(require 'org-drill) + +;;; Test Data + +(defconst test-integration-simple-workflow-basic-entries + "#+TITLE: Basic Drill Session Test + +* First Card :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: 4 +:DRILL_REPEATS_SINCE_FAIL: 2 +:DRILL_TOTAL_REPEATS: 5 +:DRILL_FAILURE_COUNT: 1 +:DRILL_AVERAGE_QUALITY: 3.8 +:DRILL_EASE: 2.5 +:END: + +Question: What is 2+2? + +Answer: 4 + +* Second Card :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: 10 +:DRILL_REPEATS_SINCE_FAIL: 4 +:DRILL_TOTAL_REPEATS: 4 +:DRILL_FAILURE_COUNT: 0 +:DRILL_AVERAGE_QUALITY: 4.5 +:DRILL_EASE: 2.6 +:END: + +Question: What is the capital of France? + +Answer: Paris + +* Not a drill card + +This heading has no drill tag, so it should be ignored. + +* Third Card :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: 1 +:DRILL_REPEATS_SINCE_FAIL: 1 +:DRILL_TOTAL_REPEATS: 3 +:DRILL_FAILURE_COUNT: 2 +:DRILL_AVERAGE_QUALITY: 2.3 +:DRILL_EASE: 2.0 +:END: + +Question: What is the largest planet? + +Answer: Jupiter +" + "Basic drill entries with varying scheduling data.") + +(defconst test-integration-simple-workflow-new-entries + "#+TITLE: New Drill Entries Test + +* Brand New Card :drill: + +Question: What is Emacs? + +Answer: A powerful text editor. + +* Another New Card :drill: + +Question: What is org-mode? + +Answer: An Emacs mode for note-taking and organization. +" + "New drill entries with no scheduling data.") + +;;; Helper Functions + +(defun test-integration-simple-workflow--with-drill-buffer (content callback) + "Execute CALLBACK in temporary org-mode buffer with drill CONTENT." + (with-temp-buffer + (org-mode) + (insert content) + (goto-char (point-min)) + (funcall callback))) + +(defun test-integration-simple-workflow--count-drill-entries (content) + "Count number of drill entries in CONTENT." + (test-integration-simple-workflow--with-drill-buffer + content + (lambda () + (length (org-drill-map-entries (lambda () (point)) 'file nil))))) + +;;; Normal Cases - Entry Detection + +(ert-deftest test-integration-simple-workflow-normal-detect-multiple-entries () + "Test that multiple drill entries are detected in a file. +Should find exactly 3 drill entries, ignoring non-drill headings." + (let ((count (test-integration-simple-workflow--count-drill-entries + test-integration-simple-workflow-basic-entries))) + (should (= count 3)))) + +(ert-deftest test-integration-simple-workflow-normal-detect-new-entries () + "Test detection of new drill entries without scheduling data." + (let ((count (test-integration-simple-workflow--count-drill-entries + test-integration-simple-workflow-new-entries))) + (should (= count 2)))) + +;;; Normal Cases - Entry Data Retrieval + +(ert-deftest test-integration-simple-workflow-normal-retrieve-entry-data () + "Test retrieving scheduling data from drill entry. +Should correctly parse all DRILL_* properties." + (test-integration-simple-workflow--with-drill-buffer + test-integration-simple-workflow-basic-entries + (lambda () + ;; Find first drill entry + (re-search-forward "^\\* First Card :drill:") + (beginning-of-line) + (let ((data (org-drill-get-item-data))) + ;; Verify structure: (last-interval repeats failures total-repeats meanq ease) + (should (listp data)) + (should (= (length data) 6)) + ;; Verify values match properties + (should (= (nth 0 data) 4)) ; DRILL_LAST_INTERVAL + (should (= (nth 1 data) 2)) ; DRILL_REPEATS_SINCE_FAIL + (should (= (nth 2 data) 1)) ; DRILL_FAILURE_COUNT + (should (= (nth 3 data) 5)) ; DRILL_TOTAL_REPEATS + (should (= (nth 4 data) 3.8)) ; DRILL_AVERAGE_QUALITY + (should (= (nth 5 data) 2.5)) ; DRILL_EASE + )))) + +(ert-deftest test-integration-simple-workflow-normal-retrieve-new-entry-data () + "Test retrieving data from new entry with no properties. +Should return default values for new items." + (test-integration-simple-workflow--with-drill-buffer + test-integration-simple-workflow-new-entries + (lambda () + ;; Find first drill entry + (re-search-forward "^\\* Brand New Card :drill:") + (beginning-of-line) + (let ((data (org-drill-get-item-data))) + ;; Verify structure exists + (should (listp data)) + (should (= (length data) 6)) + ;; New items should have sensible defaults (or nil) + (should (or (null (nth 0 data)) (numberp (nth 0 data)))) ; last-interval + (should (numberp (nth 1 data))) ; repeats + (should (numberp (nth 2 data))) ; failures + (should (numberp (nth 3 data))) ; total-repeats + ;; meanq (nth 4) can be nil for new items + ;; ease (nth 5) can be nil for new items + )))) + +;;; Normal Cases - Scheduling Algorithm Integration + +(ert-deftest test-integration-simple-workflow-normal-schedule-from-entry-data () + "Test that entry data can be fed to scheduling algorithm. +Verifies integration between data retrieval and SM2 algorithm." + (test-integration-simple-workflow--with-drill-buffer + test-integration-simple-workflow-basic-entries + (lambda () + ;; Find second drill entry (good performance history) + (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) + ;; Simulate quality rating of 4 (good recall) + (let* ((quality 4) + (result (org-drill-determine-next-interval-sm2 + last-interval repeats ease quality + failures meanq total-repeats)) + (next-interval (nth 0 result)) + (new-repeats (nth 1 result)) + (new-ease (nth 2 result))) + ;; Verify scheduling result makes sense + (should (> next-interval last-interval)) ; Interval should increase + (should (= new-repeats (1+ repeats))) ; Repeats incremented + (should (numberp new-ease)) ; EF calculated + ))))) + +(ert-deftest test-integration-simple-workflow-normal-schedule-failed-recall () + "Test scheduling when card is failed. +Verifies that failure handling works correctly in integrated workflow." + (test-integration-simple-workflow--with-drill-buffer + test-integration-simple-workflow-basic-entries + (lambda () + ;; Find third drill entry (struggling item) + (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) + ;; Simulate complete failure (quality 0) + (let* ((quality 0) + (result (org-drill-determine-next-interval-sm2 + last-interval repeats ease quality + failures meanq total-repeats)) + (next-interval (nth 0 result)) + (new-repeats (nth 1 result)) + (new-failures (nth 3 result))) + ;; Verify failure handling + (should (= next-interval -1)) ; Failed cards get -1 + (should (= new-repeats 1)) ; Repeats reset to 1 + (should (= new-failures (1+ failures)))))))) ; Failure count incremented + +;;; Normal Cases - Data Storage Integration + +(ert-deftest test-integration-simple-workflow-normal-store-item-data () + "Test that scheduling results can be stored back to entry. +Verifies org-drill-store-item-data updates properties correctly." + (test-integration-simple-workflow--with-drill-buffer + test-integration-simple-workflow-basic-entries + (lambda () + ;; Find first drill entry + (re-search-forward "^\\* First Card :drill:") + (beginning-of-line) + + ;; Get original data + (cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease) + (org-drill-get-item-data) + + ;; Calculate new scheduling data + (let* ((quality 5) ; Perfect recall + (result (org-drill-determine-next-interval-sm2 + last-interval repeats ease quality + failures meanq total-repeats)) + (next-interval (nth 0 result)) + (new-repeats (nth 1 result)) + (new-ease (nth 2 result)) + (new-failures (nth 3 result)) + (new-meanq (nth 4 result)) + (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) + + ;; Verify data was stored (properties exist and are valid) + (should (org-entry-get (point) "DRILL_LAST_INTERVAL")) + (should (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL")) + (should (org-entry-get (point) "DRILL_FAILURE_COUNT")) + (should (org-entry-get (point) "DRILL_TOTAL_REPEATS")) + (should (org-entry-get (point) "DRILL_AVERAGE_QUALITY")) + (should (org-entry-get (point) "DRILL_EASE")) + + ;; Verify values are numeric and match expectations + (should (= (string-to-number (org-entry-get (point) "DRILL_REPEATS_SINCE_FAIL")) + new-repeats)) + (should (= (string-to-number (org-entry-get (point) "DRILL_FAILURE_COUNT")) + new-failures)) + (should (= (string-to-number (org-entry-get (point) "DRILL_TOTAL_REPEATS")) + new-total)) + ))))) + +;;; Boundary Cases - Edge Conditions + +(ert-deftest test-integration-simple-workflow-boundary-empty-file () + "Test drill entry detection in empty file. +Should handle empty files gracefully." + (let ((count (test-integration-simple-workflow--count-drill-entries ""))) + (should (= count 0)))) + +(ert-deftest test-integration-simple-workflow-boundary-no-drill-entries () + "Test file with headings but no drill tags." + (let* ((content "* Heading One\n\nContent.\n\n* Heading Two\n\nMore content.\n") + (count (test-integration-simple-workflow--count-drill-entries content))) + (should (= count 0)))) + +(ert-deftest test-integration-simple-workflow-boundary-mixed-drill-and-non-drill () + "Test that drill and non-drill entries are correctly distinguished." + (test-integration-simple-workflow--with-drill-buffer + test-integration-simple-workflow-basic-entries + (lambda () + (let ((drill-count 0) + (non-drill-count 0)) + ;; Count drill entries + (goto-char (point-min)) + (while (re-search-forward "^\\* " nil t) + (beginning-of-line) + (if (org-drill-entry-p) + (cl-incf drill-count) + (cl-incf non-drill-count)) + (forward-line)) + (should (= drill-count 3)) + (should (= non-drill-count 1)))))) + +;;; Integration - Complete Workflow Simulation + +(ert-deftest test-integration-simple-workflow-integration-complete-review-cycle () + "Test complete review cycle: detect -> retrieve -> schedule -> store. +Simulates reviewing a card and verifies all components work together." + (test-integration-simple-workflow--with-drill-buffer + test-integration-simple-workflow-basic-entries + (lambda () + ;; Step 1: Detect drill entries + (let ((entries (org-drill-map-entries (lambda () (point)) 'file nil))) + (should (= (length entries) 3)) + + ;; Step 2: Navigate to first entry + (goto-char (car entries)) + (should (org-drill-entry-p)) + + ;; Step 3: Retrieve current data + (cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease) + (org-drill-get-item-data) + (should (numberp last-interval)) + (should (numberp repeats)) + + ;; Step 4: Simulate review with quality 4 + (let* ((quality 4) + (result (org-drill-determine-next-interval-sm2 + last-interval repeats ease quality + failures meanq total-repeats)) + (next-interval (nth 0 result)) + (new-repeats (nth 1 result)) + (new-ease (nth 2 result)) + (new-failures (nth 3 result)) + (new-meanq (nth 4 result)) + (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) + + ;; Step 6: Verify data persisted + (let ((retrieved-data (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)) + (should (= (nth 3 retrieved-data) new-total))))))))) + +(provide 'test-integration-drill-session-simple-workflow-integration-test) +;;; test-integration-drill-session-simple-workflow-integration-test.el ends here diff --git a/tests/test-org-drill-determine-next-interval-sm2.el b/tests/test-org-drill-determine-next-interval-sm2.el new file mode 100644 index 0000000..abb7a0b --- /dev/null +++ b/tests/test-org-drill-determine-next-interval-sm2.el @@ -0,0 +1,307 @@ +;;; test-org-drill-determine-next-interval-sm2.el --- Tests for SM2 algorithm + +;;; Commentary: +;; Unit tests for org-drill-determine-next-interval-sm2, the SuperMemo 2 (SM2) +;; spaced repetition algorithm implementation. +;; +;; The SM2 algorithm calculates the next review interval based on: +;; - Last interval (days since last review) +;; - Number of successful repeats (n) +;; - Easiness factor (EF) - modified by recall quality +;; - Quality of recall (0-5 scale) +;; - Failure count and statistics +;; +;; Function signature: +;; (org-drill-determine-next-interval-sm2 last-interval n ef quality +;; failures meanq total-repeats) +;; +;; Returns: (INTERVAL REPEATS EF FAILURES MEAN TOTAL-REPEATS OFMATRIX) + +;;; Code: + +(require 'ert) +(require 'assess) +(require 'org-drill) + +;;; Test Data and Constants + +;; Default SM2 parameters +(defconst test-sm2-default-ef 2.5 + "Default easiness factor for new items.") + +(defconst test-sm2-min-ef 1.3 + "Minimum easiness factor (SM2 floor).") + +;;; Helper Functions + +(defun test-sm2--extract-interval (result) + "Extract interval from SM2 result list." + (nth 0 result)) + +(defun test-sm2--extract-repeats (result) + "Extract repeats from SM2 result list." + (nth 1 result)) + +(defun test-sm2--extract-ef (result) + "Extract easiness factor from SM2 result list." + (nth 2 result)) + +(defun test-sm2--extract-failures (result) + "Extract failures from SM2 result list." + (nth 3 result)) + +(defun test-sm2--extract-meanq (result) + "Extract mean quality from SM2 result list." + (nth 4 result)) + +(defun test-sm2--extract-total-repeats (result) + "Extract total repeats from SM2 result list." + (nth 5 result)) + +;;; Normal Cases - Successful Reviews + +(ert-deftest test-org-drill-determine-next-interval-sm2-normal-first-review-quality-4 () + "Test first successful review with quality 4. +First review (n=1) should return interval of 1 day." + (let* ((result (org-drill-determine-next-interval-sm2 0 1 nil 4 0 nil 0)) + (interval (test-sm2--extract-interval result)) + (repeats (test-sm2--extract-repeats result)) + (ef (test-sm2--extract-ef result))) + (should (= interval 1)) + (should (= repeats 2)) + (should ef))) ; EF should be calculated + +(ert-deftest test-org-drill-determine-next-interval-sm2-normal-second-review-quality-4 () + "Test second successful review with quality 4. +Second review (n=2) should return interval of 6 days (no random noise)." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((result (org-drill-determine-next-interval-sm2 1 2 2.5 4 0 nil 0)) + (interval (test-sm2--extract-interval result)) + (repeats (test-sm2--extract-repeats result))) + (should (= interval 6)) + (should (= repeats 3))))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-normal-third-review-quality-4 () + "Test third successful review with quality 4. +Third review (n=3) uses formula: last-interval * EF." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((ef 2.5) + (last-interval 6) + (result (org-drill-determine-next-interval-sm2 last-interval 3 ef 4 0 nil 0)) + (interval (test-sm2--extract-interval result)) + (new-ef (test-sm2--extract-ef result))) + ;; Interval should be approximately last-interval * new-ef + (should (> interval last-interval)) + (should (= (test-sm2--extract-repeats result) 4))))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-normal-quality-5-perfect-recall () + "Test review with perfect recall (quality 5). +Quality 5 should increase EF and result in longer intervals." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((result (org-drill-determine-next-interval-sm2 10 3 2.5 5 0 nil 0)) + (ef (test-sm2--extract-ef result))) + ;; Quality 5 should maintain or increase EF + (should (>= ef 2.5))))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-normal-quality-3-adequate-recall () + "Test review with adequate recall (quality 3). +Quality 3 should maintain relatively stable EF." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((result (org-drill-determine-next-interval-sm2 10 3 2.5 3 0 nil 0)) + (ef (test-sm2--extract-ef result))) + ;; Quality 3 should result in moderate EF + (should (> ef test-sm2-min-ef)) + (should (< ef 2.5))))) ; EF likely decreases slightly + +;;; Normal Cases - Failed Reviews + +(ert-deftest test-org-drill-determine-next-interval-sm2-normal-failure-quality-0 () + "Test failed review with quality 0. +Quality 0 (<= org-drill-failure-quality) should reset interval to -1." + (let* ((result (org-drill-determine-next-interval-sm2 10 3 2.5 0 0 nil 0)) + (interval (test-sm2--extract-interval result)) + (repeats (test-sm2--extract-repeats result)) + (ef (test-sm2--extract-ef result)) + (failures (test-sm2--extract-failures result))) + (should (= interval -1)) + (should (= repeats 1)) ; Reset to 1 + (should (= ef 2.5)) ; EF unchanged on failure + (should (= failures 1)))) ; Failure count incremented + +(ert-deftest test-org-drill-determine-next-interval-sm2-normal-failure-quality-1 () + "Test failed review with quality 1. +Quality 1 (<= org-drill-failure-quality) should reset interval." + (let* ((result (org-drill-determine-next-interval-sm2 15 5 2.6 1 2 3.5 10)) + (interval (test-sm2--extract-interval result)) + (ef (test-sm2--extract-ef result)) + (failures (test-sm2--extract-failures result))) + (should (= interval -1)) + (should (= ef 2.6)) ; EF unchanged + (should (= failures 3)))) ; Previous failures + 1 + +(ert-deftest test-org-drill-determine-next-interval-sm2-normal-failure-quality-2 () + "Test failed review with quality 2. +Quality 2 (= org-drill-failure-quality default) should reset interval." + (let* ((result (org-drill-determine-next-interval-sm2 10 3 2.5 2 0 nil 0)) + (interval (test-sm2--extract-interval result))) + (should (= interval -1)))) + +;;; Boundary Cases - Default Values + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-nil-ef-uses-default () + "Test that nil EF defaults to 2.5." + (let* ((result (org-drill-determine-next-interval-sm2 0 1 nil 4 0 nil 0)) + (ef (test-sm2--extract-ef result))) + (should ef) ; EF should be set (modified from default 2.5) + (should (> ef 0)))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-zero-n-becomes-one () + "Test that n=0 is treated as n=1." + (let* ((result (org-drill-determine-next-interval-sm2 0 0 2.5 4 0 nil 0)) + (interval (test-sm2--extract-interval result)) + (repeats (test-sm2--extract-repeats result))) + (should (= interval 1)) ; First review interval + (should (= repeats 2)))) ; n incremented from 1 to 2 + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-nil-meanq-uses-quality () + "Test that nil meanq initializes to current quality." + (let* ((quality 4) + (result (org-drill-determine-next-interval-sm2 0 1 2.5 quality 0 nil 0)) + (meanq (test-sm2--extract-meanq result))) + (should (= meanq quality)))) + +;;; Boundary Cases - Quality Extremes + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-quality-5-maximum () + "Test maximum quality (5) - perfect recall." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((result (org-drill-determine-next-interval-sm2 1 2 2.5 5 0 nil 0)) + (interval (test-sm2--extract-interval result))) + (should (= interval 6))))) ; Second review with quality 5 + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-quality-0-minimum () + "Test minimum quality (0) - complete failure." + (let* ((result (org-drill-determine-next-interval-sm2 10 3 2.5 0 0 nil 0)) + (interval (test-sm2--extract-interval result))) + (should (= interval -1)))) ; Failed review + +;;; Boundary Cases - Repeat Count + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-n-equals-1 () + "Test first successful review (n=1) returns interval of 1." + (let* ((result (org-drill-determine-next-interval-sm2 0 1 2.5 4 0 nil 0)) + (interval (test-sm2--extract-interval result))) + (should (= interval 1)))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-n-equals-2-no-noise () + "Test second review (n=2) with no random noise. +Should return interval of 6 days regardless of quality (if passing)." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((result (org-drill-determine-next-interval-sm2 1 2 2.5 4 0 nil 0)) + (interval (test-sm2--extract-interval result))) + (should (= interval 6))))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-n-equals-2-with-noise () + "Test second review (n=2) with random noise enabled. +Interval should vary by quality, with quality 5 having highest base interval (6)." + (let ((org-drill-add-random-noise-to-intervals-p t)) + (let* ((result-q5 (org-drill-determine-next-interval-sm2 1 2 2.5 5 0 nil 0)) + (result-q4 (org-drill-determine-next-interval-sm2 1 2 2.5 4 0 nil 0)) + (result-q3 (org-drill-determine-next-interval-sm2 1 2 2.5 3 0 nil 0)) + (interval-q5 (test-sm2--extract-interval result-q5)) + (interval-q4 (test-sm2--extract-interval result-q4)) + (interval-q3 (test-sm2--extract-interval result-q3))) + ;; Base intervals before noise are: Q5=6, Q4=4, Q3=3 + ;; After random noise, verify intervals are positive and roughly in expected ranges + (should (> interval-q5 0)) + (should (> interval-q4 0)) + (should (> interval-q3 0)) + ;; Quality 5 base interval (6) should generally be higher than quality 3 base (3) + ;; even with noise applied + (should (> interval-q5 2))))) + +;;; Boundary Cases - Mean Quality Calculation + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-meanq-weighted-average () + "Test that meanq is correctly calculated as weighted average. +meanq = (quality + meanq * total-repeats) / (total-repeats + 1)" + (let* ((quality 4) + (meanq 3.0) + (total-repeats 10) + (result (org-drill-determine-next-interval-sm2 10 3 2.5 quality 0 meanq total-repeats)) + (new-meanq (test-sm2--extract-meanq result)) + (expected-meanq (/ (+ quality (* meanq total-repeats 1.0)) + (1+ total-repeats)))) + (should (< (abs (- new-meanq expected-meanq)) 0.0001)))) ; Floating point comparison + +;;; Boundary Cases - EF Modification + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-ef-minimum-floor () + "Test that EF floor of 1.3 is applied to input EF when it's below 1.3. +If input EF < 1.3, org-drill-modify-e-factor returns 1.3 exactly." + (let* ((result (org-drill-determine-next-interval-sm2 10 3 1.0 4 0 nil 0)) + (ef (test-sm2--extract-ef result))) + ;; Input EF was 1.0 < 1.3, so it should be raised to 1.3 first, + ;; then modified based on quality 4 (which increases EF) + (should (>= ef 1.3)))) + +;;; Boundary Cases - Total Repeats + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-total-repeats-increments () + "Test that total-repeats is always incremented by 1." + (let* ((total-repeats 42) + (result (org-drill-determine-next-interval-sm2 10 3 2.5 4 0 3.5 total-repeats)) + (new-total (test-sm2--extract-total-repeats result))) + (should (= new-total (1+ total-repeats))))) + +;;; Return Value Structure + +(ert-deftest test-org-drill-determine-next-interval-sm2-normal-return-value-structure () + "Test that return value has correct structure. +Should return 7-element list: (INTERVAL REPEATS EF FAILURES MEAN TOTAL-REPEATS OFMATRIX)" + (let ((result (org-drill-determine-next-interval-sm2 10 3 2.5 4 0 3.5 10))) + (should (listp result)) + (should (= (length result) 7)) + ;; Verify first 6 elements are numbers + (should (numberp (nth 0 result))) ; interval + (should (numberp (nth 1 result))) ; repeats + (should (numberp (nth 2 result))) ; ef + (should (numberp (nth 3 result))) ; failures + (should (numberp (nth 4 result))) ; meanq + (should (numberp (nth 5 result))) ; total-repeats + ;; 7th element (index 6) is OFMATRIX - can be nil or a matrix + ;; Just verify list has 7 elements (already checked above) + )) + +;;; Algorithm Verification + +(ert-deftest test-org-drill-determine-next-interval-sm2-algorithm-ef-increases-with-quality () + "Test that higher quality results in higher EF (or maintains it). +Quality 5 should result in EF >= initial EF." + (let* ((initial-ef 2.5) + (result (org-drill-determine-next-interval-sm2 10 3 initial-ef 5 0 nil 0)) + (new-ef (test-sm2--extract-ef result))) + (should (>= new-ef initial-ef)))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-algorithm-ef-decreases-with-low-quality () + "Test that low quality results in decreased EF. +Quality 3 should result in EF < initial EF." + (let* ((initial-ef 2.5) + (result (org-drill-determine-next-interval-sm2 10 3 initial-ef 3 0 nil 0)) + (new-ef (test-sm2--extract-ef result))) + (should (< new-ef initial-ef)))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-algorithm-interval-grows-exponentially () + "Test that intervals grow approximately exponentially for consistent quality. +Third review interval should be significantly larger than second." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((ef 2.5) + (result-2nd (org-drill-determine-next-interval-sm2 1 2 ef 4 0 nil 0)) + (interval-2nd (test-sm2--extract-interval result-2nd)) + (ef-2nd (test-sm2--extract-ef result-2nd)) + (result-3rd (org-drill-determine-next-interval-sm2 interval-2nd 3 ef-2nd 4 0 nil 1)) + (interval-3rd (test-sm2--extract-interval result-3rd))) + (should (> interval-3rd (* interval-2nd 1.5)))))) ; Significant growth + +(provide 'test-org-drill-determine-next-interval-sm2) +;;; test-org-drill-determine-next-interval-sm2.el ends here diff --git a/tests/test-org-drill-entry-p.el b/tests/test-org-drill-entry-p.el new file mode 100644 index 0000000..c298c94 --- /dev/null +++ b/tests/test-org-drill-entry-p.el @@ -0,0 +1,182 @@ +;;; test-org-drill-entry-p.el --- Tests for org-drill-entry-p function + +;;; Commentary: +;; Unit tests for org-drill-entry-p, which determines if point is at a +;; drill entry heading (not a subheading within a drill entry). +;; +;; The function checks for the drill tag at the current heading only, +;; not inherited tags. Use org-drill-part-of-drill-entry-p for inherited tags. + +;;; Code: + +(require 'ert) +(require 'assess) +(require 'org-drill) + +;;; Test Data + +(defconst test-org-drill-entry-p-simple-entry + "* Heading with drill tag :drill: + +Body content here." + "Simple drill entry with drill tag on main heading.") + +(defconst test-org-drill-entry-p-no-tag + "* Heading without drill tag + +Body content here." + "Heading without any drill tag.") + +(defconst test-org-drill-entry-p-nested-entry + "* Parent heading :drill: + +Parent content. + +** Child heading + +Child content." + "Drill entry with nested child heading.") + +(defconst test-org-drill-entry-p-multiple-tags + "* Heading :drill:important:review: + +Body with multiple tags." + "Drill entry with multiple tags including drill.") + +(defconst test-org-drill-entry-p-custom-tag + "* Heading :customtag: + +Body with custom tag." + "Heading with custom tag (not drill).") + +;;; Helper Functions + +(defun test-org-drill-entry-p--with-org-buffer (content callback) + "Execute CALLBACK in temporary org-mode buffer with CONTENT." + (with-temp-buffer + (org-mode) + (insert content) + (goto-char (point-min)) + (funcall callback))) + +;;; Normal Cases + +(ert-deftest test-org-drill-entry-p-normal-valid-tag-returns-true () + "Test that heading with drill tag is identified as drill entry." + (test-org-drill-entry-p--with-org-buffer + test-org-drill-entry-p-simple-entry + (lambda () + (should (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-normal-no-tag-returns-nil () + "Test that heading without drill tag returns nil." + (test-org-drill-entry-p--with-org-buffer + test-org-drill-entry-p-no-tag + (lambda () + (should-not (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-normal-multiple-tags-returns-true () + "Test that heading with multiple tags including drill is identified." + (test-org-drill-entry-p--with-org-buffer + test-org-drill-entry-p-multiple-tags + (lambda () + (should (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-normal-at-parent-heading-returns-true () + "Test that function works when point is at parent drill heading." + (test-org-drill-entry-p--with-org-buffer + test-org-drill-entry-p-nested-entry + (lambda () + ;; Point should be at parent heading after goto-char (point-min) + (should (org-drill-entry-p))))) + +;;; Boundary Cases + +(ert-deftest test-org-drill-entry-p-boundary-at-child-heading-returns-nil () + "Test that child heading of drill entry returns nil. +Only the heading with the actual drill tag should return true." + (test-org-drill-entry-p--with-org-buffer + test-org-drill-entry-p-nested-entry + (lambda () + ;; Move to child heading + (re-search-forward "^\\*\\* Child heading") + (beginning-of-line) + (should-not (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-boundary-empty-heading-no-tag-returns-nil () + "Test that empty heading without tags returns nil." + (test-org-drill-entry-p--with-org-buffer + "* Empty heading\n" + (lambda () + (should-not (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-boundary-heading-with-only-whitespace-tag () + "Test heading with whitespace around tags." + (test-org-drill-entry-p--with-org-buffer + "* Heading :drill: \n\nContent" + (lambda () + (should (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-boundary-custom-tag-not-drill () + "Test that custom tag (not drill) returns nil." + (test-org-drill-entry-p--with-org-buffer + test-org-drill-entry-p-custom-tag + (lambda () + (should-not (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-boundary-drill-substring-in-other-tag () + "Test that drill as substring of another tag does not match. +Tag 'drilling' should not be identified as drill entry." + (test-org-drill-entry-p--with-org-buffer + "* Heading :drilling:\n\nContent" + (lambda () + (should-not (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-boundary-case-sensitive-tag () + "Test that drill tag matching is case-sensitive. +Tag 'DRILL' should not match 'drill'." + (test-org-drill-entry-p--with-org-buffer + "* Heading :DRILL:\n\nContent" + (lambda () + ;; org-mode tags are case-sensitive by default + (should-not (org-drill-entry-p))))) + +;;; Error Cases + +(ert-deftest test-org-drill-entry-p-error-point-in-body-text () + "Test behavior when point is in body text, not at heading. +Function should check current heading context." + (test-org-drill-entry-p--with-org-buffer + test-org-drill-entry-p-simple-entry + (lambda () + ;; Move to body text + (re-search-forward "Body content") + (beginning-of-line) + ;; Even though we're in a drill entry's body, org-drill-entry-p + ;; checks the current heading when point is in body + (should (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-error-empty-buffer () + "Test behavior in empty buffer. +Should not error, just return nil." + (with-temp-buffer + (org-mode) + (should-not (org-drill-entry-p)))) + +(ert-deftest test-org-drill-entry-p-error-buffer-without-headings () + "Test behavior in buffer with only text, no headings." + (test-org-drill-entry-p--with-org-buffer + "Just some text without any headings." + (lambda () + (should-not (org-drill-entry-p))))) + +(ert-deftest test-org-drill-entry-p-error-malformed-heading () + "Test behavior with malformed heading syntax. +Single asterisk should still work as heading." + (test-org-drill-entry-p--with-org-buffer + "* Malformed :drill:\nNo blank line before content" + (lambda () + (should (org-drill-entry-p))))) + +(provide 'test-org-drill-entry-p) +;;; test-org-drill-entry-p.el ends here diff --git a/tests/test-org-drill-part-of-drill-entry-p.el b/tests/test-org-drill-part-of-drill-entry-p.el new file mode 100644 index 0000000..c9a09b8 --- /dev/null +++ b/tests/test-org-drill-part-of-drill-entry-p.el @@ -0,0 +1,231 @@ +;;; test-org-drill-part-of-drill-entry-p.el --- Tests for org-drill-part-of-drill-entry-p + +;;; Commentary: +;; Unit tests for org-drill-part-of-drill-entry-p, which determines if point +;; is at a drill entry heading OR within a subheading that inherits the drill tag. +;; +;; This function differs from org-drill-entry-p in that it checks BOTH: +;; 1. Direct drill tag on current heading (via org-drill-entry-p) +;; 2. Inherited drill tag from parent headings (via org-get-tags) + +;;; Code: + +(require 'ert) +(require 'assess) +(require 'org-drill) + +;;; Test Data + +(defconst test-org-drill-part-of-drill-entry-p-parent-drill + "* Parent heading :drill: + +Parent content. + +** Child heading + +Child content. + +*** Grandchild heading + +Grandchild content." + "Drill entry with nested children that inherit the tag.") + +(defconst test-org-drill-part-of-drill-entry-p-no-drill + "* Parent heading + +Parent content. + +** Child heading + +Child content." + "Headings without drill tag.") + +(defconst test-org-drill-part-of-drill-entry-p-child-has-drill + "* Parent heading + +Parent content. + +** Child heading :drill: + +Child content." + "Child has drill tag, parent doesn't.") + +(defconst test-org-drill-part-of-drill-entry-p-deep-nesting + "* Level 1 :drill: + +Content 1. + +** Level 2 + +Content 2. + +*** Level 3 + +Content 3. + +**** Level 4 + +Content 4." + "Deeply nested structure with drill tag on top level.") + +;;; Helper Functions + +(defun test-org-drill-part-of-drill-entry-p--with-org-buffer (content callback) + "Execute CALLBACK in temporary org-mode buffer with CONTENT." + (with-temp-buffer + (org-mode) + (insert content) + (goto-char (point-min)) + (funcall callback))) + +;;; Normal Cases + +(ert-deftest test-org-drill-part-of-drill-entry-p-normal-at-parent-returns-true () + "Test that function returns true when at parent heading with drill tag." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-parent-drill + (lambda () + ;; Point at parent heading + (should (org-drill-part-of-drill-entry-p))))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-normal-at-child-returns-true () + "Test that function returns true when at child heading that inherits drill tag." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-parent-drill + (lambda () + ;; Move to child heading + (re-search-forward "^\\*\\* Child heading") + (beginning-of-line) + (should (org-drill-part-of-drill-entry-p))))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-normal-no-drill-returns-nil () + "Test that function returns nil when no drill tag present or inherited." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-no-drill + (lambda () + (should-not (org-drill-part-of-drill-entry-p))))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-normal-child-not-parent-has-drill () + "Test when child has drill tag but parent doesn't. +Child heading itself should return true." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-child-has-drill + (lambda () + ;; Move to child heading with drill tag + (re-search-forward "^\\*\\* Child heading") + (beginning-of-line) + (should (org-drill-part-of-drill-entry-p))))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-normal-parent-no-drill-returns-nil () + "Test that parent without drill tag returns nil." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-child-has-drill + (lambda () + ;; At parent heading (no drill tag) + (should-not (org-drill-part-of-drill-entry-p))))) + +;;; Boundary Cases + +(ert-deftest test-org-drill-part-of-drill-entry-p-boundary-grandchild-inherits () + "Test that grandchild heading inherits drill tag from grandparent." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-parent-drill + (lambda () + ;; Move to grandchild heading + (re-search-forward "^\\*\\*\\* Grandchild heading") + (beginning-of-line) + (should (org-drill-part-of-drill-entry-p))))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-boundary-deep-nesting-all-inherit () + "Test that deeply nested headings all inherit drill tag. +All levels 2, 3, and 4 should return true." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-deep-nesting + (lambda () + ;; Test level 2 + (re-search-forward "^\\*\\* Level 2") + (beginning-of-line) + (should (org-drill-part-of-drill-entry-p)) + + ;; Test level 3 + (re-search-forward "^\\*\\*\\* Level 3") + (beginning-of-line) + (should (org-drill-part-of-drill-entry-p)) + + ;; Test level 4 + (re-search-forward "^\\*\\*\\*\\* Level 4") + (beginning-of-line) + (should (org-drill-part-of-drill-entry-p))))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-boundary-point-in-child-body () + "Test behavior when point is in child body text. +Should still detect inherited drill tag." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-parent-drill + (lambda () + ;; Move to child body content + (re-search-forward "Child content") + (beginning-of-line) + (should (org-drill-part-of-drill-entry-p))))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-boundary-empty-child-heading () + "Test child heading with no content still inherits drill tag." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + "* Parent :drill:\n** Child\n" + (lambda () + (re-search-forward "^\\*\\* Child") + (beginning-of-line) + (should (org-drill-part-of-drill-entry-p))))) + +;;; Error Cases + +(ert-deftest test-org-drill-part-of-drill-entry-p-error-empty-buffer () + "Test behavior in empty buffer. +Should not error, just return nil." + (with-temp-buffer + (org-mode) + (should-not (org-drill-part-of-drill-entry-p)))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-error-no-headings () + "Test behavior in buffer with only text, no headings." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + "Just some text without any headings." + (lambda () + (should-not (org-drill-part-of-drill-entry-p))))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-error-point-before-first-heading () + "Test behavior when point is before any headings." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + "Some preamble text\n\n* Heading :drill:\n\nContent" + (lambda () + ;; Point at preamble + (goto-char (point-min)) + (should-not (org-drill-part-of-drill-entry-p))))) + +;;; Comparison Tests (org-drill-entry-p vs org-drill-part-of-drill-entry-p) + +(ert-deftest test-org-drill-part-of-drill-entry-p-comparison-child-differs () + "Test that org-drill-entry-p and org-drill-part-of-drill-entry-p differ at child. +At child heading: entry-p returns nil, part-of returns true." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-parent-drill + (lambda () + ;; Move to child heading + (re-search-forward "^\\*\\* Child heading") + (beginning-of-line) + ;; org-drill-entry-p should be nil (no direct tag) + (should-not (org-drill-entry-p)) + ;; org-drill-part-of-drill-entry-p should be true (inherited tag) + (should (org-drill-part-of-drill-entry-p))))) + +(ert-deftest test-org-drill-part-of-drill-entry-p-comparison-parent-same () + "Test that both functions return true at parent heading with drill tag." + (test-org-drill-part-of-drill-entry-p--with-org-buffer + test-org-drill-part-of-drill-entry-p-parent-drill + (lambda () + ;; At parent heading + (should (org-drill-entry-p)) + (should (org-drill-part-of-drill-entry-p))))) + +(provide 'test-org-drill-part-of-drill-entry-p) +;;; test-org-drill-part-of-drill-entry-p.el ends here diff --git a/tests/org-drill-test.el b/tests/test-org-drill.el index 4765a99..4765a99 100644 --- a/tests/org-drill-test.el +++ b/tests/test-org-drill.el |
