summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-clear-blank-lines.el.disabled47
-rw-r--r--tests/test-custom-functions-join-line-or-region.el.disabled84
-rw-r--r--tests/test-custom-org-agenda-functions.el.disabled94
-rw-r--r--tests/test-fixup-whitespace.el.disabled159
-rw-r--r--tests/test-flyspell-config-functions.el.disabled149
-rw-r--r--tests/test-format-region.el.disabled110
-rw-r--r--tests/test-fs--mode-to-permissions.el36
-rw-r--r--tests/test-fs-filter-by-extension.el68
-rw-r--r--tests/test-fs-format-file-info.el40
-rw-r--r--tests/test-fs-get-file-info.el75
-rw-r--r--tests/test-fs-list-directory-recursive-extra.el106
-rw-r--r--tests/test-fs-list-directory-recursive.el71
-rw-r--r--tests/test-fs-validate-path.el45
-rw-r--r--tests/test-testutil-filesystem-directory-entries.el317
-rw-r--r--tests/test-theme-theme-persistence.el.disabled135
-rw-r--r--tests/test-title-case-region.el.disabled44
-rw-r--r--tests/testutil-filesystem.el180
-rw-r--r--tests/testutil-general.el191
18 files changed, 1951 insertions, 0 deletions
diff --git a/tests/test-clear-blank-lines.el.disabled b/tests/test-clear-blank-lines.el.disabled
new file mode 100644
index 00000000..2190aba0
--- /dev/null
+++ b/tests/test-clear-blank-lines.el.disabled
@@ -0,0 +1,47 @@
+;;; test-clear-blank-lines.el --- -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (concat user-emacs-directory "modules"))
+(require 'custom-functions)
+
+(ert-deftest test-cj/clear-blank-lines-region ()
+ (let ((testdata "Some\n\n\n\nText")
+ (expected "Some\nText")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (cj/clear-blank-lines (point-min) (point-max))
+ (setq actual (buffer-string))
+ (message "buffer is:\n'%s'" actual)
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/clear-blank-lines-region-multiple-lines ()
+ (let ((testdata "Some\n\n\n\nText")
+ (expected "Some\n\n\n\nText")
+ (midpoint)
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (insert "\n")
+ (setq midpoint (point))
+ (insert testdata)
+ (cj/clear-blank-lines (point-min) midpoint)
+ (setq actual (buffer-substring (- (point-max)
+ (length testdata)) (point-max)))
+ (message "buffer is:\n'%s'" (buffer-string))
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/clear-blank-lines-negative ()
+ (with-temp-buffer
+ (insert "Some\nText")
+ (cj/clear-blank-lines (point-min) (point-max))
+ (should (equal (buffer-string) "Some\nText"))))
+
+
+(provide 'test-clear-blank-lines)
+;;; test-clear-blank-lines.el ends here.
diff --git a/tests/test-custom-functions-join-line-or-region.el.disabled b/tests/test-custom-functions-join-line-or-region.el.disabled
new file mode 100644
index 00000000..d694e407
--- /dev/null
+++ b/tests/test-custom-functions-join-line-or-region.el.disabled
@@ -0,0 +1,84 @@
+;;; test-custom-functions-join-line-or-region.el --- Test cj/join-line-or-region -*- lexical-binding: t; -*-
+;; Author: Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;; Tests for the cj/join-line-or-region function in custom-functions.el
+
+;;; Code:
+
+(add-to-list 'load-path (concat user-emacs-directory "modules"))
+(require 'ert)
+(require 'custom-functions)
+
+
+(ert-deftest test-cj/join-line-or-region-normal-case ()
+ (let* ((given "Line1\nLine2\nLine3\n")
+ (expected "Line1 Line2 Line3\n")) ; Note: join-line adds newline.
+ (with-temp-buffer
+ (insert given)
+
+ ;; Properly set and activate the region
+ (push-mark (point-min) t t) ; Set mark, no message, activate
+ (goto-char (point-max)) ; This creates active region from min to max
+
+ ;; Call the function being tested
+ (cj/join-line-or-region)
+
+ ;; Perform assertions to check the expected result
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ expected)))))
+
+(ert-deftest test-cj/join-line-or-region-multiple-spaces ()
+ (let* ((given "Line1\n\n\n\n\nLine2\nLine3\n")
+ (expected "Line1 Line2 Line3\n")) ; Note: join-line adds newline.
+ (with-temp-buffer
+ (insert given)
+
+ ;; Properly set and activate the region
+ (push-mark (point-min) t t)
+ (goto-char (point-max))
+
+ ;; Call the function being tested
+ (cj/join-line-or-region)
+
+ ;; Perform assertions to check the expected result
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ expected)))))
+
+
+(ert-deftest test-cj/join-line-or-region-single-line ()
+ (let* ((given "Line1\n")
+ (expected "Line1\n")) ; Note: join-line adds newline.
+ (with-temp-buffer
+ (insert given)
+
+ ;; push the mark mid-way on the line
+ (goto-char (/ (point-max) 2))
+
+ ;; Call the function being tested
+ (cj/join-line-or-region)
+
+ ;; Perform assertions to check the expected result
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ expected)))))
+
+(ert-deftest test-cj/join-line-or-region-nothing ()
+ (let* ((given "")
+ (expected "\n")) ; Note: join-line adds newline.
+ (with-temp-buffer
+ (insert given)
+
+ ;; Properly set and activate the region
+ (push-mark (point-min) t t)
+ (goto-char (point-max))
+
+ ;; Call the function being tested
+ (cj/join-line-or-region)
+
+ ;; Perform assertions to check the expected result
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ expected)))))
+
+
+(provide 'test-custom-functions.el-join-line-or-region)
+;;; test-custom-functions-join-line-or-region.el ends here.
diff --git a/tests/test-custom-org-agenda-functions.el.disabled b/tests/test-custom-org-agenda-functions.el.disabled
new file mode 100644
index 00000000..44f9f43d
--- /dev/null
+++ b/tests/test-custom-org-agenda-functions.el.disabled
@@ -0,0 +1,94 @@
+;;; test-custom-org-agenda-functions.el --- Tests for custom functions in org-agenda -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; This tests the custom functions created to build the main agenda in org-agenda-config.el
+
+;;; Code:
+
+(add-to-list 'load-path (concat user-emacs-directory "modules"))
+(require 'org-agenda-config)
+
+(ert-deftest test-cj/org-skip-subtree-if-habit-positive ()
+ (with-temp-buffer
+ (insert "* TODO [#A] Test task\n")
+ (insert ":PROPERTIES:\n")
+ (insert ":STYLE: habit\n")
+ (insert ":RESET_CHECK_BOXES: t\n")
+ (insert ":END:\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (not (eq nil (cj/org-skip-subtree-if-habit))))))
+
+(ert-deftest test-cj/org-skip-subtree-if-habit-negative ()
+ (with-temp-buffer
+ (insert "* TODO [#A] Test task\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (eq nil (cj/org-skip-subtree-if-habit)))))
+
+(ert-deftest test-cj/org-skip-subtree-if-priority-positive ()
+ (with-temp-buffer
+ (insert "* TODO [#A] Test task\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (not (eq nil (cj/org-skip-subtree-if-priority ?A))))))
+
+(ert-deftest test-cj/org-skip-subtree-if-priority-negative ()
+ (erase-buffer)
+ (insert "* TODO [#B] Test task\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (eq nil (cj/org-skip-subtree-if-priority ?A))))
+
+(ert-deftest test-cj/org-skip-subtree-if-priority-boundary0 ()
+ (erase-buffer)
+ (insert "* TODO Test task\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (eq nil (cj/org-skip-subtree-if-priority ?A))))
+
+(ert-deftest test-cj/org-skip-subtree-if-priority-boundary1 ()
+ (erase-buffer)
+ (insert "* Test entry\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (eq nil (cj/org-skip-subtree-if-priority ?A))))
+
+(ert-deftest test-cj/org-skip-subtree-if-keyword-positive ()
+ (with-temp-buffer
+ (insert "* TODO [#A] Test task\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (not (eq nil (cj/org-skip-subtree-if-keyword '("TODO")))))))
+
+(ert-deftest test-cj/org-skip-subtree-if-keyword-positive-multiple ()
+ (with-temp-buffer
+ (insert "* PROJECT Test entry\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (not (eq nil (cj/org-skip-subtree-if-keyword '("TODO" "PROJECT")))))))
+
+(ert-deftest test-cj/org-skip-subtree-if-keyword-negative ()
+ (erase-buffer)
+ (insert "* PROJECT [#A] Test task\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (eq nil (cj/org-skip-subtree-if-keyword '("TODO")))))
+
+(ert-deftest test-cj/org-skip-subtree-if-keyword-negative-superset ()
+ (erase-buffer)
+ (insert "* PROJECT [#A] Test task\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (eq nil (cj/org-skip-subtree-if-keyword '("TODOTODO")))))
+
+(ert-deftest test-cj/org-skip-subtree-if-keyword-negative-multiple ()
+ (erase-buffer)
+ (insert "* PROJECT [#A] Test task\n")
+ (org-mode)
+ (goto-char (point-min))
+ (should (eq nil (cj/org-skip-subtree-if-keyword '("TODO" "DONE")))))
+
+
+(provide 'test-custom-org-agenda-functions)
+;;; test-custom-org-agenda-functions.el ends here.
diff --git a/tests/test-fixup-whitespace.el.disabled b/tests/test-fixup-whitespace.el.disabled
new file mode 100644
index 00000000..0126801a
--- /dev/null
+++ b/tests/test-fixup-whitespace.el.disabled
@@ -0,0 +1,159 @@
+;;; test-fixup-whitespace.el --- -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Test cj/fixup-whitespace-line-or-region in custom-functions.el
+
+;; The function under test should:
+;; - ensure there is exactly one space between words
+;; - remove tab characters
+;; - remove leading and trailing whitespace
+;; - operate on a line, or a region, if selected
+
+;;; Code:
+
+
+(require 'ert)
+(add-to-list 'load-path (concat user-emacs-directory "modules"))
+(require 'custom-functions)
+
+(ert-deftest test-cj/fixup-whitespace-positive-first-line-only ()
+ "Test a positive case with two lines.
+Both lines have whitespace at the beginning and the end. This tests that when
+this function is called on the first line, only that line is affected."
+ (let ((testdata " Hello, world! \n Foo bar ")
+ (expected "Hello, world!\n Foo bar ")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (cj/fixup-whitespace-line-or-region)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/fixup-whitespace-positive-first-line-only-tabs ()
+ "Test a positive case with two lines.
+Both lines have extraneous whitespace at the beginning and the end, includuing
+tabs. This tests that when this function is called on the first line, only that
+line is affected."
+ (let ((testdata " Hello,\t world! \n Foo\tbar ")
+ (expected "Hello, world!\n Foo\tbar ")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (cj/fixup-whitespace-line-or-region)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/fixup-whitespace-positive-first-line-only-tabs2 ()
+ "Test a positive case with two lines.
+Both lines have extraneous whitespace at the beginning and the end, includuing
+tabs. This tests that when this function is called on the first line, only that
+line is affected."
+ (let ((testdata "\t Hello,\tworld! \n Foo\t bar\t ")
+ (expected "Hello, world!\n Foo\t bar\t ")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (cj/fixup-whitespace-line-or-region)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/fixup-whitespace-negative-first-line-only ()
+ "Test a negative case with two lines.
+Only the second line has whitespace at the beginning and the end. This tests
+that when this function is called on the first line, neither line changes."
+ (let ((testdata "Hello, world!\n Foo bar ")
+ (expected "Hello, world!\n Foo bar ")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (cj/fixup-whitespace-line-or-region)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/fixup-whitespace-positive-second-line-only ()
+ "Test a positive case with two lines.
+Both lines have whitespace at the beginning and the end. This tests that when
+function is called on the second line, only that line is affected."
+ (let ((testdata " Hello, world! \n Foo bar ")
+ (expected " Hello, world! \nFoo bar")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (forward-line)
+ (cj/fixup-whitespace-line-or-region)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/fixup-whitespace-negative-second-line-only ()
+ "Test a negative case with two lines.
+Only the first line has whitespace at the beginning and the end. This tests
+that when this function is called on the first line, neither line changes."
+ (let ((testdata " Hello, world! \nFoo bar")
+ (expected " Hello, world! \nFoo bar")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (forward-line)
+ (cj/fixup-whitespace-line-or-region)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/fixup-whitespace-positive-region ()
+ "Test a positive case with a region.
+Two lines have whitespace at the beginning, the middle, and the end. This tests
+that when this function is called with a region, all whitespace is cleaned up as
+expected."
+ (let ((testdata " Hello, world! \n Foo bar ")
+ (expected "Hello, world!\nFoo bar")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (set-mark (point))
+ (goto-char (point-max))
+ (cj/fixup-whitespace-line-or-region t)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/fixup-whitespace-positive-region-tabs ()
+ "Test a positive case with a region and tabs.
+Two lines have extraneous whitespace at the beginning, the middle, and the end.
+This tests that when this function is called with a region, all whitespace is
+cleaned up as expected."
+ (let ((testdata " \t \t Hello, world! \n Foo\t bar ")
+ (expected "Hello, world!\nFoo bar")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (set-mark (point))
+ (goto-char (point-max))
+ (cj/fixup-whitespace-line-or-region t)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+(ert-deftest test-cj/fixup-whitespace-negative-region ()
+ "Test a negative case with a region.
+Two lines are inserted, neither of which have extraneous whitespace. This tests
+that when this function is called with a region, there's no unwanted
+side-effects and nothing changes."
+ (let ((testdata "Hello, world!\nFoo bar")
+ (expected "Hello, world!\nFoo bar")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (set-mark (point))
+ (goto-char (point-max))
+ (cj/fixup-whitespace-line-or-region t)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+(provide 'test-fixup-whitespace)
+;;; test-fixup-whitespace.el ends here.
diff --git a/tests/test-flyspell-config-functions.el.disabled b/tests/test-flyspell-config-functions.el.disabled
new file mode 100644
index 00000000..d12ac167
--- /dev/null
+++ b/tests/test-flyspell-config-functions.el.disabled
@@ -0,0 +1,149 @@
+;;; test-flyspell-config-functions.el --- -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Evaluate the buffer, then run (ert-all-tests).
+
+;;; Code:
+
+(add-to-list 'load-path (concat user-emacs-directory "modules"))
+(require 'flyspell-and-abbrev)
+
+;; --------------------------- Flyspell Overlay Tests --------------------------
+
+(ert-deftest cj/flyspell-overlay-test-positive ()
+ "Simplest positive test for \='cj/find-previous-flyspell-overlay\='.
+With one misspelling, cj/find-previous-flyspell-overlay should return the
+character position at the beginning of the misspelled word."
+ (with-temp-buffer
+ (let ((misspelled "mispeled")
+ (overlay-pos))
+ ;; insert some text
+ (insert (format "some text for testing. %s" misspelled))
+
+ ;; trigger flyspell and wait for it to complete
+ (flyspell-buffer)
+ (sit-for 1)
+
+ ;; call the function with position at end of the buffer
+ (setq overlay-pos (cj/find-previous-flyspell-overlay (point-max)))
+
+ ;; test flyspell-auto-correct-previous-pos is at char position of 'mispeled'.
+ (should (eq (- (point-max) (length misspelled)) overlay-pos)))))
+
+(ert-deftest cj/flyspell-overlay-test-negative ()
+ "Simplest negative test for \='cj/find-previous-flyspell-overlay\='.
+With no misspelled words, cj/find-previous-flyspell-overlay should return nil."
+ (with-temp-buffer
+ (insert "This is a correctly spelled sentence.")
+ (flyspell-buffer)
+ ;; No overlay should exist, so test the result is nil.
+ (should-not (cj/find-previous-flyspell-overlay (point-max)))))
+
+(ert-deftest cj/flyspell-overlay-test-positive-multiple ()
+ "Positive test for \='cj/find-previous-flyspell-overlay\='.
+With several misspellings above and below, cj/find-previous-flyspell-overlay
+should return the character position at the beginning of the previous misspelled
+word."
+ (with-temp-buffer
+ (let ((misspelled0 "incorect")
+ (misspelled1 "wrongg")
+ (misspelled2 "erroor")
+ (misspelled3 "mistken")
+ (actual-pos)
+ (expected-pos)
+ (between-pos))
+
+ ;; insert some text with misspellings
+ (insert (format "flyspell should catch this: %s" misspelled0))
+ (insert (format "flyspell should catch this: %s" misspelled1))
+
+ ;; calculate the overlay's expected position based on our current position
+ (setq expected-pos (- (point) (length misspelled1)))
+
+ ;; calculate a position in between misspellings
+ (setq between-pos (+ expected-pos (length misspelled1) 5))
+
+ ;; insert the rest of the misspellings
+ (insert (format "flyspell should catch this: %s" misspelled2))
+ (insert (format "flyspell should catch this: %s" misspelled3))
+
+ ;; trigger Flyspell and wait for it to identify all misspellings.
+ (flyspell-buffer)
+ (sit-for 1)
+
+ ;; call the function with position in between misspellings
+ (setq actual-pos (cj/find-previous-flyspell-overlay between-pos))
+ (should (eq expected-pos actual-pos)))))
+
+
+(ert-deftest cj/flyspell-goto-previous-misspelling-positive ()
+ "Positive test for \='cj/flyspell-goto-previous-misspelling\='.
+With a simple misspelling above, cj/flyspell-goto-previous-misspelling
+should land on the next misspelled word."
+ (with-temp-buffer
+ (let ((misspelled-word "incorect")
+ (actual-word))
+
+ ;; insert some text with misspellings
+ (insert (format "flyspell should catch this: %s" misspelled-word))
+
+ ;; trigger Flyspell and wait for it to identify all misspellings.
+ (flyspell-buffer)
+ (sit-for 1)
+
+ ;; call the function with position in between misspellings
+ (setq actual-word (cj/flyspell-goto-previous-misspelling (point-max)))
+ (should (string= misspelled-word actual-word)))))
+
+(ert-deftest cj/flyspell-goto-previous-misspelling-negative ()
+ "Negative test for \='cj/flyspell-goto-previous-misspelling\='.
+With no misspellings, cj/flyspell-goto-previous-misspelling return nil."
+ (with-temp-buffer
+ (let ((expected nil)
+ (result))
+
+ ;; insert some text with misspellings
+ (insert (format "None of these words are misspelled."))
+
+ ;; trigger Flyspell and wait for it to identify all misspellings.
+ (flyspell-buffer)
+ (sit-for 1)
+
+ ;; call the function with position in between misspellings
+ (setq result (cj/flyspell-goto-previous-misspelling (point-max)))
+ (message "result is %s" result)
+ (should (eq result expected)))))
+
+(ert-deftest cj/flyspell-goto-previous-misspelling-positive-multiple ()
+ "Positive test for \='cj/flyspell-goto-previous-misspelling\='.
+With several misspellings above and below, cj/flyspell-goto-previous-misspelling
+should return the misspelled word just previous to the position of the cursor."
+ (with-temp-buffer
+ (let ((misspelled0 "incorect")
+ (misspelled1 "wrongg")
+ (misspelled2 "erroor")
+ (misspelled3 "mistken")
+ (result)
+ (between-pos))
+
+ ;; insert some text with misspellings
+ (insert (format "flyspell should catch this: %s\n" misspelled0))
+ (insert (format "flyspell should catch this: %s\n" misspelled1))
+
+ ;; calculate a position in between misspellings
+ (setq between-pos (+ (point) (length misspelled1) 5))
+
+ ;; insert the rest of the misspellings
+ (insert (format "flyspell should catch this: %s\n" misspelled2))
+ (insert (format "flyspell should catch this: %s\n" misspelled3))
+
+ ;; trigger Flyspell and wait for it to identify all misspellings.
+ (flyspell-buffer)
+ (sit-for 1)
+
+ ;; call the function with position in between misspellings
+ (setq result (cj/flyspell-goto-previous-misspelling between-pos))
+ (should (string= result misspelled1)))))
+
+(provide 'test-flyspell-config-functions)
+;;; test-flyspell-config-functions.el ends here.
diff --git a/tests/test-format-region.el.disabled b/tests/test-format-region.el.disabled
new file mode 100644
index 00000000..25d2e52e
--- /dev/null
+++ b/tests/test-format-region.el.disabled
@@ -0,0 +1,110 @@
+;;; test-format-region.el --- -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Some basic tests for the custom function cj/format-region-or-buffer in
+;; custom-functions.el
+
+;;; Code:
+
+(add-to-list 'load-path (concat user-emacs-directory "modules"))
+(require 'custom-functions)
+
+
+;; ----------------------------------- Tests -----------------------------------
+
+(defvar test-format-rob-text-data
+ '((" spaces in front\nspaces behind " .
+ "spaces in front\nspaces behind")
+ ("\t tabs and spaces in front\ntabs and spaces behind\t " .
+ "tabs and spaces in front\ntabs and spaces behind")))
+
+(defvar test-format-rob-elisp-data
+ '(("(defun existential ()\n(if (eq (+ 3 4) 7)\n(order)\n(chaos)))" .
+ "(defun existential ()\n (if (eq (+ 3 4) 7)\n (order)\n (chaos)))")))
+
+
+(ert-deftest test-format-rob-positive-text-region ()
+ "Test cj/format-region-or-buffer on a selected region.
+This tests "
+ (dolist (data-pair test-format-rob-text-data)
+ (let* ((testdata (car data-pair))
+ (expected (cdr data-pair))
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (set-mark (point))
+ (goto-char (point-max))
+ (cj/format-region-or-buffer)
+ (setq actual (buffer-string))
+ (should (string= actual expected))))))
+
+(ert-deftest test-format-rob-positive-text-buffer ()
+ "Test cj/format-region-or-buffer on the entire buffer.
+This is the same as testing the region without setting a region in the temp
+buffer."
+ (dolist (data-pair test-format-rob-text-data)
+ (let* ((testdata (car data-pair))
+ (expected (cdr data-pair))
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (cj/format-region-or-buffer)
+ (setq actual (buffer-string))
+ (should (string= actual expected))))))
+
+(ert-deftest test-format-rob-positive-region-text-multiple-paragraphs ()
+ "Test cj/format-region-or-buffer on the entire buffer."
+ (dolist (data-pair test-format-rob-text-data)
+ (let ((testdata (car data-pair))
+ (expected1 (cdr data-pair))
+ (expected2 (car data-pair))
+ (actual1)
+ (actual2))
+ (with-temp-buffer
+ ;; insert data twice with newline char in between
+ (insert testdata)
+ (insert"\n")
+ (insert testdata)
+
+ ;; select the first set of data
+ (goto-char (point-min))
+ (set-mark (point))
+ (forward-line 2)
+
+ ;; run format and return to top
+ (cj/format-region-or-buffer)
+ (message "buffer is:\n'%s'" (buffer-string))
+
+ ;; assert the first set is formatted
+ (goto-char (point-min))
+ (setq actual1 (buffer-substring (point-min) (line-end-position 2)))
+ (should (string= actual1 expected1))
+
+ ;; assert the second set is unformatted
+ (goto-char (point-min))
+ (setq actual2 (buffer-substring (line-beginning-position 3) (point-max)))
+ (should (string= actual2 expected2))))))
+
+(ert-deftest test-format-rob-positive-elisp-region ()
+ "Test cj/format-region-or-buffer on a selected region.
+This tests that emacs-lisp specific formatting is applied."
+ (ws-butler-mode nil)
+ (dolist (data-pair test-format-rob-elisp-data)
+ (let* ((testdata (car data-pair))
+ (expected (cdr data-pair))
+ (actual))
+ (with-temp-buffer
+ (emacs-lisp-mode)
+ (insert testdata)
+ (goto-char (point-min))
+ (set-mark (point))
+ (goto-char (point-max))
+ (message "buffer before:\n'%s'" (buffer-string))
+ (cj/format-region-or-buffer)
+ (message "buffer after:\n'%s'" (buffer-string))
+ (setq actual (buffer-string))
+ (should (string= actual expected))))))
+
+(provide 'test-format-region)
+;;; test-format-region.el ends here.
diff --git a/tests/test-fs--mode-to-permissions.el b/tests/test-fs--mode-to-permissions.el
new file mode 100644
index 00000000..3d27ac08
--- /dev/null
+++ b/tests/test-fs--mode-to-permissions.el
@@ -0,0 +1,36 @@
+;;; test-tool-library-fs--mode-to-permissions.el --- ERT tests for cj/fs--mode-to-permissions -*- lexical-binding: t; -*-
+
+;; Author: gptel-tool-writer and cjennings
+;; Keywords: tests, filesystem, tools
+
+;;; Commentary:
+;; ERT tests for the cj/fs--mode-to-permissions function from tool-filesystem-library.el.
+;; Place this file in ~/.emacs.d/tests/ and load it to run tests.
+
+;;; Code:
+
+(require 'ert)
+(require 'tool-filesystem-library)
+
+(ert-deftest test-cj/fs--mode-to-permissions-normal-directory ()
+ "Normal: directory permissions string."
+ (should (string-prefix-p "d"
+ (cj/fs--mode-to-permissions #o40755))))
+
+(ert-deftest test-cj/fs--mode-to-permissions-normal-regular-file ()
+ "Normal: regular file permissions string."
+ (should (string-prefix-p "-"
+ (cj/fs--mode-to-permissions #o100644))))
+
+(ert-deftest test-cj/fs--mode-to-permissions-boundary-zero ()
+ "Boundary: no permissions."
+ (should (string= "----------"
+ (cj/fs--mode-to-permissions 0))))
+
+(ert-deftest test-cj/fs--mode-to-permissions-boundary-full ()
+ "Boundary: full permissions string."
+ (should (string= "-rwxrwxrwx"
+ (cj/fs--mode-to-permissions #o777))))
+
+(provide 'test-tool-library-fs--mode-to-permissions)
+;;; test-tool-library-fs--mode-to-permissions.el ends here
diff --git a/tests/test-fs-filter-by-extension.el b/tests/test-fs-filter-by-extension.el
new file mode 100644
index 00000000..254cf47c
--- /dev/null
+++ b/tests/test-fs-filter-by-extension.el
@@ -0,0 +1,68 @@
+;;; test-tool-library-fs-filter-by-extension.el --- ERT tests for cj/fs-filter-by-extension -*- lexical-binding: t; -*-
+
+;; Author: gptel-tool-writer and cjennings
+;; Keywords: tests, filesystem, tools
+
+;;; Commentary:
+;; ERT tests for the cj/fs-filter-by-extension function from tool-filesystem-library.el.
+;; Place this file in ~/.emacs.d/tests/ and load it to run tests.
+
+;;; Code:
+
+(require 'ert)
+(require 'f)
+(require 'tool-filesystem-library)
+
+(defvar cj/fs-test--temp-dir nil "Temporary test directory for fs-filter-by-extension tests.")
+
+(defun cj/fs-test--setup ()
+ "Set up temp directory for fs-filter-by-extension tests."
+ (setq cj/fs-test--temp-dir (make-temp-file "fs-lib-test" t))
+ ;; Create files
+ (with-temp-buffer (insert "Org file") (write-file (f-join cj/fs-test--temp-dir "file1.org")))
+ (with-temp-buffer (insert "Txt file") (write-file (f-join cj/fs-test--temp-dir "file2.txt")))
+ (make-directory (f-join cj/fs-test--temp-dir "subdir") t))
+
+(defun cj/fs-test--teardown ()
+ "Clean up temp directory for fs-filter-by-extension tests."
+ (when (and cj/fs-test--temp-dir (file-directory-p cj/fs-test--temp-dir))
+ (delete-directory cj/fs-test--temp-dir t))
+ (setq cj/fs-test--temp-dir nil))
+
+(ert-deftest test-cj/fs-filter-by-extension-normal-match ()
+ "Normal: match single extension in list."
+ (cj/fs-test--setup)
+ (unwind-protect
+ (let* ((infos (mapcar #'cj/fs-get-file-info (cj/fs-directory-entries cj/fs-test--temp-dir)))
+ (filtered (cj/fs-filter-by-extension infos "org")))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file1.org")) filtered))
+ (should-not (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file2.txt")) filtered)))
+ (cj/fs-test--teardown)))
+
+(ert-deftest test-cj/fs-filter-by-extension-normal-no-filter ()
+ "Normal: no extension filter returns full list."
+ (cj/fs-test--setup)
+ (unwind-protect
+ (let* ((infos (mapcar #'cj/fs-get-file-info (cj/fs-directory-entries cj/fs-test--temp-dir)))
+ (filtered (cj/fs-filter-by-extension infos nil)))
+ (should (= (length filtered) (length infos))))
+ (cj/fs-test--teardown)))
+
+(ert-deftest test-cj/fs-filter-by-extension-error-empty-list ()
+ "Error: empty file info list handled."
+ (should (equal (cj/fs-filter-by-extension nil "org") nil)))
+
+(ert-deftest test-cj/fs-filter-by-extension-boundary-mixed-files ()
+ "Boundary: mixed extensions and directories handled."
+ (cj/fs-test--setup)
+ (unwind-protect
+ (let* ((entries (cj/fs-directory-entries cj/fs-test--temp-dir))
+ (infos (mapcar #'cj/fs-get-file-info entries))
+ (filtered (cj/fs-filter-by-extension infos "org")))
+ (should (cl-some (lambda (fi) (plist-get fi :directory)) filtered))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file1.org")) filtered))
+ (should-not (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file2.txt")) filtered)))
+ (cj/fs-test--teardown)))
+
+(provide 'test-tool-library-fs-filter-by-extension)
+;;; test-tool-library-fs-filter-by-extension.el ends here
diff --git a/tests/test-fs-format-file-info.el b/tests/test-fs-format-file-info.el
new file mode 100644
index 00000000..b5a82f4b
--- /dev/null
+++ b/tests/test-fs-format-file-info.el
@@ -0,0 +1,40 @@
+;;; test-tool-library-fs-format-file-info.el --- ERT tests for cj/fs-format-file-info -*- lexical-binding: t; -*-
+
+;; Author: gptel-tool-writer and cjennings
+;; Keywords: tests, filesystem, tools
+
+;;; Commentary:
+;; ERT tests for the cj/fs-format-file-info function from tool-filesystem-library.el.
+;; Place this file in ~/.emacs.d/tests/ and load it to run tests.
+
+;;; Code:
+
+(require 'ert)
+(require 'f)
+(require 'tool-filesystem-library)
+
+(ert-deftest test-cj/fs-format-file-info-normal-typical ()
+ "Normal: format typical file info plist."
+ (let ((info (list :permissions "-rw-r--r--"
+ :executable nil
+ :size 1024
+ :last-modified (current-time)
+ :path "~/test-file.txt")))
+ (should (string-match-p "test-file.txt" (cj/fs-format-file-info info "~")))))
+
+(ert-deftest test-cj/fs-format-file-info-error-missing-keys ()
+ "Error: format with missing keys handled."
+ (let ((info (list)))
+ (should (cj/fs-format-file-info info "~"))))
+
+(ert-deftest test-cj/fs-format-file-info-boundary-zero-size ()
+ "Boundary: format with zero size."
+ (let ((info (list :permissions "-rw-r--r--"
+ :executable nil
+ :size 0
+ :last-modified (current-time)
+ :path "~/empty-file.txt")))
+ (should (string-match-p "empty-file.txt" (cj/fs-format-file-info info "~")))))
+
+(provide 'test-tool-library-fs-format-file-info)
+;;; test-tool-library-fs-format-file-info.el ends here
diff --git a/tests/test-fs-get-file-info.el b/tests/test-fs-get-file-info.el
new file mode 100644
index 00000000..9e7e337c
--- /dev/null
+++ b/tests/test-fs-get-file-info.el
@@ -0,0 +1,75 @@
+;;; test-tool-library-fs-get-file-info.el --- ERT tests for cj/fs-get-file-info -*- lexical-binding: t; -*-
+
+;; Author: gptel-tool-writer and cjennings
+;; Keywords: tests, filesystem, tools
+
+;;; Commentary:
+;; ERT tests for the cj/fs-get-file-info function from tool-filesystem-library.el.
+;; Place this file in ~/.emacs.d/tests/ and load it to run tests.
+
+;;; Code:
+
+(require 'ert)
+(require 'f)
+(require 'tool-filesystem-library)
+
+(defvar cj/fs-test--temp-dir nil "Temporary test directory for fs-get-file-info tests.")
+
+(defun cj/fs-test--setup ()
+ "Setup temporary directory for fs-get-file-info tests."
+ (setq cj/fs-test--temp-dir (make-temp-file "fs-lib-test" t))
+ ;; Create test files and directories
+ (make-directory (f-join cj/fs-test--temp-dir "subdir") t)
+ (with-temp-buffer (insert "Test content") (write-file (f-join cj/fs-test--temp-dir "test-file.txt")))
+ (make-directory (f-join cj/fs-test--temp-dir "subdir") t)
+ (with-temp-buffer (insert "Nested test") (write-file (f-join cj/fs-test--temp-dir "subdir/nested-file.txt"))))
+
+(defun cj/fs-test--teardown ()
+ "Clean up temporary directory for fs-get-file-info tests."
+ (when (and cj/fs-test--temp-dir (file-directory-p cj/fs-test--temp-dir))
+ (delete-directory cj/fs-test--temp-dir t))
+ (setq cj/fs-test--temp-dir nil))
+
+(ert-deftest test-cj/fs-get-file-info-normal-regular-file ()
+ "Normal: info for regular file."
+ (cj/fs-test--setup)
+ (unwind-protect
+ (let ((info (cj/fs-get-file-info (f-join cj/fs-test--temp-dir "test-file.txt"))))
+ (should (plist-get info :success))
+ (should (string-suffix-p "test-file.txt" (plist-get info :path)))
+ (should (not (plist-get info :directory))))
+ (cj/fs-test--teardown)))
+
+(ert-deftest test-cj/fs-get-file-info-normal-directory ()
+ "Normal: info for directory."
+ (cj/fs-test--setup)
+ (unwind-protect
+ (let ((info (cj/fs-get-file-info (f-join cj/fs-test--temp-dir "subdir"))))
+ (should (plist-get info :success))
+ (should (string-suffix-p "subdir" (plist-get info :path)))
+ (should (plist-get info :directory)))
+ (cj/fs-test--teardown)))
+
+(ert-deftest test-cj/fs-get-file-info-error-nonexistent ()
+ "Error: non-existent file returns :success nil plist."
+ (let ((info (cj/fs-get-file-info "/tmp/nonexistent-file-1234567890")))
+ (should (not (plist-get info :success)))
+ (should (stringp (plist-get info :error)))))
+
+(ert-deftest test-cj/fs-get-file-info-error-permission-denied ()
+ "Error: permission denied file returns :success nil plist."
+ (cj/fs-test--setup)
+ (let ((file (f-join cj/fs-test--temp-dir "protected-file")))
+ (unwind-protect
+ (progn
+ (with-temp-buffer (insert "secret") (write-file file))
+ (set-file-modes file #o000)
+ (let ((info (cj/fs-get-file-info file)))
+ (should (not (plist-get info :success)))
+ (should (stringp (plist-get info :error)))))
+ (set-file-modes file #o644)
+ (delete-file file)
+ (cj/fs-test--teardown))))
+
+(provide 'test-tool-library-fs-get-file-info)
+;;; test-tool-library-fs-get-file-info.el ends here
diff --git a/tests/test-fs-list-directory-recursive-extra.el b/tests/test-fs-list-directory-recursive-extra.el
new file mode 100644
index 00000000..53ce3c8d
--- /dev/null
+++ b/tests/test-fs-list-directory-recursive-extra.el
@@ -0,0 +1,106 @@
+;;; test-tool-library-fs-list-directory-recursive-extra.el --- Additional ERT tests for cj/fs-list-directory-recursive -*- lexical-binding: t; -*-
+
+;; Author: gptel-tool-writer and cjennings
+;; Keywords: tests, filesystem, tools
+
+;;; Commentary:
+;; Additional tests to verify combined filters, boundary cases,
+;; symlink protection, and permission issue handling in
+;; cj/fs-list-directory-recursive.
+
+;;; Code:
+
+(require 'ert)
+(require 'f)
+(require 'tool-filesystem-library)
+
+(defvar cj/fs-extra-test--temp-dir nil "Temporary temp directory for extra fs-list-directory-recursive tests.")
+
+(defun cj/fs-extra-test--setup ()
+ "Set up temp directory for extra fs-list-directory-recursive tests."
+ (setq cj/fs-extra-test--temp-dir (make-temp-file "fs-lib-test" t))
+ ;; Create directory structure
+ (make-directory (f-join cj/fs-extra-test--temp-dir "subdir") t)
+ (make-directory (f-join cj/fs-extra-test--temp-dir "subdir2") t)
+ ;; Files at root level
+ (with-temp-buffer (insert "Root org file") (write-file (f-join cj/fs-extra-test--temp-dir "file1.org")))
+ (with-temp-buffer (insert "Root txt file") (write-file (f-join cj/fs-extra-test--temp-dir "file2.txt")))
+ ;; Files in subdirectories
+ (with-temp-buffer (insert "Subdir txt file") (write-file (f-join cj/fs-extra-test--temp-dir "subdir" "file3.txt")))
+ (with-temp-buffer (insert "Subdir2 org file") (write-file (f-join cj/fs-extra-test--temp-dir "subdir2" "file4.org")))
+ ;; Symlink to subdir2 inside subdir (potential for loops)
+ (let ((target (f-join cj/fs-extra-test--temp-dir "subdir2"))
+ (link (f-join cj/fs-extra-test--temp-dir "subdir" "link-to-subdir2")))
+ (ignore-errors (delete-file link))
+ (make-symbolic-link target link))
+
+ ;; Create protected directory inside subdir to test permission issues
+ (let ((protected-dir (f-join cj/fs-extra-test--temp-dir "subdir" "protected-dir")))
+ (make-directory protected-dir t)
+ ;; Remove read & execute permissions
+ (set-file-modes protected-dir #o000)))
+
+(defun cj/fs-extra-test--teardown ()
+ "Clean up temp directory for extra tests."
+ (when (and cj/fs-extra-test--temp-dir (file-directory-p cj/fs-extra-test--temp-dir))
+ ;; Reset permissions to allow deletion
+ (let ((protected-dir (f-join cj/fs-extra-test--temp-dir "subdir" "protected-dir")))
+ (when (file-exists-p protected-dir)
+ (set-file-modes protected-dir #o755)))
+ (delete-directory cj/fs-extra-test--temp-dir t))
+ (setq cj/fs-extra-test--temp-dir nil))
+
+(ert-deftest test-cj/fs-list-directory-recursive-normal-combined-filter-maxdepth ()
+ "Normal: recursive listing combining extension filter and max depth."
+ (cj/fs-extra-test--setup)
+ (unwind-protect
+ (let* ((filter-fn (lambda (fi)
+ (string-suffix-p ".org" (f-filename (plist-get fi :path)))))
+ ;; max-depth 1 means root directory only, no recursion into subdirs
+ (files (cj/fs-list-directory-recursive cj/fs-extra-test--temp-dir filter-fn 1)))
+ ;; Should find only root level org files, not ones nested
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file1.org")) files))
+ (should-not (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file4.org")) files)))
+ (cj/fs-extra-test--teardown)))
+
+(ert-deftest test-cj/fs-list-directory-recursive-boundary-max-depth-zero ()
+ "Boundary: max depth zero lists no files (no recursion)."
+ (cj/fs-extra-test--setup)
+ (unwind-protect
+ (let ((files (cj/fs-list-directory-recursive cj/fs-extra-test--temp-dir nil 0)))
+ ;; Should be empty as depth 0 means no entries processed
+ (should (equal files nil)))
+ (cj/fs-extra-test--teardown)))
+
+(ert-deftest test-cj/fs-list-directory-recursive-error-negative-max-depth ()
+ "Error: negative max depth results in error."
+ (cj/fs-extra-test--setup)
+ (unwind-protect
+ (should-error (cj/fs-list-directory-recursive cj/fs-extra-test--temp-dir nil -1))
+ (cj/fs-extra-test--teardown)))
+
+(ert-deftest test-cj/fs-list-directory-recursive-boundary-symlink-no-infinite-loop ()
+ "Boundary: symlinked directories do not cause infinite recursion."
+ (cj/fs-extra-test--setup)
+ (unwind-protect
+ (let ((files (cj/fs-list-directory-recursive cj/fs-extra-test--temp-dir nil 5)))
+ ;; There should be files from subdirs, but no infinite loop crashes
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file4.org")) files))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file1.org")) files)))
+ (cj/fs-extra-test--teardown)))
+
+(ert-deftest test-cj/fs-list-directory-recursive-normal-permission-issue-handling ()
+ "Normal: files in directories with permission issues are handled gracefully."
+ (cj/fs-extra-test--setup)
+ (unwind-protect
+ (let ((caught-warning nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (&rest args)
+ (when (string-match "Warning:" (apply #'format args))
+ (setq caught-warning t)))))
+ (cj/fs-list-directory-recursive cj/fs-extra-test--temp-dir nil 5)
+ (should caught-warning)))
+ (cj/fs-extra-test--teardown)))
+
+(provide 'test-tool-library-fs-list-directory-recursive-extra)
+;;; test-tool-library-fs-list-directory-recursive-extra.el ends here
diff --git a/tests/test-fs-list-directory-recursive.el b/tests/test-fs-list-directory-recursive.el
new file mode 100644
index 00000000..25dd1439
--- /dev/null
+++ b/tests/test-fs-list-directory-recursive.el
@@ -0,0 +1,71 @@
+;;; test-tool-library-fs-list-directory-recursive.el --- ERT tests for cj/fs-list-directory-recursive -*- lexical-binding: t; -*-
+
+;; Author: gptel-tool-writer and cjennings
+;; Keywords: tests, filesystem, tools
+
+;;; Commentary:
+;; ERT tests for the cj/fs-list-directory-recursive function from tool-filesystem-library.el.
+;; Place this file in ~/.emacs.d/tests/ and load it to run tests.
+
+;;; Code:
+
+(require 'ert)
+(require 'f)
+(require 'tool-filesystem-library)
+
+(defvar cj/fs-test--temp-dir nil "Temporary temp directory for fs-list-directory-recursive tests.")
+
+(defun cj/fs-test--setup ()
+ "Set up temp directory for fs-list-directory-recursive tests."
+ (setq cj/fs-test--temp-dir (make-temp-file "fs-lib-test" t))
+ ;; Create test directory structure
+ (make-directory (f-join cj/fs-test--temp-dir "subdir") t)
+ (make-directory (f-join cj/fs-test--temp-dir "subdir2") t)
+ (with-temp-buffer (insert "Test file 1") (write-file (f-join cj/fs-test--temp-dir "file1.org")))
+ (with-temp-buffer (insert "Test file 2") (write-file (f-join cj/fs-test--temp-dir "subdir" "file2.txt")))
+ (with-temp-buffer (insert "Test file 3") (write-file (f-join cj/fs-test--temp-dir "subdir2" "file3.org")))
+ (make-directory (f-join cj/fs-test--temp-dir ".hiddendir") t)
+ (with-temp-buffer (insert "Secret") (write-file (f-join cj/fs-test--temp-dir ".hiddendir" "secret.txt"))))
+
+(defun cj/fs-test--teardown ()
+ "Clean up temp directory for fs-list-directory-recursive tests."
+ (when (and cj/fs-test--temp-dir (file-directory-p cj/fs-test--temp-dir))
+ (delete-directory cj/fs-test--temp-dir t))
+ (setq cj/fs-test--temp-dir nil))
+
+(ert-deftest test-cj/fs-list-directory-recursive-normal-recursive-filter ()
+ "Normal: recursive listing with filter."
+ (cj/fs-test--setup)
+ (unwind-protect
+ (let* ((filter-fn (lambda (fi) (string-suffix-p ".org" (f-filename (plist-get fi :path)))))
+ (files (cj/fs-list-directory-recursive cj/fs-test--temp-dir filter-fn)))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file1.org")) files))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file3.org")) files))
+ (should-not (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file2.txt")) files)))
+ (cj/fs-test--teardown)))
+
+(ert-deftest test-cj/fs-list-directory-recursive-normal-max-depth ()
+ "Normal: recursive listing with max depth limit."
+ (cj/fs-test--setup)
+ (unwind-protect
+ (let* ((filter-fn (lambda (_) t))
+ (files (cj/fs-list-directory-recursive cj/fs-test--temp-dir filter-fn 1)))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file1.org")) files))
+ (should-not (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file3.org")) files)))
+ (cj/fs-test--teardown)))
+
+(ert-deftest test-cj/fs-list-directory-recursive-error-non-directory ()
+ "Error: non-directory input."
+ (should-error (cj/fs-list-directory-recursive "/etc/hosts")))
+
+(ert-deftest test-cj/fs-list-directory-recursive-boundary-empty-dir ()
+ "Boundary: recursive listing in empty directory."
+ (make-temp-file "empty-dir" t)
+ (let ((empty (make-temp-file "empty-dir" t)))
+ (unwind-protect
+ (progn
+ (should (equal (cj/fs-list-directory-recursive empty) nil))
+ (delete-directory empty)))))
+
+(provide 'test-tool-library-fs-list-directory-recursive)
+;;; test-tool-library-fs-list-directory-recursive.el ends here
diff --git a/tests/test-fs-validate-path.el b/tests/test-fs-validate-path.el
new file mode 100644
index 00000000..011789e0
--- /dev/null
+++ b/tests/test-fs-validate-path.el
@@ -0,0 +1,45 @@
+;;; test-tool-library-cj/fs-validate-path.el --- ERT tests for cj/fs-validate-path -*- lexical-binding: t; -*-
+
+;; Author: gptel-tool-writer and cjennings
+;; Keywords: tests, filesystem, tools
+
+;;; Commentary:
+;; ERT tests for the cj/fs-validate-path function from tool-filesystem-library.el.
+;; Place this file in ~/.emacs.d/tests/ and load it to run tests.
+
+;;; Code:
+
+(require 'ert)
+(require 'f)
+(require 'tool-filesystem-library)
+
+(ert-deftest test-cj/fs-validate-path-normal-home ()
+ "Normal: validate home directory path."
+ (should (string-prefix-p (expand-file-name "~")
+ (cj/fs-validate-path "~"))))
+
+(ert-deftest test-cj/fs-validate-path-normal-temp ()
+ "Normal: validate temp directory path."
+ (let ((temp (expand-file-name temporary-file-directory)))
+ (should (string-prefix-p temp (cj/fs-validate-path temp)))))
+
+(ert-deftest test-cj/fs-validate-path-error-outside ()
+ "Error: path outside allowed directories."
+ (should-error (cj/fs-validate-path "/etc/passwd")))
+
+(ert-deftest test-cj/fs-validate-path-error-nonexistent ()
+ "Error: non-existent path."
+ (should-error (cj/fs-validate-path (format "/tmp/nonexistent-%d" (random 100000)))))
+
+(ert-deftest test-cj/fs-validate-path-error-unreadable ()
+ "Error: unreadable path."
+ (let ((file (make-temp-file "test-unreadable")))
+ (unwind-protect
+ (progn
+ (set-file-modes file 0)
+ (should-error (cj/fs-validate-path file)))
+ (set-file-modes file #o644)
+ (delete-file file))))
+
+(provide 'test-tool-library-cj/fs-validate-path)
+;;; test-tool-library-cj/fs-validate-path.el ends here
diff --git a/tests/test-testutil-filesystem-directory-entries.el b/tests/test-testutil-filesystem-directory-entries.el
new file mode 100644
index 00000000..7ddbf426
--- /dev/null
+++ b/tests/test-testutil-filesystem-directory-entries.el
@@ -0,0 +1,317 @@
+;;; test-testutil-filesystem-directory-entries.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;
+;; Author: Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;; ERT tests for testutil-filesystem.el
+;; Tests cj/list-directory-recursive and it's helper function cj/get--directory-entries.
+;;
+;;; Code:
+
+(require 'ert)
+(require 'f)
+
+;; load test directory
+(add-to-list 'load-path (concat user-emacs-directory "tests/"))
+(require 'testutil-general) ;; helper functions
+(require 'testutil-filesystem) ;; file under test
+
+(defun cj/test--setup ()
+ "Create the test base directory using `cj/create-test-base-dir'."
+ (cj/create-test-base-dir))
+
+(defun cj/test--teardown ()
+ "Remove the test base directory using `cj/delete-test-base-dir'."
+ (cj/delete-test-base-dir))
+
+;;; ---------------------- CJ/GET--DIRECTORY-ENTRIES TESTS ----------------------
+;;;; Normal Case Tests
+
+(ert-deftest test-normal-one-file ()
+ "Test a single file at the base directory."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ (cj/create-directory-or-file-ensuring-parents "file.txt" "Test file")
+ (let
+ ;; get paths to all files
+ ((entries (cj/get--directory-entries cj/test-base-dir)))
+ ;; check for files of different types and in subdirectories
+ (should (cl-some (lambda (e) (string= (f-filename e) "file.txt")) entries))))
+ (cj/test--teardown)))
+
+(ert-deftest test-normal-includes-subdirectories-but-no-contents ()
+ "Test that we do include subdirectories themselves."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ ;; create yoru test assets
+ (cj/create-directory-or-file-ensuring-parents "file1.org" "Test file 1" t)
+ (cj/create-directory-or-file-ensuring-parents "subdir/file2.org" "Nested file")
+ ;; get paths to all files
+ (let ((entries (cj/get--directory-entries cj/test-base-dir)))
+ (should (cl-some (lambda (e) (and (file-directory-p e)
+ (string= (f-filename e) "subdir"))) entries))
+ (should-not (cl-some (lambda (e) (string= (f-filename e) "file2.org")) entries))))
+ (cj/test--teardown)))
+
+(ert-deftest test-normal-excludes-hidden-by-default ()
+ "Test that hidden files (i.e.,begin with a dot) are excluded by default.
+Asserts no subdirectories or hidden files or visible files in hidden subdirectories are returned."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ ;; create your test assets
+ (cj/create-directory-or-file-ensuring-parents ".hiddenfile" "Hidden content")
+ ;; get paths to all files
+ (let ((entries (cj/get--directory-entries cj/test-base-dir)))
+ ;; should not see hidden file
+ (should-not (cl-some (lambda (e) (string= (f-filename e) ".hiddenfile")) entries))))
+ (cj/test--teardown)))
+
+(ert-deftest test-normal-includes-hidden-with-flag ()
+ "Non-nil means hidden files are included."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ ;; create your test assets
+ (cj/create-directory-or-file-ensuring-parents ".hiddenfile" "Hidden content")
+ ;; get paths to all files passing in t to reveal hidden files
+ (let ((entries (cj/get--directory-entries cj/test-base-dir t)))
+ ;; should not see hidden file
+ (should (cl-some (lambda (e) (string= (f-filename e) ".hiddenfile")) entries))))
+ (cj/test--teardown)))
+
+;;
+;;;; Boundary Cases
+
+(ert-deftest test-boundary-empty-directory ()
+ "Test an empty directory returns empty list."
+ (cj/test--setup)
+ (unwind-protect
+ (let ((entries (cj/get--directory-entries cj/test-base-dir)))
+ (should (equal entries nil)))
+ (cj/test--teardown)))
+
+(ert-deftest test-boundary-files-with-unusual-names ()
+ "Test files with unusual names."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ (cj/create-directory-or-file-ensuring-parents "file with spaces.org" "content")
+ (cj/create-directory-or-file-ensuring-parents "unicode-ß₄©.org" "content") ;; Direct Unicode chars
+ ;; Or use proper escape sequences:
+ ;; (cj/create-directory-or-file-ensuring-parents "unicode-\u00DF\u2074\u00A9.org" "content")
+ (let ((entries (cj/get--directory-entries cj/test-base-dir)))
+ (should (cl-some (lambda (e) (string= (f-filename e) "file with spaces.org")) entries))
+ (should (cl-some (lambda (e) (string= (f-filename e) "unicode-ß₄©.org")) entries))))
+ (cj/test--teardown)))
+
+;;;; Error Cases
+
+(ert-deftest test-error-nonexistent-directory ()
+ "Test calling on nonexistent directory returns nil or error handled."
+ (should-error (cj/get--directory-entries "/path/does/not/exist")))
+ ;
+(ert-deftest test-error-not-a-directory-path ()
+ "Test calling on a file path signals error."
+ (cj/test--setup)
+ (unwind-protect
+ (let ((filepath (cj/create-directory-or-file-ensuring-parents "file.txt" "data")))
+ (should-error (cj/get--directory-entries filepath)))
+ (cj/test--teardown)))
+
+(ert-deftest test-error-permission-denied ()
+ "Test directory with no permission signals error or returns nil."
+ (cj/test--setup)
+ (unwind-protect
+ (let ((dir (expand-file-name "noperm" cj/test-base-dir)))
+ (cj/create-directory-or-file-ensuring-parents "noperm/file2.org" "Nested file")
+ (let ((original-mode (file-modes dir))) ; Save original permissions
+ (set-file-modes dir #o000) ; Remove all permissions
+ (unwind-protect
+ (should-error (cj/get--directory-entries dir))
+ (set-file-modes dir original-mode)))) ; Restore permissions - extra paren here
+ (cj/test--teardown)))
+
+;;; --------------------- CJ/LIST-DIRECTORY-RECURSIVE TESTS ---------------------
+;;;; Normal Cases
+
+(ert-deftest test-normal-single-file-at-root ()
+ "Test the normal base case: one single file at the root."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ (cj/create-directory-or-file-ensuring-parents "file.txt" "Content")
+ (let ((file-infos (cj/list-directory-recursive cj/test-base-dir)))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file.txt")) file-infos))))
+ (cj/test--teardown)))
+
+(ert-deftest test-normal-multiple-files-at-root ()
+ "Test finding multiple files at the root directory."
+ (cj/test--setup)
+ (unwind-protect
+ (cj/create-directory-or-file-ensuring-parents "file1.txt" "Content in File 1")
+ (cj/create-directory-or-file-ensuring-parents "file2.org" "Content in File 2")
+ (cj/create-directory-or-file-ensuring-parents "file3.md" "Content in File 3")
+ (let ((file-infos (cj/list-directory-recursive cj/test-base-dir)))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file1.txt")) file-infos))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file2.org")) file-infos))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file3.md")) file-infos)))
+ (cj/test--teardown)))
+
+(ert-deftest test-normal-multiple-files-in-subdirectories ()
+ "Test finding multiple files at the root directory."
+ (cj/test--setup)
+ (unwind-protect
+ (cj/create-directory-or-file-ensuring-parents "one/file1.txt" "Content in File 1")
+ (cj/create-directory-or-file-ensuring-parents "two/file2.org" "Content in File 2")
+ (cj/create-directory-or-file-ensuring-parents "three/file3.md" "Content in File 3")
+ (let ((file-infos (cj/list-directory-recursive cj/test-base-dir)))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file1.txt")) file-infos))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file2.org")) file-infos))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "file3.md")) file-infos)))
+ (cj/test--teardown)))
+
+(ert-deftest test-recursive-excludes-hidden-by-default ()
+ "Test that hidden files are excluded by default in recursive listing.
+Verify that files beginning with a dot, hidden directories, and files
+within hidden directories are all excluded when include-hidden is nil."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ ;; Create test assets including hidden files at various levels
+ (cj/create-directory-or-file-ensuring-parents ".hiddenfile" "Hidden content")
+ (cj/create-directory-or-file-ensuring-parents ".hiddendir/visible-in-hidden.txt" "File in hidden dir")
+ (cj/create-directory-or-file-ensuring-parents "visible/normal.txt" "Normal file")
+ (cj/create-directory-or-file-ensuring-parents "visible/.hidden-in-visible.txt" "Hidden in visible dir")
+
+ ;; Get all files recursively (default excludes hidden)
+ (let ((file-infos (cj/list-directory-recursive cj/test-base-dir)))
+ ;; Should not see .hiddenfile at root
+ (should-not (cl-some (lambda (fi)
+ (string= (f-filename (plist-get fi :path)) ".hiddenfile"))
+ file-infos))
+ ;; Should not see .hiddendir directory
+ (should-not (cl-some (lambda (fi)
+ (string= (f-filename (plist-get fi :path)) ".hiddendir"))
+ file-infos))
+ ;; Should not see files inside hidden directory
+ (should-not (cl-some (lambda (fi)
+ (string= (f-filename (plist-get fi :path)) "visible-in-hidden.txt"))
+ file-infos))
+ ;; Should not see hidden file in visible directory
+ (should-not (cl-some (lambda (fi)
+ (string= (f-filename (plist-get fi :path)) ".hidden-in-visible.txt"))
+ file-infos))
+ ;; Should see normal visible file
+ (should (cl-some (lambda (fi)
+ (string= (f-filename (plist-get fi :path)) "normal.txt"))
+ file-infos))))
+ (cj/test--teardown)))
+
+(ert-deftest test-recursive-includes-hidden-with-flag ()
+ "Non-nil means hidden files are included.
+Verifies that files beginning with a dot, hidden directories, and files
+within hidden directories are all included when include-hidden is t."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ ;; Create test assets including hidden files at various levels
+ (cj/create-directory-or-file-ensuring-parents ".hiddenfile" "Hidden content")
+ (cj/create-directory-or-file-ensuring-parents ".hiddendir/visible-in-hidden.txt" "File in hidden dir")
+ (cj/create-directory-or-file-ensuring-parents "visible/normal.txt" "Normal file")
+ (cj/create-directory-or-file-ensuring-parents "visible/.hidden-in-visible.txt" "Hidden in visible dir")
+
+ ;; Get all files recursively with include-hidden = t
+ (let ((file-infos (cj/list-directory-recursive cj/test-base-dir t)))
+ ;; Should see .hiddenfile at root
+ (should (cl-some (lambda (fi)
+ (string= (f-filename (plist-get fi :path)) ".hiddenfile")) file-infos))
+ ;; Should see .hiddendir directory
+ (should (cl-some (lambda (fi) (and (plist-get fi :directory)
+ (string= (f-filename (plist-get fi :path)) ".hiddendir"))) file-infos))
+ ;; Should see files inside hidden directory
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "visible-in-hidden.txt")) file-infos))
+ ;; Should see hidden file in visible directory
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) ".hidden-in-visible.txt")) file-infos))
+ ;; Should still see normal visible file
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "normal.txt")) file-infos))))
+ (cj/test--teardown)))
+
+(ert-deftest test-normal-deeply-nested-structure ()
+ "Tests with deeply nested directory trees."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ (cj/create-directory-or-file-ensuring-parents
+ "one/two/three/four/five/six/seven/eight/nine/ten/eleven/twelve/13.txt" "thirteen")
+ (cj/create-directory-or-file-ensuring-parents
+ "1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/thirty.txt" "30")
+ (let ((file-infos (cj/list-directory-recursive cj/test-base-dir)))
+ ;; validate the files
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "13.txt")) file-infos))
+ (should (cl-some (lambda (fi) (string= (f-filename (plist-get fi :path)) "thirty.txt")) file-infos))))
+ (cj/test--teardown)))
+
+(ert-deftest test-normal-only-directory-entries ()
+ "Tests with deeply nested directory trees without files."
+ (cj/test--setup)
+ (unwind-protect
+ (progn
+ (cj/create-directory-or-file-ensuring-parents
+ "one/two/three/four/five/six/seven/eight/nine/ten/eleven/twelve/thirteen/")
+ (cj/create-directory-or-file-ensuring-parents
+ "1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/")
+ (let ((file-infos (cj/list-directory-recursive cj/test-base-dir)))
+ ;; validate the directories
+ (should (cl-some (lambda (fi)
+ (and (string= (f-filename (plist-get fi :path)) "thirteen")
+ (plist-get fi :directory)
+ (file-directory-p (plist-get fi :path))))
+ file-infos))
+
+ (should (cl-some (lambda (fi)
+ (and (string= (f-filename (plist-get fi :path)) "30")
+ (plist-get fi :directory)
+ (file-directory-p (plist-get fi :path))))
+ file-infos))))
+ (cj/test--teardown)))
+
+;; 5. =test-normal-filter-by-extension= - Filter predicate correctly filters .org files
+
+
+;; 6. =test-normal-filter-by-size= - Filter predicate filters files > 1KB
+;; 7. =test-normal-filter-excludes-directories= - Filter can exclude directories themselves
+;; 8. =test-normal-max-depth-one= - Respects max-depth=1 (only immediate children)
+;; 9. =test-normal-max-depth-three= - Respects max-depth=3 limit
+;; 11. =test-normal-executable-files= - Correctly identifies executable files
+;; 12. =test-normal-file-info-plist-structure= - Verifies correct plist keys/values returned
+
+;;;; Boundary Cases
+;; 1. =test-boundary-empty-directory= - Empty directory returns empty list
+;; 2. =test-boundary-single-empty-subdirectory= - Directory with only empty subdirectory
+;; 3. =test-boundary-unicode-filenames= - Files with unicode characters (emoji, Chinese, etc.)
+;; 4. =test-boundary-spaces-in-names= - Files/dirs with spaces in names
+;; 5. =test-boundary-special-characters= - Files with special chars (@#$%^&*()_+)
+;; 6. =test-boundary-very-long-filename= - File with 255 character name
+;; 8. =test-boundary-many-files= - Directory with 1000+ files
+;; 9. =test-boundary-max-depth-zero= - max-depth=0 (unlimited) works correctly
+;; 10. =test-boundary-symlinks= - How it handles symbolic links
+;; 11. =test-boundary-filter-returns-all-nil= - Filter that rejects everything
+;; 12. =test-boundary-filter-returns-all-true= - Filter that accepts everything
+
+;;;; Error Cases
+;; 1. =test-error-nonexistent-path= - Path that doesn't exist
+;; 2. =test-error-file-not-directory= - PATH is a file, not directory
+;; 3. =test-error-permission-denied= - Directory without read permissions
+;; 4. =test-error-permission-denied-subdirectory= - Subdirectory without permissions
+;; 5. =test-error-invalid-max-depth= - Negative max-depth value
+;; 6. =test-error-filter-predicate-errors= - Filter function that throws error
+;; 7. =test-error-circular-symlinks= - Circular symbolic link reference
+;; 8. =test-error-path-outside-home= - Attempt to access system directories (if restricted)
+;; 9. =test-error-nil-path= - PATH is nil
+;; 10. =test-error-empty-string-path= - PATH is empty string
+
+(provide 'test-testutil-filesystem-directory-entries)
+;;; test-testutil-filesystem-directory-entries.el ends here.
diff --git a/tests/test-theme-theme-persistence.el.disabled b/tests/test-theme-theme-persistence.el.disabled
new file mode 100644
index 00000000..e0b2f9e3
--- /dev/null
+++ b/tests/test-theme-theme-persistence.el.disabled
@@ -0,0 +1,135 @@
+;;; test-theme-theme-persistence.el --- Tests theme persistence mechanism -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for the persistence of the chosen theme
+
+;;; Code:
+
+(add-to-list 'load-path (concat user-emacs-directory "modules"))
+(require 'ui-theme)
+
+;; ------------------------ Constants / Setup / Teardown -----------------------
+
+(defvar cj/original-theme-name nil)
+(defvar cj/original-newline-setting nil)
+
+(defun cj/test-setup ()
+ "Required settings and save state before each test."
+
+ ;; save the current theme for restoration
+ (setq cj/original-theme-name (symbol-name (car custom-enabled-themes)))
+ (setq cj/original-newline-setting mode-require-final-newline)
+
+ ;; unload all themes before starting test
+ (mapcar #'disable-theme custom-enabled-themes)
+
+ ;; no EOF newlines
+ (custom-set-variables
+ '(require-final-newline nil))
+ (setq mode-require-final-newline nil))
+
+(defun cj/test-teardown ()
+ "Restore the state before each test."
+ ;; restore newline setting
+ (setq require-final-newline cj/original-newline-setting)
+
+ ;; if there wasn't an original theme, remove all themes
+ (if (string= cj/original-theme-name "nil")
+ (mapcar #'disable-theme custom-enabled-themes)
+ ;; otherwise, restore it
+ (load-theme (intern cj/original-theme-name))))
+
+;; ----------------------------------- Tests -----------------------------------
+
+(ert-deftest test-write-file-contents ()
+ "Normal Case: Uses function to write a string, reads it back, and compares."
+ (cj/test-setup)
+ (let ((teststring "testing123")
+ (testfilename "test-write-file-contents.txt"))
+ ;; call the function
+ (should (equal (cj/write-file-contents teststring testfilename)
+ 't))
+ ;; Read the file and check it's contents
+ (should (equal (with-temp-buffer(insert-file-contents testfilename)
+ (buffer-string))
+ teststring))
+ ;; clean up test file
+ (delete-file testfilename))
+ (cj/test-teardown))
+
+(ert-deftest test-write-file-not-writable ()
+ "Test writing to a non-writable file."
+ (cl-flet ((file-writeable-p (file) nil))
+ (let* ((non-writable-file (make-temp-file "test-non-writable"))
+ (should (equal (cj/write-file-contents "cowabunga" non-writable-file) 'nil)))
+ (delete-file non-writable-file))))
+
+(ert-deftest test-read-file-contents ()
+ "Normal Case: Writes string to file and reads contents using function."
+ (cj/test-setup)
+ (let ((teststring "testing123")
+ (testfilename "test-read-file-contents.txt"))
+ ;; write the file
+ (with-temp-buffer
+ (insert teststring)
+ (write-file testfilename))
+ ;; call the function
+ (should (equal (cj/read-file-contents testfilename)
+ teststring))
+ ;; clean up test file
+ (delete-file testfilename))
+ (cj/test-teardown))
+
+(ert-deftest test-read-file-nonexistent ()
+ "Test reading from a non-existent file returns nil."
+ (cj/test-setup)
+ (let* ((filename (concat (number-to-string (random 99999999)) "nonexistent-file.txt"))
+ (result (cj/read-file-contents filename)))
+ (should (equal result nil)))
+ (cj/test-teardown))
+
+(ert-deftest test-get-active-theme ()
+ (cj/test-setup)
+ "Normal Case: Sets theme, gets theme-name, and compares."
+ (let ((expected "wombat"))
+ (load-theme (intern expected))
+ (should (string= (cj/get-active-theme-name) expected))
+ (cj/test-teardown)))
+
+(ert-deftest test-get-active-theme ()
+ (cj/test-setup)
+ "Normal Case: Sets theme, gets theme-name, and compares."
+ (let ((expected "nil"))
+ (mapcar #'disable-theme custom-enabled-themes)
+ (should (equal (cj/get-active-theme-name) expected))
+ (cj/test-teardown)))
+
+(ert-deftest test-save-theme-to-file ()
+ "Normal case: sets theme, saves it, reads from file, and compares."
+ (cj/test-setup)
+ (let ((expected "wombat"))
+ (load-theme (intern expected))
+ (cj/save-theme-to-file)
+ (should (equal (cj/read-file-contents theme-file) expected))
+ (cj/test-teardown)))
+
+(ert-deftest test-load-theme-from-file ()
+ "Normal case: saves new theme to file, loads it from file, and compares."
+ (cj/test-setup)
+ (let ((expected "wombat")) ;; the ui theme that test-setup uses.
+ (cj/write-file-contents expected theme-file)
+ (cj/load-theme-from-file)
+ (should (equal expected (cj/get-active-theme-name))))
+ (cj/test-teardown))
+
+(ert-deftest test-load-nil-theme ()
+ "Corner case: saves 'nil as theme name to file, loads it, and compares to not having a theme."
+ (cj/test-setup)
+ (let ((expected "nil")) ;; the ui theme that test-setup uses.
+ (cj/write-file-contents expected theme-file)
+ (cj/load-theme-from-file)
+ (should (equal expected (cj/get-active-theme-name))))
+ (cj/test-teardown))
+
+(provide 'test-theme-theme-persistence)
+;;; test-theme-theme-persistence.el ends here.
diff --git a/tests/test-title-case-region.el.disabled b/tests/test-title-case-region.el.disabled
new file mode 100644
index 00000000..ffab0c24
--- /dev/null
+++ b/tests/test-title-case-region.el.disabled
@@ -0,0 +1,44 @@
+;;; test-title-case-region.el --- -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the title-case region function in custom-functions.el
+
+;; Note on Title Case
+;; Title case is a capitalization convention where major words are
+;; capitalized,and most minor words are lowercase. Nouns,verbs (including
+;; linking verbs), adjectives, adverbs,pronouns,and all words of four letters or
+;; more are considered major words. Short (i.e., three letters or fewer)
+;; conjunctions, short prepositions,and all articles are considered minor
+;; words."
+
+;; positive case (single line, all lowercase, no skip words)
+;; positive case (six lines, mixed case, skip words)
+;; negative case (single line, all skip-words)
+;; negative case (a long empty string)
+
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (concat user-emacs-directory "modules"))
+(require 'custom-functions)
+
+(ert-deftest test-cj/fixup-whitespace-positive-first-line-only ()
+ "Test a positive case with two lines.
+Both lines have whitespace at the beginning and the end. This tests that when
+this function is called on the first line, only that line is affected."
+ (let ((testdata " Hello, world! \n Foo bar ")
+ (expected "Hello, world!\n Foo bar ")
+ (actual))
+ (with-temp-buffer
+ (insert testdata)
+ (goto-char (point-min))
+ (cj/fixup-whitespace-line-or-region)
+ (setq actual (buffer-string))
+ (should (string= actual expected)))))
+
+
+
+
+(provide 'test-title-case-region)
+;;; test-title-case-region.el ends here.
diff --git a/tests/testutil-filesystem.el b/tests/testutil-filesystem.el
new file mode 100644
index 00000000..b1970b62
--- /dev/null
+++ b/tests/testutil-filesystem.el
@@ -0,0 +1,180 @@
+;;; testutil-filesystem.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;
+;; Author: Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;; This library provides reusable helper functions for GPTel filesystem tools.
+;;
+;; It uses f.el and core Emacs libraries for path manipulation, directory listing,
+;; file info retrieval, filtering, and recursive traversal.
+;;
+;; Designed to be used by multiple tools that operate on the filesystem.
+;;
+;;; Code:
+
+(require 'f)
+(require 'cl-lib)
+(require 'subr-x)
+
+;; Get directory entries in PATH. Returns list of absolute paths.
+;; Default excludes hidden files and directories (name begins with dot).
+;; Optional INCLUDE-HIDDEN to include hidden entries.
+;; Optional FILTER-PREDICATE is a function called on each absolute path to filter.
+(defun cj/get--directory-entries (path &optional include-hidden filter-predicate)
+ "Return a list of entries (absolute paths) in directory PATH.
+Entries exclude '.' and '..'.
+By default, hidden entries (starting with '.') are excluded unless
+INCLUDE-HIDDEN is non-nil. FILTER-PREDICATE, if non-nil, is a predicate
+function called on each entry's absolute path; only entries where it returns
+non-nil are included."
+ ;; Convert 'path' to an absolute filename string
+ (let* ((expanded-path (expand-file-name path))
+ ;; get absolute paths in expanded directory
+ (entries (directory-files expanded-path t nil t))
+ ;; remove "." ".." entries
+ (filtered-entries
+ (cl-remove-if
+ (lambda (entry)
+ (or (member (f-filename entry) '("." ".."))
+ ;; and hidden files include-hidden is non-nil.
+ (and (not include-hidden)
+ (string-prefix-p "." (f-filename entry)))))
+ entries)))
+ ;; apply filtered predicate if provided
+ (if filter-predicate
+ (seq-filter filter-predicate filtered-entries)
+ ;; retun filtered-entries
+ filtered-entries)))
+
+(defun cj/get-file-info (path)
+ "Get file information for PATH.
+Returned plist keys:
+:success t or nil
+:error string error message if :success is nil
+:path absolute file path (string)
+:size file size (integer)
+:last-modified last modification time (time value)
+:directory boolean: t if a directory
+:permissions string with symbolic permissions, e.g. \"drwxr-xr-x\"
+:executable boolean: t if executable file
+:owner string: owner name or UID if name unavailable
+:group string: group name or GID if name unavailable"
+ ;; handle errors during evaluation
+ (condition-case err
+ (let* ((expanded-path (expand-file-name path)))
+ (if (not (file-readable-p expanded-path))
+ ;; Explicit permission denied check
+ (list :success nil :path expanded-path :error
+ (format "Permission denied: %s" expanded-path))
+ (let*
+ ;; t = return string names for uid/gid
+ ((attrs (file-attributes expanded-path t))
+ (size (file-attribute-size attrs))
+ (mod (file-attribute-modification-time attrs))
+ (dirp (eq t (file-attribute-type attrs)))
+ (modes (file-modes expanded-path))
+ (perm (cj/-mode-to-permissions modes))
+ (execp (file-executable-p expanded-path))
+ (owner (file-attribute-user-id attrs)) ; Get owner
+ (group (file-attribute-group-id attrs))) ; Get group
+ (list :success t :path expanded-path :size size :last-modified mod
+ :directory dirp :permissions perm :executable execp
+ :owner (or owner "unknown")
+ :group (or group "unknown")))))
+ ;; if error, return failure plist with error info
+ (error (list :success nil :path path :error (error-message-string err)))))
+
+(defun cj/format-file-info (file-info base-path)
+ "Format FILE-INFO plist relative to BASE-PATH as a string.
+Handles missing keys gracefully by supplying default values."
+ (let ((permissions (or (plist-get file-info :permissions) ""))
+ (executable (if (plist-get file-info :executable) "*" " "))
+ (size (file-size-human-readable (or (plist-get file-info :size) 0)))
+ (last-modified (or (plist-get file-info :last-modified) (current-time)))
+ (path (or (plist-get file-info :path) base-path)))
+ (format " %s%s %10s %s %s"
+ permissions
+ executable
+ size
+ (format-time-string "%Y-%m-%d %H:%M" last-modified)
+ (file-relative-name path base-path))))
+
+;; Convert file mode bits integer to string like ls -l, e.g. drwxr-xr-x
+(defun cj/-mode-to-permissions (mode)
+ "Convert file MODE (returned by `file-modes') to symbolic permission string."
+ (concat
+ (if (eq (logand #o40000 mode) #o40000) "d" "-")
+ (mapconcat
+ (lambda (bits)
+ (concat (if (/= 0 (logand bits 4)) "r" "-")
+ (if (/= 0 (logand bits 2)) "w" "-")
+ (if (/= 0 (logand bits 1)) "x" "-")))
+ (list (logand (/ mode 64) 7)
+ (logand (/ mode 8) 7)
+ (logand mode 7))
+ "")))
+
+;; Filter a list of file info plists by extension (case insensitive).
+;; Always includes directories.
+(defun cj/filter-by-extension (file-info-list extension)
+ "Keep only directories and files with EXTENSION from FILE-INFO-LIST.
+EXTENSION should not include leading dot, e.g. \"org\"."
+ ;; return full list if no extension
+ (if (not extension)
+ file-info-list
+ (cl-remove-if-not
+ (lambda (fi)
+ ;; always keep directories
+ (or (plist-get fi :directory)
+ ;; and successful file entries
+ (and (plist-get fi :success)
+ ;; and file extensions that match case-insensitively
+ (string-suffix-p (concat "." extension)
+ (f-filename (plist-get fi :path))
+ t))))
+ file-info-list)))
+
+(defun cj/list-directory-recursive (path &optional include-hidden filter-predicate max-depth)
+ "Recursively list files under PATH applying FILTER-PREDICATE.
+PATH is the directory to list.
+INCLUDE-HIDDEN if non-nil, includes hidden files (those starting with '.').
+FILTER-PREDICATE, if non-nil, is a function called on file info plist and
+returns non-nil to include file.
+MAX-DEPTH limits recursion depth (nil or 0 = unlimited)."
+ ;; set up cl-recursive function with path and current depth
+ (cl-labels ((recurse (path depth)
+ (let ((expanded-path (expand-file-name path))
+ ;; empty list to accumulate file info plists
+ (file-info-list '()))
+ ;; ensure we're working with directories only
+ (when (not (file-directory-p expanded-path))
+ (error "Not a directory: %s" expanded-path))
+
+ ;; loop over each file in the path
+ (dolist (file-entry
+ (cj/get--directory-entries expanded-path include-hidden))
+ ;; get the metadata for the file
+ (let ((file-metadata (cj/get-file-info file-entry)))
+ ;; if retrieving metadata was successful
+ (when (and file-metadata (plist-get file-metadata :success))
+ ;; if there's no custom filter or it matches, add it to the list
+ (when (or (not filter-predicate)
+ (funcall filter-predicate file-metadata))
+ (push file-metadata file-info-list))
+ ;; if it's a directory and we're not at the max-depth
+ (when (and (plist-get file-metadata :directory)
+ (or (not max-depth) (< depth (1- max-depth))))
+ ;; gather all the files and recurse with that file
+ (setq file-info-list
+ (nconc file-info-list (recurse file-entry (1+ depth)))))
+ ;; warn if recursion returned received both a success and error
+ (when (and (plist-get file-metadata :success)
+ (plist-get file-metadata :error))
+ (message "Warning: %s" (plist-get file-metadata :error))))))
+ ;; restore the file order (as they were pushed into reverse order)
+ (nreverse file-info-list))))
+ ;; start recursion at the top level
+ (recurse path 0)))
+
+(provide 'testutil-filesystem)
+;;; testutil-filesystem.el ends here.
diff --git a/tests/testutil-general.el b/tests/testutil-general.el
new file mode 100644
index 00000000..c96a4a38
--- /dev/null
+++ b/tests/testutil-general.el
@@ -0,0 +1,191 @@
+;;; testutil-general.el --- -*- coding: utf-8; lexical-binding: t; -*-
+;;
+;; Author: Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;; This library provides general helper functions and constants for managing
+;; test directories and files across test suites.
+;;
+;; It establishes a user-local hidden directory as the root for all test assets,
+;; provides utilities to create this directory safely, create temporary files
+;; and subdirectories within it, and clean up after tests.
+;;
+;; This library should be required by test suites to ensure consistent,
+;; reliable, and isolated file-system resources.
+;;
+;;; Code:
+
+(defconst cj/test-base-dir
+ (expand-file-name "~/.temp-gptel-tests/")
+ "Base directory for all GPTel test files and directories.
+
+All test file-system artifacts should be created under this hidden
+directory in the user's home. This avoids relying on ephemeral system
+directories like /tmp and reduces flaky test failures caused by external
+cleanup.")
+
+(defun cj/create-test-base-dir ()
+ "Create the test base directory `cj/test-base-dir' if it does not exist.
+
+Returns the absolute path to the test base directory.
+Signals an error if creation fails."
+ (let ((dir (file-name-as-directory cj/test-base-dir)))
+ (unless (file-directory-p dir)
+ (make-directory dir t))
+ (if (file-directory-p dir) dir
+ (error "Failed to create test base directory %s" dir))))
+
+(defun cj/create--directory-ensuring-parents (dirpath)
+ "Create nested directories specified by DIRPATH.
+Error if DIRPATH exists already.
+Ensure DIRPATH is within `cj/test-base-dir`."
+ (let* ((base (file-name-as-directory cj/test-base-dir))
+ (fullpath (expand-file-name dirpath base)))
+ (unless (string-prefix-p base fullpath)
+ (error "Directory path %s is outside base test directory %s" fullpath base))
+ (when (file-exists-p fullpath)
+ (error "Directory path already exists: %s" fullpath))
+ (make-directory fullpath t)
+ fullpath))
+
+(defun cj/create--file-ensuring-parents (filepath content &optional executable)
+ "Create file at FILEPATH (relative to `cj/test-base-dir`) with CONTENT.
+Error if file exists already.
+Create parent directories as needed.
+If EXECUTABLE is non-nil, set execute permissions on file.
+Ensure FILEPATH is within `cj/test-base-dir`."
+ (let* ((base (file-name-as-directory cj/test-base-dir))
+ (fullpath (expand-file-name filepath base))
+ (parent-dir (file-name-directory fullpath)))
+ (unless (string-prefix-p base fullpath)
+ (error "File path %s is outside base test directory %s" fullpath base))
+ (when (file-exists-p fullpath)
+ (error "File already exists: %s" fullpath))
+ (unless (file-directory-p parent-dir)
+ (make-directory parent-dir t))
+ (with-temp-buffer
+ (when content
+ (insert content))
+ (write-file fullpath))
+ (when executable
+ (chmod fullpath #o755))
+ fullpath))
+
+(defun cj/create-directory-or-file-ensuring-parents (path &optional content executable)
+ "Create a directory or file specified by PATH relative to `cj/test-base-dir`.
+If PATH ends with a slash, create nested directories.
+Else create a file with optional CONTENT.
+If EXECUTABLE is non-nil and creating a file, set executable permissions.
+Error if the target path already exists.
+Return the full created path."
+ (let ((is-dir (string-suffix-p "/" path)))
+ (if is-dir
+ (cj/create--directory-ensuring-parents path)
+ (cj/create--file-ensuring-parents path content executable))))
+
+
+;; (defun cj/create-file-with-content-ensuring-parents (filepath content &optional executable)
+;; "Create a file at FILEPATH with CONTENT, ensuring parent directories exist.
+;; FILEPATH will be relative to `cj/test-base-dir'.
+;; Signals an error if the file already exists.
+;; If EXECUTABLE is non-nil, set executable permission on the file.
+;; Errors if the resulting path is outside `cj/test-base-dir`."
+;; (let* ((base (file-name-as-directory cj/test-base-dir))
+;; (fullpath (if (file-name-absolute-p filepath)
+;; (expand-file-name filepath)
+;; (expand-file-name filepath base))))
+;; (unless (string-prefix-p base fullpath)
+;; (error "File path %s is outside base test directory %s" fullpath base))
+;; (let ((parent-dir (file-name-directory fullpath)))
+;; (when (file-exists-p fullpath)
+;; (error "File already exists: %s" fullpath))
+;; (unless (file-directory-p parent-dir)
+;; (make-directory parent-dir t))
+;; (with-temp-buffer
+;; (insert content)
+;; (write-file fullpath))
+;; (when executable
+;; (chmod fullpath #o755))
+;; fullpath)))
+
+(defun cj/fix-permissions-recursively (dir)
+ "Recursively set read/write permissions for user under DIR.
+Directories get user read, write, and execute permissions to allow recursive
+operations."
+ (when (file-directory-p dir)
+ (dolist (entry (directory-files-recursively dir ".*" t))
+ (when (file-exists-p entry)
+ (let* ((attrs (file-attributes entry))
+ (is-dir (car attrs))
+ (mode (file-modes entry))
+ (user-r (logand #o400 mode))
+ (user-w (logand #o200 mode))
+ (user-x (logand #o100 mode))
+ new-mode)
+ (setq new-mode mode)
+ (unless user-r
+ (setq new-mode (logior new-mode #o400)))
+ (unless user-w
+ (setq new-mode (logior new-mode #o200)))
+ (when is-dir
+ ;; Ensure user-execute for directories
+ (unless user-x
+ (setq new-mode (logior new-mode #o100))))
+ (unless (= mode new-mode)
+ (set-file-modes entry new-mode)))))))
+
+(defun cj/delete-test-base-dir ()
+ "Recursively delete test base directory `cj/test-base-dir' and contents.
+Ensures all contained files and directories have user read/write permissions
+so deletion is not blocked by permissions.
+After deletion, verifies that the directory no longer exists.
+Signals an error if the directory still exists after deletion attempt."
+ (let ((dir (file-name-as-directory cj/test-base-dir)))
+ (when (file-directory-p dir)
+ (cj/fix-permissions-recursively dir)
+ (delete-directory dir t))
+ (when (file-directory-p dir)
+ (error "Test base directory %s still exists after deletion" dir))))
+
+(defun cj/create-temp-test-file (&optional prefix)
+ "Create a uniquely named temporary file under `cj/test-base-dir'.
+
+Optional argument PREFIX is a string to prefix the filename, defaults
+to \"tempfile-\". Returns the absolute path to the newly created empty file.
+Errors if base test directory cannot be created or file creation fails."
+ (let ((base (cj/create-test-base-dir))
+ (file nil))
+ (setq file (make-temp-file (expand-file-name (or prefix "tempfile-") base)))
+ (unless (file-exists-p file)
+ (error "Failed to create temporary test file under %s" base))
+ file))
+
+(defun cj/create-test-subdirectory (subdir)
+ "Ensure subdirectory SUBDIR (relative to `cj/test-base-dir') exists.
+
+Creates parent directories as needed.
+
+Returns the absolute path to the subdirectory.
+
+Signals an error if creation fails.
+
+SUBDIR must be a relative path string."
+ (let* ((base (cj/create-test-base-dir))
+ (fullpath (expand-file-name subdir base)))
+ (unless (file-directory-p fullpath)
+ (make-directory fullpath t))
+ (if (file-directory-p fullpath) fullpath
+ (error "Failed to create test subdirectory %s" subdir))))
+
+(defun cj/create-temp-test-file-with-content (content &optional prefix)
+ "Create uniquely named temp file in =cj/test-base-dir= and write CONTENT to it.
+Optional PREFIX is a filename prefix string, default \"tempfile-\".
+Returns absolute path to the created file."
+ (let ((file (cj/create-temp-test-file prefix)))
+ (with-temp-buffer
+ (insert content)
+ (write-file file))
+ file))
+
+(provide 'testutil-general)
+;;; testutil-general.el ends here.