diff options
| author | Craig Jennings <c@cjennings.net> | 2025-11-08 16:11:58 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-11-08 16:11:58 -0600 |
| commit | 8176eff73b826f7fec9d7f458f7d2f36f4d12e58 (patch) | |
| tree | 3e73394b689f0e32dce6930431d9060d946b0b79 /tests | |
| parent | d093a4a96c653d3f9adcbba17b4094d6d9a5a85a (diff) | |
feat: Fix modeline lag and add org multi-level sort with comprehensive tests
Performance improvement and new feature with full test coverage.
## Changes
### 1. Fix modeline line/column position lag (#A priority)
- Replace expensive line-number-at-pos with cached %l/%c format specifiers
- Enable line-number-mode explicitly for caching
- Result: Instant modeline updates, zero performance overhead
- Files: modules/modeline-config.el:81-83, modules/ui-config.el:53
### 2. Implement multi-level org sorting
- New function: cj/org-sort-by-todo-and-priority
- Sorts by TODO status (TODO before DONE) AND priority (A→B→C→D)
- Uses stable sorting: priority first, then TODO state
- Gracefully handles empty sections (no error)
- Bound to C-; o o (ordering → org sort)
- Files: modules/org-config.el:278-299, modules/custom-ordering.el:253,267
### 3. Comprehensive ERT test suite (12/12 passing)
- Normal cases: Mixed TODO/DONE, multiple of same type, same priority
- Boundary cases: Empty sections, single entries, no priorities
- Error cases: Non-org-mode buffer
- Test file: tests/test-org-sort-by-todo-and-priority.el
### 4. Testing improvements discovered
- Disable org-mode hooks to avoid package dependencies in batch mode
- org-sort-entries must be called from parent heading
- Preserve priority cookie in org-get-heading (t t nil t)
- Add condition-case to handle "Nothing to sort" gracefully
### 5. Minor cleanup
- Comment out chime-debug setting (org-agenda-config.el:267)
- Mark modeline lag task as DONE in todo.org
## Technical Details
Modeline optimization:
- line-number-at-pos is O(n) where n = current line
- %l and %c are O(1) lookups from cached values
Org sorting algorithm uses stable sort:
1. Sort by priority (A, B, C, D, unprioritized)
2. Sort by TODO status (preserves priority order within groups)
Result: TODO [#A], TODO [#B], DONE [#A], DONE [#B], etc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-org-sort-by-todo-and-priority.el | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/tests/test-org-sort-by-todo-and-priority.el b/tests/test-org-sort-by-todo-and-priority.el new file mode 100644 index 00000000..873f37c2 --- /dev/null +++ b/tests/test-org-sort-by-todo-and-priority.el @@ -0,0 +1,283 @@ +;;; test-org-sort-by-todo-and-priority.el --- Tests for cj/org-sort-by-todo-and-priority -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Unit tests for cj/org-sort-by-todo-and-priority function. +;; Tests multi-level sorting: TODO status (TODO before DONE) and priority (A before B before C). +;; +;; Testing approach: +;; - Use real org-mode buffers (don't mock org-sort-entries) +;; - Trust org-mode framework works correctly +;; - Test OUR integration logic: calling org-sort-entries twice in correct order +;; - Verify final sort order matches expected TODO/priority combination +;; +;; The function uses stable sorting: +;; 1. First sort by priority (A, B, C, D, none) +;; 2. Then sort by TODO status (TODO before DONE) +;; Result: Priority order preserved within each TODO state group + +;;; Code: + +(require 'ert) +(require 'org) +(require 'org-config) ; Defines cj/org-sort-by-todo-and-priority + +;;; Test Helpers + +(defun test-org-sort-by-todo-and-priority--create-buffer (content) + "Create a temporary org-mode buffer with CONTENT. +Returns the buffer object. +Disables org-mode hooks to avoid missing package dependencies in batch mode." + (let ((buf (generate-new-buffer "*test-org-sort*"))) + (with-current-buffer buf + ;; Disable hooks to prevent org-superstar and other package loads + (let ((org-mode-hook nil)) + (org-mode)) + (insert content) + (goto-char (point-min))) + buf)) + +(defun test-org-sort-by-todo-and-priority--get-entry-order (buffer) + "Extract ordered list of TODO states and priorities from BUFFER. +Returns list of strings like \"TODO [#A]\" or \"DONE\" for each heading." + (with-current-buffer buffer + (goto-char (point-min)) + (let (entries) + (org-map-entries + (lambda () + (let* ((todo-state (org-get-todo-state)) + ;; Get heading: no-tags, no-todo, KEEP priority, no-comment + (heading (org-get-heading t t nil t)) + ;; Extract priority cookie from heading text + (priority (when (string-match "\\[#\\([A-Z]\\)\\]" heading) + (match-string 1 heading)))) + (push (if priority + (format "%s [#%s]" (or todo-state "") priority) + (or todo-state "")) + entries))) + nil 'tree) + (nreverse entries)))) + +(defun test-org-sort-by-todo-and-priority--sort-children (buffer) + "Position cursor on parent heading in BUFFER and sort its children. +Moves to first * heading (Parent) and calls sort function to sort children." + (with-current-buffer buffer + (goto-char (point-min)) + (when (re-search-forward "^\\* " nil t) + (beginning-of-line) + (cj/org-sort-by-todo-and-priority)))) + +;;; Normal Cases + +(ert-deftest test-org-sort-by-todo-and-priority-normal-mixed-todo-done-sorts-correctly () + "Test mixed TODO and DONE entries with various priorities sort correctly. + +Input: TODO [#A], DONE [#B], TODO [#C], DONE [#A] +Expected: TODO [#A], TODO [#C], DONE [#A], DONE [#B]" + (let* ((content "* Parent +** TODO [#A] First task +** DONE [#B] Second task +** TODO [#C] Third task +** DONE [#A] Fourth task +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "TODO [#A]" "TODO [#C]" "DONE [#A]" "DONE [#B]"))))) + (kill-buffer buf)))) + +(ert-deftest test-org-sort-by-todo-and-priority-normal-multiple-todos-sorts-by-priority () + "Test multiple TODO entries sort by priority A before B before C. + +Input: TODO [#C], TODO [#A], TODO [#B] +Expected: TODO [#A], TODO [#B], TODO [#C]" + (let* ((content "* Parent +** TODO [#C] Task C +** TODO [#A] Task A +** TODO [#B] Task B +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "TODO [#A]" "TODO [#B]" "TODO [#C]"))))) + (kill-buffer buf)))) + +(ert-deftest test-org-sort-by-todo-and-priority-normal-multiple-dones-sorts-by-priority () + "Test multiple DONE entries sort by priority A before B before C. + +Input: DONE [#C], DONE [#A], DONE [#B] +Expected: DONE [#A], DONE [#B], DONE [#C]" + (let* ((content "* Parent +** DONE [#C] Done C +** DONE [#A] Done A +** DONE [#B] Done B +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "DONE [#A]" "DONE [#B]" "DONE [#C]"))))) + (kill-buffer buf)))) + +(ert-deftest test-org-sort-by-todo-and-priority-normal-same-priority-todo-before-done () + "Test entries with same priority sort TODO before DONE. + +Input: DONE [#A], TODO [#A] +Expected: TODO [#A], DONE [#A]" + (let* ((content "* Parent +** DONE [#A] Done task +** TODO [#A] Todo task +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "TODO [#A]" "DONE [#A]"))))) + (kill-buffer buf)))) + +;;; Boundary Cases + +(ert-deftest test-org-sort-by-todo-and-priority-boundary-empty-section-no-error () + "Test sorting empty section does not signal error. + +Input: Heading with no children +Expected: No error, no change" + (let* ((content "* Parent\n") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (with-current-buffer buf + (goto-char (point-min)) + (should-not (condition-case err + (progn + (cj/org-sort-by-todo-and-priority) + nil) + (error err)))) + (kill-buffer buf)))) + +(ert-deftest test-org-sort-by-todo-and-priority-boundary-single-todo-no-change () + "Test sorting single TODO entry does not change order. + +Input: Single TODO [#A] +Expected: Same order (no change)" + (let* ((content "* Parent +** TODO [#A] Only task +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "TODO [#A]"))))) + (kill-buffer buf)))) + +(ert-deftest test-org-sort-by-todo-and-priority-boundary-single-done-no-change () + "Test sorting single DONE entry does not change order. + +Input: Single DONE [#B] +Expected: Same order (no change)" + (let* ((content "* Parent +** DONE [#B] Only task +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "DONE [#B]"))))) + (kill-buffer buf)))) + +(ert-deftest test-org-sort-by-todo-and-priority-boundary-all-todos-sorts-by-priority () + "Test all TODO entries sort by priority only. + +Input: TODO [#C], TODO [#A], TODO [#B] +Expected: TODO [#A], TODO [#B], TODO [#C]" + (let* ((content "* Parent +** TODO [#C] Task C +** TODO [#A] Task A +** TODO [#B] Task B +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "TODO [#A]" "TODO [#B]" "TODO [#C]"))))) + (kill-buffer buf)))) + +(ert-deftest test-org-sort-by-todo-and-priority-boundary-all-dones-sorts-by-priority () + "Test all DONE entries sort by priority only. + +Input: DONE [#B], DONE [#D], DONE [#A] +Expected: DONE [#A], DONE [#B], DONE [#D]" + (let* ((content "* Parent +** DONE [#B] Done B +** DONE [#D] Done D +** DONE [#A] Done A +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "DONE [#A]" "DONE [#B]" "DONE [#D]"))))) + (kill-buffer buf)))) + +(ert-deftest test-org-sort-by-todo-and-priority-boundary-no-priorities-sorts-by-todo () + "Test entries without priorities sort by TODO status only. + +Input: TODO (no priority), DONE (no priority), TODO (no priority) +Expected: TODO, TODO, DONE" + (let* ((content "* Parent +** TODO Task 1 +** DONE Task 2 +** TODO Task 3 +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "TODO" "TODO" "DONE"))))) + (kill-buffer buf)))) + +(ert-deftest test-org-sort-by-todo-and-priority-boundary-unprioritized-after-prioritized () + "Test unprioritized entries appear after prioritized within TODO/DONE groups. + +Input: TODO (no priority), TODO [#A], DONE [#B], DONE (no priority) +Expected: TODO [#A], TODO (no priority), DONE [#B], DONE (no priority)" + (let* ((content "* Parent +** TODO Task no priority +** TODO [#A] Task A +** DONE [#B] Done B +** DONE Done no priority +") + (buf (test-org-sort-by-todo-and-priority--create-buffer content))) + (unwind-protect + (progn + (test-org-sort-by-todo-and-priority--sort-children buf) + (let ((order (test-org-sort-by-todo-and-priority--get-entry-order buf))) + (should (equal order '("" "TODO [#A]" "TODO" "DONE [#B]" "DONE"))))) + (kill-buffer buf)))) + +;;; Error Cases + +(ert-deftest test-org-sort-by-todo-and-priority-error-non-org-buffer-signals-error () + "Test calling in non-org-mode buffer signals user-error. + +Input: fundamental-mode buffer +Expected: user-error" + (let ((buf (generate-new-buffer "*test-non-org*"))) + (unwind-protect + (with-current-buffer buf + (fundamental-mode) + (should-error (cj/org-sort-by-todo-and-priority) :type 'user-error)) + (kill-buffer buf)))) + +(provide 'test-org-sort-by-todo-and-priority) +;;; test-org-sort-by-todo-and-priority.el ends here |
