diff options
| author | Craig Jennings <c@cjennings.net> | 2025-11-13 13:16:44 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-11-13 13:16:44 -0600 |
| commit | a064c5d7659d84393fd8f586bb1c7d18a5436b38 (patch) | |
| tree | 58ce7ac137226bc6f40a2a359b138ee58cbb33db | |
| parent | c4c64e2762d7b75f588c20bc7d43ddf8ba30c97e (diff) | |
| download | org-drill-a064c5d7659d84393fd8f586bb1c7d18a5436b38.tar.gz org-drill-a064c5d7659d84393fd8f586bb1c7d18a5436b38.zip | |
test: Add boundary, error, and edge case tests
Added 66 comprehensive tests covering:
- Entry detection with extreme values and Unicode
- SM2 algorithm with boundary conditions
- Workflow error handling with malformed data
- Card types with complex content structures
| -rw-r--r-- | tests/test-card-type-simple.el | 247 | ||||
| -rw-r--r-- | tests/test-card-type-twosided.el | 345 | ||||
| -rw-r--r-- | tests/test-integration-drill-session-simple-workflow-integration-test.el | 268 | ||||
| -rw-r--r-- | tests/test-org-drill-determine-next-interval-sm2.el | 136 | ||||
| -rw-r--r-- | tests/test-org-drill-entry-p.el | 86 |
5 files changed, 1082 insertions, 0 deletions
diff --git a/tests/test-card-type-simple.el b/tests/test-card-type-simple.el index 5770883..17e4d07 100644 --- a/tests/test-card-type-simple.el +++ b/tests/test-card-type-simple.el @@ -209,5 +209,252 @@ Card should still be valid, answer would come from DRILL_ANSWER property." (should (eq default-fn explicit-fn)) (should (eq default-fn 'org-drill-present-simple-card)))) +;;; Aggressive Edge Cases + +(ert-deftest test-card-type-simple-edge-unicode-in-question-and-answer () + "Test simple card with Unicode characters in question and answer. +Unicode should be preserved correctly." + (let ((content "* Unicode Card :drill: + +Question: What is 北京 (Beijing) in Japanese? + +** Answer + +東京 (Tokyo) is different. 北京 is Běijīng in Chinese. +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-special-chars-in-content () + "Test simple card with special characters (@#$%^&*) in content." + (let ((content "* Special Chars Card :drill: + +Question: What does @#$%^&*() mean? + +** Answer + +These are special characters used in programming: @#$%^&*() +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-deeply-nested-answer-subheadings () + "Test simple card with very deeply nested answer structure. +Should handle deep nesting without issues." + (let ((content "* Nested Answers :drill: + +Question text. + +** Level 2 Answer +*** Level 3 +**** Level 4 +***** Level 5 +****** Level 6 + +Deep answer content. +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-answer-with-lists () + "Test simple card with lists in answer. +Org-mode lists should be preserved in answer." + (let ((content "* Card with List Answer :drill: + +Question: List three primary colors? + +** Answer + +- Red +- Blue +- Yellow +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-answer-with-table () + "Test simple card with org-mode table in answer." + (let ((content "* Card with Table :drill: + +Question: Show conversion table. + +** Answer + +| Celsius | Fahrenheit | +|---------+------------| +| 0 | 32 | +| 100 | 212 | +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-answer-with-code-block () + "Test simple card with source code block in answer." + (let ((content "* Card with Code :drill: + +Question: How to print in Python? + +** Answer + +#+BEGIN_SRC python +print(\"Hello, World!\") +#+END_SRC +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-both-property-and-subheading-answer () + "Test simple card with both DRILL_ANSWER property and answer subheading. +One of them should take precedence (typically property)." + (let ((content "* Dual Answer Card :drill: +:PROPERTIES: +:DRILL_ANSWER: Property answer +:END: + +Question text. + +** Answer + +Subheading answer. +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (equal "Property answer" (org-entry-get (point) "DRILL_ANSWER"))))))) + +(ert-deftest test-card-type-simple-edge-extremely-long-drill-answer-property () + "Test simple card with very long DRILL_ANSWER property. +Long property values should be handled correctly." + (let* ((long-answer (make-string 10000 ?x)) + (content (format "* Long Answer Property Card :drill: +:PROPERTIES: +:DRILL_ANSWER: %s +:END: + +Question. +" long-answer))) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)) + (let ((answer (org-entry-get (point) "DRILL_ANSWER"))) + (should answer) + (should (> (length answer) 5000))))))) + +(ert-deftest test-card-type-simple-edge-answer-subheading-with-tags () + "Test simple card where answer subheading has tags. +Tags in subheadings should not interfere with card behavior." + (let ((content "* Question Card :drill: + +Question text. + +** Answer :important:note: + +Answer content. +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-multiple-drill-answer-properties () + "Test behavior with duplicate DRILL_ANSWER properties (malformed). +Should handle gracefully, using first or last value." + (let ((content "* Duplicate Properties Card :drill: +:PROPERTIES: +:DRILL_ANSWER: First answer +:DRILL_ANSWER: Second answer +:END: + +Question. +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)) + ;; Org-mode typically uses last value for duplicate properties + (let ((answer (org-entry-get (point) "DRILL_ANSWER"))) + (should answer)))))) + +(ert-deftest test-card-type-simple-edge-answer-with-links () + "Test simple card with org-mode links in answer." + (let ((content "* Card with Links :drill: + +Question: What is Emacs? + +** Answer + +Emacs is a [[https://www.gnu.org/software/emacs/][text editor]]. +See also: [[file:manual.org][manual]]. +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-question-with-formatting () + "Test simple card with org-mode formatting in question. +Bold, italic, code, etc. should be preserved." + (let ((content "* Formatted Question :drill: + +Question: What is *bold*, /italic/, =code=, and ~verbatim~? + +** Answer + +These are org-mode text formatting options. +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-empty-answer-subheading () + "Test simple card with empty answer subheading. +Answer subheading exists but has no content." + (let ((content "* Empty Answer Card :drill: + +Question text. + +** Answer + +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-card-type-simple-edge-answer-with-drawers () + "Test simple card with drawers in answer section. +Org-mode drawers should be preserved." + (let ((content "* Card with Drawer Answer :drill: + +Question text. + +** Answer + +:LOGBOOK: +- Note taken on [2024-01-01] +:END: + +Answer content after drawer. +")) + (test-card-type-simple--with-card + content + (lambda () + (should (org-drill-entry-p)))))) + (provide 'test-card-type-simple) ;;; test-card-type-simple.el ends here diff --git a/tests/test-card-type-twosided.el b/tests/test-card-type-twosided.el index cda508f..bda4f79 100644 --- a/tests/test-card-type-twosided.el +++ b/tests/test-card-type-twosided.el @@ -214,5 +214,350 @@ Two sides for word translation pairs." (should (eq twosided-fn 'org-drill-present-two-sided-card)) (should (eq simple-fn 'org-drill-present-simple-card)))) +;;; Aggressive Edge Cases + +(ert-deftest test-card-type-twosided-edge-unicode-in-sides () + "Test two-sided card with Unicode in both sides. +Unicode characters should be preserved correctly." + (let ((content "* Unicode Card :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** English + +Hello + +** 日本語 + +こんにちは +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-very-long-side-content () + "Test two-sided card with very long content in sides. +Should handle large amounts of text without issues." + (let* ((long-text (make-string 5000 ?x)) + (content (format "* Long Sides :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Side A + +%s + +** Side B + +%s +" long-text long-text))) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-sides-with-lists () + "Test two-sided card with org-mode lists in sides." + (let ((content "* Lists Card :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Question + +What are primary colors? + +** Answer + +- Red +- Blue +- Yellow +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-sides-with-tables () + "Test two-sided card with org-mode tables in sides." + (let ((content "* Table Card :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Question + +Temperature conversion? + +** Table + +| Celsius | Fahrenheit | +|---------+------------| +| 0 | 32 | +| 100 | 212 | +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-sides-with-code-blocks () + "Test two-sided card with source code blocks in sides." + (let ((content "* Code Card :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Question + +Python hello world? + +** Code + +#+BEGIN_SRC python +print(\"Hello, World!\") +#+END_SRC +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-sides-with-links () + "Test two-sided card with org-mode links in sides." + (let ((content "* Links Card :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Term + +Emacs + +** Definition + +A [[https://www.gnu.org/software/emacs/][powerful text editor]]. +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-sides-with-formatting () + "Test two-sided card with org-mode text formatting in sides." + (let ((content "* Formatted Card :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Question + +What is *bold*, /italic/, =code=? + +** Answer + +These are *org-mode* /text/ =formatting= options. +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-sides-with-tags () + "Test two-sided card where sides have tags. +Tags on subheadings should not interfere with card." + (let ((content "* Card with Tagged Sides :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Question :important: + +What is the answer? + +** Answer :note: + +42 +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-nested-subheadings-in-sides () + "Test two-sided card with nested subheadings within sides. +Nested content should be part of the side." + (let ((content "* Nested Content :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Side A + +Content A + +*** Nested Under A + +More details. + +** Side B + +Content B +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-empty-headings () + "Test two-sided card where heading text is minimal." + (let ((content "* Minimal Headings :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** A + +Full content for A. + +** B + +Full content for B. +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-special-characters-in-sides () + "Test two-sided card with special characters in side content." + (let ((content "* Special Chars :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Question + +What are these: @#$%^&*()? + +** Answer + +Special programming characters. +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-many-extra-sides () + "Test two-sided card with many subheadings (10+). +Only first two should be used for presentation." + (let ((content "* Many Sides :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Side 1 + +Content 1 + +** Side 2 + +Content 2 + +** Side 3 + +Extra + +** Side 4 + +Extra + +** Side 5 + +Extra + +** Side 6 + +Extra + +** Side 7 + +Extra + +** Side 8 + +Extra + +** Side 9 + +Extra + +** Side 10 + +Extra +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (>= (test-card-type-twosided--count-subheadings) 10)))))) + +(ert-deftest test-card-type-twosided-edge-no-subheadings () + "Test two-sided card with no subheadings at all. +Card is invalid but should not crash." + (let ((content "* No Sides :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +Just main body, no subheadings. +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 0 (test-card-type-twosided--count-subheadings))))))) + +(ert-deftest test-card-type-twosided-edge-sides-with-drawers () + "Test two-sided card with drawers in sides. +Drawers should be preserved." + (let ((content "* Drawer Card :drill: +:PROPERTIES: +:DRILL_CARD_TYPE: twosided +:END: + +** Side A + +:LOGBOOK: +- Note taken on [2024-01-01] +:END: + +Content A + +** Side B + +Content B +")) + (test-card-type-twosided--with-card + content + (lambda () + (should (org-drill-entry-p)) + (should (= 2 (test-card-type-twosided--count-subheadings))))))) + (provide 'test-card-type-twosided) ;;; test-card-type-twosided.el ends here 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 9e839a2..7cc72c3 100644 --- a/tests/test-integration-drill-session-simple-workflow-integration-test.el +++ b/tests/test-integration-drill-session-simple-workflow-integration-test.el @@ -344,5 +344,273 @@ Simulates reviewing a card and verifies all components work together." (should (= (nth 2 retrieved-data) new-failures)) (should (= (nth 3 retrieved-data) new-total))))))))) +;;; Error Cases - Data Corruption and Malformation + +(ert-deftest test-integration-simple-workflow-error-invalid-property-values () + "Test behavior with invalid (non-numeric) property values. +Should handle gracefully or provide sensible defaults." + (let ((content "* Corrupted Card :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: invalid_number +:DRILL_REPEATS_SINCE_FAIL: abc +:DRILL_TOTAL_REPEATS: xyz +:DRILL_FAILURE_COUNT: 0 +:DRILL_AVERAGE_QUALITY: not_a_float +:DRILL_EASE: 2.5 +:END: + +Question content. +")) + (test-integration-simple-workflow--with-drill-buffer + content + (lambda () + (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))) + (should (listp data)) + ;; Verify function handles corruption gracefully + (should (= (length data) 6))))))) + +(ert-deftest test-integration-simple-workflow-error-missing-properties () + "Test behavior with completely missing drill properties. +Should provide sensible defaults for new/uninitialized cards." + (let ((content "* Card Without Properties :drill: + +Question: Test question? +")) + (test-integration-simple-workflow--with-drill-buffer + content + (lambda () + (re-search-forward "^\\* Card Without Properties :drill:") + (beginning-of-line) + ;; Should not error, provide defaults + (let ((data (org-drill-get-item-data))) + (should (listp data)) + (should (= (length data) 6))))))) + +(ert-deftest test-integration-simple-workflow-error-negative-property-values () + "Test behavior with negative values in properties. +Negative values should be handled gracefully or sanitized." + (let ((content "* Negative Values Card :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: -5 +:DRILL_REPEATS_SINCE_FAIL: -1 +:DRILL_TOTAL_REPEATS: -10 +:DRILL_FAILURE_COUNT: -3 +:DRILL_AVERAGE_QUALITY: -2.5 +:DRILL_EASE: -1.0 +:END: + +Question content. +")) + (test-integration-simple-workflow--with-drill-buffer + content + (lambda () + (re-search-forward "^\\* Negative Values Card :drill:") + (beginning-of-line) + (let ((data (org-drill-get-item-data))) + (should (listp data)) + ;; Verify system doesn't crash with negative values + (should (= (length data) 6))))))) + +(ert-deftest test-integration-simple-workflow-error-extremely-large-values () + "Test behavior with extremely large property values. +Should handle large numbers without overflow or crash." + (let ((content "* Large Values Card :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: 999999999 +:DRILL_REPEATS_SINCE_FAIL: 1000000 +:DRILL_TOTAL_REPEATS: 9999999 +:DRILL_FAILURE_COUNT: 500000 +:DRILL_AVERAGE_QUALITY: 5.0 +:DRILL_EASE: 10.0 +:END: + +Question content. +")) + (test-integration-simple-workflow--with-drill-buffer + content + (lambda () + (re-search-forward "^\\* Large Values Card :drill:") + (beginning-of-line) + (let ((data (org-drill-get-item-data))) + (should (listp data)) + (should (= (length data) 6)) + ;; Verify scheduling can handle extreme values + (cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease) + data + (let ((result (org-drill-determine-next-interval-sm2 + last-interval repeats ease 4 + failures meanq total-repeats))) + (should (listp result))))))))) + +;;; Error Cases - Workflow Error Handling + +(ert-deftest test-integration-simple-workflow-error-retrieve-data-not-at-heading () + "Test retrieving data when point is not at a heading. +Should handle gracefully without crashing." + (test-integration-simple-workflow--with-drill-buffer + test-integration-simple-workflow-basic-entries + (lambda () + ;; Move to body text, not a heading + (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))) + ;; Just verify it doesn't crash + (should (or (null data) (listp data))))))) + +(ert-deftest test-integration-simple-workflow-error-retrieve-data-at-non-drill-heading () + "Test retrieving data from heading without drill tag. +Should handle non-drill headings gracefully." + (test-integration-simple-workflow--with-drill-buffer + test-integration-simple-workflow-basic-entries + (lambda () + ;; Find non-drill heading + (re-search-forward "^\\* Not a drill card") + (beginning-of-line) + (should-not (org-drill-entry-p)) + ;; Retrieving data from non-drill entry + (let ((data (org-drill-get-item-data))) + ;; Should not crash + (should (or (null data) (listp data))))))) + +(ert-deftest test-integration-simple-workflow-error-nested-drill-entries () + "Test handling of nested drill entries (parent and child both tagged). +Should detect both as separate drill entries." + (let ((content "* Parent Drill Entry :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: 5 +:DRILL_REPEATS_SINCE_FAIL: 2 +:DRILL_TOTAL_REPEATS: 3 +:DRILL_FAILURE_COUNT: 0 +:DRILL_AVERAGE_QUALITY: 4.0 +:DRILL_EASE: 2.5 +:END: + +Parent question. + +** Child Drill Entry :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: 3 +:DRILL_REPEATS_SINCE_FAIL: 1 +:DRILL_TOTAL_REPEATS: 2 +:DRILL_FAILURE_COUNT: 1 +:DRILL_AVERAGE_QUALITY: 3.5 +:DRILL_EASE: 2.3 +:END: + +Child question. +")) + (let ((count (test-integration-simple-workflow--count-drill-entries content))) + ;; Should find both parent and child as separate entries + (should (= count 2))))) + +(ert-deftest test-integration-simple-workflow-error-very-deep-nesting () + "Test drill entries at very deep nesting levels. +Should handle deep heading structures without issues." + (let ((content "* Level 1 +** Level 2 +*** Level 3 +**** Level 4 +***** Level 5 +****** Level 6 :drill: + +Deep question. +")) + (let ((count (test-integration-simple-workflow--count-drill-entries content))) + (should (= count 1))))) + +;;; Error Cases - Robustness + +(ert-deftest test-integration-simple-workflow-error-unicode-in-properties () + "Test handling of Unicode characters in drill properties. +Should handle or reject Unicode in numeric properties gracefully." + (let ((content "* Unicode Property Card :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: 5 +:DRILL_REPEATS_SINCE_FAIL: 2 +:DRILL_TOTAL_REPEATS: 3 +:DRILL_FAILURE_COUNT: 0 +:DRILL_AVERAGE_QUALITY: 4.0 +:DRILL_EASE: 2.5 +:END: + +Question with Unicode: 日本語 Café. +")) + (test-integration-simple-workflow--with-drill-buffer + content + (lambda () + (re-search-forward "^\\* Unicode Property Card :drill:") + (beginning-of-line) + ;; Should handle Unicode in content without issues + (let ((data (org-drill-get-item-data))) + (should (listp data)) + (should (= (length data) 6))))))) + +(ert-deftest test-integration-simple-workflow-error-special-chars-in-heading () + "Test drill entries with special characters in heading. +Special chars should not interfere with entry detection or data handling." + (let ((content "* Card @#$%^&*() Special :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: 5 +:DRILL_REPEATS_SINCE_FAIL: 2 +:DRILL_TOTAL_REPEATS: 3 +:DRILL_FAILURE_COUNT: 0 +:DRILL_AVERAGE_QUALITY: 4.0 +:DRILL_EASE: 2.5 +:END: + +Question. +")) + (let ((count (test-integration-simple-workflow--count-drill-entries content))) + (should (= count 1))))) + +(ert-deftest test-integration-simple-workflow-error-empty-properties-drawer () + "Test drill entry with empty PROPERTIES drawer. +Should handle empty drawer gracefully." + (let ((content "* Empty Drawer Card :drill: +:PROPERTIES: +:END: + +Question. +")) + (test-integration-simple-workflow--with-drill-buffer + content + (lambda () + (re-search-forward "^\\* Empty Drawer Card :drill:") + (beginning-of-line) + (let ((data (org-drill-get-item-data))) + ;; Should provide defaults for missing properties + (should (listp data)) + (should (= (length data) 6))))))) + +(ert-deftest test-integration-simple-workflow-error-float-in-integer-property () + "Test behavior with float values in expected-integer properties. +System should handle type coercion or rounding." + (let ((content "* Float Values Card :drill: +:PROPERTIES: +:DRILL_LAST_INTERVAL: 5.7 +:DRILL_REPEATS_SINCE_FAIL: 2.3 +:DRILL_TOTAL_REPEATS: 3.9 +:DRILL_FAILURE_COUNT: 0.5 +:DRILL_AVERAGE_QUALITY: 4.0 +:DRILL_EASE: 2.5 +:END: + +Question. +")) + (test-integration-simple-workflow--with-drill-buffer + content + (lambda () + (re-search-forward "^\\* Float Values Card :drill:") + (beginning-of-line) + (let ((data (org-drill-get-item-data))) + (should (listp data)) + ;; Verify system handles float->int conversion + (should (numberp (nth 0 data))) + (should (numberp (nth 1 data)))))))) + (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 index abb7a0b..a895ccd 100644 --- a/tests/test-org-drill-determine-next-interval-sm2.el +++ b/tests/test-org-drill-determine-next-interval-sm2.el @@ -303,5 +303,141 @@ Third review interval should be significantly larger than second." (interval-3rd (test-sm2--extract-interval result-3rd))) (should (> interval-3rd (* interval-2nd 1.5)))))) ; Significant growth +;;; Aggressive Boundary Cases + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-very-high-repeat-count () + "Test SM2 with very high repeat count (1000 reviews). +Algorithm should remain stable with extreme n values." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((result (org-drill-determine-next-interval-sm2 365 1000 2.5 4 0 4.0 1000)) + (interval (test-sm2--extract-interval result)) + (repeats (test-sm2--extract-repeats result)) + (ef (test-sm2--extract-ef result))) + (should (numberp interval)) + (should (> interval 0)) + (should (= repeats 1001)) + (should (>= ef test-sm2-min-ef))))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-very-long-interval () + "Test SM2 with very long last interval (10 years = 3650 days). +Algorithm should handle extreme intervals without overflow." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((result (org-drill-determine-next-interval-sm2 3650 100 2.5 4 0 4.0 100)) + (interval (test-sm2--extract-interval result))) + (should (numberp interval)) + (should (> interval 0))))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-very-high-failure-count () + "Test SM2 with very high failure count (100+ failures). +Failure count should be tracked correctly even at extreme values." + (let* ((failures 100) + (result (org-drill-determine-next-interval-sm2 10 3 2.5 0 failures 3.0 150)) + (new-failures (test-sm2--extract-failures result))) + (should (= new-failures (1+ failures))))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-very-high-total-repeats () + "Test SM2 with very high total-repeats (10000+). +Total repeats should increment correctly even at extreme values." + (let* ((total-repeats 10000) + (result (org-drill-determine-next-interval-sm2 10 3 2.5 4 0 4.0 total-repeats)) + (new-total (test-sm2--extract-total-repeats result))) + (should (= new-total (1+ total-repeats))))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-ef-just-above-floor () + "Test SM2 with EF just above minimum floor (1.31). +EF should be maintained or modified normally, not clamped." + (let* ((result (org-drill-determine-next-interval-sm2 10 3 1.31 4 0 4.0 10)) + (ef (test-sm2--extract-ef result))) + (should (>= ef test-sm2-min-ef)))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-ef-at-floor () + "Test SM2 with EF exactly at minimum floor (1.3). +EF at floor with quality 4 should increase slightly." + (let* ((result (org-drill-determine-next-interval-sm2 10 3 1.3 4 0 4.0 10)) + (ef (test-sm2--extract-ef result))) + (should (>= ef test-sm2-min-ef)))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-perfect-recall-after-many-failures () + "Test perfect recall (quality 5) after long failure streak. +Perfect recall should not reset failures, but should calculate normal interval." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((failures 50) + (result (org-drill-determine-next-interval-sm2 10 3 2.5 5 failures 3.0 100)) + (interval (test-sm2--extract-interval result)) + (new-failures (test-sm2--extract-failures result))) + (should (> interval 0)) + (should (= new-failures failures))))) ; Failures unchanged on success + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-failure-after-long-streak () + "Test complete failure (quality 0) after long successful streak. +Failure should reset interval to -1 regardless of previous success." + (let* ((result (org-drill-determine-next-interval-sm2 365 100 2.8 0 0 4.5 100)) + (interval (test-sm2--extract-interval result)) + (repeats (test-sm2--extract-repeats result)) + (failures (test-sm2--extract-failures result))) + (should (= interval -1)) + (should (= repeats 1)) ; Reset to 1 + (should (= failures 1)))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-meanq-at-maximum () + "Test SM2 with meanq at maximum (5.0). +High quality review with already-perfect meanq." + (let* ((result (org-drill-determine-next-interval-sm2 10 3 2.5 5 0 5.0 100)) + (meanq (test-sm2--extract-meanq result))) + (should (<= meanq 5.0)) + (should (>= meanq 4.5)))) ; Should remain very high + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-meanq-at-minimum () + "Test SM2 with meanq at minimum (0.0). +Low quality history with poor current recall." + (let* ((result (org-drill-determine-next-interval-sm2 10 3 2.5 3 5 0.0 100)) + (meanq (test-sm2--extract-meanq result))) + (should (>= meanq 0.0)) + (should (<= meanq 1.0)))) ; Should remain low with quality 3 + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-high-total-repeats-low-meanq () + "Test SM2 with very high total-repeats but low meanq. +Indicates item reviewed many times but poorly recalled." + (let* ((total-repeats 1000) + (meanq 2.0) + (result (org-drill-determine-next-interval-sm2 5 10 1.5 3 20 meanq total-repeats)) + (new-meanq (test-sm2--extract-meanq result))) + (should (numberp new-meanq)) + ;; meanq should remain low given poor history + (should (< new-meanq 3.0)))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-zero-last-interval () + "Test SM2 with zero last-interval. +Should handle as first review (n=1 case)." + (let* ((result (org-drill-determine-next-interval-sm2 0 1 2.5 4 0 4.0 0)) + (interval (test-sm2--extract-interval result))) + (should (= interval 1)))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-float-vs-int-intervals () + "Test SM2 with float last-interval vs integer. +Algorithm should handle both correctly." + (let ((org-drill-add-random-noise-to-intervals-p nil)) + (let* ((result-float (org-drill-determine-next-interval-sm2 10.5 3 2.5 4 0 4.0 10)) + (result-int (org-drill-determine-next-interval-sm2 10 3 2.5 4 0 4.0 10)) + (interval-float (test-sm2--extract-interval result-float)) + (interval-int (test-sm2--extract-interval result-int))) + (should (numberp interval-float)) + (should (numberp interval-int)) + ;; Both should produce reasonable intervals + (should (> interval-float 10)) + (should (> interval-int 10))))) + +(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-alternating-quality-pattern () + "Test SM2 behavior with alternating quality (5, 0, 5, 0 pattern). +Simulates inconsistent learning." + (let* ((result-1 (org-drill-determine-next-interval-sm2 1 2 2.5 5 0 5.0 1)) + (ef-1 (test-sm2--extract-ef result-1)) + (result-2 (org-drill-determine-next-interval-sm2 6 3 ef-1 0 0 4.5 2)) + (interval-2 (test-sm2--extract-interval result-2)) + (failures-2 (test-sm2--extract-failures result-2))) + ;; After perfect then fail, should reset interval + (should (= interval-2 -1)) + (should (= failures-2 1)))) + (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 index c298c94..4af9169 100644 --- a/tests/test-org-drill-entry-p.el +++ b/tests/test-org-drill-entry-p.el @@ -178,5 +178,91 @@ Single asterisk should still work as heading." (lambda () (should (org-drill-entry-p))))) +;;; Aggressive Boundary Cases + +(ert-deftest test-org-drill-entry-p-boundary-very-long-tag-name () + "Test drill tag with very long name (edge case). +Tag name length should not cause issues." + (let* ((long-tag (make-string 200 ?d)) + (content (format "* Heading :%s:\n\nContent\n" long-tag))) + (test-org-drill-entry-p--with-org-buffer + content + (lambda () + ;; Should not match 'drill' tag + (should-not (org-drill-entry-p)))))) + +(ert-deftest test-org-drill-entry-p-boundary-many-tags () + "Test heading with many tags including drill. +Should still correctly identify drill tag among many others." + (let ((content "* Heading :tag1:tag2:tag3:drill:tag4:tag5:tag6:tag7:tag8:tag9:tag10:\n\nContent\n")) + (test-org-drill-entry-p--with-org-buffer + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-org-drill-entry-p-boundary-unicode-in-heading () + "Test heading with unicode characters and drill tag. +Unicode should not interfere with tag detection." + (let ((content "* Café München 北京 :drill:\n\nUnicode content 日本語\n")) + (test-org-drill-entry-p--with-org-buffer + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-org-drill-entry-p-boundary-deep-nesting () + "Test drill tag detection at deep nesting level. +Should work at any heading level." + (let ((content "* Level 1\n** Level 2\n*** Level 3\n**** Level 4\n***** Level 5 :drill:\n\nDeep content\n")) + (test-org-drill-entry-p--with-org-buffer + content + (lambda () + ;; Navigate to deeply nested heading + (re-search-forward "Level 5") + (beginning-of-line) + (should (org-drill-entry-p)))))) + +(ert-deftest test-org-drill-entry-p-boundary-tag-with-numbers () + "Test that tags containing 'drill' substring but with numbers don't match. +drill123 should not be recognized as drill tag." + (let ((content "* Heading :drill123:\n\nContent\n")) + (test-org-drill-entry-p--with-org-buffer + content + (lambda () + (should-not (org-drill-entry-p)))))) + +(ert-deftest test-org-drill-entry-p-boundary-drill-at-tag-boundary () + "Test drill tag at beginning and end of tag list." + (let ((content1 "* Heading :drill:other:tags:\n\nContent\n") + (content2 "* Heading :other:tags:drill:\n\nContent\n")) + ;; drill at start + (test-org-drill-entry-p--with-org-buffer + content1 + (lambda () + (should (org-drill-entry-p)))) + ;; drill at end + (test-org-drill-entry-p--with-org-buffer + content2 + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-org-drill-entry-p-boundary-special-chars-in-heading () + "Test heading with special characters and drill tag. +Special chars should not break tag parsing." + (let ((content "* Heading with @#$%^&*() special chars :drill:\n\nContent\n")) + (test-org-drill-entry-p--with-org-buffer + content + (lambda () + (should (org-drill-entry-p)))))) + +(ert-deftest test-org-drill-entry-p-boundary-very-long-heading-line () + "Test heading with very long title text. +Long heading should not affect tag detection." + (let* ((long-title (make-string 1000 ?x)) + (content (format "* %s :drill:\n\nContent\n" long-title))) + (test-org-drill-entry-p--with-org-buffer + content + (lambda () + (should (org-drill-entry-p)))))) + (provide 'test-org-drill-entry-p) ;;; test-org-drill-entry-p.el ends here |
