tp/tests/tp-render-tests.el
2026-07-28 23:08:25 +08:00

1063 lines
47 KiB
EmacsLisp

;;; tp-render-tests.el --- ERT regression tests for tp-render.el -*- lexical-binding: t -*-
;;; Commentary:
;; Regression tests for confirmed bugs fixed in the reactive-render
;; module (tp-render.el, with supporting fixes in tp-reactive.el).
;; Each section is tagged with the canonical bug id it guards against.
;;; Code:
(require 'ert)
(require 'tp)
;; Reactive test variables must be dynamically bound so watcher and
;; compute machinery can see them through `symbol-value'.
(defvar tp-rt-b9-var nil)
(defvar tp-rt-b10-data nil)
(defvar tp-rt-b10-full nil)
(defvar tp-rt-b11-face nil)
(defvar tp-rt-b11b-color nil)
(defvar tp-rt-b12-color nil)
(defvar tp-rt-b13-text nil)
(defvar tp-rt-b13b-text nil)
(defvar tp-rt-b14-flag nil)
(defvar tp-rt-b14-inv nil)
(defvar tp-rt-b14b-init nil)
(defvar tp-rt-b16-data nil)
(defvar tp-rt-b16-comp nil)
(defvar tp-rt-b16-count 0)
(defvar tp-rt-b17-color nil)
(defvar tp-rt-b17-text nil)
(defvar tp-rt-b17b-color nil)
(defvar tp-rt-b17b-echo nil)
(defvar tp-rt-b18-text nil)
(defvar tp-rt-b19-amount nil)
(defvar tp-rt-b19s-amount nil)
(defvar tp-rt-r1-color nil)
(defvar tp-rt-r1b-color nil)
(defvar tp-rt-r1c-color nil)
(defvar tp-rt-r1d-color nil)
(defvar tp-rt-r2-text nil)
(defvar tp-rt-r2m-text nil)
(defvar tp-rt-r2n-text nil)
(defvar tp-rt-r2s-text nil)
(defvar tp-rt-r3a-color nil)
(defvar tp-rt-r3b-color nil)
(defvar tp-rt-r3c-color nil)
(defvar tp-rt-a02-old-color nil)
(defvar tp-rt-a02-new-color nil)
(defvar tp-rt-a11-data nil)
(defvar tp-rt-a11-computed nil)
(defvar tp-rt-a11-watched nil)
(defvar tp-rt-a11-text nil)
(defmacro tp-rt-with-cleanup (layers vars &rest body)
"Run BODY, then undefine LAYERS and reset VARS to nil (teardown)."
(declare (indent 2))
`(unwind-protect
(progn ,@body)
,@(mapcar (lambda (l) `(tp-undefine-layer ',l)) layers)
,@(mapcar (lambda (v) `(setq ,v nil)) vars)
(setq tp-reactive-observer-errors nil)))
;;; B9: sub-region tp-text on a string must splice, not replace the whole string
(ert-deftest tp-render-test-tp-text-string-region-keeps-rest ()
"Region-form tp-text on a string keeps the text outside the region."
(let ((result (tp-set 0 1 '(tp-text "X") (copy-sequence "abc"))))
(should (equal result "Xbc"))
(should (equal (get-text-property 0 'tp-text result) "X"))
;; The preserved suffix must not receive the layer's props
(should (null (get-text-property 1 'tp-text result)))
(should (null (get-text-property 2 'tp-text result)))))
(ert-deftest tp-render-test-tp-text-string-mid-region-splices ()
"A mid-string tp-text region splices prefix + replacement + suffix."
(let ((result (tp-set 1 2 '(face bold tp-text "XY") (copy-sequence "abc"))))
(should (equal result "aXYc"))
;; Props only on the replaced span [1, 3)
(should (null (get-text-property 0 'face result)))
(should (eq (get-text-property 1 'face result) 'bold))
(should (eq (get-text-property 2 'face result) 'bold))
(should (null (get-text-property 3 'face result)))))
(ert-deftest tp-render-test-tp-text-string-region-preserves-outside-props ()
"Splicing keeps the original string's properties outside the region."
(let* ((source (propertize "abc" 'face 'italic 'my-prop 1))
(result (tp-set 1 2 '(tp-text "X") source)))
(should (equal result "aXc"))
;; Prefix and suffix keep their original props
(should (eq (get-text-property 0 'face result) 'italic))
(should (eq (get-text-property 2 'face result) 'italic))
;; Replaced span preserves non-conflicting props (tp-set preserves)
(should (eq (get-text-property 1 'my-prop result) 1))))
(ert-deftest tp-render-test-tp-text-whole-string-still-replaces ()
"Whole-string form still returns just the replacement (legacy semantics)."
(let ((result (tp-set "2" 'face '(:background "green") 'tp-text "6")))
(should (equal result "6"))
(should (equal (get-text-property 0 'face result) '(:background "green")))
(should (equal (get-text-property 0 'tp-text result) "6"))))
;;; TP-A01 / TP-A03: initial tp-text application uses per-run properties
(ert-deftest tp-render-test-initial-tp-text-keeps-string-property-runs ()
"Initial string tp-text application does not smear position-zero props."
(let* ((payload (concat (propertize "AB" 'face 'bold)
(propertize "CD" 'face 'italic)))
(result (tp-set "xxxx" 'tp-text payload)))
(should (eq (get-text-property 0 'face result) 'bold))
(should (eq (get-text-property 2 'face result) 'italic))))
(ert-deftest tp-render-test-initial-tp-text-keeps-buffer-property-runs ()
"Initial buffer tp-text application preserves every embedded prop run."
(let ((payload (concat (propertize "AB" 'face 'bold)
(propertize "CD" 'face 'italic))))
(with-temp-buffer
(insert "xxxx")
(tp-set 1 5 (list 'tp-text payload))
(should (eq (get-text-property 1 'face) 'bold))
(should (eq (get-text-property 3 'face) 'italic)))))
(ert-deftest tp-render-test-tp-text-explicit-nil-overrides-embedded-value ()
"A caller-provided nil remains present and wins over embedded props."
(let* ((payload (propertize "X" 'custom 'embedded))
(result (tp-set "x" 'tp-text payload 'custom nil)))
(should (equal (tp-member 0 'custom result) '(custom nil)))
(with-temp-buffer
(insert "x")
(tp-set 1 2 (list 'tp-text payload 'custom nil))
(should (equal (tp-member 1 'custom) '(custom nil))))))
(ert-deftest tp-render-test-same-text-reset-removes-old-properties ()
"A same-text tp-reset still replaces the complete property set."
(with-temp-buffer
(insert (propertize "AB" 'help-echo "old" 'face 'italic))
(tp-reset 1 3 '(tp-text "AB" face bold))
(should-not (plist-member (text-properties-at 1) 'help-echo))
(should (eq (get-text-property 1 'face) 'bold)))
(let* ((source (propertize "AB" 'help-echo "old" 'face 'italic))
(result (tp-reset source 'tp-text "AB" 'face 'bold)))
(should-not (plist-member (text-properties-at 0 result) 'help-echo))
(should (eq (get-text-property 0 'face result) 'bold))))
(ert-deftest tp-render-test-same-text-add-merges-existing-face ()
"A same-text tp-add keeps add semantics while applying per-run props."
(with-temp-buffer
(insert (propertize "AB" 'face 'italic))
(tp-add 1 3 '(tp-text "AB" face bold))
(should (equal (get-text-property 1 'face) '(bold italic))))
(let* ((source (propertize "AB" 'face 'italic))
(result (tp-add source 'tp-text "AB" 'face 'bold)))
(should (equal (get-text-property 0 'face result) '(bold italic))))
(let* ((source (propertize "AB" 'face 'italic))
(result (tp-add source 'tp-text nil 'face 'bold)))
(should (equal (get-text-property 0 'face result) '(bold italic)))))
;;; TP-A02: layer redefinition refreshes with full old/new ownership
(ert-deftest tp-render-test-static-redefinition-refreshes-managed-region ()
"A simple static redefinition refreshes an already-mounted layer."
(tp-rt-with-cleanup (tp-rt-a02-static) ()
(define-tp tp-rt-a02-static () '(face bold help-echo "old"))
(with-temp-buffer
(insert "Hello")
(tp-push-layer 1 6 'tp-rt-a02-static)
(define-tp tp-rt-a02-static () '(face italic))
(should (eq (get-text-property 1 'face) 'italic))
(should-not (plist-member (text-properties-at 1) 'help-echo)))))
(ert-deftest tp-render-test-redefinition-preserves-external-value ()
"A value changed after mounting is not deleted as stale layer output."
(tp-rt-with-cleanup (tp-rt-a02-external) ()
(define-tp tp-rt-a02-external () '(face bold help-echo "owned"))
(with-temp-buffer
(insert "Hello")
(tp-push-layer 1 6 'tp-rt-a02-external)
(put-text-property 1 6 'help-echo "external")
(define-tp tp-rt-a02-external () '(face italic))
(should (eq (get-text-property 1 'face) 'italic))
(should (equal (get-text-property 1 'help-echo) "external")))))
(ert-deftest tp-render-test-reactive-redefinition-removes-old-owned-keys ()
"Reactive redefinition removes keys and nested face data it no longer owns."
(tp-rt-with-cleanup
(tp-rt-a02-reactive) (tp-rt-a02-old-color tp-rt-a02-new-color)
(setq tp-rt-a02-old-color "red"
tp-rt-a02-new-color "blue")
(define-tp tp-rt-a02-reactive ()
:props '(face (:foreground $tp-rt-a02-old-color)
help-echo "old"))
(with-temp-buffer
(insert "Hello")
(tp-push-layer 1 6 'tp-rt-a02-reactive)
(define-tp tp-rt-a02-reactive ()
:props '(face (:background $tp-rt-a02-new-color)))
(let ((face (get-text-property 1 'face)))
(should (equal (plist-get face :background) "blue"))
(should-not (plist-member face :foreground)))
(should-not (plist-member (text-properties-at 1) 'help-echo)))))
;;; TP-A11: business computations fail; observers are isolated and recorded
(ert-deftest tp-render-test-transform-error-propagates ()
"Transform failures and non-string results both propagate."
(tp-rt-with-cleanup
(tp-rt-a11-transform tp-rt-a11-nonstring) (tp-rt-a11-text)
(setq tp-rt-a11-text "raw")
(define-tp tp-rt-a11-transform ()
:props '(tp-text $tp-rt-a11-text)
:transform (lambda (_text) (error "transform failed")))
(should-error (tp-set "old" 'tp-rt-a11-transform)
:type 'error)
(define-tp tp-rt-a11-nonstring ()
:props '(tp-text $tp-rt-a11-text)
:transform (lambda (_text) 42))
(should-error (tp-set "old" 'tp-rt-a11-nonstring)
:type 'error)))
(ert-deftest tp-render-test-compute-errors-propagate ()
"Initial and update-time compute failures reach the caller."
(tp-rt-with-cleanup (tp-rt-a11-initial tp-rt-a11-update)
(tp-rt-a11-data tp-rt-a11-computed)
(should-error
(define-tp tp-rt-a11-initial ()
:props '(help-echo $tp-rt-a11-computed)
:compute '((tp-rt-a11-computed
(lambda () (error "initial compute failed")))))
:type 'error)
(setq tp-rt-a11-data "ok")
(define-tp tp-rt-a11-update ()
:props '(help-echo $tp-rt-a11-computed)
:data '(tp-rt-a11-data)
:compute '((tp-rt-a11-computed
(lambda ()
(if (equal tp-rt-a11-data "ok")
"ready"
(error "update compute failed"))))))
(should-error (setq tp-rt-a11-data "fail")
:type 'error)))
(ert-deftest tp-render-test-watcher-error-is-recorded-and-update-continues ()
"Watcher failures are isolated, queryable, and do not block rendering."
(tp-rt-with-cleanup (tp-rt-a11-watcher) (tp-rt-a11-watched)
(setq tp-rt-a11-watched "red")
(when (boundp 'tp-reactive-observer-errors)
(set 'tp-reactive-observer-errors nil))
(define-tp tp-rt-a11-watcher ()
:props '(face (:foreground $tp-rt-a11-watched))
:watch '((tp-rt-a11-watched
(lambda (_new _old _layer)
(error "watcher failed")))))
(with-temp-buffer
(insert "x")
(tp-push-layer 1 2 'tp-rt-a11-watcher)
(setq tp-rt-a11-watched "blue")
(should (equal (plist-get (tp-at 1 'face) :foreground) "blue"))
(should (boundp 'tp-reactive-observer-errors))
(let ((failure (car (symbol-value 'tp-reactive-observer-errors))))
(should (eq (plist-get failure :kind) 'watcher))
(should (eq (plist-get failure :layer) 'tp-rt-a11-watcher))
(should (eq (plist-get failure :symbol) 'tp-rt-a11-watched))
(should (eq (car (plist-get failure :condition)) 'error))))))
;;; B10: computed-variable path must not clobber sibling static attributes
(ert-deftest tp-render-test-computed-update-keeps-static-siblings ()
"A computed update deep-merges, keeping static nested attributes."
(tp-rt-with-cleanup (tp-rt-b10-layer) (tp-rt-b10-data tp-rt-b10-full)
(setq tp-rt-b10-data "red")
(define-tp tp-rt-b10-layer ()
:props '(face (:foreground $tp-rt-b10-full :background "green"))
:data '(tp-rt-b10-data)
:compute '((tp-rt-b10-full (lambda () (concat "col-" tp-rt-b10-data)))))
(setq tp-rt-b10-data "blue")
(let ((face (plist-get (cdr (assoc 'tp-rt-b10-layer tp-layer-alist)) 'face)))
(should (equal (plist-get face :foreground) "col-blue"))
;; The sibling static attribute must survive the update
(should (equal (plist-get face :background) "green")))))
;;; B11: reactive refresh replaces the layer's own keys instead of accumulating
(ert-deftest tp-render-test-reactive-refresh-replaces-face ()
"Changing a symbol-valued face variable replaces the face, not stacks it."
(tp-rt-with-cleanup (tp-rt-b11-layer) (tp-rt-b11-face)
(setq tp-rt-b11-face 'bold)
(define-tp tp-rt-b11-layer () '(face $tp-rt-b11-face))
(with-temp-buffer
(insert "Hello")
(tp-set 1 6 'tp-rt-b11-layer)
(should (eq (get-text-property 1 'face) 'bold))
(setq tp-rt-b11-face 'italic)
;; Must be italic alone, not (italic bold)
(should (eq (get-text-property 1 'face) 'italic)))))
(ert-deftest tp-render-test-reactive-refresh-keeps-unrelated-props ()
"Reactive refresh leaves property keys the layer does not own alone."
(tp-rt-with-cleanup (tp-rt-b11b-layer) (tp-rt-b11b-color)
(setq tp-rt-b11b-color "red")
(define-tp tp-rt-b11b-layer () '(face (:foreground $tp-rt-b11b-color)))
(with-temp-buffer
(insert "Hello")
(tp-set 1 6 'tp-rt-b11b-layer)
(put-text-property 1 6 'help-echo "keep me")
(setq tp-rt-b11b-color "green")
(should (equal (plist-get (get-text-property 1 'face) :foreground) "green"))
(should (equal (get-text-property 1 'help-echo) "keep me")))))
;;; B12: setq-local must not leak into the global layer definition
(ert-deftest tp-render-test-setq-local-does-not-touch-global-def ()
"A buffer-local change re-renders the buffer but keeps the global def."
(tp-rt-with-cleanup (tp-rt-b12-layer) ()
(setq-default tp-rt-b12-color "red")
(define-tp tp-rt-b12-layer () '(face (:foreground $tp-rt-b12-color)))
(let ((buf-a (generate-new-buffer " tp-rt-b12-a"))
(buf-b (generate-new-buffer " tp-rt-b12-b")))
(unwind-protect
(progn
(with-current-buffer buf-a
(insert "Hello")
(tp-set 1 6 'tp-rt-b12-layer))
(with-current-buffer buf-b
(insert "Hello")
(tp-set 1 6 'tp-rt-b12-layer))
(with-current-buffer buf-a
(setq-local tp-rt-b12-color "purple"))
;; Buffer A is re-rendered with its local value
(with-current-buffer buf-a
(should (equal (plist-get (get-text-property 1 'face) :foreground)
"purple")))
;; The GLOBAL definition must not absorb the local value
(should (equal (plist-get
(plist-get (cdr (assoc 'tp-rt-b12-layer tp-layer-alist))
'face)
:foreground)
"red"))
(should (equal (default-value 'tp-rt-b12-color) "red"))
;; Other buffers keep rendering the global value
(with-current-buffer buf-b
(should (equal (plist-get (get-text-property 1 'face) :foreground)
"red"))))
(kill-buffer buf-a)
(kill-buffer buf-b)
(setq-default tp-rt-b12-color nil)))))
;;; B13: reactive tp-text replacement preserves unrelated properties
(ert-deftest tp-render-test-reactive-text-update-preserves-other-props ()
"Replacing reactive text keeps properties other layers put on the region."
(tp-rt-with-cleanup (tp-rt-b13-layer) (tp-rt-b13-text)
(setq tp-rt-b13-text "aaa")
(define-tp tp-rt-b13-layer () '(face bold tp-text $tp-rt-b13-text))
(with-temp-buffer
(insert "Hello")
(tp-set 1 6 'tp-rt-b13-layer)
(put-text-property 1 3 'my-other-prop 42)
(setq tp-rt-b13-text "bbb")
(should (equal (buffer-substring-no-properties (point-min) (point-max))
"bbb"))
;; The unrelated property survives the text replacement
(should (eq (get-text-property 1 'my-other-prop) 42))
;; The layer's own props are still applied
(should (eq (get-text-property 1 'face) 'bold)))))
(ert-deftest tp-render-test-reactive-text-same-text-preserves-other-props ()
"A same-text properties-only update keeps unrelated properties too."
(tp-rt-with-cleanup (tp-rt-b13b-layer) (tp-rt-b13b-text)
(setq tp-rt-b13b-text "emacs")
(define-tp tp-rt-b13b-layer () '(tp-text $tp-rt-b13b-text))
(with-temp-buffer
(insert "emacs")
(tp-set 1 6 'tp-rt-b13b-layer)
(put-text-property 1 6 'my-other-prop 'yes)
;; Same text, new embedded properties
(setq tp-rt-b13b-text (propertize "emacs" 'face 'bold))
(should (eq (get-text-property 1 'face) 'bold))
(should (eq (get-text-property 1 'my-other-prop) 'yes)))))
;;; B14: computed values of nil must propagate
(ert-deftest tp-render-test-computed-nil-propagates-on-update ()
"A compute function returning nil updates the variable and the layer."
(tp-rt-with-cleanup (tp-rt-b14-layer) (tp-rt-b14-flag tp-rt-b14-inv)
(setq tp-rt-b14-flag t)
(define-tp tp-rt-b14-layer ()
:props '(invisible $tp-rt-b14-inv)
:data '(tp-rt-b14-flag)
:compute '((tp-rt-b14-inv (lambda () tp-rt-b14-flag))))
(should (eq tp-rt-b14-inv t))
(setq tp-rt-b14-flag nil)
;; nil is a legitimate computed value, not an error sentinel
(should (eq tp-rt-b14-inv nil))
(should (eq (plist-get (cdr (assoc 'tp-rt-b14-layer tp-layer-alist))
'invisible)
nil))))
(ert-deftest tp-render-test-computed-nil-applies-initially ()
"An initial computed value of nil overwrites a stale non-nil value."
(tp-rt-with-cleanup (tp-rt-b14b-layer) (tp-rt-b14b-init)
(setq tp-rt-b14b-init 'stale)
(define-tp tp-rt-b14b-layer ()
:props '(invisible $tp-rt-b14b-init)
:compute '((tp-rt-b14b-init (lambda () nil))))
(should (eq tp-rt-b14b-init nil))))
;;; B16: no watcher recursion from nested variable writes
(ert-deftest tp-render-test-compute-runs-once-per-change ()
"One data change runs each compute function exactly once (no recursion)."
(tp-rt-with-cleanup (tp-rt-b16-layer) (tp-rt-b16-data tp-rt-b16-comp)
(setq tp-rt-b16-data "a" tp-rt-b16-count 0)
(define-tp tp-rt-b16-layer ()
:props '(help-echo $tp-rt-b16-comp)
:data '(tp-rt-b16-data)
:compute '((tp-rt-b16-comp
(lambda ()
(setq tp-rt-b16-count (1+ tp-rt-b16-count))
(concat tp-rt-b16-data "!")))))
(with-temp-buffer
(insert "Hello")
(tp-set 1 6 'tp-rt-b16-layer)
(setq tp-rt-b16-count 0)
(setq tp-rt-b16-data "b")
;; The nested (set comp ...) must queue its re-render, not re-enter
;; the compute machinery.
(should (= tp-rt-b16-count 1))
;; The nested change's re-render still lands in the buffer
(should (equal tp-rt-b16-comp "b!"))
(should (equal (get-text-property 1 'help-echo) "b!")))))
;;; B17: batched entries must union WHERE and the tp-text-affected flag
(ert-deftest tp-render-test-batch-tp-text-flag-is-sticky ()
"A tp-text change deferred after a non-tp-text change still replaces text."
(tp-rt-with-cleanup (tp-rt-b17-layer) (tp-rt-b17-color tp-rt-b17-text)
(setq tp-rt-b17-color "red" tp-rt-b17-text "one")
(define-tp tp-rt-b17-layer ()
'(face (:foreground $tp-rt-b17-color) tp-text $tp-rt-b17-text))
(with-temp-buffer
(insert "one")
(tp-set 1 4 'tp-rt-b17-layer)
(tp-with-batch-updates
(setq tp-rt-b17-color "blue") ; first change: no tp-text
(setq tp-rt-b17-text "two")) ; second change: tp-text affected
(should (equal (buffer-substring-no-properties (point-min) (point-max))
"two"))
(should (equal (plist-get (get-text-property 1 'face) :foreground)
"blue")))))
(ert-deftest tp-render-test-batch-where-widens-to-all-buffers ()
"A global change after a buffer-local one must reach other buffers."
(tp-rt-with-cleanup (tp-rt-b17b-layer) ()
(setq-default tp-rt-b17b-color "red")
(setq-default tp-rt-b17b-echo "old")
(define-tp tp-rt-b17b-layer ()
'(face (:foreground $tp-rt-b17b-color) help-echo $tp-rt-b17b-echo))
(let ((buf-a (generate-new-buffer " tp-rt-b17b-a"))
(buf-b (generate-new-buffer " tp-rt-b17b-b")))
(unwind-protect
(progn
(with-current-buffer buf-a
(insert "Hello") (tp-set 1 6 'tp-rt-b17b-layer))
(with-current-buffer buf-b
(insert "Hello") (tp-set 1 6 'tp-rt-b17b-layer))
(with-current-buffer buf-a
(tp-with-batch-updates
(setq-local tp-rt-b17b-color "blue") ; WHERE = buf-a
(setq tp-rt-b17b-echo "new"))) ; WHERE = global
;; The global change must not be trapped in buf-a's WHERE
(with-current-buffer buf-b
(should (equal (get-text-property 1 'help-echo) "new"))
(should (equal (plist-get (get-text-property 1 'face) :foreground)
"red")))
;; buf-a gets both, with its local color honored
(with-current-buffer buf-a
(should (equal (get-text-property 1 'help-echo) "new"))
(should (equal (plist-get (get-text-property 1 'face) :foreground)
"blue"))))
(kill-buffer buf-a)
(kill-buffer buf-b)
(setq-default tp-rt-b17b-color nil)
(setq-default tp-rt-b17b-echo nil)))))
;;; B18: multi-interval reactive strings keep per-interval styling
(ert-deftest tp-render-test-reactive-text-keeps-per-interval-props ()
"A propertized reactive string renders each interval's own props."
(tp-rt-with-cleanup (tp-rt-b18-layer) (tp-rt-b18-text)
(setq tp-rt-b18-text "init")
(define-tp tp-rt-b18-layer () '(tp-text $tp-rt-b18-text))
(with-temp-buffer
(insert "init")
(tp-set 1 5 'tp-rt-b18-layer)
(setq tp-rt-b18-text (concat (propertize "AB" 'face 'bold)
(propertize "CD" 'face 'italic)))
(should (equal (buffer-substring-no-properties (point-min) (point-max))
"ABCD"))
;; Position-0 props must not smear over the whole region
(should (eq (get-text-property 1 'face) 'bold))
(should (eq (get-text-property 2 'face) 'bold))
(should (eq (get-text-property 3 'face) 'italic))
(should (eq (get-text-property 4 'face) 'italic)))))
;;; B19: :transform applies on the initial nil-tp-text render too
(ert-deftest tp-render-test-transform-applies-on-initial-render ()
"First render of a nil tp-text layer shows the transformed text."
(tp-rt-with-cleanup (tp-rt-b19-layer) (tp-rt-b19-amount)
(setq tp-rt-b19-amount nil)
(define-tp tp-rt-b19-layer ()
:props '(face bold tp-text $tp-rt-b19-amount)
:transform (lambda (s) (concat "$" s)))
(with-temp-buffer
(insert "5.00")
(tp-set 1 5 'tp-rt-b19-layer)
;; Initial rendering must match later reactive renderings
(should (equal (buffer-substring-no-properties (point-min) (point-max))
"$5.00"))
;; The model (variable and tp-text prop) keeps the raw value
(should (equal tp-rt-b19-amount "5.00"))
(should (equal (get-text-property 1 'tp-text) "5.00"))
(should (eq (get-text-property 1 'face) 'bold))
;; And a later update stays consistent
(setq tp-rt-b19-amount "6.00")
(should (equal (buffer-substring-no-properties (point-min) (point-max))
"$6.00")))))
(ert-deftest tp-render-test-transform-applies-on-initial-string-render ()
"String form of a nil tp-text layer also shows the transformed text."
(tp-rt-with-cleanup (tp-rt-b19s-layer) (tp-rt-b19s-amount)
(setq tp-rt-b19s-amount nil)
(define-tp tp-rt-b19s-layer ()
:props '(face bold tp-text $tp-rt-b19s-amount)
:transform (lambda (s) (concat "$" s)))
(let ((result (tp-set "5.00" 'tp-rt-b19s-layer)))
(should (equal result "$5.00"))
;; Model keeps the raw value
(should (equal tp-rt-b19s-amount "5.00"))
(should (equal (get-text-property 0 'tp-text result) "5.00"))
(should (eq (get-text-property 0 'face result) 'bold)))))
;;; R1 (0.3.0): reactive buffer registry replaces the buffer-list scan
(ert-deftest tp-render-test-registry-update-visits-only-registered ()
"A reactive update walks only registered buffers, not `buffer-list'."
(tp-rt-with-cleanup (tp-rt-r1-layer) (tp-rt-r1-color)
(setq tp-rt-r1-color "red")
(define-tp tp-rt-r1-layer () '(face (:foreground $tp-rt-r1-color)))
(let ((buf-a (generate-new-buffer " tp-rt-r1-a"))
(buf-b (generate-new-buffer " tp-rt-r1-b"))
(visited nil))
(unwind-protect
(progn
(with-current-buffer buf-a
(insert "Hello")
(tp-set 1 6 'tp-rt-r1-layer))
(with-current-buffer buf-b (insert "Hello"))
;; Applying through tp-ops registered the buffer
(should (equal (tp-reactive-layer-buffers 'tp-rt-r1-layer)
(list buf-a)))
;; Count per-buffer visits of the update walk
(let ((orig (symbol-function 'tp--render-visit-buffer)))
(cl-letf (((symbol-function 'tp--render-visit-buffer)
(lambda (buf fn)
(push buf visited)
(funcall orig buf fn))))
(setq tp-rt-r1-color "blue")))
;; Only the registered buffer was visited
(should (equal visited (list buf-a)))
(with-current-buffer buf-a
(should (equal (plist-get (get-text-property 1 'face)
:foreground)
"blue"))))
(kill-buffer buf-a)
(kill-buffer buf-b)))))
(ert-deftest tp-render-test-registry-prunes-on-kill-buffer ()
"Killing a buffer removes it from the layer-buffer registry."
(tp-rt-with-cleanup (tp-rt-r1b-layer) (tp-rt-r1b-color)
(setq tp-rt-r1b-color "red")
(define-tp tp-rt-r1b-layer () '(face (:foreground $tp-rt-r1b-color)))
(let ((buf (generate-new-buffer " tp-rt-r1b")))
(unwind-protect
(progn
(with-current-buffer buf
(insert "Hello")
(tp-set 1 6 'tp-rt-r1b-layer))
(should (equal (tp-reactive-layer-buffers 'tp-rt-r1b-layer)
(list buf)))
(kill-buffer buf)
;; The kill-buffer hook pruned the raw registry entry ...
(should-not (memq buf (gethash 'tp-rt-r1b-layer
tp--layer-buffers)))
;; ... and the accessor answers "known: none", NOT `unknown'.
(should (null (tp-reactive-layer-buffers 'tp-rt-r1b-layer)))
(should-not (eq (tp-reactive-layer-buffers 'tp-rt-r1b-layer)
'unknown)))
(when (buffer-live-p buf) (kill-buffer buf))))))
(ert-deftest tp-render-test-registry-unknown-full-scan-learns ()
"An `unknown' layer falls back to a full scan and learns its buffers."
(tp-rt-with-cleanup (tp-rt-r1c-layer) (tp-rt-r1c-color)
(setq tp-rt-r1c-color "red")
(define-tp tp-rt-r1c-layer () '(face (:foreground $tp-rt-r1c-color)))
(let ((buf (generate-new-buffer " tp-rt-r1c")))
(unwind-protect
(progn
(with-current-buffer buf
(insert "Hello")
(tp-set 1 6 'tp-rt-r1c-layer))
;; Simulate a buffer that got the layer outside the
;; registering paths: erase the registry knowledge.
(remhash 'tp-rt-r1c-layer tp--layer-buffers)
(should (eq (tp-reactive-layer-buffers 'tp-rt-r1c-layer)
'unknown))
;; The update still reaches the buffer (conservative fallback)
(setq tp-rt-r1c-color "blue")
(with-current-buffer buf
(should (equal (plist-get (get-text-property 1 'face)
:foreground)
"blue")))
;; ... and the scan registered the buffer it found (learning)
(should (equal (tp-reactive-layer-buffers 'tp-rt-r1c-layer)
(list buf))))
(kill-buffer buf)))))
(ert-deftest tp-render-test-track-buffer-closes-string-insert-gap ()
"`tp-reactive-track-buffer' registers a buffer filled by string insert."
(tp-rt-with-cleanup (tp-rt-r1d-layer) (tp-rt-r1d-color)
(setq tp-rt-r1d-color "red")
(define-tp tp-rt-r1d-layer () '(face (:foreground $tp-rt-r1d-color)))
(let ((buf-a (generate-new-buffer " tp-rt-r1d-a"))
(buf-b (generate-new-buffer " tp-rt-r1d-b")))
(unwind-protect
(progn
(with-current-buffer buf-a
(insert "Hello")
(tp-set 1 6 'tp-rt-r1d-layer))
;; Inserting an already-propertized STRING bypasses the
;; registering buffer operations.
(let ((s (tp-set "Hi" 'tp-rt-r1d-layer)))
(with-current-buffer buf-b (insert s)))
(should-not (memq buf-b
(tp-reactive-layer-buffers 'tp-rt-r1d-layer)))
;; The layer is known, so buf-b is NOT updated (the gap) ...
(setq tp-rt-r1d-color "blue")
(with-current-buffer buf-b
(should (equal (plist-get (get-text-property 1 'face)
:foreground)
"red")))
;; ... until tp-reactive-track-buffer closes it.
(should (equal (with-current-buffer buf-b
(tp-reactive-track-buffer))
'(tp-rt-r1d-layer)))
(should (memq buf-b
(tp-reactive-layer-buffers 'tp-rt-r1d-layer)))
(setq tp-rt-r1d-color "green")
(with-current-buffer buf-b
(should (equal (plist-get (get-text-property 1 'face)
:foreground)
"green")))
(with-current-buffer buf-a
(should (equal (plist-get (get-text-property 1 'face)
:foreground)
"green"))))
(kill-buffer buf-a)
(kill-buffer buf-b)))))
;;; R2 (0.3.0): minimal-diff tp-text replacement
(ert-deftest tp-render-test-minimal-diff-point-in-prefix-stays ()
"Point in the common prefix survives a reactive text edit unmoved."
(tp-rt-with-cleanup (tp-rt-r2-layer) (tp-rt-r2-text)
(setq tp-rt-r2-text "abcdef")
(define-tp tp-rt-r2-layer () '(tp-text $tp-rt-r2-text))
(with-temp-buffer
(insert "abcdef")
(tp-set 1 7 'tp-rt-r2-layer)
(goto-char 2) ; inside the common prefix "ab"
(setq tp-rt-r2-text "abXYef")
(should (equal (buffer-substring-no-properties (point-min) (point-max))
"abXYef"))
(should (= (point) 2)))))
(ert-deftest tp-render-test-minimal-diff-point-in-suffix-stays ()
"Point in the common suffix stays glued to its character."
(tp-rt-with-cleanup (tp-rt-r2-layer) (tp-rt-r2-text)
(setq tp-rt-r2-text "abcdef")
(define-tp tp-rt-r2-layer () '(tp-text $tp-rt-r2-text))
(with-temp-buffer
(insert "abcdef")
(tp-set 1 7 'tp-rt-r2-layer)
(goto-char 6) ; on the "f" of the suffix "ef"
;; Same-length edit: point must not move at all
(setq tp-rt-r2-text "abXYef")
(should (= (point) 6))
(should (eq (char-after) ?f))
;; Length-changing edit: point stays glued to its character
(setq tp-rt-r2-text "abXYZWef")
(should (= (point) 8))
(should (eq (char-after) ?f)))))
(ert-deftest tp-render-test-minimal-diff-point-inside-diff-clamps ()
"Point inside the differing span ends up at the edit start."
(tp-rt-with-cleanup (tp-rt-r2-layer) (tp-rt-r2-text)
(setq tp-rt-r2-text "abcdef")
(define-tp tp-rt-r2-layer () '(tp-text $tp-rt-r2-text))
(with-temp-buffer
(insert "abcdef")
(tp-set 1 7 'tp-rt-r2-layer)
(goto-char 4) ; on "d", inside the "cd" -> "XY" span
(setq tp-rt-r2-text "abXYef")
(should (= (point) 3)))))
(ert-deftest tp-render-test-minimal-diff-markers-survive ()
"Markers in the unchanged prefix and suffix survive a text update."
(tp-rt-with-cleanup (tp-rt-r2m-layer) (tp-rt-r2m-text)
(setq tp-rt-r2m-text "abcdef")
(define-tp tp-rt-r2m-layer () '(tp-text $tp-rt-r2m-text))
(with-temp-buffer
(insert "abcdef")
(tp-set 1 7 'tp-rt-r2m-layer)
(let ((m-prefix (copy-marker 2)) ; on "b"
(m-suffix (copy-marker 6))) ; on "f"
(setq tp-rt-r2m-text "abXYZef") ; "cd" -> "XYZ", one char longer
(should (equal (buffer-substring-no-properties (point-min)
(point-max))
"abXYZef"))
(should (= (marker-position m-prefix) 2))
(should (eq (char-after m-prefix) ?b))
(should (= (marker-position m-suffix) 7))
(should (eq (char-after m-suffix) ?f))
(set-marker m-prefix nil)
(set-marker m-suffix nil)))))
;;; TXT-1: the suffix-boundary marker must track its character
(defun tp-rt--txt1-marker-after-edit (old new marker-offset)
"Run a minimal-diff replacement of OLD by NEW with a boundary marker.
Insert \"HEAD \" OLD \" TAIL\" in a temp buffer, tag OLD with a
tp-name, put an insertion-type-nil marker at OLD's start plus
MARKER-OFFSET, replace via `tp--replace-reactive-text-in-buffer' and
return (MARKER-POSITION CHAR-AT-MARKER ORIGINAL-CHAR)."
(with-temp-buffer
(insert "HEAD ")
(let ((m-start (point)))
(insert old " TAIL")
(put-text-property m-start (+ m-start (length old))
'tp-name 'tp-rt-txt1-layer)
(let* ((mpos (+ m-start marker-offset))
(mchar (char-after mpos))
(mk (copy-marker mpos)))
(tp--replace-reactive-text-in-buffer 'tp-rt-txt1-layer new nil)
(prog1 (list (marker-position mk) (char-after mk) mchar)
(set-marker mk nil))))))
(ert-deftest tp-render-test-minimal-diff-suffix-start-marker-tracks ()
"A marker on the FIRST character of the preserved suffix tracks it.
TXT-1: delete-then-insert collapsed such a marker onto the edit
start, stranding it before the inserted text; insert-then-delete
shifts it right with its character. Grow, same-length (the clearest
docstring violation) and shrink edits are all covered."
;; Grow: "0" -> "42"; marker on the space before "items" (offset 8).
(pcase-let ((`(,pos ,got ,want)
(tp-rt--txt1-marker-after-edit
"count: 0 items" "count: 42 items" 8)))
(should (eq got want))
(should (= pos 15))) ; 14 shifted right by 1
;; Same length: "0" -> "9"; the marker's correct position is
;; numerically unchanged.
(pcase-let ((`(,pos ,got ,want)
(tp-rt--txt1-marker-after-edit
"count: 0 items" "count: 9 items" 8)))
(should (eq got want))
(should (= pos 14)))
;; Shrink: "42" -> "0".
(pcase-let ((`(,pos ,got ,want)
(tp-rt--txt1-marker-after-edit
"count: 42 items" "count: 0 items" 9)))
(should (eq got want))
(should (= pos 14))))
(ert-deftest tp-render-test-minimal-diff-deleted-char-marker-at-edit-end ()
"A marker whose character was deleted ends at the END of the edit.
The documented side effect of inserting before deleting; previously
such markers collapsed to the edit start. Either way they stay
inside the replacement span."
;; "100" -> "42": marker on the middle "0" (strictly inside the
;; edited span) ends after the inserted "42".
(pcase-let ((`(,pos ,_got ,_want)
(tp-rt--txt1-marker-after-edit
"count: 100 items" "count: 42 items" 8)))
;; Edit span starts at buffer position 13 ("100"), insert "42":
;; the marker lands at the end of the inserted text.
(should (= pos 15))))
(ert-deftest tp-render-test-minimal-diff-suffix-marker-real-path ()
"The suffix-start marker tracks through a real setq-driven update."
(tp-rt-with-cleanup (tp-rt-r2s-layer) (tp-rt-r2s-text)
(setq tp-rt-r2s-text "count: 0 items")
(define-tp tp-rt-r2s-layer () '(tp-text $tp-rt-r2s-text))
(with-temp-buffer
(insert "count: 0 items")
(tp-set 1 15 'tp-rt-r2s-layer)
(let ((m (copy-marker 9))) ; the space before "items"
(setq tp-rt-r2s-text "count: 42 items")
(should (equal (buffer-substring-no-properties (point-min)
(point-max))
"count: 42 items"))
(should (eq (char-after m) ?\s))
(should (= (marker-position m) 10))
(set-marker m nil)))))
(ert-deftest tp-render-test-minimal-diff-identical-update-is-noop ()
"An identical-text reactive replacement leaves the buffer unmodified."
(tp-rt-with-cleanup (tp-rt-r2n-layer) (tp-rt-r2n-text)
(setq tp-rt-r2n-text "emacs")
(define-tp tp-rt-r2n-layer () '(face bold tp-text $tp-rt-r2n-text))
(with-temp-buffer
(insert "emacs")
(tp-set 1 6 'tp-rt-r2n-layer)
(set-buffer-modified-p nil)
(save-excursion
(tp--replace-reactive-text-in-buffer
'tp-rt-r2n-layer "emacs" (tp-layer-props 'tp-rt-r2n-layer t)))
;; No text edit and no property churn: the flag must stay clear
(should-not (buffer-modified-p))
(should (equal (buffer-substring-no-properties (point-min) (point-max))
"emacs"))
(should (eq (get-text-property 1 'face) 'bold)))))
;;; ARCH-4: the pending queue must survive neither reset nor errors
(defvar tp-rt-a4-face nil)
(defvar tp-rt-a4-color nil)
(ert-deftest tp-render-test-reactive-reset-clears-pending-queue ()
"tp-reactive-reset drops queued batch re-renders (ARCH-4).
Stranded entries would otherwise survive the reset and replay against
freshly (re)defined layers on the next flush."
(unwind-protect
(progn
(tp--queue-batch-update 'tp-rt-a4-ghost 'tp-rt-a4-ghost-var nil nil)
(should tp--batch-update-pending)
(tp-reactive-reset)
(should (null tp--batch-update-pending)))
(setq tp--batch-update-pending nil)))
(ert-deftest tp-render-test-error-escaping-update-flushes-nested-queue ()
"An error escaping a re-render cannot strand nested queued updates.
A modification hook that writes a second reactive variable and then
signals used to strand the nested entry in the global queue - the
flush tail sat outside any unwind-protect. The flush now runs as the
update unwinds, so the nested variable's re-render still lands and
the queue is drained (ARCH-4)."
(setq tp-rt-a4-face 'bold
tp-rt-a4-color "red")
(unwind-protect
(progn
(define-tp tp-rt-a4-layer-a () '(face $tp-rt-a4-face))
(define-tp tp-rt-a4-layer-b ()
'(face (:foreground $tp-rt-a4-color)))
(with-temp-buffer
(insert "Hello world")
(tp-set 1 6 'tp-rt-a4-layer-a)
(tp-set 7 12 'tp-rt-a4-layer-b)
(let ((armed t))
(add-hook 'before-change-functions
(lambda (_beg _end)
(when armed
(setq armed nil)
;; Nested reactive write from within the
;; re-render: goes to the global queue.
(setq tp-rt-a4-color "green")
(error "boom from modification hook")))
nil t)
(should-error (setq tp-rt-a4-face 'italic))
;; The nested entry was flushed on the way out, not
;; stranded...
(should (null tp--batch-update-pending))
;; ...and its re-render landed despite the error.
(should (equal (get-text-property 7 'face)
'(:foreground "green"))))))
(tp-undefine-layer 'tp-rt-a4-layer-a)
(tp-undefine-layer 'tp-rt-a4-layer-b)
(setq tp-rt-a4-face nil
tp-rt-a4-color nil
tp--batch-update-pending nil)))
;;; R3 (0.3.0): anonymous-layer garbage collection
(ert-deftest tp-render-test-gc-collects-unreferenced-anonymous-layer ()
"GC collects an anonymous layer whose only buffer was killed."
(setq tp-rt-r3a-color "red")
(let ((buf (generate-new-buffer " tp-rt-r3a"))
(name nil))
(unwind-protect
(progn
(with-current-buffer buf
(insert "Hello")
(tp-set 1 6 '(face (:foreground $tp-rt-r3a-color)))
(setq name (get-text-property 1 'tp-name)))
(should name)
(should (assoc name tp-layer-alist))
(kill-buffer buf)
(should (memq name (tp-gc-anonymous-layers)))
(should-not (assoc name tp-layer-alist))
(should-not (rassq name tp--anonymous-layer-registry)))
(when (buffer-live-p buf) (kill-buffer buf))
(when (and name (assoc name tp-layer-alist))
(tp-undefine-layer name))
(setq tp-rt-r3a-color nil))))
(ert-deftest tp-render-test-gc-keeps-layer-still-displayed ()
"GC keeps an anonymous layer that a live buffer still shows."
(setq tp-rt-r3b-color "red")
(let ((buf (generate-new-buffer " tp-rt-r3b"))
(name nil))
(unwind-protect
(progn
(with-current-buffer buf
(insert "Hello")
(tp-set 1 6 '(face (:foreground $tp-rt-r3b-color)))
(setq name (get-text-property 1 'tp-name)))
(should name)
(should-not (memq name (tp-gc-anonymous-layers)))
(should (assoc name tp-layer-alist)))
(kill-buffer buf)
(when (and name (assoc name tp-layer-alist))
(tp-undefine-layer name))
(setq tp-rt-r3b-color nil))))
(ert-deftest tp-render-test-gc-keeps-unknown-registry-layer ()
"GC keeps an anonymous layer whose registry state is `unknown'."
(setq tp-rt-r3c-color "red")
(let* ((s (tp-set "Hello" '(face (:foreground $tp-rt-r3c-color))))
(name (get-text-property 0 'tp-name s)))
(unwind-protect
(progn
(should name)
;; Applied to a string only: the registry knows nothing
(should (eq (tp-reactive-layer-buffers name) 'unknown))
(should-not (memq name (tp-gc-anonymous-layers)))
(should (assoc name tp-layer-alist)))
(when (and name (assoc name tp-layer-alist))
(tp-undefine-layer name))
(setq tp-rt-r3c-color nil))))
;;; GC-1: buried and hidden layers are ALIVE for GC and track-buffer
(defvar tp-rt-gc1-color nil)
(defvar tp-rt-gc1b-color nil)
(defvar tp-rt-gc1c-color nil)
(ert-deftest tp-render-test-gc-keeps-layer-buried-under-push ()
"GC keeps an anonymous layer buried below a pushed top layer.
The buried layer's tp-name lives inside `tp-layers' storage, not as a
direct property; the stack-aware liveness scan must still see it, and
reactivity must survive a later pop (GC-1)."
(setq tp-rt-gc1-color "blue")
(let ((buf (generate-new-buffer " tp-rt-gc1"))
(name nil))
(unwind-protect
(progn
(define-tp tp-rt-gc1-top () '(face bold))
(with-current-buffer buf
(insert "0123456789")
(tp-set 1 6 '(face (:foreground $tp-rt-gc1-color)))
(setq name (get-text-property 1 'tp-name))
(should name)
(tp-push-layer 1 6 'tp-rt-gc1-top)
;; Now buried: direct tp-name is the pushed top's.
(should (eq (get-text-property 1 'tp-name) 'tp-rt-gc1-top))
;; The buffer is live and still holds the layer: GC must
;; keep it.
(should-not (memq name (tp-gc-anonymous-layers)))
(should (assoc name tp-layer-alist))
;; Reactivity survives: pop and update.
(tp-pop-layer 1 6)
(setq tp-rt-gc1-color "red")
(should (equal (get-text-property 1 'face)
'(:foreground "red")))))
(kill-buffer buf)
(when (and name (assoc name tp-layer-alist))
(tp-undefine-layer name))
(tp-undefine-layer 'tp-rt-gc1-top)
(setq tp-rt-gc1-color nil))))
(ert-deftest tp-render-test-gc-keeps-hidden-layer ()
"GC keeps an anonymous layer hidden via tp-hide-layer.
An all-hidden run carries no direct tp-name at all; the layer lives
only inside `tp-layers' storage yet is queryable and re-showable, so
GC must not collect it and show+setq must still re-render (GC-1,
XM-02)."
(setq tp-rt-gc1b-color "green")
(let ((buf (generate-new-buffer " tp-rt-gc1b"))
(name nil))
(unwind-protect
(with-current-buffer buf
(insert "abcdefghij")
(tp-set 1 6 '(face (:foreground $tp-rt-gc1b-color)))
(setq name (get-text-property 1 'tp-name))
(should name)
(tp-hide-layer 1 6 name)
(should-not (get-text-property 1 'tp-name))
;; Live buffer still holds the hidden layer: keep it.
(should-not (memq name (tp-gc-anonymous-layers)))
(should (assoc name tp-layer-alist))
;; Show and update: reactivity must be intact.
(tp-show-layer 1 6 name)
(setq tp-rt-gc1b-color "purple")
(should (equal (get-text-property 1 'face)
'(:foreground "purple"))))
(kill-buffer buf)
(when (and name (assoc name tp-layer-alist))
(tp-undefine-layer name))
(setq tp-rt-gc1b-color nil))))
(ert-deftest tp-render-test-track-buffer-finds-buried-and-hidden-layers ()
"tp-reactive-track-buffer registers layers buried or hidden in storage.
A propertized string carrying a stacked (buried) layer and an
all-hidden string are inserted into a fresh buffer; the track scan
must register every layer name, not just the rendered top ones
\(GC-1, XM-04)."
(setq tp-rt-gc1c-color "gold")
(let ((buf (generate-new-buffer " tp-rt-gc1c"))
(name nil))
(unwind-protect
(progn
(define-tp tp-rt-gc1c-top () '(face bold))
(define-tp tp-rt-gc1c-hidden () '(face italic))
(let ((s (with-temp-buffer
(insert "trackme")
(tp-set 1 6 '(face (:foreground $tp-rt-gc1c-color)))
(setq name (get-text-property 1 'tp-name))
(tp-push-layer 1 6 'tp-rt-gc1c-top)
(buffer-string)))
(h (let ((h (copy-sequence " hideme")))
(tp-push-layer h 'tp-rt-gc1c-hidden)
(tp-hide-layer h 'tp-rt-gc1c-hidden)
h)))
(with-current-buffer buf
(insert s)
(insert h)
(let ((found (tp-reactive-track-buffer)))
;; Rendered top, buried layer, and all-hidden layer.
(should (memq 'tp-rt-gc1c-top found))
(should (memq name found))
(should (memq 'tp-rt-gc1c-hidden found)))
(should (memq buf (tp-reactive-layer-buffers name)))
(should (memq buf (tp-reactive-layer-buffers
'tp-rt-gc1c-hidden))))))
(kill-buffer buf)
(when (and name (assoc name tp-layer-alist))
(tp-undefine-layer name))
(tp-undefine-layer 'tp-rt-gc1c-top)
(tp-undefine-layer 'tp-rt-gc1c-hidden)
(setq tp-rt-gc1c-color nil))))
(provide 'tp-render-tests)
;;; tp-render-tests.el ends here