tp-forward-do/tp-backward-do target the TIMES-th match specifically; when fewer matches exist the string paths applied FUNCTION to the last available match -- the wrong target -- while buffer paths applied nothing. Both now apply nothing on shortfall and return the available count. Updates the two legacy with-range tests that codified the string behavior, adds four shortfall/exact-count regression tests, and documents the contract in both READMEs and the docstrings. Suite 443/443 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
377 lines
16 KiB
EmacsLisp
377 lines
16 KiB
EmacsLisp
;;; tp-search-tests.el --- ERT regression tests for tp-search.el -*- lexical-binding: t -*-
|
|
|
|
;;; Commentary:
|
|
|
|
;; Regression tests for confirmed bugs fixed in the search module
|
|
;; (tp-search.el). Each section is tagged with the canonical bug id
|
|
;; it guards against.
|
|
|
|
;;; Code:
|
|
|
|
(require 'ert)
|
|
(require 'tp)
|
|
|
|
;;; B37: backward searches must use `equal' matching like tp-forward
|
|
|
|
(ert-deftest tp-search-test-backward-value-matches-forward ()
|
|
"tp-backward with a non-nil VALUE finds the same region tp-forward finds.
|
|
The old code passed no predicate to `text-property-search-backward',
|
|
whose default matches values NOT `equal' to VALUE (inverted)."
|
|
(with-temp-buffer
|
|
(insert "aaa bbb aaa ")
|
|
(put-text-property 1 4 'k 'x)
|
|
(put-text-property 5 8 'k 'y)
|
|
(put-text-property 9 12 'k 'x)
|
|
(goto-char (point-min))
|
|
(let ((fwd (tp-forward 'k 'y)))
|
|
(should fwd)
|
|
(should (equal (list (prop-match-beginning fwd)
|
|
(prop-match-end fwd)
|
|
(prop-match-value fwd))
|
|
'(5 8 y))))
|
|
(goto-char (point-max))
|
|
(let ((bwd (tp-backward 'k 'y)))
|
|
(should bwd)
|
|
(should (equal (list (prop-match-beginning bwd)
|
|
(prop-match-end bwd)
|
|
(prop-match-value bwd))
|
|
'(5 8 y))))))
|
|
|
|
(ert-deftest tp-search-test-backward-value-adjacent-regions ()
|
|
"tp-backward finds a matching region among adjacent (gap-free) runs."
|
|
(with-temp-buffer
|
|
(insert "aaabbbccc")
|
|
(put-text-property 1 4 'k 'x)
|
|
(put-text-property 4 7 'k 'y)
|
|
(put-text-property 7 10 'k 'x)
|
|
(goto-char (point-max))
|
|
(let ((m (tp-backward 'k 'y)))
|
|
(should m)
|
|
(should (= (prop-match-beginning m) 4))
|
|
(should (= (prop-match-end m) 7))
|
|
(should (eq (prop-match-value m) 'y)))))
|
|
|
|
(ert-deftest tp-search-test-backward-value-n-walks-regions ()
|
|
"tp-backward with N=2 walks two matching regions backward."
|
|
(with-temp-buffer
|
|
(insert "aaa bbb aaa ")
|
|
(put-text-property 1 4 'k 'x)
|
|
(put-text-property 5 8 'k 'x)
|
|
(put-text-property 9 12 'k 'x)
|
|
(goto-char (point-max))
|
|
(let ((m (tp-backward 'k 'x nil 2)))
|
|
(should m)
|
|
(should (= (prop-match-beginning m) 5)))))
|
|
|
|
(ert-deftest tp-search-test-backward-value-no-match-returns-nil ()
|
|
"tp-backward returns nil when no region has an `equal' value."
|
|
(with-temp-buffer
|
|
(insert "aaa bbb")
|
|
(put-text-property 1 4 'k 'x)
|
|
(goto-char (point-max))
|
|
(should (null (tp-backward 'k 'missing)))))
|
|
|
|
(ert-deftest tp-search-test-backward-do-value-buffer ()
|
|
"tp-backward-do with a non-nil VALUE rewrites the matching region."
|
|
(with-temp-buffer
|
|
(insert "aaa bbb aaa ")
|
|
(put-text-property 1 4 'k 'x)
|
|
(put-text-property 5 8 'k 'y)
|
|
(put-text-property 9 12 'k 'x)
|
|
(let ((count (tp-backward-do #'upcase 'k 'y)))
|
|
(should (= count 1))
|
|
(should (equal (buffer-substring-no-properties 1 13)
|
|
"aaa BBB aaa ")))))
|
|
|
|
;;; B38: zero-width patterns must not loop forever in buffer branches
|
|
|
|
(ert-deftest tp-search-test-match-empty-pattern-buffer-terminates ()
|
|
"tp-match-set with an empty literal pattern terminates on buffers."
|
|
(with-temp-buffer
|
|
(insert "abc")
|
|
(let ((regions (tp-match-set "" '(face bold))))
|
|
;; Zero-width matches are recorded at each position, like the
|
|
;; string branch records them.
|
|
(should (equal regions '((1 . 1) (2 . 2) (3 . 3) (4 . 4)))))))
|
|
|
|
(ert-deftest tp-search-test-regexp-zero-width-buffer-terminates ()
|
|
"tp-regexp-set with a regexp matching empty terminates on buffers."
|
|
(with-temp-buffer
|
|
(insert "axbxc")
|
|
(let ((regions (tp-regexp-set "x*" '(face bold))))
|
|
(should regions)
|
|
;; The actual x's still got their property.
|
|
(should (eq (get-text-property 2 'face) 'bold))
|
|
(should (eq (get-text-property 4 'face) 'bold)))))
|
|
|
|
(ert-deftest tp-search-test-match-empty-pattern-string-clean ()
|
|
"tp-match-set with an empty pattern on a string no-ops cleanly.
|
|
The old string branch signaled args-out-of-range after scanning past
|
|
the end of the string."
|
|
(let ((result (tp-match-set "" '(face bold) "abc")))
|
|
(should (equal result "abc"))))
|
|
|
|
(ert-deftest tp-search-test-regexp-zero-width-string-clean ()
|
|
"tp-regexp-set with a zero-width-capable regexp works on strings."
|
|
(let ((result (tp-regexp-set "x*" '(face bold) "axb")))
|
|
(should (equal (substring-no-properties result) "axb"))
|
|
(should (eq (get-text-property 1 'face result) 'bold))))
|
|
|
|
;;; B39: length-changing replacements error on strings, work in buffers
|
|
|
|
(ert-deftest tp-search-test-forward-do-longer-replacement-errors ()
|
|
"A replacement longer than the match signals a clear error on strings.
|
|
Strings cannot change length in place; the old code silently truncated
|
|
(or signaled args-out-of-range past the string end). The string is
|
|
left unchanged."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 6 11 '(marker t) str)
|
|
(should-error (tp-forward-do (lambda (txt) (concat (upcase txt) "XYZ"))
|
|
'marker nil str))
|
|
(should (equal (substring-no-properties str) "hello world"))))
|
|
|
|
(ert-deftest tp-search-test-forward-do-longer-in-bounds-errors ()
|
|
"A longer in-bounds replacement errors instead of clobbering.
|
|
Old code silently wrote 10 chars, yielding \"hellohellod\"."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(should-error (tp-forward-do (lambda (txt) (concat txt txt))
|
|
'marker nil str))
|
|
(should (equal (substring-no-properties str) "hello world"))))
|
|
|
|
(ert-deftest tp-search-test-backward-do-longer-replacement-errors ()
|
|
"tp-backward-do rejects length-changing replacements on strings."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 6 11 '(marker t) str)
|
|
(should-error (tp-backward-do (lambda (txt) (concat (upcase txt) "12345"))
|
|
'marker nil str))
|
|
(should (equal (substring-no-properties str) "hello world"))))
|
|
|
|
(ert-deftest tp-search-test-search-map-longer-replacement-errors ()
|
|
"tp-search-map rejects length-changing replacements on strings."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 6 11 '(marker t) str)
|
|
(should-error (tp-search-map (lambda (txt) (concat (upcase txt) "!!!"))
|
|
'marker nil str))
|
|
(should (equal (substring-no-properties str) "hello world"))))
|
|
|
|
(ert-deftest tp-search-test-forward-do-shorter-replacement-errors ()
|
|
"A shorter replacement errors instead of leaving residue (\"ABllo\")."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(should-error (tp-forward-do (lambda (_txt) "AB") 'marker nil str))
|
|
(should (equal (substring-no-properties str) "hello world"))))
|
|
|
|
(ert-deftest tp-search-test-search-map-same-length-string-ok ()
|
|
"Same-length replacements still mutate the string in place."
|
|
(let ((str (copy-sequence "hello world hello")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(tp-set 12 17 '(marker t) str)
|
|
(should (= (tp-search-map #'upcase 'marker nil str) 2))
|
|
(should (equal (substring-no-properties str) "HELLO world HELLO"))))
|
|
|
|
;;; B43: -do shortfall is all-or-nothing on strings and buffers alike
|
|
|
|
(ert-deftest tp-search-test-forward-do-shortfall-string ()
|
|
"Requesting the Nth match when fewer exist applies nothing (string).
|
|
The count of available matches is still returned."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 0 5 '(marker t) str)
|
|
(should (= (tp-forward-do #'upcase 'marker nil str 3) 1))
|
|
(should (equal (substring-no-properties str) "hello world"))))
|
|
|
|
(ert-deftest tp-search-test-forward-do-shortfall-buffer ()
|
|
"Requesting the Nth match when fewer exist applies nothing (buffer)."
|
|
(with-temp-buffer
|
|
(insert "hello world")
|
|
(put-text-property 1 6 'marker t)
|
|
(should (= (tp-forward-do #'upcase 'marker t nil 3) 1))
|
|
(should (equal (buffer-substring-no-properties (point-min) (point-max))
|
|
"hello world"))))
|
|
|
|
(ert-deftest tp-search-test-backward-do-shortfall-string ()
|
|
"tp-backward-do shortfall applies nothing on strings."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 6 11 '(marker t) str)
|
|
(should (= (tp-backward-do #'upcase 'marker nil str 2) 1))
|
|
(should (equal (substring-no-properties str) "hello world"))))
|
|
|
|
(ert-deftest tp-search-test-forward-do-exact-count-applies ()
|
|
"With exactly TIMES matches, FUNCTION is applied to the TIMES-th."
|
|
(let ((str (copy-sequence "aaa bbb aaa")))
|
|
(tp-set 0 3 '(marker t) str)
|
|
(tp-set 8 11 '(marker t) str)
|
|
(should (= (tp-forward-do #'upcase 'marker nil str 2) 2))
|
|
(should (equal (substring-no-properties str) "aaa bbb AAA"))))
|
|
|
|
(ert-deftest tp-search-test-forward-do-buffer-longer-replacement-grows ()
|
|
"Buffers may grow on longer replacements (delete-region + insert).
|
|
Uses an explicit VALUE: the buffer paths of the -do functions match
|
|
with predicate t, where VALUE nil matches property-absent runs."
|
|
(with-temp-buffer
|
|
(insert "hello world")
|
|
(put-text-property 1 6 'marker t)
|
|
(tp-forward-do (lambda (txt) (concat txt txt)) 'marker t)
|
|
(should (equal (buffer-substring-no-properties (point-min) (point-max))
|
|
"hellohello world"))))
|
|
|
|
;;; B40: tp-search-map must operate on OBJECT, not the current buffer
|
|
|
|
(ert-deftest tp-search-test-search-map-non-current-buffer ()
|
|
"tp-search-map with a buffer OBJECT mutates that buffer only."
|
|
(let ((target (generate-new-buffer " tp-search-test-target")))
|
|
(unwind-protect
|
|
(progn
|
|
(with-current-buffer target
|
|
(insert "aaa bbb")
|
|
(put-text-property 1 4 'marker t))
|
|
(with-temp-buffer
|
|
(insert "current buffer text")
|
|
(let ((count (tp-search-map #'upcase 'marker nil target)))
|
|
(should (= count 1)))
|
|
;; Current buffer untouched.
|
|
(should (equal (buffer-string) "current buffer text")))
|
|
;; Target buffer modified.
|
|
(should (equal (with-current-buffer target
|
|
(buffer-substring-no-properties (point-min)
|
|
(point-max)))
|
|
"AAA bbb")))
|
|
(kill-buffer target))))
|
|
|
|
(ert-deftest tp-search-test-search-do-non-current-buffer-bounds ()
|
|
"tp--search-do computes default bounds in OBJECT, not the current buffer."
|
|
(let ((target (generate-new-buffer " tp-search-test-target2")))
|
|
(unwind-protect
|
|
(progn
|
|
(with-current-buffer target
|
|
(insert "aaa bbb ccc")
|
|
(put-text-property 9 12 'marker t))
|
|
(with-temp-buffer
|
|
;; Current buffer is much shorter than the target.
|
|
(insert "x")
|
|
(let ((seen nil))
|
|
(tp--search-do (lambda (match _obj) (push match seen))
|
|
'marker nil target)
|
|
(should (equal seen '((9 12 t)))))))
|
|
(kill-buffer target))))
|
|
|
|
;;; B41: length-changing replacements over multiple matches
|
|
|
|
(ert-deftest tp-search-test-search-map-growing-replacements ()
|
|
"Growing replacements do not corrupt later match positions."
|
|
(with-temp-buffer
|
|
(insert "aaa bbb ccc")
|
|
(put-text-property 1 4 'marker t)
|
|
(put-text-property 5 8 'marker t)
|
|
(put-text-property 9 12 'marker t)
|
|
(let ((count (tp-search-map (lambda (_txt) "XXXXXX") 'marker nil nil)))
|
|
(should (= count 3))
|
|
(should (equal (buffer-substring-no-properties (point-min) (point-max))
|
|
"XXXXXX XXXXXX XXXXXX")))))
|
|
|
|
(ert-deftest tp-search-test-search-map-shrinking-replacements ()
|
|
"Shrinking replacements do not corrupt later match positions."
|
|
(with-temp-buffer
|
|
(insert "aaa bbb ccc")
|
|
(put-text-property 1 4 'marker t)
|
|
(put-text-property 5 8 'marker t)
|
|
(put-text-property 9 12 'marker t)
|
|
(tp-search-map (lambda (_txt) "-") 'marker nil nil)
|
|
(should (equal (buffer-substring-no-properties (point-min) (point-max))
|
|
"- - -"))))
|
|
|
|
(ert-deftest tp-search-test-search-map-mixed-length-positions ()
|
|
"Callbacks see up-to-date positions after earlier length changes."
|
|
(with-temp-buffer
|
|
(insert "aaa bbb ccc")
|
|
(put-text-property 1 4 'marker t)
|
|
(put-text-property 5 8 'marker t)
|
|
(put-text-property 9 12 'marker t)
|
|
(let ((texts nil))
|
|
(tp-search-map (lambda (txt _start _end idx)
|
|
(push txt texts)
|
|
(format "<%d>%s" idx txt))
|
|
'marker nil nil)
|
|
;; Each callback received the intact matched text, not garbage
|
|
;; from stale positions.
|
|
(should (equal (nreverse texts) '("aaa" "bbb" "ccc")))
|
|
(should (equal (buffer-substring-no-properties (point-min) (point-max))
|
|
"<0>aaa <1>bbb <2>ccc")))))
|
|
|
|
;;; B42: tp-match-add / tp-regexp-add preserve existing faces in buffers
|
|
|
|
(ert-deftest tp-search-test-match-add-buffer-preserves-face ()
|
|
"tp-match-add on a buffer merges faces instead of replacing them."
|
|
(with-temp-buffer
|
|
(insert "hello")
|
|
(put-text-property 1 6 'face 'italic)
|
|
(tp-match-add "hello" '(face bold))
|
|
(should (equal (get-text-property 1 'face) '(bold italic)))))
|
|
|
|
(ert-deftest tp-search-test-match-add-face-string-buffer-parity ()
|
|
"tp-match-add merges faces identically for strings and buffers."
|
|
(let* ((str (propertize "hello" 'face 'italic))
|
|
(str-face (get-text-property 0 'face
|
|
(tp-match-add "hello" '(face bold) str)))
|
|
(buf-face (with-temp-buffer
|
|
(insert "hello")
|
|
(put-text-property 1 6 'face 'italic)
|
|
(tp-match-add "hello" '(face bold))
|
|
(get-text-property 1 'face))))
|
|
(should (equal str-face buf-face))
|
|
(should (equal buf-face '(bold italic)))))
|
|
|
|
(ert-deftest tp-search-test-regexp-add-buffer-preserves-face ()
|
|
"tp-regexp-add on a buffer merges faces instead of replacing them."
|
|
(with-temp-buffer
|
|
(insert "abc 123")
|
|
(put-text-property 5 8 'face 'underline)
|
|
(tp-regexp-add "[0-9]+" '(face bold))
|
|
(should (equal (get-text-property 5 'face) '(bold underline)))))
|
|
|
|
(ert-deftest tp-search-test-match-add-buffer-non-face-deep-merge ()
|
|
"tp-match-add still deep-merges non-face plist properties in buffers."
|
|
(with-temp-buffer
|
|
(insert "hello")
|
|
(put-text-property 1 6 'data '(:a 1))
|
|
(tp-match-add "hello" '(data (:b 2)))
|
|
(let ((val (get-text-property 1 'data)))
|
|
(should (equal (plist-get val :a) 1))
|
|
(should (equal (plist-get val :b) 2)))))
|
|
|
|
;;; B44: property removal through tp-search-map on strings
|
|
|
|
(ert-deftest tp-search-test-search-map-removes-props-on-string ()
|
|
"A callback returning a stripped string removes properties."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 0 5 '(marker t face bold) str)
|
|
(tp-search-map (lambda (txt) (substring-no-properties txt))
|
|
'marker nil str)
|
|
(should (null (text-properties-at 0 str)))
|
|
(should (equal (substring-no-properties str) "hello world"))))
|
|
|
|
(ert-deftest tp-search-test-search-map-removes-single-prop-on-string ()
|
|
"A callback removing one property keeps the others."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 0 5 '(marker t face bold) str)
|
|
(tp-search-map (lambda (txt)
|
|
(remove-text-properties 0 (length txt) '(face nil) txt)
|
|
txt)
|
|
'marker nil str)
|
|
(should (null (get-text-property 0 'face str)))
|
|
(should (eq (get-text-property 0 'marker str) t))))
|
|
|
|
;;; Guard: nil return still means "no replacement" (used by tp-render)
|
|
|
|
(ert-deftest tp-search-test-search-map-nil-return-no-replacement ()
|
|
"A callback returning nil leaves text and properties untouched."
|
|
(let ((str (copy-sequence "hello world")))
|
|
(tp-set 0 5 '(marker t face bold) str)
|
|
(let ((count (tp-search-map (lambda (_txt) nil) 'marker nil str)))
|
|
(should (= count 1))
|
|
(should (equal (substring-no-properties str) "hello world"))
|
|
(should (eq (get-text-property 0 'face str) 'bold)))))
|
|
|
|
(provide 'tp-search-tests)
|
|
;;; tp-search-tests.el ends here
|