summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-10-26 01:24:42 -0500
committerCraig Jennings <c@cjennings.net>2025-10-26 01:24:42 -0500
commit7e038cce93981e2b78bf6e160694d81cfa5dda67 (patch)
tree81a7d02fa85b438bb7df6001a83cdb4f033c2ef0 /tests
parente712c8b00e47b6f9d9cbb6e6bf0a0732b4ef2e5c (diff)
fix+test:custom-line-paragraph:infinite loop remove-lines-containing
- Add guard clause to handle empty string input gracefully (would cause infinite loop) - Add 33 comprehensive tests covering normal cases, boundary cases, and error cases - Tests verify Unicode support, regex character escaping, region vs buffer operations - All 33 tests passing
Diffstat (limited to 'tests')
-rw-r--r--tests/test-custom-line-paragraph-remove-lines-containing.el456
1 files changed, 456 insertions, 0 deletions
diff --git a/tests/test-custom-line-paragraph-remove-lines-containing.el b/tests/test-custom-line-paragraph-remove-lines-containing.el
new file mode 100644
index 00000000..61fab89c
--- /dev/null
+++ b/tests/test-custom-line-paragraph-remove-lines-containing.el
@@ -0,0 +1,456 @@
+;;; test-custom-line-paragraph-remove-lines-containing.el --- Tests for cj/remove-lines-containing -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/remove-lines-containing function from custom-line-paragraph.el
+;;
+;; This function removes all lines containing TEXT.
+;; If region is active, operate only on the region, otherwise on entire buffer.
+;; The operation is undoable and reports the count of removed lines.
+;;
+;; The function uses (regexp-quote text) to treat special regex characters literally.
+;;
+;; IMPORTANT NOTE ON REGION ACTIVATION IN BATCH MODE:
+;; When testing functions that use (use-region-p) in batch mode, you must
+;; explicitly activate the region. Unlike interactive Emacs, batch mode does
+;; not automatically activate regions when you set mark and point.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Stub expand-region package
+(provide 'expand-region)
+
+;; Now load the actual production module
+(require 'custom-line-paragraph)
+
+;;; Setup and Teardown
+
+(defun test-remove-lines-containing-setup ()
+ "Setup for remove-lines-containing tests."
+ (cj/create-test-base-dir))
+
+(defun test-remove-lines-containing-teardown ()
+ "Teardown for remove-lines-containing tests."
+ (cj/delete-test-base-dir))
+
+;;; Normal Cases
+
+(ert-deftest test-remove-lines-containing-single-match ()
+ "Should remove single line containing text."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "line one\nline two\nline three")
+ (cj/remove-lines-containing "two")
+ (should-not (string-match-p "two" (buffer-string)))
+ (should (string-match-p "one" (buffer-string)))
+ (should (string-match-p "three" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-multiple-matches ()
+ "Should remove multiple lines containing text."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "alpha test\nbeta\ngamma test\ndelta")
+ (cj/remove-lines-containing "test")
+ (should-not (string-match-p "alpha" (buffer-string)))
+ (should-not (string-match-p "gamma" (buffer-string)))
+ (should (string-match-p "beta" (buffer-string)))
+ (should (string-match-p "delta" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-preserve-non-matching ()
+ "Should preserve lines not containing text."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "keep this\nremove BAD this\nkeep that\nremove BAD that")
+ (cj/remove-lines-containing "BAD")
+ (should (string= "keep this\nkeep that\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-region-only ()
+ "Should only affect active region."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "before target\ntarget middle\ntarget end\nafter")
+ (goto-char (point-min))
+ (forward-line 1)
+ (set-mark (point))
+ (forward-line 2)
+ (transient-mark-mode 1)
+ (activate-mark)
+ (cj/remove-lines-containing "target")
+ ;; Should keep "before target" and "after"
+ (should (string-match-p "before target" (buffer-string)))
+ (should (string-match-p "after" (buffer-string)))
+ ;; Should remove middle and end
+ (should-not (string-match-p "target middle" (buffer-string)))
+ (should-not (string-match-p "target end" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-whole-buffer ()
+ "Should operate on entire buffer when no region active."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "one X\ntwo\nthree X\nfour")
+ (cj/remove-lines-containing "X")
+ (should (string= "two\nfour" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-partial-match ()
+ "Should match text appearing anywhere in line."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "prefix MATCH suffix\nno match here\nMATCH at start\nat end MATCH")
+ (cj/remove-lines-containing "MATCH")
+ (should (string= "no match here\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-multiple-occurrences-per-line ()
+ "Should remove line with text appearing multiple times."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "FOO and FOO and FOO\nbar\nFOO again")
+ (cj/remove-lines-containing "FOO")
+ (should (string= "bar\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-no-matches ()
+ "Should leave buffer unchanged when text not found."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "line one\nline two\nline three")
+ (let ((original (buffer-string)))
+ (cj/remove-lines-containing "NOTFOUND")
+ (should (string= original (buffer-string)))))
+ (test-remove-lines-containing-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-remove-lines-containing-empty-buffer ()
+ "Should handle empty buffer gracefully."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (cj/remove-lines-containing "anything")
+ (should (string= "" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-single-line-with-match ()
+ "Should remove only line when it matches."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "only line with TARGET")
+ (cj/remove-lines-containing "TARGET")
+ (should (string= "" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-single-line-without-match ()
+ "Should keep only line when it doesn't match."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "only line")
+ (cj/remove-lines-containing "NOTHERE")
+ (should (string= "only line" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-text-at-beginning ()
+ "Should match text at beginning of line."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "TARGET at start\nmiddle TARGET middle\nkeep this")
+ (cj/remove-lines-containing "TARGET")
+ (should (string= "keep this" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-text-at-end ()
+ "Should match text at end of line."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "at end TARGET\nkeep this\nanother TARGET")
+ (cj/remove-lines-containing "TARGET")
+ (should (string= "keep this\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-text-in-middle ()
+ "Should match text in middle of line."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "prefix TARGET suffix\nkeep\nanother TARGET here")
+ (cj/remove-lines-containing "TARGET")
+ (should (string= "keep\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-empty-string ()
+ "Should handle empty string gracefully without hanging."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "line one\nline two\nline three")
+ (let ((original (buffer-string)))
+ ;; Should not hang or remove anything
+ (cj/remove-lines-containing "")
+ ;; Buffer should be unchanged
+ (should (string= original (buffer-string)))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-whitespace-only-text ()
+ "Should remove lines with specific whitespace."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "has double space\nsingle space\nhas double space again")
+ (cj/remove-lines-containing " ")
+ (should (string= "single space\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-very-long-line ()
+ "Should handle very long lines (5000+ chars)."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (let ((long-line (concat (make-string 2500 ?x) "TARGET" (make-string 2500 ?y))))
+ (insert long-line "\nshort line\n" long-line)
+ (cj/remove-lines-containing "TARGET")
+ (should (string= "short line\n" (buffer-string)))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-unicode-emoji ()
+ "Should handle Unicode and emoji text."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "hello 👋 world\nno emoji\nbye 👋 friend")
+ (cj/remove-lines-containing "👋")
+ (should (string= "no emoji\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-special-regex-chars ()
+ "Should treat regex special characters literally (regexp-quote)."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "line with .\nanother line\nline with * here\nkeep")
+ (cj/remove-lines-containing ".")
+ ;; Should remove only the line with literal ".", not all lines (which . would match)
+ (should-not (string-match-p "line with \\." (buffer-string)))
+ (should (string-match-p "another line" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-case-sensitive ()
+ "Should perform case-sensitive matching."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (let ((case-fold-search nil)) ; Ensure case-sensitive
+ (insert "Line with Target\nLine with target\nLine with TARGET")
+ (cj/remove-lines-containing "target")
+ ;; Only lowercase "target" should match
+ (should (string-match-p "Target" (buffer-string)))
+ (should (string-match-p "TARGET" (buffer-string)))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-all-lines-match ()
+ "Should remove all lines when every line contains text."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "X one\nX two\nX three")
+ (cj/remove-lines-containing "X")
+ (should (string= "" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-first-line-matches ()
+ "Should handle match at buffer start."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "REMOVE first\nkeep second\nkeep third")
+ (cj/remove-lines-containing "REMOVE")
+ (should (string= "keep second\nkeep third" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-last-line-matches ()
+ "Should handle match at buffer end."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "keep first\nkeep second\nREMOVE last")
+ (cj/remove-lines-containing "REMOVE")
+ (should (string= "keep first\nkeep second\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-alternating-matches ()
+ "Should handle alternating matching lines."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "keep\nREMOVE\nkeep\nREMOVE\nkeep")
+ (cj/remove-lines-containing "REMOVE")
+ (should (string= "keep\nkeep\nkeep" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-narrowed-buffer ()
+ "Should respect buffer narrowing (save-restriction)."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "before TARGET\nmiddle TARGET\nend TARGET\nafter")
+ (goto-char (point-min))
+ (forward-line 1)
+ (let ((beg (point)))
+ (forward-line 2)
+ (narrow-to-region beg (point))
+ (cj/remove-lines-containing "TARGET")
+ (widen)
+ ;; Should keep "before TARGET" and "after"
+ (should (string-match-p "before TARGET" (buffer-string)))
+ (should (string-match-p "after" (buffer-string)))
+ ;; Should remove middle and end
+ (should-not (string-match-p "middle TARGET" (buffer-string)))
+ (should-not (string-match-p "end TARGET" (buffer-string)))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-backwards-region ()
+ "Should handle backwards region (mark after point)."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "keep\nREMOVE\nREMOVE\nkeep")
+ (goto-char (point-max))
+ (set-mark (point))
+ (goto-char (point-min))
+ (forward-line 1)
+ (transient-mark-mode 1)
+ (activate-mark)
+ (cj/remove-lines-containing "REMOVE")
+ ;; Should work same as forward region
+ (should (= 2 (how-many "keep" (point-min) (point-max)))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-entire-buffer-as-region ()
+ "Should handle entire buffer selected as region."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "keep\nREMOVE\nkeep\nREMOVE")
+ (transient-mark-mode 1)
+ (mark-whole-buffer)
+ (activate-mark)
+ (cj/remove-lines-containing "REMOVE")
+ (should (string= "keep\nkeep\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-tab-characters ()
+ "Should match lines with tab characters."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "line\twith\ttab\nno tabs here\nanother\ttab")
+ (cj/remove-lines-containing "\t")
+ (should (string= "no tabs here\n" (buffer-string))))
+ (test-remove-lines-containing-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-remove-lines-containing-read-only-buffer ()
+ "Should error when attempting to modify read-only buffer."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "line to remove\nline to keep")
+ (read-only-mode 1)
+ (should-error (cj/remove-lines-containing "remove")))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-buffer-modified-flag ()
+ "Should set buffer modified flag when lines removed."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "REMOVE this\nkeep this")
+ (set-buffer-modified-p nil)
+ (cj/remove-lines-containing "REMOVE")
+ (should (buffer-modified-p)))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-undo-behavior ()
+ "Should support undo after removing lines."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (let* ((temp-file (expand-file-name "test-undo-rmlines.txt" cj/test-base-dir))
+ (original-content "REMOVE this\nkeep this"))
+ ;; Create file with initial content
+ (with-temp-file temp-file
+ (insert original-content))
+ ;; Open file and test undo
+ (find-file temp-file)
+ (buffer-enable-undo)
+ ;; Establish undo history
+ (goto-char (point-min))
+ (insert " ")
+ (delete-char -1)
+ (undo-boundary)
+ (let ((before-remove (buffer-string)))
+ (cj/remove-lines-containing "REMOVE")
+ (undo-boundary)
+ (let ((after-remove (buffer-string)))
+ (should-not (string= before-remove after-remove))
+ (undo)
+ (should (string= before-remove (buffer-string)))))
+ (kill-buffer (current-buffer)))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-cursor-position-preserved ()
+ "Should preserve cursor position (save-excursion)."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "line one\nREMOVE\nline three")
+ (goto-char (point-min))
+ (forward-char 5) ; Position in middle of first line
+ (let ((original-pos (point)))
+ (cj/remove-lines-containing "REMOVE")
+ (should (= (point) original-pos))))
+ (test-remove-lines-containing-teardown)))
+
+(ert-deftest test-remove-lines-containing-widen-after-narrowing ()
+ "Should restore narrowing state (save-restriction)."
+ (test-remove-lines-containing-setup)
+ (unwind-protect
+ (with-temp-buffer
+ (insert "line one\nREMOVE\nline three\nline four")
+ (goto-char (point-min))
+ (forward-line 1)
+ (let ((beg (point)))
+ (forward-line 2)
+ (narrow-to-region beg (point))
+ (cj/remove-lines-containing "REMOVE")
+ ;; After function, narrowing should be restored
+ (should (= (point-min) beg))
+ (should (< (point-max) (buffer-size)))))
+ (test-remove-lines-containing-teardown)))
+
+(provide 'test-custom-line-paragraph-remove-lines-containing)
+;;; test-custom-line-paragraph-remove-lines-containing.el ends here