fix: scope node memos to copied runtime generations
This commit is contained in:
parent
3fadb19121
commit
5143d17a56
7
Makefile
7
Makefile
@ -12,6 +12,8 @@ NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
|
||||
|
||||
.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests viewport-topology-tests vertical-materialization-tests box-line-flow-tests m0a-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance style-schema-tests style-schema-performance spi-tests spi-performance c1b-contract-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
|
||||
|
||||
.PHONY: node-memo-generation-tests
|
||||
|
||||
all: check
|
||||
|
||||
check: ci
|
||||
@ -25,7 +27,7 @@ compile:
|
||||
rm -f *.elc tests/*.elc scripts/*.elc
|
||||
$(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t byte-compile-warnings (quote (not obsolete)))' -l ebox.el --eval '(ebox-byte-compile)'
|
||||
|
||||
test: source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests viewport-topology-tests vertical-materialization-tests box-line-flow-tests m0a-tests state-contract-tests layout-boundary-tests patch-plan-tests style-schema-tests spi-tests docs-contract-tests ci-contract-tests
|
||||
test: source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests viewport-topology-tests vertical-materialization-tests box-line-flow-tests node-memo-generation-tests m0a-tests state-contract-tests layout-boundary-tests patch-plan-tests style-schema-tests spi-tests docs-contract-tests ci-contract-tests
|
||||
|
||||
source-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
|
||||
@ -72,6 +74,9 @@ vertical-materialization-tests:
|
||||
box-line-flow-tests:
|
||||
$(EMACS_TEST) -l tests/ebox-box-line-flow-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
node-memo-generation-tests:
|
||||
$(EMACS_TEST) -l tests/ebox-node-memo-generation-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
m0a-tests:
|
||||
$(EMACS_TEST) -l tests/ebox-m0a-inventory-fixture.el -l tests/ebox-m0a-inventory-tests.el -l tests/ebox-m0a-characterization-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
|
||||
@ -10829,8 +10829,14 @@ fresh regardless of whether any stable producers exist."
|
||||
buffer old-state old-state
|
||||
(plist-get old-state :region-box-table)))
|
||||
(candidate-state
|
||||
(ebox-incremental--isolate-candidate-caches
|
||||
(copy-sequence old-state)))
|
||||
(let ((state (copy-sequence old-state)))
|
||||
;; EQ memo keys belong to one node generation. Start fresh
|
||||
;; before isolation can copy historical node keys.
|
||||
(dolist (key '(:render-signature-cache
|
||||
:viewport-height-dependent-subtree-cache
|
||||
:flex-content-min-widths))
|
||||
(plist-put state key (make-hash-table :test 'eq)))
|
||||
(ebox-incremental--isolate-candidate-caches state)))
|
||||
(prepared
|
||||
(list :root (plist-get old-state :root-node)
|
||||
:dirty-set dirty-set
|
||||
|
||||
@ -2277,14 +2277,11 @@ VALIDATED-P means the caller owns a current topology certificate for SOURCE."
|
||||
candidate))
|
||||
|
||||
(defun ebox-surface--isolated-viewport-overrides (overrides)
|
||||
"Copy mutable viewport caches in OVERRIDES for a private reflow candidate.
|
||||
The retained viewport projection may reuse the TP object topology, but its
|
||||
layout pass still writes Ebox-side caches. Keep those writes out of the
|
||||
published generation so a failed transaction has no cache state to restore."
|
||||
"Isolate persistent viewport caches in OVERRIDES for a reflow candidate.
|
||||
Keep writes to retained output and layout fragments out of the published
|
||||
generation. Node memos follow the source-copy policy in the projector."
|
||||
(let ((copy (copy-sequence overrides)))
|
||||
(dolist (key '(:render-cache :layout-fragments :render-signature-cache
|
||||
:flex-content-min-widths
|
||||
:viewport-height-dependent-subtree-cache))
|
||||
(dolist (key '(:render-cache :layout-fragments))
|
||||
(when (plist-member copy key)
|
||||
(let ((value (plist-get copy key)))
|
||||
(when (hash-table-p value)
|
||||
@ -5468,6 +5465,17 @@ published runtime and must be consumed without clearing those shared nodes."
|
||||
(_source-index
|
||||
(progn
|
||||
(setq state-overrides (copy-sequence state-overrides))
|
||||
;; A copied runtime root starts a new EQ memo generation on
|
||||
;; every producer evaluation, including ordinary fallbacks.
|
||||
;; Already isolated or path-copied sources keep their prepared
|
||||
;; seed/prune contract and same-candidate native handoff.
|
||||
(unless (eq root source)
|
||||
(dolist (key '(:render-signature-cache
|
||||
:viewport-height-dependent-subtree-cache
|
||||
:flex-content-min-widths))
|
||||
(setq state-overrides
|
||||
(plist-put state-overrides key
|
||||
(make-hash-table :test 'eq)))))
|
||||
(cl-remf state-overrides :source-base-index)
|
||||
(setq state-overrides
|
||||
(plist-put state-overrides :source-index source-index))))
|
||||
|
||||
442
tests/ebox-node-memo-generation-tests.el
Normal file
442
tests/ebox-node-memo-generation-tests.el
Normal file
@ -0,0 +1,442 @@
|
||||
;;; ebox-node-memo-generation-tests.el --- Node memo ownership -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'ert)
|
||||
(setq load-prefer-newer t)
|
||||
(load-file (expand-file-name "../ebox.el"
|
||||
(file-name-directory load-file-name)))
|
||||
|
||||
(defconst ebox-node-memo-test--keys
|
||||
'(:render-signature-cache :viewport-height-dependent-subtree-cache
|
||||
:flex-content-min-widths))
|
||||
|
||||
(defun ebox-node-memo-test--input (map)
|
||||
"Build an ordinary responsive Flex/Grid tree with nested scroll and MAP."
|
||||
(ebox-build
|
||||
`(column :width (viewport)
|
||||
(text "Header")
|
||||
(column :id "body" :width stretch :height (viewport-height)
|
||||
:overflow scroll :padding (0 (2))
|
||||
,@(cl-loop
|
||||
for index below 6
|
||||
collect
|
||||
`(flex :width stretch :align-items stretch
|
||||
(box :flex-grow 1 :flex-basis (40) :wrap-mode char
|
||||
(text ,(propertize
|
||||
(format "Open %02d %s" index (make-string 48 ?x))
|
||||
'keymap map 'mouse-face 'highlight 'help-echo "Open row")))
|
||||
(grid :width (40) :grid-template-columns ((fr 1))
|
||||
(text "Tag")))))
|
||||
(text "Footer"))))
|
||||
|
||||
(defun ebox-node-memo-test--counts (state)
|
||||
"Return total/current/stale key counts for the node memos in STATE."
|
||||
(let ((current (make-hash-table :test 'eq)))
|
||||
(maphash (lambda (_ node) (puthash node t current))
|
||||
(plist-get state :node-table))
|
||||
(mapcar
|
||||
(lambda (key)
|
||||
(let ((memo (plist-get state key)) (live 0) (stale 0))
|
||||
(should (hash-table-p memo))
|
||||
(should (eq (hash-table-test memo) 'eq))
|
||||
(maphash (lambda (node _value)
|
||||
(if (gethash node current) (cl-incf live) (cl-incf stale)))
|
||||
memo)
|
||||
(list key :total (hash-table-count memo) :current live :stale stale)))
|
||||
ebox-node-memo-test--keys)))
|
||||
|
||||
(defmacro ebox-node-memo-test--with-mounted (&rest body)
|
||||
"Run BODY with a pure responsive fixture in BUFFER and INPUT."
|
||||
(declare (indent 0))
|
||||
`(let* ((buffer (generate-new-buffer " *ebox-node-memo-boundary*"))
|
||||
(map (make-sparse-keymap))
|
||||
(_key (define-key map [mouse-1] #'ignore))
|
||||
(input (ebox-node-memo-test--input map))
|
||||
(ebox-viewport-width 180) (ebox-viewport-height 6)
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(unwind-protect
|
||||
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||
(lambda () nil)))
|
||||
(ebox-render-to-buffer buffer input)
|
||||
,@body)
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(defun ebox-node-memo-test--resize (count axes)
|
||||
"Repeat COUNT real viewport changes on AXES and check generation ownership."
|
||||
(let* ((buffer (generate-new-buffer " *ebox-node-memo*"))
|
||||
(map (make-sparse-keymap))
|
||||
(_key (define-key map [mouse-1] #'ignore))
|
||||
(input (ebox-node-memo-test--input map))
|
||||
(host (ebox-canonical-input-root-host-ref input))
|
||||
(ebox-viewport-width 180)
|
||||
(ebox-viewport-height 6)
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||
history)
|
||||
(unwind-protect
|
||||
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||
(lambda () nil)))
|
||||
(ebox-render-to-buffer buffer input)
|
||||
(let* ((initial-state (ebox--buffer-render-state buffer))
|
||||
(initial-text (with-current-buffer buffer (buffer-string)))
|
||||
(objects (copy-hash-table
|
||||
(plist-get initial-state :surface-node-object-table)))
|
||||
(node-count (hash-table-count (plist-get initial-state :node-table))))
|
||||
(should (ebox-host-ref-position buffer host))
|
||||
(push (cons 0 (ebox-node-memo-test--counts initial-state)) history)
|
||||
;; A later cache hit may legitimately leave a node memo empty;
|
||||
;; require real population at mount, not a forced fill each turn.
|
||||
(dolist (entry (cdar history))
|
||||
(should (> (plist-get (cdr entry) :current) 0)))
|
||||
(dotimes (iteration count)
|
||||
(let* ((alternate (= (% iteration 2) 0))
|
||||
(width (if (and alternate (memq axes '(width both))) 220 180))
|
||||
(height (if (and alternate (memq axes '(height both))) 9 6)))
|
||||
(ebox-rerender-buffer-with-context buffer width height)
|
||||
(let ((state (ebox--buffer-render-state buffer)))
|
||||
(should (eq (plist-get (ebox-buffer-update-report buffer) :viewport-axes)
|
||||
axes))
|
||||
(should (= node-count (hash-table-count (plist-get state :node-table))))
|
||||
(should (ebox-host-ref-position buffer host))
|
||||
(maphash
|
||||
(lambda (id object)
|
||||
(should (eq object
|
||||
(gethash id (plist-get state :surface-node-object-table)))))
|
||||
objects)
|
||||
(push (cons (1+ iteration) (ebox-node-memo-test--counts state)) history))))
|
||||
;; The sequence ends at A. Keep the public text/property and Host
|
||||
;; contracts independent of the memo-size assertion below.
|
||||
(should (equal-including-properties
|
||||
initial-text (with-current-buffer buffer (buffer-string))))
|
||||
(with-current-buffer buffer
|
||||
(let* ((pos (string-match "Open 00" (buffer-string)))
|
||||
(output (buffer-string)))
|
||||
(should pos)
|
||||
(should (eq (lookup-key (get-text-property pos 'keymap output)
|
||||
[mouse-1]) #'ignore))
|
||||
(should (equal (get-text-property pos 'help-echo output) "Open row"))))
|
||||
(setq history (nreverse history))
|
||||
(ert-info ((format "axes=%S nodes=%d history=%S" axes node-count history))
|
||||
(dolist (generation history)
|
||||
(dolist (entry (cdr generation))
|
||||
(should (= (plist-get (cdr entry) :stale) 0))
|
||||
(should (<= (plist-get (cdr entry) :total) node-count)))))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-width-eight-resizes ()
|
||||
"Eight width changes must not retain preceding generations' node keys."
|
||||
(ebox-node-memo-test--resize 8 'width))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-height-eight-resizes ()
|
||||
"Eight height changes must not retain preceding generations' node keys."
|
||||
(ebox-node-memo-test--resize 8 'height))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-both-twenty-four-resizes ()
|
||||
"Twenty-four two-axis changes keep memo ownership bounded to live nodes."
|
||||
(ebox-node-memo-test--resize 24 'both))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-scroll-signatures-match-current-body ()
|
||||
"Height changes after a real scroll retain only correct recursive facts."
|
||||
(ebox-node-memo-test--with-mounted
|
||||
(ebox-rerender-buffer-with-context buffer 180 3)
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(id (car (plist-get state :scroll-region-ids))))
|
||||
(should (= (ebox--surface-scroll-region-by buffer id 1 nil) 1))
|
||||
(setq state (ebox--buffer-render-state buffer))
|
||||
(should (= (plist-get (gethash id (plist-get state :scroll-state-table))
|
||||
:scroll-offset) 1))
|
||||
;; The memoizer accepts every current node, including a recursive root.
|
||||
;; Populate a valid old fact through its real computation, not a mock.
|
||||
(let ((ebox--render-cache-signature-cache
|
||||
(plist-get state :render-signature-cache)))
|
||||
(ebox--render-cache-node-body-signature (plist-get state :root-node)))
|
||||
(ebox-rerender-buffer-with-context buffer 180 20)
|
||||
(setq state (ebox--buffer-render-state buffer))
|
||||
(let ((ebox--render-cache-signature-cache nil))
|
||||
(maphash
|
||||
(lambda (node cached)
|
||||
(ert-info ((format "Current node %S, scroll-offset=%S"
|
||||
(plist-get node :node-id) (plist-get node :scroll-offset)))
|
||||
(should (equal cached (ebox--render-cache-node-body-signature node)))))
|
||||
(plist-get state :render-signature-cache))))))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-retains-canonical-region-index ()
|
||||
"Resize keeps offscreen regions and canonical owners across real cache hits."
|
||||
(let* ((buffer (generate-new-buffer " *ebox-node-memo-regions*"))
|
||||
(ebox-viewport-width 180) (ebox-viewport-height 6)
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil)
|
||||
(input
|
||||
(ebox-build
|
||||
`(column :width (viewport)
|
||||
(box :id "fixed" :width (80) (box (text "Cached child")))
|
||||
(flex :width stretch
|
||||
(box :flex-grow 1 :flex-basis (40) (text "Allocated")))
|
||||
(column :width stretch :height 2 :overflow scroll
|
||||
,@(cl-loop for index below 64 collect
|
||||
`(box :height 1 (text ,(format "Row %02d" index))))))))
|
||||
(probe (symbol-function 'ebox--render-cache-probe))
|
||||
fixed-node-id cache-hits)
|
||||
(unwind-protect
|
||||
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||
(lambda () nil)))
|
||||
(ebox-render-to-buffer buffer input)
|
||||
(setq fixed-node-id
|
||||
(plist-get (car (ebox-selector-query-buffer buffer "#fixed"))
|
||||
:node-id))
|
||||
(should fixed-node-id)
|
||||
(cl-letf (((symbol-function 'ebox--render-cache-probe)
|
||||
(lambda (node &rest args)
|
||||
(let ((result (apply probe node args)))
|
||||
(when (and (equal (plist-get node :node-id) fixed-node-id)
|
||||
(plist-get result :rendered))
|
||||
(push t cache-hits))
|
||||
result))))
|
||||
(dolist (width '(220 180))
|
||||
(ebox-rerender-buffer-with-context buffer width 6)
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(regions (plist-get state :region-box-table))
|
||||
(ids (plist-get state :region-id-set))
|
||||
(nodes (plist-get state :node-table))
|
||||
(scroll (gethash (car (plist-get state :scroll-region-ids))
|
||||
(plist-get state :scroll-state-table))))
|
||||
(should scroll)
|
||||
(should-not (plist-get scroll :content-lines-complete-p))
|
||||
(should-not (with-current-buffer buffer
|
||||
(string-match-p "Row 63" (buffer-string))))
|
||||
(should (= (hash-table-count regions) (hash-table-count ids)))
|
||||
(maphash
|
||||
(lambda (id _)
|
||||
(let ((box (gethash id regions)))
|
||||
(should box)
|
||||
(should (eq box (gethash (plist-get box :node-id) nodes)))))
|
||||
ids))))
|
||||
(should cache-hits))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-seed-preserves-nil-and-zero ()
|
||||
"Seeding reads the exact current source key, including cached nil or zero."
|
||||
(dolist (value '(nil 0))
|
||||
(let* ((source (list :node-id 1))
|
||||
(orphan (list :node-id 1))
|
||||
(candidate (list :node-id 1))
|
||||
(old-nodes (make-hash-table :test 'eql))
|
||||
(new-nodes (make-hash-table :test 'eql))
|
||||
(memo (make-hash-table :test 'eq))
|
||||
(missing (make-symbol "missing")))
|
||||
(puthash 1 source old-nodes)
|
||||
(puthash 1 candidate new-nodes)
|
||||
(puthash source value memo)
|
||||
(puthash orphan 'poison memo)
|
||||
(let ((seeded (ebox-incremental--seed-candidate-node-cache
|
||||
memo old-nodes new-nodes)))
|
||||
(should (= (hash-table-count seeded) 1))
|
||||
(should (eq (gethash candidate seeded missing) value))
|
||||
(should (eq (gethash orphan seeded missing) missing)))
|
||||
(remhash source memo)
|
||||
(should (= (hash-table-count
|
||||
(ebox-incremental--seed-candidate-node-cache
|
||||
memo old-nodes new-nodes)) 0)))))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-isolates-persistent-caches-only ()
|
||||
"Viewport cache isolation leaves node memo policy to the source-copy boundary."
|
||||
(let* ((signature (make-hash-table :test 'eq))
|
||||
(height (make-hash-table :test 'eq))
|
||||
(width (make-hash-table :test 'eq))
|
||||
(render (make-hash-table :test 'equal))
|
||||
(fragments (make-hash-table :test 'equal))
|
||||
(overrides (list :render-signature-cache signature
|
||||
:viewport-height-dependent-subtree-cache height
|
||||
:flex-content-min-widths width :render-cache render
|
||||
:layout-fragments fragments))
|
||||
(first (ebox-surface--isolated-viewport-overrides overrides))
|
||||
(second (ebox-surface--isolated-viewport-overrides overrides)))
|
||||
(dolist (key '(:render-cache :layout-fragments))
|
||||
(should-not (eq (plist-get first key) (plist-get overrides key)))
|
||||
(should-not (eq (plist-get second key) (plist-get first key))))
|
||||
(dolist (key ebox-node-memo-test--keys)
|
||||
(should (eq (plist-get first key) (plist-get overrides key)))
|
||||
(should (eq (plist-get second key) (plist-get overrides key))))))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-does-not-copy-historical-memo-keys ()
|
||||
"Thirty thousand same-ID orphans per memo do not enter viewport table copies."
|
||||
(ebox-node-memo-test--with-mounted
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(id (plist-get (plist-get state :root-node) :node-id))
|
||||
(memos (mapcar (lambda (key) (plist-get state key)) ebox-node-memo-test--keys))
|
||||
(initial (with-current-buffer buffer (buffer-string)))
|
||||
(copy-function (symbol-function 'copy-hash-table))
|
||||
copied-memo-sizes)
|
||||
(dolist (memo memos)
|
||||
(dotimes (index 30000)
|
||||
(puthash (list :node-id id :orphan index) 'poison memo)))
|
||||
(let ((sizes (mapcar #'hash-table-count memos)))
|
||||
(cl-letf (((symbol-function 'copy-hash-table)
|
||||
(lambda (table)
|
||||
(when (memq table memos)
|
||||
(push (hash-table-count table) copied-memo-sizes))
|
||||
(funcall copy-function table))))
|
||||
(ebox-rerender-buffer-with-context buffer 220 9)
|
||||
(ebox-rerender-buffer-with-context buffer 180 6))
|
||||
(should-not copied-memo-sizes)
|
||||
(should (equal sizes (mapcar #'hash-table-count memos))))
|
||||
(should (equal-including-properties
|
||||
initial (with-current-buffer buffer (buffer-string))))
|
||||
(dolist (entry (ebox-node-memo-test--counts (ebox--buffer-render-state buffer)))
|
||||
(should (= (plist-get (cdr entry) :stale) 0))
|
||||
(should (<= (plist-get (cdr entry) :total) 34))))))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-context-miss-starts-fresh ()
|
||||
"Display and stylesheet proof misses get fresh node memos before rendering."
|
||||
(dolist (change '(display style))
|
||||
(let ((ebox-style-stylesheet (ecss-stylesheet-create)))
|
||||
(ebox-style-add-rule "#body" '(:background-color "#ddeeff") :layer 'base)
|
||||
(ebox-node-memo-test--with-mounted
|
||||
(let ((render (symbol-function 'ebox-surface--render-candidate))
|
||||
(display (symbol-function 'ebox--display-signature-for-window))
|
||||
(seen nil))
|
||||
(when (eq change 'style)
|
||||
(ebox-style-add-rule "#body" '(:background-color "#112233") :layer 'base))
|
||||
(cl-letf (((symbol-function 'ebox--display-signature-for-window)
|
||||
(lambda (window)
|
||||
(if (eq change 'display) '(ebox-node-memo-display changed)
|
||||
(funcall display window))))
|
||||
((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (state)
|
||||
(push t seen)
|
||||
(dolist (key ebox-node-memo-test--keys)
|
||||
(should (= (hash-table-count (plist-get state key)) 0)))
|
||||
(funcall render state))))
|
||||
(ebox-rerender-buffer-with-context buffer 220 9))
|
||||
(should seen)
|
||||
(should-not (plist-get (ebox-buffer-update-report buffer) :projection-kind))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(ebox-viewport-width 220) (ebox-viewport-height 9)
|
||||
(fresh-input (ebox-canonical-input-create
|
||||
(list (plist-get state :root-node))
|
||||
(plist-get state :source-index))))
|
||||
(should (equal-including-properties
|
||||
(with-current-buffer buffer (buffer-string)) (ebox-render fresh-input)))
|
||||
(dolist (entry (ebox-node-memo-test--counts state))
|
||||
(should (= (plist-get (cdr entry) :stale) 0)))))))))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-failed-producers-own-private-tables ()
|
||||
"Late failures preserve source memo entries; every retry gets private tables."
|
||||
(ebox-node-memo-test--with-mounted
|
||||
(let* ((old (ebox--buffer-render-state buffer))
|
||||
(surface (plist-get old :surface))
|
||||
(revision (tp-surface-revision surface))
|
||||
(initial (with-current-buffer buffer (buffer-string)))
|
||||
(memos (mapcar (lambda (key) (plist-get old key)) ebox-node-memo-test--keys))
|
||||
(snapshots (mapcar (lambda (memo)
|
||||
(let ((copy (copy-hash-table memo)))
|
||||
(maphash (lambda (node value)
|
||||
(puthash node (copy-tree value t) copy)) memo)
|
||||
copy)) memos))
|
||||
(render (symbol-function 'ebox-surface--render-candidate))
|
||||
produced-tables)
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (state)
|
||||
(dolist (key ebox-node-memo-test--keys)
|
||||
(should (= (hash-table-count (plist-get state key)) 0)))
|
||||
(let ((tables (mapcar (lambda (key) (plist-get state key))
|
||||
ebox-node-memo-test--keys)))
|
||||
(dolist (previous (cons memos produced-tables))
|
||||
(cl-mapc (lambda (a b) (should-not (eq a b))) tables previous))
|
||||
(push tables produced-tables))
|
||||
(let ((output (funcall render state)))
|
||||
(dolist (entry (ebox-node-memo-test--counts state))
|
||||
(should (= (plist-get (cdr entry) :stale) 0)))
|
||||
output))))
|
||||
(dotimes (_ 2)
|
||||
(let ((tp--surface-publication-step-function
|
||||
(lambda (step _surface)
|
||||
(when (eq step 'client-state) (error "Reject memo candidate")))))
|
||||
(should-error (ebox-rerender-buffer-with-context buffer 220 9)))
|
||||
(should (eq old (ebox--buffer-render-state buffer)))
|
||||
(should (= revision (tp-surface-revision surface)))
|
||||
(should (equal-including-properties
|
||||
initial (with-current-buffer buffer (buffer-string)))))
|
||||
(ebox-rerender-buffer-with-context buffer 220 9))
|
||||
(should (= (length produced-tables) 3))
|
||||
(cl-mapc (lambda (memo before)
|
||||
(should (= (hash-table-count memo) (hash-table-count before)))
|
||||
(maphash (lambda (node value)
|
||||
(should (equal-including-properties value (gethash node memo))))
|
||||
before)) memos snapshots)
|
||||
(should (ebox-host-ref-position buffer (ebox-canonical-input-root-host-ref input))))))
|
||||
|
||||
(ert-deftest ebox-node-memo-generation-same-fallback-producer-owns-fresh-memos ()
|
||||
"Repeated evaluation of one fallback closure must not write captured memos."
|
||||
(ebox-node-memo-test--with-mounted
|
||||
(let* ((old (ebox--buffer-render-state buffer))
|
||||
(display '(ebox-node-memo-display fallback))
|
||||
;; Prepare once. A changed display requires the ordinary fallback.
|
||||
(prepared (ebox-incremental-prepare-viewport-commit
|
||||
buffer 220 9 'both display t))
|
||||
(overrides (plist-get prepared :state-overrides))
|
||||
(source-memos (mapcar (lambda (key) (plist-get overrides key))
|
||||
ebox-node-memo-test--keys))
|
||||
(sizes (mapcar #'hash-table-count source-memos))
|
||||
(root (plist-get prepared :root))
|
||||
;; Both public materializations below invoke this exact closure.
|
||||
(producer (ebox-surface-producer root old t overrides))
|
||||
(render (symbol-function 'ebox-surface--render-candidate))
|
||||
states history outputs first-memos first-snapshots)
|
||||
(should-not (plist-get prepared :projection-kind))
|
||||
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||
(lambda (state)
|
||||
(let ((output (funcall render state)))
|
||||
(push state states)
|
||||
(push (ebox-node-memo-test--counts state) history)
|
||||
(unless first-memos
|
||||
(setq first-memos
|
||||
(mapcar (lambda (key) (plist-get state key))
|
||||
ebox-node-memo-test--keys)
|
||||
first-snapshots
|
||||
(mapcar (lambda (memo)
|
||||
(let ((copy (copy-hash-table memo)))
|
||||
(maphash
|
||||
(lambda (node value)
|
||||
(puthash node (copy-tree value t) copy))
|
||||
memo)
|
||||
copy)) first-memos)))
|
||||
output))))
|
||||
(dotimes (_ 2)
|
||||
(push (tp-surface-materialize-string producer) outputs)))
|
||||
(should (= (length states) 2))
|
||||
(should-not (eq (plist-get (car states) :root-node)
|
||||
(plist-get (cadr states) :root-node)))
|
||||
(should (equal-including-properties (car outputs) (cadr outputs)))
|
||||
(let ((reference
|
||||
(tp-surface-materialize-string
|
||||
(ebox-surface-producer
|
||||
root old t
|
||||
(list :viewport-width 220 :viewport-height 9
|
||||
:display-signature display
|
||||
:source-base-index (plist-get old :source-index))))))
|
||||
(should (equal-including-properties (car outputs) reference)))
|
||||
(let* ((output (car outputs))
|
||||
(position (string-match "Open 00" output)))
|
||||
(should position)
|
||||
(should (eq (lookup-key (get-text-property position 'keymap output)
|
||||
[mouse-1]) #'ignore)))
|
||||
(ert-info ((format "same closure, captured sizes %S -> %S; history %S"
|
||||
sizes (mapcar #'hash-table-count source-memos) history))
|
||||
(should (equal sizes (mapcar #'hash-table-count source-memos)))
|
||||
(dolist (key ebox-node-memo-test--keys)
|
||||
(should-not (eq (plist-get (car states) key) (plist-get overrides key)))
|
||||
(should-not (eq (plist-get (car states) key) (plist-get (cadr states) key))))
|
||||
(cl-mapc
|
||||
(lambda (memo before)
|
||||
(should (= (hash-table-count memo) (hash-table-count before)))
|
||||
(maphash (lambda (node value)
|
||||
(should (equal-including-properties value (gethash node memo))))
|
||||
before)) first-memos first-snapshots)
|
||||
(dolist (generation history)
|
||||
(dolist (entry generation)
|
||||
(should (= (plist-get (cdr entry) :stale) 0))))))))
|
||||
|
||||
(provide 'ebox-node-memo-generation-tests)
|
||||
;;; ebox-node-memo-generation-tests.el ends here
|
||||
Loading…
Reference in New Issue
Block a user