From 5143d17a561de851bcbdc49f10278ba8a46b4e46 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Sun, 6 Sep 2026 15:30:18 +0800 Subject: [PATCH] fix: scope node memos to copied runtime generations --- Makefile | 7 +- ebox-incremental.el | 10 +- ebox-surface.el | 22 +- tests/ebox-node-memo-generation-tests.el | 442 +++++++++++++++++++++++ 4 files changed, 471 insertions(+), 10 deletions(-) create mode 100644 tests/ebox-node-memo-generation-tests.el diff --git a/Makefile b/Makefile index 6264a1a..dd09c00 100644 --- a/Makefile +++ b/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 diff --git a/ebox-incremental.el b/ebox-incremental.el index 17ba131..ee8fa6d 100644 --- a/ebox-incremental.el +++ b/ebox-incremental.el @@ -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 diff --git a/ebox-surface.el b/ebox-surface.el index bb97bae..595088f 100644 --- a/ebox-surface.el +++ b/ebox-surface.el @@ -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)))) diff --git a/tests/ebox-node-memo-generation-tests.el b/tests/ebox-node-memo-generation-tests.el new file mode 100644 index 0000000..2933584 --- /dev/null +++ b/tests/ebox-node-memo-generation-tests.el @@ -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