;;; etaf-gui-verifier-tests.el --- Generic GUI scenario engine tests -*- lexical-binding: t; -*- ;;; Code: (require 'ert) (require 'cl-lib) (require 'json) (require 'emacs-gui-verifier) (ert-deftest etaf-gui-verifier-measures-success-and-original-failure () "Each callback retains its own timing, result, and error condition." (let* ((scenario (etaf-gui-verifier-scenario-create :name "measurement")) (context (etaf-gui-verifier--context-create :scenario scenario)) (result (list 'exact-result)) (error-data (list "original failure" result)) (wall-times '(1.0 1.002 10.0 10.03)) (cpu-times '((1000 . 1000) (1004 . 1000) (2000 . 1000) (2010 . 1000))) (gcs-done 5) (gc-elapsed 0.1) (calls 0)) (cl-letf (((symbol-function 'float-time) (lambda (&optional _time) (pop wall-times))) ((symbol-function 'current-cpu-time) (lambda () (pop cpu-times)))) (should (eq result (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id "success" :execute (lambda (current) (should (eq current context)) (cl-incf calls) result))))) (should (equal (cons 'error error-data) (should-error (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id "failure" :execute (lambda (_current) (cl-incf calls) (cl-incf gcs-done 2) (cl-incf gc-elapsed 0.003) (signal 'error error-data)))))))) (should (= calls 2)) (let* ((measurements (etaf-gui-verifier-context-measurements context)) (failed (car measurements)) (succeeded (cadr measurements)) (adapter (etaf-gui-verifier--adapter-data context))) (should (= (length measurements) 2)) (should (equal (alist-get 'status succeeded) "success")) (should (equal (alist-get 'status failed) "error")) (should (equal (alist-get 'action_id failed) "failure")) (should-not (alist-get 'valid failed)) (should (< (abs (- (alist-get 'wall_ms failed) 30.0)) 0.001)) (should (< (abs (- (alist-get 'wall_ms succeeded) 2.0)) 0.001)) (should (< (abs (- (alist-get 'cpu_ms failed) 10.0)) 0.001)) (should (= (alist-get 'gc_count failed) 2)) (should (< (abs (- (alist-get 'gc_ms failed) 3.0)) 0.001)) (should (= (alist-get 'duration_ms adapter) (alist-get 'wall_ms failed))) (should (eq (alist-get 'last_measurement adapter) failed)) (should-not (assq 'measurements adapter))))) (ert-deftest etaf-gui-verifier-labels-callback-context-without-changing-it () "Foreground, background, and unknown callbacks keep their execution context." (let* ((focus t) (calls 0) (inhibit-quit t) (inhibit-redisplay t) (threshold gc-cons-threshold) (percentage gc-cons-percentage) (context (etaf-gui-verifier--context-create :target-buffer (window-buffer (selected-window))))) (cl-letf (((symbol-function 'display-graphic-p) (lambda (&optional _frame) t)) ((symbol-function 'frame-focus-state) (lambda (&optional _frame) focus)) ((symbol-function 'redisplay) (lambda (&rest _arguments) (ert-fail "Forced redisplay"))) ((symbol-function 'sit-for) (lambda (&rest _arguments) (ert-fail "Waited for display"))) ((symbol-function 'garbage-collect) (lambda () (ert-fail "Forced GC")))) (dolist (state '((t "foreground") (nil "background") (unknown "unknown"))) (setq focus (car state)) (should (eq 'exact-result (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id "context" :execute (lambda (_context) (cl-incf calls) (should inhibit-quit) (should inhibit-redisplay) (should (= gc-cons-threshold threshold)) (should (= gc-cons-percentage percentage)) 'exact-result)) (eq focus t)))) (let* ((measurement (car (etaf-gui-verifier-context-measurements context))) (before (alist-get 'before measurement))) (should (equal (alist-get 'foreground measurement) (cadr state))) (should (equal (alist-get 'status measurement) "success")) (should (alist-get 'valid measurement)) (should (alist-get 'inhibit_quit before)) (should (alist-get 'inhibit_redisplay before)) (should (eq (alist-get 'noninteractive before) noninteractive)) (should (stringp (json-serialize measurement)))))) (should (= calls 3)))) (ert-deftest etaf-gui-verifier-unselected-target-runs-with-invalid-timing () "An unchanged unselected target permits the callback but invalidates timing." (let* ((target (generate-new-buffer " *etaf-gui-unselected-target*")) (context (etaf-gui-verifier--context-create :target-buffer target)) (result (list 'exact-result)) (calls 0)) (unwind-protect (cl-letf (((symbol-function 'display-graphic-p) (lambda (&optional _frame) t)) ((symbol-function 'frame-focus-state) (lambda (&optional _frame) nil))) (should (eq result (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id "unselected-target" :execute (lambda (_context) (cl-incf calls) result))))) (should (= calls 1)) (let ((measurement (car (etaf-gui-verifier-context-measurements context)))) (should (equal (alist-get 'status measurement) "success")) (should (equal (alist-get 'foreground measurement) "background")) (should (equal (alist-get 'before measurement) (alist-get 'after measurement))) (should-not (alist-get 'target_selected (alist-get 'before measurement))) (should-not (alist-get 'valid measurement)) (should (numberp (alist-get 'wall_ms measurement))))) (kill-buffer target)))) (ert-deftest etaf-gui-verifier-foreground-rejection-does-not-execute-or-focus () "Foreground requirements reject absent focus or targets before execution." (let ((focus t) (graphic t) (calls 0) (context (etaf-gui-verifier--context-create))) (cl-letf (((symbol-function 'display-graphic-p) (lambda (&optional _frame) graphic)) ((symbol-function 'frame-focus-state) (lambda (&optional _frame) focus)) ((symbol-function 'raise-frame) (lambda (&rest _arguments) (ert-fail "Raised frame"))) ((symbol-function 'select-frame-set-input-focus) (lambda (&rest _arguments) (ert-fail "Changed focus"))) ((symbol-function 'x-focus-frame) (lambda (&rest _arguments) (ert-fail "Focused frame")))) (dolist (state '((nil t t) (unknown t t) (t nil t) (t t nil))) (setq focus (nth 0 state) graphic (nth 1 state)) (setf (etaf-gui-verifier-context-target-buffer context) (and (nth 2 state) (window-buffer (selected-window)))) (should-error (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id "foreground-only" :execute (lambda (_context) (cl-incf calls))) t)) (let ((measurement (car (etaf-gui-verifier-context-measurements context)))) (should (equal (alist-get 'status measurement) "rejected")) (should-not (alist-get 'valid measurement)) (should-not (alist-get 'wall_ms measurement))))) (should (zerop calls)))) (ert-deftest etaf-gui-verifier-retains-valid-timing-when-frame-title-changes () "A GC count in the frame title must not invalidate a stable target." (let* ((gcs-done 713) (calls 0) (result (list 'exact-result)) (frame-parameter-function (symbol-function 'frame-parameter)) (context (etaf-gui-verifier--context-create :target-buffer (window-buffer (selected-window))))) (cl-letf (((symbol-function 'display-graphic-p) (lambda (&optional _frame) t)) ((symbol-function 'frame-focus-state) (lambda (&optional _frame) t)) ((symbol-function 'frame-parameter) (lambda (frame parameter) (if (eq parameter 'name) (format "GC%d" gcs-done) (funcall frame-parameter-function frame parameter))))) (should (eq result (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id "title-update" :execute (lambda (_context) (cl-incf calls) (cl-incf gcs-done) result)) t)))) (should (= calls 1)) (let ((measurement (car (etaf-gui-verifier-context-measurements context)))) (should (equal (alist-get 'status measurement) "success")) (should (equal (alist-get 'foreground measurement) "foreground")) (should (alist-get 'valid measurement)) (should (= (alist-get 'gc_count measurement) 1)) (should (equal (alist-get 'frame_name (alist-get 'before measurement)) "GC713")) (should (equal (alist-get 'frame_name (alist-get 'after measurement)) "GC714"))))) (ert-deftest etaf-gui-verifier-invalidates-replaced-selected-target () "A different live selected buffer remains an identity change." (let ((target (generate-new-buffer " *etaf-gui-original-target*")) (replacement (generate-new-buffer " *etaf-gui-replacement-target*")) (calls 0)) (unwind-protect (save-window-excursion (switch-to-buffer target) (let ((context (etaf-gui-verifier--context-create :target-buffer target))) (cl-letf (((symbol-function 'display-graphic-p) (lambda (&optional _frame) t)) ((symbol-function 'frame-focus-state) (lambda (&optional _frame) t))) (should-error (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id "replace-selected-target" :execute (lambda (_context) (cl-incf calls) (setf (etaf-gui-verifier-context-target-buffer context) replacement) (set-window-buffer (selected-window) replacement))) t))) (should (= calls 1)) (let ((measurement (car (etaf-gui-verifier-context-measurements context)))) (should (alist-get 'target_selected (alist-get 'before measurement))) (should (alist-get 'target_selected (alist-get 'after measurement))) (should (equal (alist-get 'status measurement) "invalid")) (should-not (alist-get 'valid measurement))))) (kill-buffer target) (kill-buffer replacement)))) (ert-deftest etaf-gui-verifier-invalidates-changed-foreground-environment () "Changed focus, target, font, or dimensions invalidate strict timing." (let ((focus t) (width 800) (font "original-font") (calls 0) (frame-parameter-function (symbol-function 'frame-parameter)) (context (etaf-gui-verifier--context-create))) (cl-letf (((symbol-function 'display-graphic-p) (lambda (&optional _frame) t)) ((symbol-function 'frame-focus-state) (lambda (&optional _frame) focus)) ((symbol-function 'frame-pixel-width) (lambda (&optional _frame) width)) ((symbol-function 'frame-parameter) (lambda (frame parameter) (if (eq parameter 'font) font (funcall frame-parameter-function frame parameter))))) (dolist (change '(focus target font dimensions)) (setq focus t width 800 font "original-font") (setf (etaf-gui-verifier-context-target-buffer context) (window-buffer (selected-window))) (should-error (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id (symbol-name change) :execute (lambda (_context) (cl-incf calls) (pcase change ('focus (setq focus nil)) ('target (setf (etaf-gui-verifier-context-target-buffer context) nil)) ('font (setq font "changed-font")) ('dimensions (setq width 900))))) t)) (let ((measurement (car (etaf-gui-verifier-context-measurements context)))) (should (equal (alist-get 'status measurement) "invalid")) (should (equal (alist-get 'foreground measurement) "changed")) (should-not (alist-get 'valid measurement)) (should (numberp (alist-get 'wall_ms measurement)))))) (should (= calls 4)))) (ert-deftest etaf-gui-verifier-preserves-quit-despite-focus-change () "A quit keeps its exact data even if strict environment checks also fail." (let* ((focus t) (quit-data (list 'original-quit)) (context (etaf-gui-verifier--context-create :target-buffer (window-buffer (selected-window))))) (cl-letf (((symbol-function 'display-graphic-p) (lambda (&optional _frame) t)) ((symbol-function 'frame-focus-state) (lambda (&optional _frame) focus))) (should (equal (cons 'quit quit-data) (condition-case condition (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id "quit" :execute (lambda (_context) (setq focus nil) (signal 'quit quit-data))) t) (quit condition))))) (let ((measurement (car (etaf-gui-verifier-context-measurements context)))) (should (equal (alist-get 'status measurement) "quit")) (should (equal (alist-get 'foreground measurement) "changed")) (should-not (alist-get 'valid measurement)) (should (numberp (alist-get 'wall_ms measurement)))))) (ert-deftest etaf-gui-verifier-retains-timing-despite-closing-snapshot-failure () "Snapshot errors and quits preserve action failures and retain fresh timing." (dolist (action-outcome '(error quit success)) (dolist (snapshot-outcome '(error quit)) (let* ((context (etaf-gui-verifier--context-create :target-buffer (window-buffer (selected-window)) :last-duration-ms 999.0)) (before (etaf-gui-verifier--measurement-state context)) (result (list 'exact-result)) (action-data (list "original-action-failure" result)) (snapshot-data (list "snapshot-interrupted" result)) (wall-times '(1.0 1.025)) (calls 0) (snapshots 0) observed-condition returned) (cl-letf (((symbol-function 'float-time) (lambda (&optional _time) (pop wall-times))) ((symbol-function 'etaf-gui-verifier--measurement-state) (lambda (_context) (if (= (cl-incf snapshots) 1) before (signal snapshot-outcome snapshot-data))))) (setq returned (condition-case condition (etaf-gui-verifier-measure-action context (etaf-gui-verifier-action-create :id "snapshot-failure" :execute (lambda (_context) (cl-incf calls) (if (eq action-outcome 'success) result (signal action-outcome action-data))))) ((error quit) (setq observed-condition condition) nil)))) (should (= calls 1)) (should (= snapshots 2)) (cond ((not (eq action-outcome 'success)) (should (eq (car observed-condition) action-outcome)) (should (eq (cdr observed-condition) action-data))) ((eq snapshot-outcome 'quit) (should (eq (car observed-condition) 'quit)) (should (eq (cdr observed-condition) snapshot-data))) (t (should-not observed-condition) (should (eq returned result)))) (let* ((measurements (etaf-gui-verifier-context-measurements context)) (measurement (car measurements))) (should (= (length measurements) 1)) (should-not (alist-get 'valid measurement)) (should-not (alist-get 'after measurement)) (should (equal (alist-get 'status measurement) (symbol-name (if (and (eq action-outcome 'success) (eq snapshot-outcome 'quit)) 'quit action-outcome)))) (should (< (abs (- (alist-get 'wall_ms measurement) 25.0)) 0.001)) (should (= (etaf-gui-verifier-context-last-duration-ms context) (alist-get 'wall_ms measurement)))))))) (ert-deftest etaf-gui-verifier-measures-callback-without-settle-or-checkpoints () "The action callback is timed separately from checkpoints and settling." (let ((context (etaf-gui-verifier--context-create)) (clock 0.0) events) (cl-letf (((symbol-function 'float-time) (lambda (&optional _time) clock)) ((symbol-function 'etaf-gui-verifier--checkpoint) (lambda (_context _id phase _screenshot &optional _extra) (cl-incf clock 1.0) (push phase events))) ((symbol-function 'etaf-gui-verifier--settle-action) (lambda (_context _action) (cl-incf clock 2.0) (push 'settle events)))) (etaf-gui-verifier--run-action context (etaf-gui-verifier-action-create :id "callback-only" :execute (lambda (_context) (cl-incf clock 0.025) (push 'execute events))))) (should (< (abs (- (etaf-gui-verifier-context-last-duration-ms context) 25.0)) 0.001)) (should (equal (nreverse events) '("before-action" execute "after-action" settle "after-redisplay"))) (let ((measurement (car (etaf-gui-verifier-context-measurements context)))) (should (equal (alist-get 'boundary measurement) "action.execute callback")) (should-not (alist-get 'presentation_measured measurement))))) (ert-deftest etaf-gui-verifier-runs-and-settles-without-raising-frame () "Actions and settled checkpoints must complete without raising Emacs." (let ((context (etaf-gui-verifier--context-create)) (settle-checks 0) events) (cl-letf (((symbol-function 'raise-frame) (lambda (&rest _arguments) (error "GUI verifier must preserve application focus"))) ((symbol-function 'etaf-gui-verifier--checkpoint) (lambda (_context _action-id phase _screenshot &optional _extra) (push phase events)))) (etaf-gui-verifier--run-action context (etaf-gui-verifier-action-create :id "background-action" :settle-interval 0.001 :execute (lambda (current) (etaf-gui-verifier-context-put current 'executed t) (push 'execute events)) :settled-p (lambda (current) (should (etaf-gui-verifier-context-get current 'executed)) (cl-incf settle-checks) (push 'settle events) t) :assertions (lambda (_current) (push 'assertions events) (list (etaf-gui-verifier-assert "action-settled" t)))))) (should (= 1 (etaf-gui-verifier-context-action-count context))) (should (= 2 settle-checks)) (should (equal '("before-action" execute "after-action" settle settle assertions "after-redisplay") (nreverse events))))) (ert-deftest etaf-gui-verifier-composes-generic-scenario-actions () "The engine should own ordering while adapters own actions and assertions." (let ((buffer (generate-new-buffer " *etaf-gui-verifier-test*")) events checkpoints finished (settle-checks 0)) (unwind-protect (cl-letf (((symbol-function 'emacs-dynamic-ui-verification-start) (lambda (_directory run-id _claim phases) (push (list 'start run-id phases) events))) ((symbol-function 'emacs-dynamic-ui-verification-checkpoint) (lambda (action phase adapter assertions screenshot) (push (list action phase adapter assertions screenshot) checkpoints))) ((symbol-function 'emacs-dynamic-ui-verification-finish) (lambda (completed adapter) (setq finished (list completed adapter))))) (let* ((scenario (etaf-gui-verifier-scenario-create :name "generic" :claim "generic claim" :initialize (lambda (context) (etaf-gui-verifier-context-select-buffer context buffer)) :invariants (lambda (context) (list (etaf-gui-verifier-assert "buffer-live" (buffer-live-p (etaf-gui-verifier-context-target-buffer context))))) :adapter (lambda (context) (list (cons 'size (with-current-buffer (etaf-gui-verifier-context-target-buffer context) (buffer-size))))) :actions (list (etaf-gui-verifier-action-create :id "insert-a" :settle-interval 0.001 :execute (lambda (context) (with-current-buffer (etaf-gui-verifier-context-target-buffer context) (insert "A"))) :settled-p (lambda (_context) (>= (cl-incf settle-checks) 2)) :assertions (lambda (context) (list (etaf-gui-verifier-assert "one-character" (= 1 (with-current-buffer (etaf-gui-verifier-context-target-buffer context) (buffer-size)))))) :screenshot t) (etaf-gui-verifier-action-create :id "insert-b" :execute (lambda (context) (with-current-buffer (etaf-gui-verifier-context-target-buffer context) (insert "B"))) :settled-p (lambda (_context) t))) :completion (lambda (context) (= 2 (etaf-gui-verifier-context-action-count context))))) (context (etaf-gui-verifier-run scenario "/tmp/generic"))) (should (= 2 (etaf-gui-verifier-context-action-count context))) (should (equal "AB" (with-current-buffer buffer (buffer-string)))) (should (car finished)) (should (= settle-checks 3)) (should (= 6 (length checkpoints))) (should (equal '("before-action" "after-action" "after-redisplay" "before-action" "after-action" "after-redisplay") (mapcar #'cadr (nreverse checkpoints)))))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (ert-deftest etaf-gui-verifier-rejects-invalid-settle-contracts () "Every action should have a bounded, non-busy settle contract." (let ((context (etaf-gui-verifier--context-create))) (dolist (action (list (etaf-gui-verifier-action-create :id "missing") (etaf-gui-verifier-action-create :id "timeout" :settled-p (lambda (_context) t) :settle-timeout 0) (etaf-gui-verifier-action-create :id "interval" :settled-p (lambda (_context) t) :settle-interval 0))) (should-error (etaf-gui-verifier--settle-action context action))))) (ert-deftest etaf-gui-verifier-timeout-finishes-false-and-propagates () "A settle timeout should finish incomplete and preserve its error." (let ((buffer (generate-new-buffer " *etaf-gui-timeout-test*")) finished) (unwind-protect (cl-letf (((symbol-function 'emacs-dynamic-ui-verification-start) (lambda (&rest _arguments) nil)) ((symbol-function 'emacs-dynamic-ui-verification-checkpoint) (lambda (&rest _arguments) nil)) ((symbol-function 'emacs-dynamic-ui-verification-finish) (lambda (completed adapter) (setq finished (list completed adapter))))) (let* ((scenario (etaf-gui-verifier-scenario-create :name "timeout" :claim "timeout must fail closed" :initialize (lambda (context) (etaf-gui-verifier-context-select-buffer context buffer)) :actions (list (etaf-gui-verifier-action-create :id "never-settles" :execute (lambda (_context) nil) :settled-p (lambda (_context) nil) :settle-timeout 0.003 :settle-interval 0.001)))) (error-data (should-error (etaf-gui-verifier-run scenario "/tmp/timeout")))) (should (string-match-p "did not settle" (error-message-string error-data))) (should finished) (should-not (car finished)))) (when (buffer-live-p buffer) (kill-buffer buffer))))) (provide 'etaf-gui-verifier-tests) ;;; etaf-gui-verifier-tests.el ends here