From 77d69634bf4029e522d8a09e3a417f21dbd65f4d Mon Sep 17 00:00:00 2001 From: cashmere Date: Sat, 12 Sep 2026 14:21:17 +0200 Subject: relic: 2026-09-12T14:21:23+02:00 --- test/fossil-ui-test.el | 345 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 272 insertions(+), 73 deletions(-) (limited to 'test') diff --git a/test/fossil-ui-test.el b/test/fossil-ui-test.el index 081a701..80fd5c0 100644 --- a/test/fossil-ui-test.el +++ b/test/fossil-ui-test.el @@ -15,12 +15,18 @@ (make-directory (file-name-directory file) t) (with-temp-file file (insert contents))) +(defun fossil-ui-test--write-bytes (file bytes) + "Write exact unibyte BYTES to FILE." + (make-directory (file-name-directory file) t) + (fossil-ui--write-bytes file bytes)) + (defmacro fossil-ui-test--with-checkout (&rest body) "Create a temporary Fossil checkout and evaluate BODY there." (declare (indent 0) (debug t)) `(let* ((base (make-temp-file "fossil-ui-test-" t)) (repo (expand-file-name "project.fossil" base)) - (root (file-name-as-directory (expand-file-name "checkout" base)))) + (root (file-name-as-directory (expand-file-name "checkout" base))) + (fossil-ui-stage-directory (expand-file-name "stage/" base))) (unwind-protect (progn (make-directory root) @@ -35,6 +41,15 @@ ,@body) (delete-directory base t)))) +(defun fossil-ui-test--cat-bytes (root path) + "Return committed PATH from ROOT as exact bytes." + (with-temp-buffer + (set-buffer-multibyte nil) + (let ((default-directory root) + (coding-system-for-read 'no-conversion)) + (should (zerop (process-file fossil-ui-program nil t nil "cat" "-r" "current" "--" path)))) + (buffer-string))) + (ert-deftest fossil-ui-parse-change-line-preserves-spaces () (should (equal (fossil-ui--parse-change-line "EDITED \"dir/file name.el\"") @@ -203,18 +218,224 @@ :key (lambda (change) (plist-get change :path)) :test #'equal))))) -(ert-deftest fossil-ui-diff-produces-navigable-hunks () +(ert-deftest fossil-ui-staged-and-unstaged-inline-sections-are-independent () + (fossil-ui-test--with-checkout + (fossil-ui-test--write (expand-file-name "tracked.txt" root) "staged version\n") + (let ((buffer (save-window-excursion (fossil-ui-status root)))) + (unwind-protect + (with-current-buffer buffer + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-stage) + (fossil-ui-test--write (expand-file-name "tracked.txt" root) "working version\n") + (fossil-ui-refresh) + (fossil-ui--goto-path "tracked.txt" t) + (fossil-ui-diff) + (fossil-ui--goto-path "tracked.txt" nil) + (fossil-ui-diff) + (should (member '(t "tracked.txt") (plist-get textui-state :expanded-diffs))) + (should (member '(nil "tracked.txt") (plist-get textui-state :expanded-diffs))) + (goto-char (point-min)) + (should (text-property-search-forward 'fossil-ui-location '(file t "tracked.txt") #'equal)) + (should (text-property-search-forward 'fossil-ui-location '(file nil "tracked.txt") #'equal))) + (when (buffer-live-p buffer) (kill-buffer buffer)))))) + +(ert-deftest fossil-ui-inline-hunk-stage-and-stale-diff-guard () + (fossil-ui-test--with-checkout + (fossil-ui-test--write (expand-file-name "tracked.txt" root) "changed\n") + (let ((buffer (save-window-excursion (fossil-ui-status root)))) + (unwind-protect + (with-current-buffer buffer + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-diff) + (goto-char (point-min)) + (let ((hunk (text-property-search-forward 'fossil-ui-hunk-header t #'eq))) + (should hunk) + (goto-char (prop-match-beginning hunk)) + (fossil-ui-test--write (expand-file-name "tracked.txt" root) "changed again\n") + (should-error (fossil-ui-stage) :type 'user-error) + (fossil-ui-refresh) + (goto-char (point-min)) + (setq hunk (text-property-search-forward 'fossil-ui-hunk-header t #'eq)) + (goto-char (prop-match-beginning hunk)) + (fossil-ui-stage) + (let* ((entry (car (plist-get (fossil-ui--index root) :entries)))) + (should (equal (plist-get entry :staged) "changed again\n"))))) + (when (buffer-live-p buffer) (kill-buffer buffer)))))) + +(defun fossil-ui-test--select-replacement-lines () + "Activate a region spanning the first removed and added inline lines." + (goto-char (point-min)) + (let* ((removed (or (text-property-search-forward 'fossil-ui-line-kind ?- #'eq) + (ert-fail "No removed line"))) + (begin (prop-match-beginning removed)) + (added (or (text-property-search-forward 'fossil-ui-line-kind ?+ #'eq) + (ert-fail "No added line"))) + (end (prop-match-end added))) + (goto-char (max begin (1- end))) + (set-mark begin) + (activate-mark))) + +(ert-deftest fossil-ui-visual-lines-stage-unstage-and-discard () + (fossil-ui-test--with-checkout + (fossil-ui-test--write (expand-file-name "tracked.txt" root) "replacement\n") + (let ((buffer (save-window-excursion (fossil-ui-status root))) + (fossil-ui-confirm-revert nil)) + (unwind-protect + (with-current-buffer buffer + (setq-local transient-mark-mode t) + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-diff) + (fossil-ui-test--select-replacement-lines) + (fossil-ui-stage) + (should (equal (plist-get (car (plist-get (fossil-ui--index root) :entries)) :staged) "replacement\n")) + (fossil-ui--goto-path "tracked.txt" t) + (fossil-ui-diff) + (fossil-ui-test--select-replacement-lines) + (fossil-ui-unstage) + (should-not (plist-get (fossil-ui--index root) :entries)) + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-diff) + (fossil-ui-test--select-replacement-lines) + (fossil-ui-discard) + (should (equal (fossil-ui--text-file (expand-file-name "tracked.txt" root)) "initial\n"))) + (when (buffer-live-p buffer) (kill-buffer buffer)))))) + +(ert-deftest fossil-ui-ret-from-inline-line-visits-correct-source-line () + (fossil-ui-test--with-checkout + (fossil-ui-test--write (expand-file-name "tracked.txt" root) "initial\nsecond line\n") + (let ((buffer (save-window-excursion (fossil-ui-status root))) + visited) + (unwind-protect + (with-current-buffer buffer + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-diff) + (goto-char (point-min)) + (let ((line (text-property-search-forward 'fossil-ui-line-kind ?+ #'eq))) + (should line) + (goto-char (prop-match-beginning line)) + (should (= (fossil-ui--property-at-line 'fossil-ui-target-line) 2)) + (cl-letf (((symbol-function 'find-file) + (lambda (file) + (setq visited (find-file-noselect file)) + (set-buffer visited)))) + (fossil-ui-visit-file) + (should (= (line-number-at-pos) 2))))) + (when (buffer-live-p visited) (kill-buffer visited)) + (when (buffer-live-p buffer) (kill-buffer buffer)))))) + +(ert-deftest fossil-ui-invalid-utf8-whole-file-stage-cleans-snapshot-on-unstage () + (fossil-ui-test--with-checkout + (let ((bytes (concat (string-make-unibyte "age-encryption.org/v1\n") (unibyte-string #xff #x80) "\n"))) + (fossil-ui-test--write-bytes (expand-file-name "tracked.txt" root) bytes) + (let ((buffer (save-window-excursion (fossil-ui-status root)))) + (unwind-protect + (with-current-buffer buffer + (let ((change (cl-find "tracked.txt" (plist-get textui-state :changes) + :key (lambda (candidate) (plist-get candidate :path)) :test #'equal))) + (should (plist-get change :binary))) + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-diff) + (should (string-search "Binary files differ" (buffer-substring-no-properties (point-min) (point-max)))) + (goto-char (point-min)) + (should-not (text-property-search-forward 'fossil-ui-hunk-header t #'eq)) + (goto-char (point-min)) + (let ((binary-line (text-property-search-forward 'fossil-ui-diff-key '(nil "tracked.txt") #'equal))) + (should binary-line) + (goto-char (prop-match-beginning binary-line))) + (fossil-ui-stage) + (let* ((entry (car (plist-get (fossil-ui--index root) :entries))) + (snapshot (plist-get entry :snapshot))) + (should (plist-get entry :binary)) + (should (equal (fossil-ui--read-bytes snapshot) bytes)) + (should (= (logand (file-modes snapshot) #o777) #o600)) + (fossil-ui--goto-path "tracked.txt" t) + (fossil-ui-unstage) + (should-not (file-exists-p snapshot)) + (should-not (plist-get (fossil-ui--index root) :entries)) + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-stage) + (let* ((restaged (car (plist-get (fossil-ui--index root) :entries))) + (restaged-snapshot (plist-get restaged :snapshot))) + (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))) + (fossil-ui-clear-stage)) + (should-not (file-exists-p restaged-snapshot)) + (should-not (plist-get (fossil-ui--index root) :entries))))) + (when (buffer-live-p buffer) (kill-buffer buffer))))))) + +(ert-deftest fossil-ui-encoding-glob-allows-invalid-utf8-warning-override () + (fossil-ui-test--with-checkout + (fossil-ui-test--call root "settings" "encoding-glob" "*.txt") + (fossil-ui-test--write-bytes (expand-file-name "tracked.txt" root) (unibyte-string #xff #xfe)) + (should (equal (fossil-ui--commit-warning-arguments root '("tracked.txt")) '("--no-warnings"))))) + +(ert-deftest fossil-ui-configured-invalid-utf8-commit-is-byte-exact () + (fossil-ui-test--with-checkout + (fossil-ui-test--call root "settings" "binary-glob" "*.txt") + (let* ((staged (concat (string-make-unibyte "age-v1\n") (unibyte-string #xff #xfe) "\n")) + (working (concat (string-make-unibyte "age-v2\n") (unibyte-string #x80 #x81) "\n"))) + (fossil-ui-test--write-bytes (expand-file-name "tracked.txt" root) staged) + (let ((buffer (save-window-excursion (fossil-ui-status root)))) + (unwind-protect + (with-current-buffer buffer + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-stage) + (let* ((entry (car (plist-get (fossil-ui--index root) :entries))) + (snapshot (plist-get entry :snapshot))) + (should (equal (fossil-ui--commit-warning-arguments root '("tracked.txt") (list entry)) '("--no-warnings"))) + (fossil-ui-test--write-bytes (expand-file-name "tracked.txt" root) working) + (fossil-ui--commit-staged root "binary update") + (should (equal (fossil-ui-test--cat-bytes root "tracked.txt") staged)) + (should (equal (fossil-ui--read-bytes (expand-file-name "tracked.txt" root)) working)) + (should-not (file-exists-p snapshot)) + (should-not (plist-get (fossil-ui--index root) :entries)))) + (when (buffer-live-p buffer) (kill-buffer buffer))))))) + +(ert-deftest fossil-ui-unconfigured-invalid-utf8-commit-is-rejected () + (fossil-ui-test--with-checkout + (let ((bytes (concat (string-make-unibyte "invalid\n") (unibyte-string #xff) "\n"))) + (fossil-ui-test--write-bytes (expand-file-name "tracked.txt" root) bytes) + (let ((buffer (save-window-excursion (fossil-ui-status root)))) + (unwind-protect + (with-current-buffer buffer + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-stage) + (let ((error (should-error (fossil-ui--commit-staged root "must fail") :type 'user-error))) + (should (string-match-p "binary-glob" (error-message-string error))) + (should (plist-get (fossil-ui--index root) :entries)))) + (when (buffer-live-p buffer) (kill-buffer buffer))))))) + +(ert-deftest fossil-ui-warning-override-requires-every-invalid-file-to-be-configured () + (fossil-ui-test--with-checkout + (fossil-ui-test--call root "settings" "binary-glob" "tracked.txt") + (fossil-ui-test--write-bytes (expand-file-name "tracked.txt" root) (unibyte-string #xff)) + (fossil-ui-test--write-bytes (expand-file-name "other.bin" root) (unibyte-string #xfe)) + (let ((error (should-error (fossil-ui--commit-warning-arguments root '("tracked.txt" "other.bin")) :type 'user-error))) + (should (string-match-p "other.bin" (error-message-string error)))))) + +(ert-deftest fossil-ui-inline-diff-starts-collapsed-and-folds-file-and-hunk () (fossil-ui-test--with-checkout (fossil-ui-test--write (expand-file-name "tracked.txt" root) "first changed line\nsecond line\n") - (let ((diff (fossil-ui-test--call - root "diff" "--internal" "--unified" "--" "tracked.txt"))) - (should (string-match-p "^@@ " diff)) - (with-temp-buffer - (fossil-ui-diff-mode) - (should (eq (key-binding (kbd "]c")) #'diff-hunk-next)) - (should (eq (key-binding (kbd "[c")) #'diff-hunk-prev)) - (should (eq (key-binding (kbd "TAB")) #'outline-toggle-children)))))) + (let ((buffer (save-window-excursion (fossil-ui-status root)))) + (unwind-protect + (with-current-buffer buffer + (should-not (plist-get textui-state :expanded-diffs)) + (should-not (text-property-search-forward 'fossil-ui-hunk-header t #'eq)) + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-diff) + (should (member '(nil "tracked.txt") (plist-get textui-state :expanded-diffs))) + (goto-char (point-min)) + (let ((hunk (text-property-search-forward 'fossil-ui-hunk-header t #'eq))) + (should hunk) + (goto-char (prop-match-beginning hunk)) + (let ((id (fossil-ui--property-at-line 'fossil-ui-hunk-id))) + (should id) + (fossil-ui-diff) + (should (member id (plist-get textui-state :collapsed-hunks))) + (goto-char (point-min)) + (should-not (text-property-search-forward 'fossil-ui-line-kind ?+ #'eq)) + (should (text-property-search-forward 'fossil-ui-hunk-header t #'eq))))) + (when (buffer-live-p buffer) (kill-buffer buffer)))))) (ert-deftest fossil-ui-delta-render-keeps-hunks-and-emacs-faces () (skip-unless (executable-find fossil-ui-delta-program)) @@ -254,38 +475,26 @@ (let ((fossil-ui-delta-arguments arguments)) (should (eq (fossil-ui--effective-delta-arguments) arguments)))))) -(ert-deftest fossil-ui-theme-change-rerenders-open-diff-in-place () +(ert-deftest fossil-ui-theme-change-rerenders-inline-diff-in-place () (save-window-excursion - (with-temp-buffer - (let* ((buffer (current-buffer)) - (raw (mapconcat (lambda (number) (format "line %d" number)) (number-sequence 1 80) "\n")) - (context (list :root "/tmp/project/" :path "example.el" :diff raw))) - (fossil-ui-diff-mode) - (let ((inhibit-read-only t)) - (insert raw)) - (set-buffer-modified-p nil) - (setq-local fossil-ui-diff-context context) - (switch-to-buffer buffer) - (goto-char (point-min)) - (forward-line 40) - (let ((point-before (point)) - (window-before (selected-window))) - (set-window-start window-before (save-excursion - (goto-char (point-min)) - (forward-line 20) - (point))) - (let ((start-before (window-start window-before))) - (cl-letf (((symbol-function 'fossil-ui--display-diff) - (lambda (_diff &optional _frame) - (propertize raw 'face 'success)))) - (fossil-ui--theme-enabled 'test-theme)) - (should (eq (selected-window) window-before)) - (should (= (point) point-before)) - (should (= (window-start window-before) start-before)) - (should-not (buffer-modified-p)) - (should (equal fossil-ui-diff-context context)) - (should (equal (buffer-substring-no-properties (point-min) (point-max)) raw)) - (should (eq (get-text-property (point-min) 'face) 'success)))))))) + (fossil-ui-test--with-checkout + (fossil-ui-test--write (expand-file-name "tracked.txt" root) "theme change\n") + (let ((buffer (fossil-ui-status root))) + (unwind-protect + (with-current-buffer buffer + (fossil-ui--goto-path "tracked.txt") + (fossil-ui-diff) + (let* ((key '(nil "tracked.txt")) + (raw (plist-get (alist-get key (plist-get textui-state :inline-diffs) nil nil #'equal) :diff)) + (point-before (point))) + (cl-letf (((symbol-function 'fossil-ui--display-diff) + (lambda (_diff &optional _frame) (propertize raw 'face 'success)))) + (fossil-ui--theme-enabled 'test-theme)) + (let ((display (plist-get (alist-get key (plist-get textui-state :inline-diffs) nil nil #'equal) :display))) + (should (equal (substring-no-properties display) raw)) + (should (eq (get-text-property 0 'face display) 'success))) + (should (= (point) point-before)))) + (when (buffer-live-p buffer) (kill-buffer buffer))))))) (ert-deftest fossil-ui-status-opens-rendered-dashboard () (fossil-ui-test--with-checkout @@ -306,46 +515,36 @@ (should-error (fossil-ui--checkout-info directory) :type 'user-error) (delete-directory directory t)))) -(ert-deftest fossil-ui-preview-block-resizes-and-retains-diff-faces () - (let* ((fossil-ui-preview-lines 3) - (widget - (widget-convert 'fossil-ui-preview - :path "tracked.txt" - :value "@@ -1 +1 @@\n-old\n+new\n context"))) - (dolist (width '(20 80 120)) - (let ((rendered (textui-layout-widget widget width))) - (should (text-property-not-all 0 (length rendered) 'face nil rendered)) - (dolist (line (split-string rendered "\n")) - (should (<= (string-width line) width))) - (with-temp-buffer - (insert rendered) - (textui-attach-widget widget (point-min) (point-max)) - (should (equal (buffer-string) rendered)) - (widget-delete widget)))))) - -(ert-deftest fossil-ui-preview-toggle-refresh-and-clean-checkout () +(ert-deftest fossil-ui-display-diff-rejects-structurally-changed-delta-output () + (let ((raw "@@ -1 +1 @@\n-old\n+new\n")) + (cl-letf (((symbol-function 'fossil-ui--render-diff) + (lambda (_diff &optional _frame) (propertize "changed structure" 'face 'success)))) + (should (equal (fossil-ui--display-diff raw) raw))))) + +(ert-deftest fossil-ui-inline-diff-survives-refresh () (fossil-ui-test--with-checkout - (fossil-ui-test--write (expand-file-name "tracked.txt" root) "preview change\n") + (fossil-ui-test--write (expand-file-name "tracked.txt" root) "inline change\n") (let ((buffer (save-window-excursion (fossil-ui-status root)))) (unwind-protect (with-current-buffer buffer (fossil-ui--goto-path "tracked.txt") - (should (eq (key-binding (kbd "P")) #'fossil-ui-toggle-preview)) - (fossil-ui-toggle-preview) - (should (equal (plist-get textui-state :preview-path) "tracked.txt")) - (textui-refresh buffer) - (should (string-search "+preview change" (buffer-substring-no-properties (point-min) (point-max)))) - (fossil-ui-test--write (expand-file-name "tracked.txt" root) "new preview\n") - (fossil-ui-refresh) - (textui-refresh buffer) - (should (string-search "+new preview" (buffer-substring-no-properties (point-min) (point-max)))) + (fossil-ui-diff) + (goto-char (point-min)) + (let ((hunk (text-property-search-forward 'fossil-ui-hunk-header t #'eq))) + (should hunk) + (goto-char (prop-match-beginning hunk))) + (let ((location (fossil-ui--property-at-line 'fossil-ui-location))) + (fossil-ui-test--write (expand-file-name "tracked.txt" root) "new inline change\n") + (fossil-ui-refresh) + (should (member '(nil "tracked.txt") (plist-get textui-state :expanded-diffs))) + (should (equal (fossil-ui--property-at-line 'fossil-ui-location) location))) + (should (string-search "+new inline change" (buffer-substring-no-properties (point-min) (point-max)))) (fossil-ui--goto-path "tracked.txt") - (fossil-ui-toggle-preview) - (should-not (plist-get textui-state :preview-path)) - (fossil-ui-toggle-preview) + (fossil-ui-diff) + (should-not (plist-get textui-state :expanded-diffs)) (fossil-ui-test--write (expand-file-name "tracked.txt" root) "initial\n") (fossil-ui-refresh) - (should-not (plist-get textui-state :preview-path))) + (should-not (plist-get textui-state :expanded-diffs))) (when (buffer-live-p buffer) (kill-buffer buffer)))))) -- cgit v1.3.1