summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-browser-config.el277
-rw-r--r--tests/test-jumper.el352
-rw-r--r--tests/test-lorem-optimum-benchmark.el227
-rw-r--r--tests/test-lorem-optimum.el242
-rw-r--r--tests/test-test-runner.el359
5 files changed, 1457 insertions, 0 deletions
diff --git a/tests/test-browser-config.el b/tests/test-browser-config.el
new file mode 100644
index 00000000..6ab756dd
--- /dev/null
+++ b/tests/test-browser-config.el
@@ -0,0 +1,277 @@
+;;; test-browser-config.el --- Tests for browser-config.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for browser-config.el - browser selection and configuration.
+;;
+;; Testing approach:
+;; - Tests focus on internal `cj/--do-*` functions (pure business logic)
+;; - File I/O tests use temp files
+;; - executable-find is stubbed to control available browsers
+;; - Each test is isolated with setup/teardown
+;; - Tests verify return values, not user messages
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module with temp file to avoid polluting real config
+(defvar test-browser--temp-choice-file nil
+ "Temporary file for browser choice during tests.")
+
+(defun test-browser-setup ()
+ "Setup test environment before each test."
+ (setq test-browser--temp-choice-file (make-temp-file "browser-choice-test" nil ".el"))
+ (setq cj/browser-choice-file test-browser--temp-choice-file))
+
+(defun test-browser-teardown ()
+ "Clean up test environment after each test."
+ (when (and test-browser--temp-choice-file
+ (file-exists-p test-browser--temp-choice-file))
+ (delete-file test-browser--temp-choice-file))
+ (setq test-browser--temp-choice-file nil))
+
+;; Now require the module
+(require 'browser-config)
+
+;;; Helper Functions
+
+(defun test-browser-make-plist (name &optional executable path)
+ "Create a test browser plist with NAME, EXECUTABLE, and PATH."
+ (list :function 'eww-browse-url
+ :name name
+ :executable executable
+ :path path
+ :program-var nil))
+
+;;; Normal Cases - Discover Browsers
+
+(ert-deftest test-browser-discover-finds-eww ()
+ "Should always find built-in EWW browser."
+ (test-browser-setup)
+ (let ((browsers (cj/discover-browsers)))
+ (should (cl-find-if (lambda (b) (string= (plist-get b :name) "EWW (Emacs Browser)"))
+ browsers)))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-discover-deduplicates-names ()
+ "Should not return duplicate browser names."
+ (test-browser-setup)
+ (let ((browsers (cj/discover-browsers))
+ (names (mapcar (lambda (b) (plist-get b :name)) (cj/discover-browsers))))
+ (should (= (length names) (length (cl-remove-duplicates names :test 'string=)))))
+ (test-browser-teardown))
+
+;;; Normal Cases - Apply Browser Choice
+
+(ert-deftest test-browser-apply-valid-browser ()
+ "Should successfully apply a valid browser configuration."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Test Browser")))
+ (let ((result (cj/--do-apply-browser-choice browser)))
+ (should (eq result 'success))
+ (should (eq browse-url-browser-function 'eww-browse-url))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-apply-sets-program-var ()
+ "Should set browser program variable if specified."
+ (test-browser-setup)
+ (let ((browser (list :function 'browse-url-chrome
+ :name "Chrome"
+ :executable "chrome"
+ :path "/usr/bin/chrome"
+ :program-var 'browse-url-chrome-program)))
+ (cj/--do-apply-browser-choice browser)
+ (should (string= browse-url-chrome-program "/usr/bin/chrome")))
+ (test-browser-teardown))
+
+;;; Normal Cases - Save and Load
+
+(ert-deftest test-browser-save-and-load-choice ()
+ "Should save and load browser choice correctly."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Saved Browser" "firefox" "/usr/bin/firefox")))
+ (cj/save-browser-choice browser)
+ (let ((loaded (cj/load-browser-choice)))
+ (should loaded)
+ (should (string= (plist-get loaded :name) "Saved Browser"))
+ (should (string= (plist-get loaded :executable) "firefox"))))
+ (test-browser-teardown))
+
+;;; Normal Cases - Choose Browser
+
+(ert-deftest test-browser-choose-saves-and-applies ()
+ "Should save and apply browser choice."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Test")))
+ (let ((result (cj/--do-choose-browser browser)))
+ (should (eq result 'success))
+ ;; Verify it was saved
+ (let ((loaded (cj/load-browser-choice)))
+ (should (string= (plist-get loaded :name) "Test")))))
+ (test-browser-teardown))
+
+;;; Normal Cases - Initialize Browser
+
+(ert-deftest test-browser-initialize-with-saved-choice ()
+ "Should load and use saved browser choice."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Saved")))
+ (cj/save-browser-choice browser)
+ (let ((result (cj/--do-initialize-browser)))
+ (should (eq (car result) 'loaded))
+ (should (plist-get (cdr result) :name))
+ (should (string= (plist-get (cdr result) :name) "Saved"))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-initialize-without-saved-choice ()
+ "Should use first available browser when no saved choice."
+ (test-browser-setup)
+ ;; Delete any saved choice
+ (when (file-exists-p cj/browser-choice-file)
+ (delete-file cj/browser-choice-file))
+ (let ((result (cj/--do-initialize-browser)))
+ (should (eq (car result) 'first-available))
+ (should (plist-get (cdr result) :name)))
+ (test-browser-teardown))
+
+;;; Boundary Cases - Apply Browser
+
+(ert-deftest test-browser-apply-nil-plist ()
+ "Should return 'invalid-plist for nil browser."
+ (test-browser-setup)
+ (let ((result (cj/--do-apply-browser-choice nil)))
+ (should (eq result 'invalid-plist)))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-apply-missing-function ()
+ "Should return 'invalid-plist when :function is missing."
+ (test-browser-setup)
+ (let ((browser (list :name "Bad Browser" :function nil)))
+ (let ((result (cj/--do-apply-browser-choice browser)))
+ (should (eq result 'invalid-plist))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-apply-with-nil-path ()
+ "Should handle nil path for built-in browser."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "EWW" nil nil)))
+ (let ((result (cj/--do-apply-browser-choice browser)))
+ (should (eq result 'success))))
+ (test-browser-teardown))
+
+;;; Boundary Cases - Save and Load
+
+(ert-deftest test-browser-load-nonexistent-file ()
+ "Should return nil when loading from nonexistent file."
+ (test-browser-setup)
+ (when (file-exists-p cj/browser-choice-file)
+ (delete-file cj/browser-choice-file))
+ (let ((result (cj/load-browser-choice)))
+ (should (null result)))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-load-corrupt-file ()
+ "Should return nil when loading corrupt file."
+ (test-browser-setup)
+ (with-temp-file cj/browser-choice-file
+ (insert "this is not valid elisp {{{"))
+ (let ((result (cj/load-browser-choice)))
+ (should (null result)))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-load-file-without-variable ()
+ "Should return nil when file doesn't define expected variable."
+ (test-browser-setup)
+ (with-temp-file cj/browser-choice-file
+ (insert "(setq some-other-variable 'foo)"))
+ ;; Unset any previously loaded variable
+ (makunbound 'cj/saved-browser-choice)
+ (let ((result (cj/load-browser-choice)))
+ (should (null result)))
+ (test-browser-teardown))
+
+;;; Boundary Cases - Choose Browser
+
+(ert-deftest test-browser-choose-empty-plist ()
+ "Should handle empty plist gracefully."
+ (test-browser-setup)
+ (let ((result (cj/--do-choose-browser nil)))
+ (should (eq result 'invalid-plist)))
+ (test-browser-teardown))
+
+;;; Error Cases - File Operations
+
+(ert-deftest test-browser-save-to-readonly-location ()
+ "Should return 'save-failed when cannot write file."
+ (test-browser-setup)
+ ;; Make file read-only
+ (with-temp-file cj/browser-choice-file
+ (insert ";; test"))
+ (set-file-modes cj/browser-choice-file #o444)
+ (let ((browser (test-browser-make-plist "Test"))
+ (result nil))
+ (setq result (cj/--do-choose-browser browser))
+ ;; Restore permissions before teardown
+ (set-file-modes cj/browser-choice-file #o644)
+ (should (eq result 'save-failed)))
+ (test-browser-teardown))
+
+;;; Browser Discovery Tests
+
+(ert-deftest test-browser-discover-returns-plists ()
+ "Should return properly formatted browser plists."
+ (test-browser-setup)
+ (let ((browsers (cj/discover-browsers)))
+ (should (> (length browsers) 0))
+ (dolist (browser browsers)
+ (should (plist-member browser :function))
+ (should (plist-member browser :name))
+ (should (plist-member browser :executable))
+ (should (plist-member browser :path))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-format-location-keys ()
+ "Should have all required keys in browser plist."
+ (test-browser-setup)
+ (let ((browsers (cj/discover-browsers)))
+ (when browsers
+ (let ((browser (car browsers)))
+ (should (plist-get browser :function))
+ (should (plist-get browser :name)))))
+ (test-browser-teardown))
+
+;;; Integration Tests
+
+(ert-deftest test-browser-full-cycle ()
+ "Should handle full save-load-apply cycle."
+ (test-browser-setup)
+ (let ((browser (test-browser-make-plist "Cycle Test" "test-browser" "/usr/bin/test")))
+ ;; Choose (save and apply)
+ (should (eq (cj/--do-choose-browser browser) 'success))
+ ;; Verify it was saved
+ (let ((loaded (cj/load-browser-choice)))
+ (should loaded)
+ (should (string= (plist-get loaded :name) "Cycle Test")))
+ ;; Initialize should load the saved choice
+ (let ((result (cj/--do-initialize-browser)))
+ (should (eq (car result) 'loaded))
+ (should (string= (plist-get (cdr result) :name) "Cycle Test"))))
+ (test-browser-teardown))
+
+(ert-deftest test-browser-overwrite-choice ()
+ "Should overwrite previous browser choice."
+ (test-browser-setup)
+ (let ((browser1 (test-browser-make-plist "First"))
+ (browser2 (test-browser-make-plist "Second")))
+ (cj/--do-choose-browser browser1)
+ (cj/--do-choose-browser browser2)
+ (let ((loaded (cj/load-browser-choice)))
+ (should (string= (plist-get loaded :name) "Second"))))
+ (test-browser-teardown))
+
+(provide 'test-browser-config)
+;;; test-browser-config.el ends here
diff --git a/tests/test-jumper.el b/tests/test-jumper.el
new file mode 100644
index 00000000..fa65d3f4
--- /dev/null
+++ b/tests/test-jumper.el
@@ -0,0 +1,352 @@
+;;; test-jumper.el --- Tests for jumper.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for jumper.el - location navigation using registers.
+;;
+;; Testing approach:
+;; - Tests focus on internal `jumper--do-*` functions (pure business logic)
+;; - Interactive wrappers are thin UI layers and tested minimally
+;; - Each test is isolated with setup/teardown to reset global state
+;; - Tests verify return values, not user messages
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module
+(require 'jumper)
+
+;;; Test Utilities
+
+(defvar test-jumper--original-registers nil
+ "Backup of jumper registers before test.")
+
+(defvar test-jumper--original-index nil
+ "Backup of jumper index before test.")
+
+(defun test-jumper-setup ()
+ "Reset jumper state before each test."
+ ;; Backup current state
+ (setq test-jumper--original-registers jumper--registers)
+ (setq test-jumper--original-index jumper--next-index)
+ ;; Reset to clean state
+ (setq jumper--registers (make-vector jumper-max-locations nil))
+ (setq jumper--next-index 0))
+
+(defun test-jumper-teardown ()
+ "Restore jumper state after each test."
+ (setq jumper--registers test-jumper--original-registers)
+ (setq jumper--next-index test-jumper--original-index))
+
+;;; Normal Cases - Store Location
+
+(ert-deftest test-jumper-store-first-location ()
+ "Should store first location and return register character."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test content")
+ (goto-char (point-min))
+ (let ((result (jumper--do-store-location)))
+ (should (= result ?0))
+ (should (= jumper--next-index 1))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-store-multiple-locations ()
+ "Should store multiple locations in sequence."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (should (= (jumper--do-store-location) ?0))
+ (forward-line 1)
+ (should (= (jumper--do-store-location) ?1))
+ (forward-line 1)
+ (should (= (jumper--do-store-location) ?2))
+ (should (= jumper--next-index 3)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-store-duplicate-location ()
+ "Should detect and reject duplicate locations."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test content")
+ (goto-char (point-min))
+ (should (= (jumper--do-store-location) ?0))
+ (should (eq (jumper--do-store-location) 'already-exists))
+ (should (= jumper--next-index 1)))
+ (test-jumper-teardown))
+
+;;; Normal Cases - Jump to Location
+
+(ert-deftest test-jumper-jump-to-stored-location ()
+ "Should jump to a previously stored location."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (goto-char (point-max))
+ (let ((result (jumper--do-jump-to-location 0)))
+ (should (eq result 'jumped))
+ (should (= (point) (point-min)))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-toggle-with-single-location ()
+ "Should toggle between current and stored location."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ ;; Move away
+ (goto-char (point-max))
+ ;; Toggle should jump back
+ (let ((result (jumper--do-jump-to-location nil)))
+ (should (eq result 'jumped))
+ (should (= (point) (point-min)))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-already-at-location ()
+ "Should detect when already at the only stored location."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ ;; Try to toggle while at the location
+ (let ((result (jumper--do-jump-to-location nil)))
+ (should (eq result 'already-there))))
+ (test-jumper-teardown))
+
+;;; Normal Cases - Remove Location
+
+(ert-deftest test-jumper-remove-location ()
+ "Should remove a stored location."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test content")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (let ((result (jumper--do-remove-location 0)))
+ (should (eq result t))
+ (should (= jumper--next-index 0))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-remove-reorders-registers ()
+ "Should reorder registers after removal from middle."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location) ; Register 0
+ (forward-line 1)
+ (jumper--do-store-location) ; Register 1
+ (forward-line 1)
+ (jumper--do-store-location) ; Register 2
+ ;; Remove middle (index 1)
+ (jumper--do-remove-location 1)
+ (should (= jumper--next-index 2))
+ ;; What was at index 2 should now be at index 1
+ (should (= (aref jumper--registers 1) ?2)))
+ (test-jumper-teardown))
+
+;;; Boundary Cases - Store Location
+
+(ert-deftest test-jumper-store-at-capacity ()
+ "Should successfully store location at maximum capacity."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test content")
+ (goto-char (point-min))
+ ;; Fill to capacity
+ (dotimes (i jumper-max-locations)
+ (forward-char 1)
+ (should (= (jumper--do-store-location) (+ ?0 i))))
+ (should (= jumper--next-index jumper-max-locations)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-store-when-full ()
+ "Should return 'no-space when all registers are full."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "01234567890123456789")
+ (goto-char (point-min))
+ ;; Fill to capacity
+ (dotimes (i jumper-max-locations)
+ (forward-char 1)
+ (jumper--do-store-location))
+ ;; Try to store one more
+ (forward-char 1)
+ (should (eq (jumper--do-store-location) 'no-space))
+ (should (= jumper--next-index jumper-max-locations)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-store-in-different-buffers ()
+ "Should store locations across different buffers."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "buffer 1")
+ (goto-char (point-min))
+ (should (= (jumper--do-store-location) ?0))
+ (with-temp-buffer
+ (insert "buffer 2")
+ (goto-char (point-min))
+ (should (= (jumper--do-store-location) ?1))
+ (should (= jumper--next-index 2))))
+ (test-jumper-teardown))
+
+;;; Boundary Cases - Jump to Location
+
+(ert-deftest test-jumper-jump-with-no-locations ()
+ "Should return 'no-locations when nothing is stored."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test")
+ (let ((result (jumper--do-jump-to-location 0)))
+ (should (eq result 'no-locations))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-to-first-location ()
+ "Should jump to location at index 0."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (forward-line 1)
+ (jumper--do-store-location)
+ (goto-char (point-max))
+ (jumper--do-jump-to-location 0)
+ (should (= (point) (point-min))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-to-last-location ()
+ "Should jump to last location (register 'z)."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (let ((line2-pos (line-beginning-position 2)))
+ (goto-char line2-pos)
+ ;; Jump to location 0 (this stores current location in 'z)
+ (jumper--do-jump-to-location 0)
+ (should (= (point) (point-min)))
+ ;; Jump to last location should go back to line 2
+ (let ((result (jumper--do-jump-to-location -1)))
+ (should (eq result 'jumped))
+ (should (= (point) line2-pos)))))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-jump-to-max-index ()
+ "Should jump to location at maximum index."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "0123456789012345678")
+ (goto-char (point-min))
+ ;; Store at all positions
+ (dotimes (i jumper-max-locations)
+ (forward-char 1)
+ (jumper--do-store-location))
+ (goto-char (point-min))
+ ;; Jump to last one (index 9, which is at position 10)
+ (jumper--do-jump-to-location (1- jumper-max-locations))
+ (should (= (point) (1+ jumper-max-locations))))
+ (test-jumper-teardown))
+
+;;; Boundary Cases - Remove Location
+
+(ert-deftest test-jumper-remove-first-location ()
+ "Should remove location at index 0."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (forward-line 1)
+ (jumper--do-store-location)
+ (jumper--do-remove-location 0)
+ (should (= jumper--next-index 1))
+ ;; What was at index 1 should now be at index 0
+ (should (= (aref jumper--registers 0) ?1)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-remove-last-location ()
+ "Should remove location at last index."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "line 1\nline 2\nline 3")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (forward-line 1)
+ (jumper--do-store-location)
+ (forward-line 1)
+ (jumper--do-store-location)
+ (jumper--do-remove-location 2)
+ (should (= jumper--next-index 2)))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-remove-with-cancel ()
+ "Should return 'cancelled when index is -1."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (let ((result (jumper--do-remove-location -1)))
+ (should (eq result 'cancelled))
+ (should (= jumper--next-index 1))))
+ (test-jumper-teardown))
+
+;;; Error Cases
+
+(ert-deftest test-jumper-remove-when-empty ()
+ "Should return 'no-locations when removing from empty list."
+ (test-jumper-setup)
+ (let ((result (jumper--do-remove-location 0)))
+ (should (eq result 'no-locations)))
+ (test-jumper-teardown))
+
+;;; Helper Function Tests
+
+(ert-deftest test-jumper-location-key-format ()
+ "Should generate unique location keys."
+ (with-temp-buffer
+ (insert "line 1\nline 2")
+ (goto-char (point-min))
+ (let ((key1 (jumper--location-key)))
+ (forward-line 1)
+ (let ((key2 (jumper--location-key)))
+ (should-not (string= key1 key2))
+ ;; Keys should contain buffer name and position info
+ (should (string-match-p ":" key1))
+ (should (string-match-p ":" key2))))))
+
+(ert-deftest test-jumper-register-available-p ()
+ "Should correctly report register availability."
+ (test-jumper-setup)
+ (should (jumper--register-available-p))
+ ;; Fill to capacity
+ (setq jumper--next-index jumper-max-locations)
+ (should-not (jumper--register-available-p))
+ (test-jumper-teardown))
+
+(ert-deftest test-jumper-format-location ()
+ "Should format location for display."
+ (test-jumper-setup)
+ (with-temp-buffer
+ (insert "test line with some content")
+ (goto-char (point-min))
+ (jumper--do-store-location)
+ (let ((formatted (jumper--format-location 0)))
+ (should formatted)
+ (should (string-match-p "\\[0\\]" formatted))
+ (should (string-match-p "test line" formatted))))
+ (test-jumper-teardown))
+
+(provide 'test-jumper)
+;;; test-jumper.el ends here
diff --git a/tests/test-lorem-optimum-benchmark.el b/tests/test-lorem-optimum-benchmark.el
new file mode 100644
index 00000000..d3ca2873
--- /dev/null
+++ b/tests/test-lorem-optimum-benchmark.el
@@ -0,0 +1,227 @@
+;;; test-lorem-optimum-benchmark.el --- Performance tests for lorem-optimum.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Benchmark and performance tests for the Markov chain implementation.
+;;
+;; These tests measure:
+;; - Learning time scaling with input size
+;; - Multiple learning operations (exposes key rebuild overhead)
+;; - Generation time scaling
+;; - Memory usage (hash table growth)
+;;
+;; Performance baseline targets (on modern hardware):
+;; - Learn 1000 words: < 10ms
+;; - Learn 10,000 words: < 100ms
+;; - 100 learn operations of 100 words each: < 500ms (current bottleneck!)
+;; - Generate 100 words: < 5ms
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module
+(require 'lorem-optimum)
+
+;;; Benchmark Helpers
+
+(defun benchmark-time (func)
+ "Time execution of FUNC and return milliseconds."
+ (let ((start (current-time)))
+ (funcall func)
+ (let ((end (current-time)))
+ (* 1000.0 (float-time (time-subtract end start))))))
+
+(defun generate-test-text (word-count)
+ "Generate WORD-COUNT words of test text with some repetition."
+ (let ((words '("lorem" "ipsum" "dolor" "sit" "amet" "consectetur"
+ "adipiscing" "elit" "sed" "do" "eiusmod" "tempor"
+ "incididunt" "ut" "labore" "et" "dolore" "magna" "aliqua"))
+ (result '()))
+ (dotimes (i word-count)
+ (push (nth (mod i (length words)) words) result)
+ (when (zerop (mod i 10))
+ (push "." result)))
+ (mapconcat #'identity (nreverse result) " ")))
+
+(defun benchmark-report (name time-ms)
+ "Report benchmark NAME with TIME-MS."
+ (message "BENCHMARK [%s]: %.2f ms" name time-ms))
+
+;;; Learning Performance Tests
+
+(ert-deftest benchmark-learn-1k-words ()
+ "Benchmark learning 1000 words."
+ (let* ((text (generate-test-text 1000))
+ (chain (cj/markov-chain-create))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (benchmark-report "Learn 1K words" time)
+ (should (< time 50.0)))) ; Should be < 50ms
+
+(ert-deftest benchmark-learn-10k-words ()
+ "Benchmark learning 10,000 words."
+ (let* ((text (generate-test-text 10000))
+ (chain (cj/markov-chain-create))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (benchmark-report "Learn 10K words" time)
+ (should (< time 500.0)))) ; Should be < 500ms
+
+(ert-deftest benchmark-learn-100k-words ()
+ "Benchmark learning 100,000 words (stress test)."
+ :tags '(:slow)
+ (let* ((text (generate-test-text 100000))
+ (chain (cj/markov-chain-create))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (benchmark-report "Learn 100K words" time)
+ ;; This may be slow due to key rebuild
+ (message "Hash table size: %d bigrams"
+ (hash-table-count (cj/markov-chain-map chain)))))
+
+;;; Multiple Learning Operations (Exposes Quadratic Behavior)
+
+(ert-deftest benchmark-multiple-learns-10x100 ()
+ "Benchmark 10 learn operations of 100 words each."
+ (let ((chain (cj/markov-chain-create))
+ (times '()))
+ (dotimes (i 10)
+ (let* ((text (generate-test-text 100))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (push time times)))
+ (let ((total (apply #'+ times))
+ (avg (/ (apply #'+ times) 10.0))
+ (max-time (apply #'max times)))
+ (benchmark-report "10x learn 100 words - TOTAL" total)
+ (benchmark-report "10x learn 100 words - AVG" avg)
+ (benchmark-report "10x learn 100 words - MAX" max-time)
+ (message "Times: %S" (nreverse times))
+ ;; Note: Watch if later operations are slower (quadratic behavior)
+ (should (< total 100.0))))) ; Total should be < 100ms
+
+(ert-deftest benchmark-multiple-learns-100x100 ()
+ "Benchmark 100 learn operations of 100 words each (key rebuild overhead)."
+ :tags '(:slow)
+ (let ((chain (cj/markov-chain-create))
+ (times '())
+ (measurements '()))
+ (dotimes (i 100)
+ (let* ((text (generate-test-text 100))
+ (time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (push time times)
+ ;; Sample measurements every 10 iterations
+ (when (zerop (mod i 10))
+ (push (cons i time) measurements))))
+ (let ((total (apply #'+ times))
+ (avg (/ (apply #'+ times) 100.0))
+ (first-10-avg (/ (apply #'+ (last times 10)) 10.0))
+ (last-10-avg (/ (apply #'+ (seq-take times 10)) 10.0)))
+ (benchmark-report "100x learn 100 words - TOTAL" total)
+ (benchmark-report "100x learn 100 words - AVG" avg)
+ (benchmark-report "100x learn - First 10 AVG" first-10-avg)
+ (benchmark-report "100x learn - Last 10 AVG" last-10-avg)
+ (message "Sampled times (iteration, ms): %S" (nreverse measurements))
+ (message "Hash table size: %d bigrams"
+ (hash-table-count (cj/markov-chain-map chain)))
+ ;; This exposes the quadratic behavior: last operations much slower
+ (when (> last-10-avg (* 2.0 first-10-avg))
+ (message "WARNING: Learning slows down significantly over time!")
+ (message " First 10 avg: %.2f ms" first-10-avg)
+ (message " Last 10 avg: %.2f ms" last-10-avg)
+ (message " Ratio: %.1fx slower" (/ last-10-avg first-10-avg))))))
+
+;;; Generation Performance Tests
+
+(ert-deftest benchmark-generate-100-words ()
+ "Benchmark generating 100 words."
+ (let* ((text (generate-test-text 1000))
+ (chain (cj/markov-chain-create)))
+ (cj/markov-learn chain text)
+ (let ((time (benchmark-time
+ (lambda () (cj/markov-generate chain 100)))))
+ (benchmark-report "Generate 100 words" time)
+ (should (< time 20.0))))) ; Should be < 20ms
+
+(ert-deftest benchmark-generate-1000-words ()
+ "Benchmark generating 1000 words."
+ (let* ((text (generate-test-text 10000))
+ (chain (cj/markov-chain-create)))
+ (cj/markov-learn chain text)
+ (let ((time (benchmark-time
+ (lambda () (cj/markov-generate chain 1000)))))
+ (benchmark-report "Generate 1000 words" time)
+ (should (< time 100.0))))) ; Should be < 100ms
+
+;;; Tokenization Performance Tests
+
+(ert-deftest benchmark-tokenize-10k-words ()
+ "Benchmark tokenizing 10,000 words."
+ (let* ((text (generate-test-text 10000))
+ (time (benchmark-time
+ (lambda () (cj/markov-tokenize text)))))
+ (benchmark-report "Tokenize 10K words" time)
+ (should (< time 50.0)))) ; Tokenization should be fast
+
+;;; Memory/Size Tests
+
+(ert-deftest benchmark-chain-growth ()
+ "Measure hash table growth with increasing input."
+ (let ((chain (cj/markov-chain-create))
+ (sizes '()))
+ (dolist (word-count '(100 500 1000 5000 10000))
+ (let ((text (generate-test-text word-count)))
+ (cj/markov-learn chain text)
+ (let ((size (hash-table-count (cj/markov-chain-map chain))))
+ (push (cons word-count size) sizes)
+ (message "After %d words: %d unique bigrams" word-count size))))
+ (message "Growth pattern: %S" (nreverse sizes))))
+
+;;; Comparison: Tokenization vs Learning
+
+(ert-deftest benchmark-tokenize-vs-learn ()
+ "Compare tokenization time to total learning time."
+ (let* ((text (generate-test-text 5000))
+ (tokenize-time (benchmark-time
+ (lambda () (cj/markov-tokenize text))))
+ (chain (cj/markov-chain-create))
+ (learn-time (benchmark-time
+ (lambda () (cj/markov-learn chain text)))))
+ (benchmark-report "Tokenize 5K words" tokenize-time)
+ (benchmark-report "Learn 5K words (total)" learn-time)
+ (message "Tokenization is %.1f%% of total learning time"
+ (* 100.0 (/ tokenize-time learn-time)))))
+
+;;; Real-world Scenario
+
+(ert-deftest benchmark-realistic-usage ()
+ "Benchmark realistic usage: learn from multiple sources, generate paragraphs."
+ (let ((chain (cj/markov-chain-create))
+ (learn-total 0.0)
+ (gen-total 0.0))
+ ;; Simulate learning from 10 different sources
+ (dotimes (i 10)
+ (let ((text (generate-test-text 500)))
+ (setq learn-total
+ (+ learn-total
+ (benchmark-time (lambda () (cj/markov-learn chain text)))))))
+
+ ;; Generate 5 paragraphs
+ (dotimes (i 5)
+ (setq gen-total
+ (+ gen-total
+ (benchmark-time (lambda () (cj/markov-generate chain 50))))))
+
+ (benchmark-report "Realistic: 10 learns (500 words each)" learn-total)
+ (benchmark-report "Realistic: 5 generations (50 words each)" gen-total)
+ (benchmark-report "Realistic: TOTAL TIME" (+ learn-total gen-total))
+ (message "Final chain size: %d bigrams"
+ (hash-table-count (cj/markov-chain-map chain)))))
+
+(provide 'test-lorem-optimum-benchmark)
+;;; test-lorem-optimum-benchmark.el ends here
diff --git a/tests/test-lorem-optimum.el b/tests/test-lorem-optimum.el
new file mode 100644
index 00000000..ca2e52f4
--- /dev/null
+++ b/tests/test-lorem-optimum.el
@@ -0,0 +1,242 @@
+;;; test-lorem-optimum.el --- Tests for lorem-optimum.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for lorem-optimum.el Markov chain text generation.
+;;
+;; Tests cover:
+;; - Tokenization
+;; - Learning and chain building
+;; - Text generation
+;; - Capitalization fixing
+;; - Token joining
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module
+(require 'lorem-optimum)
+
+;;; Test Helpers
+
+(defun test-chain ()
+ "Create a fresh test chain."
+ (cj/markov-chain-create))
+
+(defun test-learn (text)
+ "Create a chain and learn TEXT."
+ (let ((chain (test-chain)))
+ (cj/markov-learn chain text)
+ chain))
+
+;;; Tokenization Tests
+
+(ert-deftest test-tokenize-simple ()
+ "Should tokenize simple words."
+ (let ((result (cj/markov-tokenize "hello world")))
+ (should (equal result '("hello" "world")))))
+
+(ert-deftest test-tokenize-with-punctuation ()
+ "Should separate punctuation as tokens."
+ (let ((result (cj/markov-tokenize "Hello, world!")))
+ (should (equal result '("Hello" "," "world" "!")))))
+
+(ert-deftest test-tokenize-multiple-spaces ()
+ "Should handle multiple spaces."
+ (let ((result (cj/markov-tokenize "hello world")))
+ (should (equal result '("hello" "world")))))
+
+(ert-deftest test-tokenize-newlines ()
+ "Should handle newlines as whitespace."
+ (let ((result (cj/markov-tokenize "hello\nworld")))
+ (should (equal result '("hello" "world")))))
+
+(ert-deftest test-tokenize-mixed-punctuation ()
+ "Should tokenize complex punctuation."
+ (let ((result (cj/markov-tokenize "one, two; three.")))
+ (should (equal result '("one" "," "two" ";" "three" ".")))))
+
+(ert-deftest test-tokenize-empty ()
+ "Should handle empty string."
+ (let ((result (cj/markov-tokenize "")))
+ (should (null result))))
+
+(ert-deftest test-tokenize-whitespace-only ()
+ "Should return nil for whitespace only."
+ (let ((result (cj/markov-tokenize " \n\t ")))
+ (should (null result))))
+
+;;; Markov Learn Tests
+
+(ert-deftest test-learn-basic ()
+ "Should learn simple text."
+ (let ((chain (test-learn "one two three four")))
+ (should (cj/markov-chain-p chain))
+ (should (> (hash-table-count (cj/markov-chain-map chain)) 0))))
+
+(ert-deftest test-learn-creates-bigrams ()
+ "Should create bigram mappings."
+ (let ((chain (test-learn "one two three")))
+ (should (gethash '("one" "two") (cj/markov-chain-map chain)))))
+
+(ert-deftest test-learn-stores-following-word ()
+ "Should store following word for bigram."
+ (let ((chain (test-learn "one two three")))
+ (should (member "three" (gethash '("one" "two") (cj/markov-chain-map chain))))))
+
+(ert-deftest test-learn-builds-keys-list ()
+ "Should build keys list lazily when accessed."
+ (let ((chain (test-learn "one two three four")))
+ ;; Keys are built lazily, so initially nil
+ (should (null (cj/markov-chain-keys chain)))
+ ;; After calling random-key, keys should be built
+ (cj/markov-random-key chain)
+ (should (> (length (cj/markov-chain-keys chain)) 0))))
+
+(ert-deftest test-learn-repeated-patterns ()
+ "Should accumulate repeated patterns."
+ (let ((chain (test-learn "one two three one two four")))
+ (let ((nexts (gethash '("one" "two") (cj/markov-chain-map chain))))
+ (should (= (length nexts) 2))
+ (should (member "three" nexts))
+ (should (member "four" nexts)))))
+
+(ert-deftest test-learn-incremental ()
+ "Should support incremental learning."
+ (let ((chain (test-chain)))
+ (cj/markov-learn chain "one two three")
+ (cj/markov-learn chain "four five six")
+ (should (> (hash-table-count (cj/markov-chain-map chain)) 0))))
+
+;;; Token Joining Tests
+
+(ert-deftest test-join-simple-words ()
+ "Should join words with spaces."
+ (let ((result (cj/markov-join-tokens '("hello" "world"))))
+ (should (string-match-p "^Hello world" result))))
+
+(ert-deftest test-join-with-punctuation ()
+ "Should attach punctuation without spaces."
+ (let ((result (cj/markov-join-tokens '("hello" "," "world"))))
+ (should (string-match-p "Hello, world" result))))
+
+(ert-deftest test-join-capitalizes-first ()
+ "Should capitalize first word."
+ (let ((result (cj/markov-join-tokens '("hello" "world"))))
+ (should (string-match-p "^H" result))))
+
+(ert-deftest test-join-adds-period ()
+ "Should add period if missing."
+ (let ((result (cj/markov-join-tokens '("hello" "world"))))
+ (should (string-match-p "\\.$" result))))
+
+(ert-deftest test-join-preserves-existing-period ()
+ "Should not double-add period."
+ (let ((result (cj/markov-join-tokens '("hello" "world" "."))))
+ (should (string-match-p "\\.$" result))
+ (should-not (string-match-p "\\.\\.$" result))))
+
+(ert-deftest test-join-empty-tokens ()
+ "Should handle empty token list."
+ (let ((result (cj/markov-join-tokens '())))
+ (should (equal result "."))))
+
+;;; Capitalization Tests
+
+(ert-deftest test-capitalize-first-word ()
+ "Should capitalize first word."
+ (let ((result (cj/markov-fix-capitalization "hello world")))
+ (should (string-match-p "^Hello" result))))
+
+(ert-deftest test-capitalize-after-period ()
+ "Should capitalize after period."
+ (let ((result (cj/markov-fix-capitalization "hello. world")))
+ (should (string-match-p "Hello\\. World" result))))
+
+(ert-deftest test-capitalize-after-exclamation ()
+ "Should capitalize after exclamation."
+ (let ((result (cj/markov-fix-capitalization "hello! world")))
+ (should (string-match-p "Hello! World" result))))
+
+(ert-deftest test-capitalize-after-question ()
+ "Should capitalize after question mark."
+ (let ((result (cj/markov-fix-capitalization "hello? world")))
+ (should (string-match-p "Hello\\? World" result))))
+
+(ert-deftest test-capitalize-skip-non-alpha ()
+ "Should skip non-alphabetic tokens."
+ (let ((result (cj/markov-fix-capitalization "hello. 123 world")))
+ (should (string-match-p "123" result))))
+
+(ert-deftest test-capitalize-multiple-sentences ()
+ "Should capitalize all sentences."
+ (let ((result (cj/markov-fix-capitalization "first. second. third")))
+ (should (string-match-p "First\\. Second\\. Third" result))))
+
+;;; Generation Tests (deterministic with fixed chain)
+
+(ert-deftest test-generate-produces-output ()
+ "Should generate non-empty output."
+ (let ((chain (test-learn "Lorem ipsum dolor sit amet consectetur adipiscing elit")))
+ (let ((result (cj/markov-generate chain 5)))
+ (should (stringp result))
+ (should (> (length result) 0)))))
+
+(ert-deftest test-generate-empty-chain ()
+ "Should handle empty chain gracefully."
+ (let ((chain (test-chain)))
+ (let ((result (cj/markov-generate chain 5)))
+ (should (or (null result) (string-empty-p result))))))
+
+(ert-deftest test-generate-respects-start ()
+ "Should use provided start state if available."
+ (let ((chain (test-learn "Lorem ipsum dolor sit amet")))
+ (let ((result (cj/markov-generate chain 3 '("Lorem" "ipsum"))))
+ (should (stringp result))
+ ;; Should start with Lorem or similar
+ (should (> (length result) 0)))))
+
+;;; Integration Tests
+
+(ert-deftest test-full-workflow ()
+ "Should complete full learn-generate workflow."
+ (let ((chain (test-chain)))
+ (cj/markov-learn chain "The quick brown fox jumps over the lazy dog")
+ (let ((result (cj/markov-generate chain 8)))
+ (should (stringp result))
+ (should (> (length result) 0))
+ (should (string-match-p "^[A-Z]" result))
+ (should (string-match-p "[.!?]$" result)))))
+
+(ert-deftest test-latin-like-output ()
+ "Should generate Latin-like text from Latin input."
+ (let ((chain (test-chain)))
+ (cj/markov-learn chain "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
+ (let ((result (cj/markov-generate chain 10)))
+ (should (stringp result))
+ (should (> (length result) 10)))))
+
+;;; Edge Cases
+
+(ert-deftest test-learn-short-text ()
+ "Should handle text shorter than trigram."
+ (let ((chain (test-learn "one two")))
+ (should (cj/markov-chain-p chain))))
+
+(ert-deftest test-learn-single-word ()
+ "Should handle single word."
+ (let ((chain (test-learn "word")))
+ (should (cj/markov-chain-p chain))))
+
+(ert-deftest test-generate-requested-count-small ()
+ "Should handle small generation count."
+ (let ((chain (test-learn "one two three four five")))
+ (let ((result (cj/markov-generate chain 2)))
+ (should (stringp result)))))
+
+(provide 'test-lorem-optimum)
+;;; test-lorem-optimum.el ends here
diff --git a/tests/test-test-runner.el b/tests/test-test-runner.el
new file mode 100644
index 00000000..0edc0d65
--- /dev/null
+++ b/tests/test-test-runner.el
@@ -0,0 +1,359 @@
+;;; test-test-runner.el --- Tests for test-runner.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for test-runner.el - ERT test runner with focus/unfocus workflow.
+;;
+;; Testing approach:
+;; - Tests focus on internal `cj/test--do-*` functions (pure business logic)
+;; - File system operations use temp directories
+;; - Tests are isolated with setup/teardown
+;; - Tests verify return values, not user messages
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Load the module (ignore keymap error in batch mode)
+(condition-case nil
+ (require 'test-runner)
+ (error nil))
+
+;;; Test Utilities
+
+(defvar test-testrunner--temp-dir nil
+ "Temporary directory for test files during tests.")
+
+(defvar test-testrunner--original-focused-files nil
+ "Backup of focused files list before test.")
+
+(defun test-testrunner-setup ()
+ "Setup test environment before each test."
+ ;; Backup current state
+ (setq test-testrunner--original-focused-files cj/test-focused-files)
+ ;; Reset to clean state
+ (setq cj/test-focused-files '())
+ ;; Create temp directory for file tests
+ (setq test-testrunner--temp-dir (make-temp-file "test-runner-test" t)))
+
+(defun test-testrunner-teardown ()
+ "Clean up test environment after each test."
+ ;; Restore state
+ (setq cj/test-focused-files test-testrunner--original-focused-files)
+ ;; Clean up temp directory
+ (when (and test-testrunner--temp-dir
+ (file-directory-p test-testrunner--temp-dir))
+ (delete-directory test-testrunner--temp-dir t))
+ (setq test-testrunner--temp-dir nil))
+
+(defun test-testrunner-create-test-file (filename content)
+ "Create test file FILENAME with CONTENT in temp directory."
+ (let ((filepath (expand-file-name filename test-testrunner--temp-dir)))
+ (with-temp-file filepath
+ (insert content))
+ filepath))
+
+;;; Normal Cases - Load Files
+
+(ert-deftest test-testrunner-load-files-success ()
+ "Should successfully load test files."
+ (test-testrunner-setup)
+ (let* ((file1 (test-testrunner-create-test-file "test-simple.el"
+ "(defun test-func () t)"))
+ (file2 (test-testrunner-create-test-file "test-other.el"
+ "(defun other-func () nil)"))
+ (result (cj/test--do-load-files test-testrunner--temp-dir
+ (list file1 file2))))
+ (should (eq (car result) 'success))
+ (should (= (cdr result) 2)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-load-files-with-errors ()
+ "Should handle errors during file loading."
+ (test-testrunner-setup)
+ (let* ((good-file (test-testrunner-create-test-file "test-good.el"
+ "(defun good () t)"))
+ (bad-file (test-testrunner-create-test-file "test-bad.el"
+ "(defun bad ( "))
+ (result (cj/test--do-load-files test-testrunner--temp-dir
+ (list good-file bad-file))))
+ (should (eq (car result) 'error))
+ (should (= (nth 1 result) 1)) ; loaded-count
+ (should (= (length (nth 2 result)) 1))) ; errors list
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Focus Add
+
+(ert-deftest test-testrunner-focus-add-success ()
+ "Should successfully add file to focus."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "test-foo.el"
+ '("test-foo.el" "test-bar.el")
+ '())))
+ (should (eq result 'success)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-already-focused ()
+ "Should detect already focused file."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "test-foo.el"
+ '("test-foo.el" "test-bar.el")
+ '("test-foo.el"))))
+ (should (eq result 'already-focused)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-not-available ()
+ "Should detect file not in available list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "test-missing.el"
+ '("test-foo.el" "test-bar.el")
+ '())))
+ (should (eq result 'not-available)))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Focus Add File
+
+(ert-deftest test-testrunner-focus-add-file-success ()
+ "Should successfully validate and add file to focus."
+ (test-testrunner-setup)
+ (let* ((filepath (expand-file-name "test-foo.el" test-testrunner--temp-dir))
+ (result (cj/test--do-focus-add-file filepath test-testrunner--temp-dir '())))
+ (should (eq (car result) 'success))
+ (should (string= (cdr result) "test-foo.el")))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-file-no-file ()
+ "Should detect nil filepath."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add-file nil test-testrunner--temp-dir '())))
+ (should (eq (car result) 'no-file)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-file-not-in-testdir ()
+ "Should detect file outside test directory."
+ (test-testrunner-setup)
+ (let* ((filepath "/tmp/outside-test.el")
+ (result (cj/test--do-focus-add-file filepath test-testrunner--temp-dir '())))
+ (should (eq (car result) 'not-in-testdir)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-file-already-focused ()
+ "Should detect already focused file."
+ (test-testrunner-setup)
+ (let* ((filepath (expand-file-name "test-foo.el" test-testrunner--temp-dir))
+ (result (cj/test--do-focus-add-file filepath
+ test-testrunner--temp-dir
+ '("test-foo.el"))))
+ (should (eq (car result) 'already-focused))
+ (should (string= (cdr result) "test-foo.el")))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Focus Remove
+
+(ert-deftest test-testrunner-focus-remove-success ()
+ "Should successfully remove file from focus."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-remove "test-foo.el" '("test-foo.el" "test-bar.el"))))
+ (should (eq result 'success)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-remove-empty-list ()
+ "Should detect empty focused list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-remove "test-foo.el" '())))
+ (should (eq result 'empty-list)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-remove-not-found ()
+ "Should detect file not in focused list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-remove "test-missing.el" '("test-foo.el"))))
+ (should (eq result 'not-found)))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Get Focused Tests
+
+(ert-deftest test-testrunner-get-focused-tests-success ()
+ "Should extract test names from focused files."
+ (test-testrunner-setup)
+ (let* ((file1 (test-testrunner-create-test-file "test-first.el"
+ "(ert-deftest test-alpha-one () (should t))\n(ert-deftest test-alpha-two () (should t))"))
+ (result (cj/test--do-get-focused-tests '("test-first.el") test-testrunner--temp-dir)))
+ (should (eq (car result) 'success))
+ (should (= (length (nth 1 result)) 2)) ; 2 test names
+ (should (= (nth 2 result) 1))) ; 1 file loaded
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-get-focused-tests-empty-list ()
+ "Should detect empty focused files list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-get-focused-tests '() test-testrunner--temp-dir)))
+ (should (eq (car result) 'empty-list)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-get-focused-tests-no-tests ()
+ "Should detect when no tests found in files."
+ (test-testrunner-setup)
+ (test-testrunner-create-test-file "test-empty.el" "(defun not-a-test () t)")
+ (let ((result (cj/test--do-get-focused-tests '("test-empty.el") test-testrunner--temp-dir)))
+ (should (eq (car result) 'no-tests)))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Extract Test Names
+
+(ert-deftest test-testrunner-extract-test-names-simple ()
+ "Should extract test names from file."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-simple.el"
+ "(ert-deftest test-foo () (should t))\n(ert-deftest test-bar () (should nil))"))
+ (names (cj/test--extract-test-names file)))
+ (should (= (length names) 2))
+ (should (member "test-foo" names))
+ (should (member "test-bar" names)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-names-with-whitespace ()
+ "Should extract test names with various whitespace."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-whitespace.el"
+ "(ert-deftest test-spaces () (should t))\n (ert-deftest test-indent () t)"))
+ (names (cj/test--extract-test-names file)))
+ (should (= (length names) 2))
+ (should (member "test-spaces" names))
+ (should (member "test-indent" names)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-names-no-tests ()
+ "Should return empty list when no tests in file."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-none.el"
+ "(defun not-a-test () t)"))
+ (names (cj/test--extract-test-names file)))
+ (should (null names)))
+ (test-testrunner-teardown))
+
+;;; Normal Cases - Extract Test at Position
+
+(ert-deftest test-testrunner-extract-test-at-pos-found ()
+ "Should extract test name at point."
+ (test-testrunner-setup)
+ (with-temp-buffer
+ (insert "(ert-deftest test-sample ()\n (should t))")
+ (goto-char (point-min))
+ (let ((name (cj/test--extract-test-at-pos)))
+ (should (eq name 'test-sample))))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-at-pos-not-found ()
+ "Should return nil when not in a test."
+ (test-testrunner-setup)
+ (with-temp-buffer
+ (insert "(defun regular-function ()\n (message \"hi\"))")
+ (goto-char (point-min))
+ (let ((name (cj/test--extract-test-at-pos)))
+ (should (null name))))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-at-pos-invalid-syntax ()
+ "Should return nil for invalid syntax."
+ (test-testrunner-setup)
+ (with-temp-buffer
+ (insert "(ert-deftest")
+ (goto-char (point-min))
+ (let ((name (cj/test--extract-test-at-pos)))
+ (should (null name))))
+ (test-testrunner-teardown))
+
+;;; Boundary Cases - Load Files
+
+(ert-deftest test-testrunner-load-files-empty-list ()
+ "Should handle empty file list."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-load-files test-testrunner--temp-dir '())))
+ (should (eq (car result) 'success))
+ (should (= (cdr result) 0)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-load-files-nonexistent ()
+ "Should handle nonexistent files."
+ (test-testrunner-setup)
+ (let* ((fake-file (expand-file-name "nonexistent.el" test-testrunner--temp-dir))
+ (result (cj/test--do-load-files test-testrunner--temp-dir (list fake-file))))
+ (should (eq (car result) 'error))
+ (should (= (nth 1 result) 0))) ; 0 files loaded
+ (test-testrunner-teardown))
+
+;;; Boundary Cases - Focus Add
+
+(ert-deftest test-testrunner-focus-add-single-available ()
+ "Should add when only one file available."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "test-only.el" '("test-only.el") '())))
+ (should (eq result 'success)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-focus-add-case-sensitive ()
+ "Should be case-sensitive for filenames."
+ (test-testrunner-setup)
+ (let ((result (cj/test--do-focus-add "Test-Foo.el"
+ '("test-foo.el")
+ '())))
+ (should (eq result 'not-available)))
+ (test-testrunner-teardown))
+
+;;; Boundary Cases - Get Focused Tests
+
+(ert-deftest test-testrunner-get-focused-tests-multiple-files ()
+ "Should collect tests from multiple files."
+ (test-testrunner-setup)
+ (test-testrunner-create-test-file "test-first.el"
+ "(ert-deftest test-beta-one () t)")
+ (test-testrunner-create-test-file "test-second.el"
+ "(ert-deftest test-beta-two () t)")
+ (let ((result (cj/test--do-get-focused-tests '("test-first.el" "test-second.el")
+ test-testrunner--temp-dir)))
+ (should (eq (car result) 'success))
+ (should (= (length (nth 1 result)) 2)) ; 2 tests total
+ (should (= (nth 2 result) 2))) ; 2 files loaded
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-get-focused-tests-skip-nonexistent ()
+ "Should skip nonexistent files."
+ (test-testrunner-setup)
+ (test-testrunner-create-test-file "test-exists.el"
+ "(ert-deftest test-gamma-one () t)")
+ (let ((result (cj/test--do-get-focused-tests '("test-exists.el" "test-missing.el")
+ test-testrunner--temp-dir)))
+ (should (eq (car result) 'success))
+ (should (= (length (nth 1 result)) 1)) ; 1 test found
+ (should (= (nth 2 result) 1))) ; 1 file loaded (missing skipped)
+ (test-testrunner-teardown))
+
+;;; Boundary Cases - Extract Test Names
+
+(ert-deftest test-testrunner-extract-test-names-hyphens-underscores ()
+ "Should handle test names with hyphens and underscores."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-names.el"
+ "(ert-deftest test-with-hyphens () t)\n(ert-deftest test_with_underscores () t)"))
+ (names (cj/test--extract-test-names file)))
+ (should (= (length names) 2))
+ (should (member "test-with-hyphens" names))
+ (should (member "test_with_underscores" names)))
+ (test-testrunner-teardown))
+
+(ert-deftest test-testrunner-extract-test-names-ignore-comments ()
+ "Should not extract test names from comments."
+ (test-testrunner-setup)
+ (let* ((file (test-testrunner-create-test-file "test-comments.el"
+ ";; (ert-deftest test-commented () t)\n(ert-deftest test-real () t)"))
+ (names (cj/test--extract-test-names file)))
+ (should (= (length names) 1))
+ (should (member "test-real" names)))
+ (test-testrunner-teardown))
+
+(provide 'test-test-runner)
+;;; test-test-runner.el ends here