emacs-patch-review

Port of Thunderbird Patch Review to mu4e.

git clone git://mccd.space/emacs-patch-review

patch-review-test.el (26604B)

      1 ;;; patch-review-test.el --- ERT suite -*- lexical-binding: t -*-
      2 
      3 (require 'ert)
      4 (require 'patch-review-parse)
      5 (require 'patch-review-reply)
      6 (require 'patch-review-git)
      7 (require 'patch-review)
      8 
      9 (defun patch-review-test-fixture (name)
     10   "Return the contents of fixture NAME."
     11   (with-temp-buffer
     12     (insert-file-contents
     13      (expand-file-name
     14       (concat "fixtures/" name)
     15       (file-name-directory (or load-file-name buffer-file-name))))
     16     (buffer-string)))
     17 
     18 (ert-deftest patch-review-test-fixtures-load ()
     19   (dolist (f '("patch1.body" "patch2.body" "patch3.body" "cover.body"
     20                "patch1.eml"))
     21     (should (stringp (patch-review-test-fixture f)))))
     22 
     23 ;;;; Parsing (ports of the diff-parse.js checks)
     24 
     25 (ert-deftest patch-review-test-parse-patch1 ()
     26   (let* ((p (patch-review-parse-email (patch-review-test-fixture "patch1.body")))
     27          (file (car (patch-review-email-files p)))
     28          (hunk (car (patch-review-file-hunks file))))
     29     (should (= 1 (length (patch-review-email-files p))))
     30     (should (equal "src/socket.c" (patch-review-file-display-path file)))
     31     (should (string-prefix-p "Reject negative descriptors"
     32                              (patch-review-email-commit-message p)))
     33     (should (string-match-p "1 file changed" (patch-review-email-diffstat p)))
     34     (should (= 1 (length (patch-review-file-hunks file))))
     35     (should (= 15 (patch-review-hunk-old-count hunk)))
     36     (should (= 20 (patch-review-hunk-new-count hunk)))
     37     (let ((first-add (cl-find 'add (patch-review-hunk-lines hunk)
     38                               :key #'patch-review-line-origin)))
     39       (should (equal "#include <errno.h>" (patch-review-line-text first-add)))
     40       (should (= 2 (patch-review-line-new-line first-add)))
     41       (should (null (patch-review-line-old-line first-add))))
     42     (let ((first-del (cl-find 'del (patch-review-hunk-lines hunk)
     43                               :key #'patch-review-line-origin)))
     44       (should (= 5 (patch-review-line-old-line first-del)))
     45       (should (null (patch-review-line-new-line first-del))))
     46     ;; Raw lines must round-trip exactly for quoting in replies.
     47     (let* ((lines (patch-review-hunk-lines hunk))
     48            (start (patch-review-line-body-line (car lines)))
     49            (body-lines (patch-review-email-body-lines p))
     50            (slice (cl-loop for i from start below (+ start (length lines))
     51                            collect (aref body-lines i))))
     52       (should (equal slice (mapcar #'patch-review-line-raw lines))))))
     53 
     54 (ert-deftest patch-review-test-parse-patch2-new-file ()
     55   (let* ((p (patch-review-parse-email (patch-review-test-fixture "patch2.body")))
     56          (file (car (patch-review-email-files p))))
     57     (should (patch-review-file-is-new file))
     58     (should (equal "src/log.c" (patch-review-file-display-path file)))
     59     (should (cl-every (lambda (l) (eq 'add (patch-review-line-origin l)))
     60                       (patch-review-hunk-lines
     61                        (car (patch-review-file-hunks file)))))))
     62 
     63 (ert-deftest patch-review-test-parse-patch3-binary ()
     64   (let* ((p (patch-review-parse-email (patch-review-test-fixture "patch3.body")))
     65          (file (car (patch-review-email-files p))))
     66     (should (patch-review-file-is-binary file))
     67     (should (null (patch-review-file-hunks file)))))
     68 
     69 (ert-deftest patch-review-test-parse-no-diff ()
     70   (let ((p (patch-review-parse-email "just a message\nwith two lines")))
     71     (should (null (patch-review-email-files p)))
     72     (should (string-match-p "just a message"
     73                             (patch-review-email-commit-message p)))))
     74 
     75 ;;;; Subject heuristics (ports of the patch-detect.js checks)
     76 
     77 (ert-deftest patch-review-test-parse-subject ()
     78   (let ((info (patch-review-parse-subject "[PATCH 1/2] x")))
     79     (should (equal "PATCH" (plist-get info :prefix)))
     80     (should (= 1 (plist-get info :version)))
     81     (should (= 1 (plist-get info :n)))
     82     (should (= 2 (plist-get info :m)))
     83     (should (equal "x" (plist-get info :title))))
     84   (let ((info (patch-review-parse-subject "[RFC PATCH v2 0/5] y")))
     85     (should (equal "RFC PATCH" (plist-get info :prefix)))
     86     (should (= 2 (plist-get info :version)))
     87     (should (= 0 (plist-get info :n)))
     88     (should (= 5 (plist-get info :m))))
     89   (let ((info (patch-review-parse-subject "[PATCH net-next v3 1/2] z")))
     90     (should (equal "PATCH NET-NEXT" (plist-get info :prefix)))
     91     (should (= 3 (plist-get info :version))))
     92   (let ((info (patch-review-parse-subject "Re: [PATCH] w")))
     93     (should info)
     94     (should (null (plist-get info :n)))
     95     (should (null (plist-get info :m))))
     96   (should (null (patch-review-parse-subject "not a patch"))))
     97 
     98 (ert-deftest patch-review-test-detect ()
     99   (should (patch-review-body-looks-like-diff-p
    100            (patch-review-test-fixture "patch1.body")))
    101   (should-not (patch-review-body-looks-like-diff-p "just some prose"))
    102   (should (patch-review-patch-message-p
    103            "[PATCH 1/2] x" (patch-review-test-fixture "patch1.body")))
    104   (should (patch-review-patch-message-p
    105            "a fix" (patch-review-test-fixture "patch1.body")))
    106   ;; Cover letter: 0/m tag, no diff, still part of a series.
    107   (should (patch-review-patch-message-p
    108            "[PATCH 0/3] cover" (patch-review-test-fixture "cover.body")))
    109   (should-not (patch-review-patch-message-p "a fix" "prose only")))
    110 
    111 (ert-deftest patch-review-test-same-series ()
    112   (should (patch-review-same-series-p
    113            (patch-review-parse-subject "[PATCH v2 1/3] a")
    114            (patch-review-parse-subject "[PATCH v2 3/3] c")))
    115   (should-not (patch-review-same-series-p
    116                (patch-review-parse-subject "[PATCH 1/3] a")
    117                (patch-review-parse-subject "[PATCH v2 3/3] c")))
    118   (should-not (patch-review-same-series-p
    119                (patch-review-parse-subject "[PATCH 1/3] a")
    120                (patch-review-parse-subject "[PATCH 1/4] b"))))
    121 
    122 ;;;; Reply formatting (ports of the reply-format.js checks)
    123 
    124 (ert-deftest patch-review-test-reply-format ()
    125   (let* ((p (patch-review-parse-email (patch-review-test-fixture "patch1.body")))
    126          (hunk (car (patch-review-file-hunks
    127                      (car (patch-review-email-files p)))))
    128          (lines (patch-review-hunk-lines hunk))
    129          (target (cl-position-if
    130                   (lambda (l) (equal "#include <errno.h>"
    131                                      (patch-review-line-text l)))
    132                   lines))
    133          (later (cl-position-if
    134                  (lambda (l) (string-match-p "errno = EBADF"
    135                                              (patch-review-line-text l)))
    136                  lines))
    137          (comments
    138           (list (cons patch-review-reply-general "Looks good overall, two nits.")
    139                 (cons (patch-review-reply-locator 0 0 target)
    140                       "Is errno.h needed on all platforms?")
    141                 (cons (patch-review-reply-locator 0 0 later)
    142                       "EBADF or EINVAL here?")))
    143          (reply (patch-review-reply-format p comments)))
    144     (should (string-prefix-p "Looks good overall" reply))
    145     (should (string-match-p
    146              (regexp-quote "> diff --git a/src/socket.c b/src/socket.c") reply))
    147     ;; The hunk header itself is not quoted; only hunk lines are.
    148     (should-not (string-match-p
    149                  (regexp-quote (concat "> " (patch-review-hunk-header hunk)))
    150                  reply))
    151     (let ((quoted (string-match (regexp-quote "> +#include <errno.h>") reply))
    152           (comment (string-match (regexp-quote "Is errno.h needed") reply))
    153           (second (string-match (regexp-quote "EBADF or EINVAL") reply)))
    154       (should quoted)
    155       (should (< quoted comment))
    156       (should (< comment second)))))
    157 
    158 (ert-deftest patch-review-test-reply-elision ()
    159   ;; A comment deep into a hunk elides the unquoted head with "> [...]".
    160   (let* ((p (patch-review-parse-email (patch-review-test-fixture "patch1.body")))
    161          (hunk (car (patch-review-file-hunks
    162                      (car (patch-review-email-files p)))))
    163          (last-idx (1- (length (patch-review-hunk-lines hunk))))
    164          (reply (patch-review-reply-format
    165                  p (list (cons (patch-review-reply-locator 0 0 last-idx)
    166                                "tail comment")))))
    167     (should (string-match-p (regexp-quote "> [...]") reply))
    168     (should (string-suffix-p "tail comment\n" reply))))
    169 
    170 (ert-deftest patch-review-test-reply-empty ()
    171   (should (equal "" (patch-review-reply-format nil nil))))
    172 
    173 ;;;; git integration
    174 
    175 (defconst patch-review-test--base-content
    176   "#include <stdio.h>\n\nint open_socket(int fd)\n{\n\tif (fd < 0)\n\t\treturn -1;\n\tprintf(\"opening %d\\n\", fd);\n\treturn fd;\n}\n\nint close_socket(int fd)\n{\n\tprintf(\"closing %d\\n\", fd);\n\treturn 0;\n}\n")
    177 
    178 (defconst patch-review-test--new-content
    179   "#include <stdio.h>\n#include <errno.h>\n\nint open_socket(int fd)\n{\n\tif (fd < 0) {\n\t\terrno = EBADF;\n\t\treturn -1;\n\t}\n\tprintf(\"opening %d\\n\", fd);\n\treturn fd;\n}\n\nint close_socket(int fd)\n{\n\tif (fd < 0)\n\t\treturn -1;\n\tprintf(\"closing %d\\n\", fd);\n\treturn 0;\n}\n")
    180 
    181 (defun patch-review-test--write (dir path text)
    182   (let ((file (expand-file-name path dir)))
    183     (make-directory (file-name-directory file) t)
    184     (write-region text nil file nil 'silent)))
    185 
    186 (defun patch-review-test--git (dir &rest args)
    187   (let ((result (apply #'patch-review-git--run dir args)))
    188     (should (= 0 (car result)))
    189     (cdr result)))
    190 
    191 (defmacro patch-review-test--with-repo (dir &rest body)
    192   "Create a throwaway git repo, bind DIR, evaluate BODY."
    193   (declare (indent 1))
    194   `(let ((,dir (make-temp-file "patch-review-repo" t))
    195          (process-environment
    196           (append '("GIT_AUTHOR_NAME=Test"
    197                     "GIT_AUTHOR_EMAIL=test@example.org"
    198                     "GIT_COMMITTER_NAME=Test"
    199                     "GIT_COMMITTER_EMAIL=test@example.org")
    200                   process-environment)))
    201      (unwind-protect
    202          (progn
    203            (patch-review-test--git ,dir "init" "-q" "-b" "main")
    204            ,@body)
    205        (delete-directory ,dir t))))
    206 
    207 (defun patch-review-test--make-patch (dir)
    208   "Build DIR's history: base commit, patch commit; reset to base.
    209 Return the path of a format-patch file for the patch commit."
    210   (patch-review-test--write dir "src/socket.c"
    211                             patch-review-test--base-content)
    212   (patch-review-test--git dir "add" ".")
    213   (patch-review-test--git dir "commit" "-q" "-m" "base socket code")
    214   (patch-review-test--write dir "src/socket.c"
    215                             patch-review-test--new-content)
    216   (patch-review-test--git dir "add" ".")
    217   (patch-review-test--git dir "commit" "-q" "-m"
    218                             "Reject negative descriptors in socket paths")
    219   (let ((mbox (expand-file-name "patch.mbox" dir)))
    220     (write-region (patch-review-test--git dir "format-patch" "-1" "--stdout")
    221                   nil mbox nil 'silent)
    222     (patch-review-test--git dir "reset" "-q" "--hard" "HEAD~1")
    223     mbox))
    224 
    225 (ert-deftest patch-review-test-git-describe-and-dirty ()
    226   (patch-review-test--with-repo dir
    227     (patch-review-test--write dir "src/socket.c"
    228                               patch-review-test--base-content)
    229     (patch-review-test--git dir "add" ".")
    230     (patch-review-test--git dir "commit" "-q" "-m" "base")
    231     (should (string-match-p (regexp-quote dir)
    232                             (patch-review-git-describe dir)))
    233     (should-not (patch-review-git-dirty-p dir))
    234     (patch-review-test--write dir "src/socket.c"
    235                               patch-review-test--new-content)
    236     (should (eq t (patch-review-git-dirty-p dir)))))
    237 
    238 (ert-deftest patch-review-test-git-check-applicable ()
    239   (patch-review-test--with-repo dir
    240     (let ((mbox (patch-review-test--make-patch dir)))
    241       (should (eq 'applicable (car (patch-review-git-check dir mbox)))))))
    242 
    243 (ert-deftest patch-review-test-git-apply-then-applied ()
    244   (patch-review-test--with-repo dir
    245     (let ((mbox (patch-review-test--make-patch dir)))
    246       (should (eq 'applied (car (patch-review-git-apply dir mbox))))
    247       (should (equal patch-review-test--new-content
    248                      (with-temp-buffer
    249                        (insert-file-contents
    250                         (expand-file-name "src/socket.c" dir))
    251                        (buffer-string))))
    252       ;; Once applied, the probe must report it.
    253       (should (eq 'applied (car (patch-review-git-check dir mbox)))))))
    254 
    255 (ert-deftest patch-review-test-git-check-conflict ()
    256   (patch-review-test--with-repo dir
    257     (let ((mbox (patch-review-test--make-patch dir)))
    258       ;; Commit a change that breaks the patch's context.
    259       (patch-review-test--write
    260        dir "src/socket.c"
    261        (string-replace "if (fd < 0)" "if (fd < -1)"
    262                        patch-review-test--base-content))
    263       (patch-review-test--git dir "add" ".")
    264       (patch-review-test--git dir "commit" "-q" "-m" "diverge")
    265       (should (eq 'conflict (car (patch-review-git-check dir mbox)))))))
    266 
    267 (ert-deftest patch-review-test-git-check-dirty ()
    268   (patch-review-test--with-repo dir
    269     (let ((mbox (patch-review-test--make-patch dir)))
    270       (patch-review-test--write dir "src/socket.c"
    271                                 patch-review-test--new-content)
    272       (should (eq 'dirty (car (patch-review-git-check dir mbox)))))))
    273 
    274 (ert-deftest patch-review-test-git-am-in-progress-guard ()
    275   (patch-review-test--with-repo dir
    276     (let ((mbox (patch-review-test--make-patch dir)))
    277       (should-not (patch-review-git-am-in-progress-p dir))
    278       (make-directory (expand-file-name ".git/rebase-apply" dir) t)
    279       (should (patch-review-git-am-in-progress-p dir))
    280       (should (eq 'am-in-progress (car (patch-review-git-apply dir mbox))))
    281       ;; The repository must be untouched.
    282       (should (string-match-p "base socket code"
    283                               (patch-review-test--git
    284                                dir "log" "-1" "--format=%s"))))))
    285 
    286 (ert-deftest patch-review-test-git-concat-mboxes ()
    287   (let ((a (make-temp-file "pr-a")) (b (make-temp-file "pr-b")))
    288     (unwind-protect
    289         (progn
    290           (write-region "first-no-eol" nil a nil 'silent)
    291           (write-region "second\n" nil b nil 'silent)
    292           (let ((cat (patch-review-git-concat-mboxes (list a b))))
    293             (unwind-protect
    294                 (should (equal "first-no-eol\nsecond\n"
    295                                (with-temp-buffer
    296                                  (insert-file-contents cat)
    297                                  (buffer-string))))
    298               (delete-file cat))))
    299       (delete-file a)
    300       (delete-file b))))
    301 
    302 ;;;; Comment extraction
    303 
    304 (defun patch-review-test--insert-after-line (text prefix insertion)
    305   "Return TEXT with INSERTION added on a new line after the first line
    306 starting with PREFIX."
    307   (let ((lines (split-string text "\n")) (out nil) (done nil))
    308     (dolist (l lines)
    309       (push l out)
    310       (when (and (not done) (string-prefix-p prefix l))
    311         (push insertion out)
    312         (setq done t)))
    313     (unless done (error "No line starting with %S" prefix))
    314     (mapconcat #'identity (nreverse out) "\n")))
    315 
    316 (defun patch-review-test--remove-line (text prefix)
    317   "Return TEXT without its first line starting with PREFIX."
    318   (let ((lines (split-string text "\n")) (out nil) (done nil))
    319     (dolist (l lines)
    320       (if (and (not done) (string-prefix-p prefix l))
    321           (setq done t)
    322         (push l out)))
    323     (unless done (error "No line starting with %S" prefix))
    324     (mapconcat #'identity (nreverse out) "\n")))
    325 
    326 (defun patch-review-test--extract (parsed pristine current)
    327   (patch-review-extract-comments parsed pristine current))
    328 
    329 (ert-deftest patch-review-test-extract-no-edits ()
    330   (let* ((body (patch-review-test-fixture "patch1.body"))
    331          (parsed (patch-review-parse-email body)))
    332     (should (equal '(nil 0 0)
    333                    (patch-review-test--extract parsed body body)))))
    334 
    335 (ert-deftest patch-review-test-extract-general ()
    336   (let* ((body (patch-review-test-fixture "patch1.body"))
    337          (parsed (patch-review-parse-email body))
    338          (edited (concat "Ship it.\n" body)))
    339     (pcase-let ((`(,comments ,del ,chg)
    340                  (patch-review-test--extract parsed body edited)))
    341       (should (equal '(0 0) (list del chg)))
    342       (should (equal "Ship it."
    343                      (cdr (assoc patch-review-reply-general comments)))))))
    344 
    345 (ert-deftest patch-review-test-extract-hunk-comment ()
    346   (let* ((body (patch-review-test-fixture "patch1.body"))
    347          (parsed (patch-review-parse-email body))
    348          (edited (patch-review-test--insert-after-line
    349                   body "+#include <errno.h>" "Is this portable?"))
    350          (hunk (car (patch-review-file-hunks
    351                      (car (patch-review-email-files parsed)))))
    352          (target (cl-position-if
    353                   (lambda (l) (equal "#include <errno.h>"
    354                                      (patch-review-line-text l)))
    355                   (patch-review-hunk-lines hunk))))
    356     (pcase-let ((`(,comments ,del ,chg)
    357                  (patch-review-test--extract parsed body edited)))
    358       (should (equal '(0 0) (list del chg)))
    359       (should (equal "Is this portable?"
    360                      (cdr (assoc (patch-review-reply-locator 0 0 target)
    361                                  comments)))))))
    362 
    363 (ert-deftest patch-review-test-extract-diffstat-is-general ()
    364   (let* ((body (patch-review-test-fixture "patch1.body"))
    365          (parsed (patch-review-parse-email body))
    366          (edited (patch-review-test--insert-after-line
    367                   body " src/socket.c |" "Nice diffstat.")))
    368     (pcase-let ((`(,comments ,_ ,_)
    369                  (patch-review-test--extract parsed body edited)))
    370       (should (equal "Nice diffstat."
    371                      (cdr (assoc patch-review-reply-general comments)))))))
    372 
    373 (ert-deftest patch-review-test-extract-deletion ()
    374   (let* ((body (patch-review-test-fixture "patch1.body"))
    375          (parsed (patch-review-parse-email body))
    376          (edited (patch-review-test--remove-line body " \tprintf(\"opening")))
    377     (pcase-let ((`(,comments ,del ,chg)
    378                  (patch-review-test--extract parsed body edited)))
    379       (should (null comments))
    380       (should (equal '(1 0) (list del chg))))))
    381 
    382 (ert-deftest patch-review-test-extract-change-is-comment ()
    383   (let* ((body (patch-review-test-fixture "patch1.body"))
    384          (parsed (patch-review-parse-email body))
    385          (edited (patch-review-test--insert-after-line
    386                   (patch-review-test--remove-line body "+#include <errno.h>")
    387                   " #include <stdio.h>" "Why not <errno.h>?")))
    388     (pcase-let ((`(,comments ,del ,chg)
    389                  (patch-review-test--extract parsed body edited)))
    390       ;; One context line was not rewritten; the removed + line counts
    391       ;; as a change and the new text is a comment.
    392       (should (= 1 chg))
    393       (should (= 0 del))
    394       (should (cl-some (lambda (e) (string-match-p "Why not" (cdr e)))
    395                        comments)))))
    396 
    397 (ert-deftest patch-review-test-extract-trailing-newline-deletion ()
    398   ;; patch1.body ends with "2.55.0\n\n": dropping one trailing newline
    399   ;; deletes the (empty) final line and is reported as a deletion.
    400   (let* ((body (patch-review-test-fixture "patch1.body"))
    401          (parsed (patch-review-parse-email body))
    402          (edited (string-remove-suffix "\n" body)))
    403     (should (equal '(nil 1 0)
    404                    (patch-review-test--extract parsed body edited)))))
    405 
    406 (ert-deftest patch-review-test-extract-final-newline-normalized ()
    407   ;; A missing final newline in the edited buffer is normalized away
    408   ;; before diffing: it produces neither comments nor deletion counts.
    409   (let* ((parsed (patch-review-parse-email "l1\nl2\n"))
    410          (pristine "l1\nl2\n")
    411          (edited "l1\nl2"))
    412     (should (equal '(nil 0 0)
    413                    (patch-review-test--extract parsed pristine edited)))))
    414 
    415 ;;;; End-to-end: open, comment, send
    416 
    417 (ert-deftest patch-review-test-open-and-send ()
    418   (let* ((eml (expand-file-name
    419                "fixtures/patch1.eml"
    420                (file-name-directory (or load-file-name buffer-file-name))))
    421          (buf (patch-review-open-message-file eml)))
    422     (unwind-protect
    423         (with-current-buffer buf
    424           (should (eq major-mode 'patch-review-mode))
    425           (should (eq (lookup-key patch-review-mode-map (kbd "C-c C-c"))
    426                       #'patch-review-send-review))
    427           (goto-char (point-min))
    428           (re-search-forward "^\+#include <errno.h>$")
    429           (end-of-line)
    430           (insert "\nIs this portable?")
    431           (patch-review-send-review)
    432           ;; Now in the message-mode draft.
    433           (let ((mail (buffer-string)))
    434             (should (string-match-p
    435                      "^To: Aisha Developer <aisha@example.org>$" mail))
    436             (should (string-match-p
    437                      (concat "^" (regexp-quote
    438                                   "Subject: Re: [PATCH 1/3] Reject negative"))
    439                      mail))
    440             (should (string-match-p
    441                      "^In-Reply-To: <patch1@example.org>$" mail))
    442             (should (string-match-p
    443                      (regexp-quote "> +#include <errno.h>") mail))
    444             (should (string-match-p
    445                      (concat "^" (regexp-quote "Is this portable?") "$")
    446                      mail))
    447             (kill-buffer)))
    448       (when (buffer-live-p buf) (kill-buffer buf)))))
    449 
    450 (ert-deftest patch-review-test-send-without-comments-aborts ()
    451   (let* ((eml (expand-file-name
    452                "fixtures/patch1.eml"
    453                (file-name-directory (or load-file-name buffer-file-name))))
    454          (buf (patch-review-open-message-file eml)))
    455     (unwind-protect
    456         (with-current-buffer buf
    457           (cl-letf (((symbol-function #'y-or-n-p) (lambda (_) nil)))
    458             (should-error (patch-review-send-review) :type 'user-error)))
    459       (when (buffer-live-p buf) (kill-buffer buf)))))
    460 
    461 (ert-deftest patch-review-test-open-in-place ()
    462   "Opening with IN-PLACE reuses the current buffer rather than making one."
    463   (let* ((eml (expand-file-name
    464                "fixtures/patch1.eml"
    465                (file-name-directory (or load-file-name buffer-file-name))))
    466          (text (with-temp-buffer
    467                  (insert-file-contents eml)
    468                  (buffer-string)))
    469          (view (get-buffer-create " *patch-review-fake-view* ")))
    470     (unwind-protect
    471         (let ((returned
    472                (with-current-buffer view
    473                  (insert "stale mu4e contents")
    474                  (patch-review--open-text text eml nil t))))
    475           (should (eq returned view))
    476           (with-current-buffer view
    477             (should (eq major-mode 'patch-review-mode))
    478             (should (string-prefix-p "*Review:" (buffer-name)))
    479             ;; The review body, not the discarded text, is now the buffer.
    480             (should (string-match-p "diff --git" patch-review--pristine))
    481             (should-not (string-match-p "stale mu4e contents"
    482                                         patch-review--pristine))
    483             ;; The on-disk message file is the git-am source.
    484             (should (file-equal-p patch-review--source-file eml))))
    485       (when (buffer-live-p view) (kill-buffer view)))))
    486 
    487 (ert-deftest patch-review-test-in-place-editable-from-readonly ()
    488   "An in-place review taken over from a read-only special-mode buffer
    489 (like a mu4e article) must be fully editable.
    490 
    491 `diff-mode' latches `diff-mode-read-only' to the read-only state its
    492 parent body sees; a buffer that started read-only would otherwise keep
    493 `diff-mode-shared-map' active, so RET became `diff-goto-source' (the
    494 read-file-name prompt from diff-find-file-name), digits hit
    495 digit-argument, and the special-mode remap of self-insert-command to
    496 undefined made every printable key a no-op.  We clear the latch in
    497 `patch-review-mode' so the review buffer behaves like a normal editable
    498 buffer."
    499   (let* ((eml (expand-file-name
    500                "fixtures/patch1.eml"
    501                (file-name-directory (or load-file-name buffer-file-name))))
    502          (text (with-temp-buffer
    503                  (insert-file-contents eml)
    504                  (buffer-string)))
    505          (view (get-buffer-create " *patch-review-fake-ro-view* ")))
    506     (unwind-protect
    507         (progn
    508           ;; Start read-only, the way a mu4e article buffer is.
    509           (with-current-buffer view
    510             (special-mode)
    511             (let ((inhibit-read-only t)) (erase-buffer) (insert "stale"))
    512             (should (eq buffer-read-only t)))
    513           (let ((returned
    514                  (with-current-buffer view
    515                    (patch-review--open-text text eml nil t))))
    516             (should (eq returned view))
    517             (with-current-buffer view
    518               (should (eq major-mode 'patch-review-mode))
    519               (should (null buffer-read-only))
    520               ;; The diff-mode short-key latch must be cleared, and with
    521               ;; it gone the inherited diff/special bindings don't hijack
    522               ;; ordinary editing keys.
    523               (should (null diff-mode-read-only))
    524               (should (eq (key-binding (kbd "RET")) #'newline))
    525               (should (eq (key-binding "1") #'self-insert-command))
    526               ;; Direct insertion succeeds (no read-only short-circuit).
    527               (let ((before (buffer-size)))
    528                 (goto-char (point-max))
    529                 (insert "new comment\n")
    530                 (should (= (- (buffer-size) before)
    531                            (length "new comment\n")))))))
    532       (when (buffer-live-p view) (kill-buffer view)))))
    533 
    534 ;;;; End-to-end: status probe and apply
    535 
    536 (ert-deftest patch-review-test-status-and-apply ()
    537   (patch-review-test--with-repo dir
    538     (let* ((mbox (patch-review-test--make-patch dir))
    539            (text (with-temp-buffer
    540                    (insert-file-contents mbox)
    541                    (buffer-string)))
    542            (buf (patch-review--open-text text mbox dir)))
    543       (unwind-protect
    544           (with-current-buffer buf
    545             (should (eq 'applicable patch-review--status))
    546             (should (string-match-p "applies cleanly"
    547                                     (patch-review--header-line)))
    548             (patch-review-apply)
    549             (should (eq 'applied patch-review--status))
    550             (should (equal patch-review-test--new-content
    551                            (with-temp-buffer
    552                              (insert-file-contents
    553                               (expand-file-name "src/socket.c" dir))
    554                              (buffer-string))))
    555             ;; And the probe agrees afterwards.
    556             (patch-review-refresh-status)
    557             (should (eq 'applied patch-review--status)))
    558         (when (buffer-live-p buf) (kill-buffer buf))))))
    559 
    560 (provide 'patch-review-test)
    561 ;;; patch-review-test.el ends here