diff options
| author | Craig Jennings <c@cjennings.net> | 2025-10-12 11:47:26 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-10-12 11:47:26 -0500 |
| commit | 092304d9e0ccc37cc0ddaa9b136457e56a1cac20 (patch) | |
| tree | ea81999b8442246c978b364dd90e8c752af50db5 /tests | |
changing repositories
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-clear-blank-lines.el.disabled | 47 | ||||
| -rw-r--r-- | tests/test-custom-functions-join-line-or-region.el.disabled | 84 | ||||
| -rw-r--r-- | tests/test-custom-org-agenda-functions.el.disabled | 94 | ||||
| -rw-r--r-- | tests/test-fixup-whitespace.el.disabled | 159 | ||||
| -rw-r--r-- | tests/test-flyspell-config-functions.el.disabled | 149 | ||||
| -rw-r--r-- | tests/test-format-region.el.disabled | 110 | ||||
| -rw-r--r-- | tests/test-fs--mode-to-permissions.el | 36 | ||||
| -rw-r--r-- | tests/test-fs-filter-by-extension.el | 68 | ||||
| -rw-r--r-- | tests/test-fs-format-file-info.el | 40 | ||||
| -rw-r--r-- | tests/test-fs-get-file-info.el | 75 | ||||
| -rw-r--r-- | tests/test-fs-list-directory-recursive-extra.el | 106 | ||||
| -rw-r--r-- | tests/test-fs-list-directory-recursive.el | 71 | ||||
| -rw-r--r-- | tests/test-fs-validate-path.el | 45 | ||||
| -rw-r--r-- | tests/test-testutil-filesystem-directory-entries.el | 317 | ||||
| -rw-r--r-- | tests/test-theme-theme-persistence.el.disabled | 135 | ||||
| -rw-r--r-- | tests/test-title-case-region.el.disabled | 44 | ||||
| -rw-r--r-- | tests/testutil-filesystem.el | 180 | ||||
| -rw-r--r-- | tests/testutil-general.el | 191 |
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. |
