branch: externals/gnosis
commit 62a17c842120fde33e51cb765590cb148235c0fe
Author: Thanos Apollo <[email protected]>
Commit: Thanos Apollo <[email protected]>

    test: gnosis-tl: Add sorting tests.
---
 tests/gnosis-test-dashboard.el | 590 +++++++++++++++++++++++++++++++++++++----
 1 file changed, 542 insertions(+), 48 deletions(-)

diff --git a/tests/gnosis-test-dashboard.el b/tests/gnosis-test-dashboard.el
index b3bfebd537..a5128785f6 100644
--- a/tests/gnosis-test-dashboard.el
+++ b/tests/gnosis-test-dashboard.el
@@ -822,59 +822,553 @@ Binds `org-gnosis-dir' to the temp directory."
       (should (equal (buffer-string) before)))))
 
 ;; ──────────────────────────────────────────────────────────
-;; Benchmark tests
+;; gnosis-tl-print tests
 ;; ──────────────────────────────────────────────────────────
 
-(ert-deftest gnosis-test-dashboard-print-entry-benchmark ()
-  "Benchmark tabulated-list-print-entry vs tabulated-list-print.
-Shows per-entry cost so we can size chunks correctly."
+(ert-deftest gnosis-test-tl-print-basic ()
+  "gnosis-tl-print renders entries into the buffer."
   (with-temp-buffer
     (tabulated-list-mode)
-    (setq tabulated-list-format
-          [("Keimenon" 30 t) ("Hypothesis" 20 t) ("Answer" 20 t)
-           ("Tags" 15 t) ("Type" 10 t) ("Suspend" 10 t)])
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)])
+    (setq tabulated-list-padding 2)
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries
+          '((1 ["Alice" "100"])
+            (2 ["Bob" "200"])
+            (3 ["Carol" "300"])))
+    (gnosis-tl-print)
+    ;; All entries present
+    (goto-char (point-min))
+    (should (search-forward "Alice" nil t))
+    (goto-char (point-min))
+    (should (search-forward "Bob" nil t))
+    (goto-char (point-min))
+    (should (search-forward "Carol" nil t))
+    ;; Correct line count
+    (should (= (count-lines (point-min) (point-max)) 3))
+    ;; tabulated-list-id properties are set
+    (goto-char (point-min))
+    (should (equal (get-text-property (point) 'tabulated-list-id) 1))
+    (forward-line 1)
+    (should (equal (get-text-property (point) 'tabulated-list-id) 2))
+    (forward-line 1)
+    (should (equal (get-text-property (point) 'tabulated-list-id) 3))))
+
+(ert-deftest gnosis-test-tl-print-sorting ()
+  "gnosis-tl-print sorts entries by tabulated-list-sort-key."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)])
+    (setq tabulated-list-padding 2)
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries
+          '((1 ["Charlie" "300"])
+            (2 ["Alice" "100"])
+            (3 ["Bob" "200"])))
+    ;; Sort by Name ascending (flip=nil)
+    (setq tabulated-list-sort-key (cons "Name" nil))
+    (gnosis-tl-print)
+    ;; First entry should be Alice
+    (goto-char (point-min))
+    (should (equal (get-text-property (point) 'tabulated-list-id) 2))
+    ;; Second should be Bob
+    (forward-line 1)
+    (should (equal (get-text-property (point) 'tabulated-list-id) 3))
+    ;; Third should be Charlie
+    (forward-line 1)
+    (should (equal (get-text-property (point) 'tabulated-list-id) 1))))
+
+(ert-deftest gnosis-test-tl-print-sorting-descending ()
+  "gnosis-tl-print respects descending sort (flip=t)."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)])
+    (setq tabulated-list-padding 2)
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries
+          '((1 ["Alice" "100"])
+            (2 ["Bob" "200"])
+            (3 ["Charlie" "300"])))
+    ;; Sort by Name descending (flip=t)
+    (setq tabulated-list-sort-key (cons "Name" t))
+    (gnosis-tl-print)
+    ;; First entry should be Charlie
+    (goto-char (point-min))
+    (should (equal (get-text-property (point) 'tabulated-list-id) 3))
+    (forward-line 1)
+    (should (equal (get-text-property (point) 'tabulated-list-id) 2))
+    (forward-line 1)
+    (should (equal (get-text-property (point) 'tabulated-list-id) 1))))
+
+(ert-deftest gnosis-test-tl-print-remember-pos ()
+  "gnosis-tl-print restores cursor to saved entry ID."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t)])
     (setq tabulated-list-padding 2)
     (tabulated-list-init-header)
-    (let ((entry '(1 ["A sample keimenon text" "some hypothesis"
-                      "the answer" "tag1,tag2" "basic" "No"]))
-          (inhibit-read-only t))
-      ;; Benchmark tabulated-list-print-entry x 2000
-      (let ((start (float-time)))
-        (dotimes (_ 2000)
-          (tabulated-list-print-entry (car entry) (cadr entry)))
-        (let ((elapsed (- (float-time) start)))
-          (message "print-entry x2000: %.3fs (%.0fus/entry)"
-                   elapsed (* 1e6 (/ elapsed 2000)))))
-      ;; Reset buffer, benchmark tabulated-list-print with 2000 entries
-      (erase-buffer)
-      (setq tabulated-list-entries
-            (cl-loop for i from 1 to 2000
-                     collect (list i (cadr entry))))
-      (let ((start (float-time)))
-        (tabulated-list-print t)
-        (message "tabulated-list-print x2000: %.3fs (%.0fus/entry)"
-                 (- (float-time) start)
-                 (* 1e6 (/ (- (float-time) start) 2000))))
-      ;; Benchmark tabulated-list-print with 10000 entries
-      (erase-buffer)
-      (setq tabulated-list-entries
-            (cl-loop for i from 1 to 10000
-                     collect (list i (cadr entry))))
-      (let ((start (float-time)))
-        (tabulated-list-print t)
-        (message "tabulated-list-print x10000: %.3fs (%.0fus/entry)"
-                 (- (float-time) start)
-                 (* 1e6 (/ (- (float-time) start) 10000))))
-      ;; Benchmark tabulated-list-print with 40000 entries
-      (erase-buffer)
-      (setq tabulated-list-entries
-            (cl-loop for i from 1 to 40000
-                     collect (list i (cadr entry))))
-      (let ((start (float-time)))
-        (tabulated-list-print t)
-        (message "tabulated-list-print x40000: %.3fs (%.0fus/entry)"
-                 (- (float-time) start)
-                 (* 1e6 (/ (- (float-time) start) 40000))))
-      (should (> (buffer-size) 0)))))
+    (setq tabulated-list-entries
+          '((1 ["Alice"])
+            (2 ["Bob"])
+            (3 ["Carol"])))
+    (gnosis-tl-print)
+    ;; Position on Bob
+    (goto-char (point-min))
+    (forward-line 1)
+    (should (equal (tabulated-list-get-id) 2))
+    ;; Re-render with remember-pos
+    (gnosis-tl-print t)
+    ;; Should still be on Bob
+    (should (equal (tabulated-list-get-id) 2))))
+
+(ert-deftest gnosis-test-tl-print-empty ()
+  "gnosis-tl-print with no entries produces empty buffer."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t)])
+    (setq tabulated-list-padding 2)
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries nil)
+    (gnosis-tl-print)
+    (should (= (buffer-size) 0))))
+
+(ert-deftest gnosis-test-tl-print-remember-pos-column ()
+  "gnosis-tl-print restores both entry and column position."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)]
+          tabulated-list-padding 2)
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries
+          '((1 ["Alice" "100"])
+            (2 ["Bob" "200"])
+            (3 ["Carol" "300"])))
+    (gnosis-tl-print)
+    ;; Position on Bob, move into the line
+    (goto-char (point-min))
+    (forward-line 1)
+    (move-to-column 10)
+    (let ((col (current-column)))
+      (should (equal (tabulated-list-get-id) 2))
+      ;; Re-render with remember-pos
+      (gnosis-tl-print t)
+      ;; Same entry and column
+      (should (equal (tabulated-list-get-id) 2))
+      (should (= (current-column) col)))))
+
+(ert-deftest gnosis-test-tl-print-sorted-same-as-tabulated-list ()
+  "gnosis-tl-print with sort key produces same order as tabulated-list-print."
+  (let ((entries '((1 ["Charlie" "300"])
+                   (2 ["Alice" "100"])
+                   (3 ["Bob" "200"]))))
+    ;; tabulated-list-print with sort
+    (let (tl-ids)
+      (with-temp-buffer
+        (tabulated-list-mode)
+        (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)]
+              tabulated-list-padding 2
+              tabulated-list-sort-key (cons "Name" nil)
+              tabulated-list-entries (copy-sequence entries))
+        (tabulated-list-init-header)
+        (tabulated-list-print)
+        (goto-char (point-min))
+        (while (not (eobp))
+          (push (tabulated-list-get-id) tl-ids)
+          (forward-line 1))
+        (setq tl-ids (nreverse tl-ids)))
+      ;; gnosis-tl-print with same sort
+      (let (our-ids)
+        (with-temp-buffer
+          (tabulated-list-mode)
+          (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)]
+                tabulated-list-padding 2
+                tabulated-list-sort-key (cons "Name" nil)
+                tabulated-list-entries (copy-sequence entries))
+          (tabulated-list-init-header)
+          (gnosis-tl-print)
+          (goto-char (point-min))
+          (while (not (eobp))
+            (push (tabulated-list-get-id) our-ids)
+            (forward-line 1))
+          (setq our-ids (nreverse our-ids)))
+        (should (equal tl-ids our-ids))))))
+
+;; ──────────────────────────────────────────────────────────
+;; gnosis-tl-render-lines tests
+;; ──────────────────────────────────────────────────────────
+
+(ert-deftest gnosis-test-tl-render-lines-basic ()
+  "render-lines returns a propertized string with all entries."
+  (let* ((fmt [("Name" 20 t) ("Val" 10 t)])
+         (entries '((1 ["Alice" "100"])
+                    (2 ["Bob" "200"])))
+         (result (gnosis-tl-render-lines entries fmt 2)))
+    ;; Returns a string
+    (should (stringp result))
+    ;; Contains both entries
+    (should (string-search "Alice" result))
+    (should (string-search "Bob" result))
+    ;; Has text properties
+    (should (equal (get-text-property 0 'tabulated-list-id result) 1))
+    ;; Two newlines (one per entry)
+    (should (= (cl-count ?\n result) 2))))
+
+(ert-deftest gnosis-test-tl-render-lines-empty ()
+  "render-lines with no entries returns empty string."
+  (let ((result (gnosis-tl-render-lines nil [("Name" 20 t)] 2)))
+    (should (equal result ""))))
+
+(ert-deftest gnosis-test-tl-render-lines-padding ()
+  "render-lines respects padding parameter."
+  (let* ((fmt [("Name" 20 t)])
+         (entries '((1 ["Alice"])))
+         (result-0 (gnosis-tl-render-lines entries fmt 0))
+         (result-4 (gnosis-tl-render-lines entries fmt 4)))
+    ;; result-4 should be 4 chars longer (leading spaces)
+    (should (= (- (length result-4) (length result-0)) 4))))
+
+;; ──────────────────────────────────────────────────────────
+;; gnosis-tl--get-sorter tests
+;; ──────────────────────────────────────────────────────────
+
+(ert-deftest gnosis-test-tl-get-sorter-nil-key ()
+  "No sorter when tabulated-list-sort-key is nil."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)])
+    (setq tabulated-list-sort-key nil)
+    (should (null (gnosis-tl--get-sorter)))))
+
+(ert-deftest gnosis-test-tl-get-sorter-unsortable ()
+  "No sorter when column has nil (not sortable) predicate."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 nil) ("Val" 10 t)])
+    (setq tabulated-list-sort-key (cons "Name" nil))
+    (should (null (gnosis-tl--get-sorter)))))
+
+(ert-deftest gnosis-test-tl-get-sorter-default-ascending ()
+  "Default sorter (t predicate) sorts by string< ascending."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)])
+    (setq tabulated-list-sort-key (cons "Name" nil))
+    (let ((sorter (gnosis-tl--get-sorter)))
+      (should (functionp sorter))
+      ;; "Alice" < "Bob"
+      (should (funcall sorter '(1 ["Alice" "x"]) '(2 ["Bob" "y"])))
+      ;; "Bob" NOT < "Alice"
+      (should-not (funcall sorter '(2 ["Bob" "y"]) '(1 ["Alice" "x"]))))))
+
+(ert-deftest gnosis-test-tl-get-sorter-default-descending ()
+  "Default sorter with flip=t reverses to descending."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)])
+    (setq tabulated-list-sort-key (cons "Name" t))
+    (let ((sorter (gnosis-tl--get-sorter)))
+      (should (functionp sorter))
+      ;; Flipped: "Bob" before "Alice"
+      (should (funcall sorter '(2 ["Bob" "y"]) '(1 ["Alice" "x"])))
+      (should-not (funcall sorter '(1 ["Alice" "x"]) '(2 ["Bob" "y"]))))))
+
+(ert-deftest gnosis-test-tl-get-sorter-equal-elements ()
+  "Sorter returns nil for equal elements in both directions.
+This is the critical bug fix: (not nil) => t was wrong."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t)])
+    ;; Ascending: equal elements
+    (setq tabulated-list-sort-key (cons "Name" nil))
+    (let ((sorter (gnosis-tl--get-sorter)))
+      (should-not (funcall sorter '(1 ["Alice"]) '(2 ["Alice"]))))
+    ;; Descending: equal elements must ALSO return nil
+    (setq tabulated-list-sort-key (cons "Name" t))
+    (let ((sorter (gnosis-tl--get-sorter)))
+      (should-not (funcall sorter '(1 ["Alice"]) '(2 ["Alice"]))))))
+
+(ert-deftest gnosis-test-tl-get-sorter-custom-predicate ()
+  "Custom function predicate is called correctly."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format
+          [("Name" 20 t)
+           ("Count" 10 (lambda (a b)
+                         (< (string-to-number (aref (cadr a) 1))
+                            (string-to-number (aref (cadr b) 1)))))])
+    (setq tabulated-list-sort-key (cons "Count" nil))
+    (let ((sorter (gnosis-tl--get-sorter)))
+      (should (funcall sorter '(1 ["x" "5"]) '(2 ["y" "10"])))
+      (should-not (funcall sorter '(2 ["y" "10"]) '(1 ["x" "5"]))))))
+
+(ert-deftest gnosis-test-tl-get-sorter-matches-tabulated-list ()
+  "gnosis-tl--get-sorter produces same sort order as 
tabulated-list--get-sorter."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)])
+    (let ((entries '((1 ["Charlie" "300"])
+                     (2 ["Alice" "100"])
+                     (3 ["Bob" "200"])
+                     (4 ["Alice" "050"]))))
+      ;; Ascending
+      (setq tabulated-list-sort-key (cons "Name" nil))
+      (let ((our (sort (copy-sequence entries) (gnosis-tl--get-sorter)))
+            (theirs (sort (copy-sequence entries) 
(tabulated-list--get-sorter))))
+        (should (equal (mapcar #'car our) (mapcar #'car theirs))))
+      ;; Descending
+      (setq tabulated-list-sort-key (cons "Name" t))
+      (let ((our (sort (copy-sequence entries) (gnosis-tl--get-sorter)))
+            (theirs (sort (copy-sequence entries) 
(tabulated-list--get-sorter))))
+        (should (equal (mapcar #'car our) (mapcar #'car theirs)))))))
+
+;; ──────────────────────────────────────────────────────────
+;; gnosis-tl-sort tests
+;; ──────────────────────────────────────────────────────────
+
+(ert-deftest gnosis-test-tl-sort-by-column-name ()
+  "gnosis-tl-sort reads column-name property and sorts."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)]
+          tabulated-list-padding 2)
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries
+          '((1 ["Charlie" "300"])
+            (2 ["Alice" "100"])
+            (3 ["Bob" "200"])))
+    (gnosis-tl-print)
+    ;; Move past padding to column text
+    (goto-char (point-min))
+    (move-to-column 2)
+    (gnosis-tl-sort)
+    ;; After sorting by Name ascending, Alice should be first
+    (goto-char (point-min))
+    (should (equal (tabulated-list-get-id) 2))
+    ;; Sort key was set
+    (should (equal (car tabulated-list-sort-key) "Name"))
+    (should-not (cdr tabulated-list-sort-key))))
+
+(ert-deftest gnosis-test-tl-sort-toggle-direction ()
+  "Pressing gnosis-tl-sort twice on same column flips direction."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)]
+          tabulated-list-padding 2)
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries
+          '((1 ["Alice" "100"])
+            (2 ["Bob" "200"])
+            (3 ["Charlie" "300"])))
+    (gnosis-tl-print)
+    ;; Move past padding to column text
+    (goto-char (point-min))
+    (move-to-column 2)
+    ;; First sort: ascending
+    (gnosis-tl-sort)
+    (goto-char (point-min))
+    (should (equal (tabulated-list-get-id) 1))  ;; Alice first
+    ;; Move past padding again for second sort
+    (move-to-column 2)
+    ;; Second sort: descending
+    (gnosis-tl-sort)
+    (goto-char (point-min))
+    (should (equal (tabulated-list-get-id) 3))))  ;; Charlie first
+
+(ert-deftest gnosis-test-tl-sort-by-column-number ()
+  "gnosis-tl-sort with numeric prefix sorts by column index."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)]
+          tabulated-list-padding 2)
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries
+          '((1 ["Charlie" "100"])
+            (2 ["Alice" "300"])
+            (3 ["Bob" "200"])))
+    (gnosis-tl-print)
+    ;; Sort by column 1 ("Val") ascending
+    (gnosis-tl-sort 1)
+    (goto-char (point-min))
+    ;; "100" < "200" < "300" (string<)
+    (should (equal (tabulated-list-get-id) 1))   ;; Charlie "100"
+    (forward-line 1)
+    (should (equal (tabulated-list-get-id) 3))   ;; Bob "200"
+    (forward-line 1)
+    (should (equal (tabulated-list-get-id) 2)))) ;; Alice "300"
+
+(ert-deftest gnosis-test-tl-sort-preserves-position ()
+  "gnosis-tl-sort preserves cursor on the same entry."
+  (with-temp-buffer
+    (tabulated-list-mode)
+    (setq tabulated-list-format [("Name" 20 t)]
+          tabulated-list-padding 2)
+    (tabulated-list-init-header)
+    (setq tabulated-list-entries
+          '((1 ["Charlie"])
+            (2 ["Alice"])
+            (3 ["Bob"])))
+    (gnosis-tl-print)
+    ;; Position on Bob (line 3)
+    (goto-char (point-min))
+    (forward-line 2)
+    (should (equal (tabulated-list-get-id) 3))
+    ;; Sort — Bob should still be under cursor
+    (gnosis-tl-sort 0)
+    (should (equal (tabulated-list-get-id) 3))))
+
+(ert-deftest gnosis-test-tl-sort-same-as-tabulated-list-sort ()
+  "gnosis-tl-sort produces same entry order as tabulated-list-sort."
+  (let ((entries '((1 ["Charlie" "300"])
+                   (2 ["Alice" "100"])
+                   (3 ["Bob" "200"]))))
+    ;; Sort with tabulated-list-sort
+    (let (tl-ids)
+      (with-temp-buffer
+        (tabulated-list-mode)
+        (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)]
+              tabulated-list-padding 2
+              tabulated-list-sort-key nil
+              tabulated-list-entries (copy-sequence entries))
+        (tabulated-list-init-header)
+        (tabulated-list-print)
+        (goto-char (point-min))
+        (tabulated-list-sort 0)
+        (goto-char (point-min))
+        (while (not (eobp))
+          (push (tabulated-list-get-id) tl-ids)
+          (forward-line 1))
+        (setq tl-ids (nreverse tl-ids)))
+      ;; Sort with gnosis-tl-sort
+      (let (our-ids)
+        (with-temp-buffer
+          (tabulated-list-mode)
+          (setq tabulated-list-format [("Name" 20 t) ("Val" 10 t)]
+                tabulated-list-padding 2
+                tabulated-list-sort-key nil
+                tabulated-list-entries (copy-sequence entries))
+          (tabulated-list-init-header)
+          (gnosis-tl-print)
+          (goto-char (point-min))
+          (gnosis-tl-sort 0)
+          (goto-char (point-min))
+          (while (not (eobp))
+            (push (tabulated-list-get-id) our-ids)
+            (forward-line 1))
+          (setq our-ids (nreverse our-ids)))
+        (should (equal tl-ids our-ids))))))
+
+;; ──────────────────────────────────────────────────────────
+;; gnosis-tl-print equivalence tests
+;; ──────────────────────────────────────────────────────────
+
+(ert-deftest gnosis-test-tl-print-same-ids-as-tabulated-list-print ()
+  "gnosis-tl-print produces the same entry IDs per line as 
tabulated-list-print."
+  (let* ((fmt [("Name" 20 t) ("Val" 10 t)])
+         (entries '((1 ["Alice" "100"])
+                    (2 ["Bob" "200"])
+                    (3 ["Carol" "300"]))))
+    ;; Render with tabulated-list-print
+    (let (tl-ids)
+      (with-temp-buffer
+        (tabulated-list-mode)
+        (setq tabulated-list-format fmt
+              tabulated-list-padding 2
+              tabulated-list-sort-key nil
+              tabulated-list-entries (copy-sequence entries))
+        (tabulated-list-init-header)
+        (tabulated-list-print)
+        (goto-char (point-min))
+        (while (not (eobp))
+          (push (tabulated-list-get-id) tl-ids)
+          (forward-line 1))
+        (setq tl-ids (nreverse tl-ids)))
+      ;; Render with gnosis-tl-print
+      (let (our-ids)
+        (with-temp-buffer
+          (tabulated-list-mode)
+          (setq tabulated-list-format fmt
+                tabulated-list-padding 2
+                tabulated-list-sort-key nil
+                tabulated-list-entries (copy-sequence entries))
+          (tabulated-list-init-header)
+          (gnosis-tl-print)
+          (goto-char (point-min))
+          (while (not (eobp))
+            (push (tabulated-list-get-id) our-ids)
+            (forward-line 1))
+          (setq our-ids (nreverse our-ids)))
+        ;; Same IDs in same order
+        (should (equal tl-ids our-ids))))))
+
+;; ──────────────────────────────────────────────────────────
+;; gnosis-dashboard--compute-column-format tests
+;; ──────────────────────────────────────────────────────────
+
+(ert-deftest gnosis-test-dashboard-compute-column-format ()
+  "compute-column-format returns a 6-column format vector."
+  (let ((fmt (gnosis-dashboard--compute-column-format 120)))
+    (should (vectorp fmt))
+    (should (= (length fmt) 6))
+    ;; Column names
+    (should (equal (car (aref fmt 0)) "Keimenon"))
+    (should (equal (car (aref fmt 5)) "Suspend"))
+    ;; Widths are positive
+    (dotimes (i 6)
+      (should (> (nth 1 (aref fmt i)) 0)))))
+
+(ert-deftest gnosis-test-dashboard-compute-column-format-narrow ()
+  "compute-column-format enforces minimum widths for narrow windows."
+  (let ((fmt (gnosis-dashboard--compute-column-format 30)))
+    ;; Minimums enforced
+    (should (>= (nth 1 (aref fmt 0)) 10))  ;; Keimenon
+    (should (>= (nth 1 (aref fmt 4)) 5))   ;; Type
+    (should (>= (nth 1 (aref fmt 5)) 3)))) ;; Suspend
+
+;; ──────────────────────────────────────────────────────────
+;; Benchmark tests
+;; ──────────────────────────────────────────────────────────
+
+(ert-deftest gnosis-test-dashboard-print-entry-benchmark ()
+  "Benchmark gnosis-tl-print vs tabulated-list-print at various sizes."
+  (let ((fmt [("Keimenon" 30 t) ("Hypothesis" 20 t) ("Answer" 20 t)
+              ("Tags" 15 t) ("Type" 10 t) ("Suspend" 10 t)])
+        (cols ["A sample keimenon text" "some hypothesis"
+               "the answer" "tag1,tag2" "basic" "No"]))
+    (dolist (n '(2000 10000 40000))
+      (let ((entries (cl-loop for i from 1 to n
+                              collect (list i cols))))
+        ;; tabulated-list-print
+        (with-temp-buffer
+          (tabulated-list-mode)
+          (setq tabulated-list-format fmt
+                tabulated-list-padding 2
+                tabulated-list-sort-key nil
+                tabulated-list-entries entries)
+          (tabulated-list-init-header)
+          (let ((start (float-time)))
+            (tabulated-list-print t)
+            (message "tabulated-list-print x%d: %.3fs (%.0fus/entry)"
+                     n (- (float-time) start)
+                     (* 1e6 (/ (- (float-time) start) n)))))
+        ;; gnosis-tl-print
+        (with-temp-buffer
+          (tabulated-list-mode)
+          (setq tabulated-list-format fmt
+                tabulated-list-padding 2
+                tabulated-list-sort-key nil
+                tabulated-list-entries entries)
+          (tabulated-list-init-header)
+          (let ((start (float-time)))
+            (gnosis-tl-print)
+            (message "gnosis-tl-print x%d: %.3fs (%.0fus/entry)"
+                     n (- (float-time) start)
+                     (* 1e6 (/ (- (float-time) start) n)))))
+        ;; gnosis-tl-render-lines (pure, no buffer)
+        (let ((start (float-time)))
+          (gnosis-tl-render-lines entries fmt 2)
+          (message "gnosis-tl-render-lines x%d: %.3fs (%.0fus/entry)"
+                   n (- (float-time) start)
+                   (* 1e6 (/ (- (float-time) start) n))))))
+    (should t)))
 
 (ert-run-tests-batch-and-exit)

Reply via email to