ebox/tests/ebox-patch-plan-tests.el
Kinneyzhang a19672a25e
Some checks are pending
CI / test (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
refactor: add pure patch planner for M2a E3
2026-08-31 20:57:54 +08:00

261 lines
11 KiB
EmacsLisp

;;; ebox-patch-plan-tests.el --- M2a pure patch planner gates -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'cl-lib)
(require 'benchmark)
(require 'ebox)
(require 'ebox-patch-plan)
(require 'ebox-fixtures)
(defconst ebox-patch-plan-test--root
(expand-file-name ".." (file-name-directory (or load-file-name buffer-file-name)))
"Repository root used by patch planner tests.")
(defun ebox-patch-plan-test--read-forms (file)
"Read and return every top-level form in repository FILE."
(with-temp-buffer
(insert-file-contents (expand-file-name file ebox-patch-plan-test--root))
(let (forms form)
(condition-case nil
(while t
(setq form (read (current-buffer)))
(push form forms))
(end-of-file nil))
(nreverse forms))))
(defun ebox-patch-plan-test--walk-symbols (form function)
"Call FUNCTION for every symbol contained by FORM."
(cond
((symbolp form) (funcall function form))
((consp form)
(ebox-patch-plan-test--walk-symbols (car form) function)
(ebox-patch-plan-test--walk-symbols (cdr form) function))
((vectorp form)
(mapc (lambda (item)
(ebox-patch-plan-test--walk-symbols item function))
form))))
(defun ebox-patch-plan-test--dirty (node-id kind keys)
"Return one planner fixture for NODE-ID, KIND, and changed KEYS."
(list :node-id node-id :dirty-kind kind :changed-keys keys))
(defun ebox-patch-plan-test--reset-runtime-state ()
"Reset planner-visible global identities and compatibility projections."
(setq ebox--region-id-counter 0
ebox--runtime-node-id-counter 0)
(dolist (table (list ebox--region-box-table ebox--scroll-global-state))
(when (hash-table-p table) (clrhash table))))
(defun ebox-patch-plan-test--op (operation owner-id dirty)
"Return one tentative OPERATION for OWNER-ID and DIRTY."
(list :op operation :owner-id owner-id :dirty dirty))
(ert-deftest ebox-patch-plan-module-has-no-surface-or-publication-capability ()
"The pure planner cannot inspect buffers or call publication APIs."
(let (forbidden)
(dolist (form (ebox-patch-plan-test--read-forms "ebox-patch-plan.el"))
(ebox-patch-plan-test--walk-symbols
form
(lambda (symbol)
(let ((name (symbol-name symbol)))
(when (or (string-prefix-p "ebox-surface-" name)
(string-prefix-p "tp-surface-" name)
(string-prefix-p "tp-object-" name)
(memq symbol '(with-current-buffer set-buffer
insert delete-region
put-text-property
ebox--buffer-render-state)))
(push symbol forbidden))))))
(should-not (delete-dups forbidden))))
(ert-deftest ebox-patch-plan-merges-one-deterministic-antichain ()
"A stronger ancestor absorbs descendant dirt without input mutation."
(let ((parents (make-hash-table :test #'equal))
operations)
(puthash 'branch 'root parents)
(puthash 'leaf 'branch parents)
(setq operations
(list
(ebox-patch-plan-test--op
'paint-patch 'leaf
(ebox-patch-plan-test--dirty 'leaf 'paint '(:color)))
(ebox-patch-plan-test--op
'span-patch 'branch
(ebox-patch-plan-test--dirty 'branch 'geometry '(:content)))))
(let ((before (copy-tree operations))
(result (ebox-patch-plan-merge-ops parents operations)))
(should (equal operations before))
(should (= (length result) 1))
(should (eq (plist-get (car result) :op) 'span-patch))
(should (eq (plist-get (car result) :owner-id) 'branch))
(should
(equal (sort (copy-sequence
(plist-get (plist-get (car result) :dirty) :node-ids))
(lambda (left right)
(string< (symbol-name left) (symbol-name right))))
'(branch leaf)))
(should
(equal (sort (copy-sequence
(plist-get (plist-get (car result) :dirty)
:changed-keys))
(lambda (left right)
(string< (symbol-name left) (symbol-name right))))
'(:color :content))))))
(ert-deftest ebox-patch-plan-rejects-malformed-and-cyclic-input ()
"Invalid artifacts fail before any downstream publication can exist."
(should-error (ebox-patch-plan-merge-ops nil nil)
:type 'ebox-patch-plan-error)
(let ((parents (make-hash-table :test #'equal)))
(should-error (ebox-patch-plan-merge-ops parents '((:op paint-patch)))
:type 'ebox-patch-plan-error)
(puthash 'first 'second parents)
(puthash 'second 'first parents)
(should-error
(ebox-patch-plan-merge-ops
parents
(list
(ebox-patch-plan-test--op
'paint-patch 'first
(ebox-patch-plan-test--dirty 'first 'paint '(:color)))))
:type 'ebox-patch-plan-error)))
(defun ebox-patch-plan-test--integration-fixture ()
"Return a retained layout covering paint, geometry, and broad planning."
(apply
#'ebox-test-column
:width '(360)
(cl-loop for index below 12
collect
(ebox-test-box
:key (intern (format "patch-plan-%d" index))
(ebox-test-text (format "Item %02d" index))
:width '(160) :height 1))))
(defun ebox-patch-plan-test--route-artifact (buffer dirty route)
"Return BUFFER's planner artifact for DIRTY through ROUTE."
(let ((ebox-incremental--patch-planner-route route))
(ebox-incremental--declarative-tentative-patch-set
buffer (copy-tree dirty))))
(ert-deftest ebox-patch-plan-pure-route-matches-legacy-artifacts ()
"Paint, geometry, and broad viewport inputs match the old planner exactly."
(ebox-patch-plan-test--reset-runtime-state)
(let ((buffer (generate-new-buffer " *ebox-m2a-e3-artifacts*")))
(unwind-protect
(progn
(let ((ebox-viewport-width 360))
(ebox-render-to-buffer
buffer (ebox-patch-plan-test--integration-fixture)))
(let* ((root (ebox--buffer-root-node buffer))
(children (ebox-tree-node-children root))
(first-id (plist-get (nth 0 children) :node-id))
(second-id (plist-get (nth 1 children) :node-id))
(paint
(list
(ebox-patch-plan-test--dirty
first-id 'paint '(:color))))
(geometry
(list
(append
(ebox-patch-plan-test--dirty
second-id 'geometry '(:content))
'(:old-signature (:content "Item 01" :width 160)
:new-signature (:content "Item 01 changed" :width 160)))))
(broad (ebox--viewport-dirty-set root)))
(dolist (dirty (list paint geometry broad))
(let ((legacy
(ebox-patch-plan-test--route-artifact
buffer dirty 'legacy))
(pure
(ebox-patch-plan-test--route-artifact
buffer dirty 'pure)))
(should (equal pure legacy))
(should
(equal
(ebox-patch-plan-test--route-artifact buffer dirty 'shadow)
legacy))))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-patch-plan-fault-cannot-publish-live-state ()
"A pure planner fault leaves buffer text, revision, and client state intact."
(ebox-patch-plan-test--reset-runtime-state)
(let ((buffer (generate-new-buffer " *ebox-m2a-e3-fault*")))
(unwind-protect
(progn
(ebox-render-to-buffer
buffer (ebox-patch-plan-test--integration-fixture))
(let* ((surface (ebox-surface--live-buffer-surface buffer))
(state (tp-surface-client-state surface))
(revision (tp-surface-revision surface))
(contents
(with-current-buffer buffer
(buffer-substring (point-min) (point-max))))
(root (plist-get state :root-node))
(dirty (ebox--viewport-dirty-set root))
(ebox-incremental--patch-planner-route 'pure))
(cl-letf (((symbol-function 'ebox-patch-plan-merge-ops)
(lambda (&rest _arguments)
(error "Injected E3 planner fault"))))
(should-error
(ebox-incremental--declarative-tentative-patch-set
buffer dirty)))
(should (eq (tp-surface-client-state surface) state))
(should (= (tp-surface-revision surface) revision))
(should
(equal-including-properties
(with-current-buffer buffer
(buffer-substring (point-min) (point-max)))
contents))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-patch-plan-feature-route-defaults-pure-and-keeps-legacy ()
"E3 enables the pure route while retaining explicit rollback selection."
(should (eq (default-value 'ebox-incremental--patch-planner-route) 'pure))
(dolist (route '(legacy pure shadow))
(should (memq route '(legacy pure shadow))))
(let ((ebox-incremental--patch-planner-route 'unknown)
(buffer (generate-new-buffer " *ebox-m2a-e3-route*")))
(unwind-protect
(should-error
(ebox-incremental--declarative-tentative-patch-set
buffer '((:node-id node :dirty-kind paint
:changed-keys (:color))))
:type 'ebox-patch-plan-error)
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-patch-plan-performance-bounds-parent-walks ()
"A wide plan performs one bounded parent walk per tentative artifact."
(let ((parents (make-hash-table :test #'equal))
operations)
(dotimes (index 512)
(let ((node-id (intern (format "ebox-patch-perf-%d" index))))
(puthash node-id 'root parents)
(push
(ebox-patch-plan-test--op
'paint-patch node-id
(ebox-patch-plan-test--dirty node-id 'paint '(:color)))
operations)))
(setq operations (nreverse operations))
(let ((walks 0)
(original (symbol-function 'ebox-patch-plan--ancestor-set))
plan benchmark)
(cl-letf (((symbol-function 'ebox-patch-plan--ancestor-set)
(lambda (&rest arguments)
(cl-incf walks)
(apply original arguments))))
(setq benchmark
(benchmark-run
1
(setq plan
(ebox-patch-plan-merge-ops parents operations)))))
(should (= (length plan) 512))
(should (= walks 512))
(should (< (car benchmark) 2.0)))))
(provide 'ebox-patch-plan-tests)
;;; ebox-patch-plan-tests.el ends here