branch: elpa/projectile
commit e4d212ba29d323cf0e1d04593dcd02369df3204a
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>

    Add a temp-file macro and a match-field matcher for the tests
    
    - projectile-test-with-temp-files: bind vars to fresh temp files (or
      directories) and clean them up on exit, replacing the manual
      make-temp-file + unwind-protect + delete-file dance.  Applied where
      it's a clean win (the file-only session specs); most temp-file specs
      also manage buffers and keep their own unwind-protect.
    - :to-be-a-match-with: a custom matcher that checks a
      projectile-replace--match's fields against a plist in one expect
      (naming every field that didn't match), plus projectile-test-find-match.
      Collapses the multi-expect match-assertion blocks in the search specs.
---
 test/projectile-search-review-test.el | 26 +++++----------
 test/projectile-session-test.el       | 21 +++++-------
 test/projectile-test-helpers.el       | 62 +++++++++++++++++++++++++++++++++++
 3 files changed, 79 insertions(+), 30 deletions(-)

diff --git a/test/projectile-search-review-test.el 
b/test/projectile-search-review-test.el
index edd733f33d..c1fb5c86de 100644
--- a/test/projectile-search-review-test.el
+++ b/test/projectile-search-review-test.el
@@ -346,13 +346,10 @@ REGEXP-P selects `projectile-search-regexp-review'."
            (ms (projectile-search--rg-parse-line json "/proj/"))
            (m (car ms)))
       (expect (length ms) :to-equal 1)
-      (expect (projectile-replace--match-file m) :to-equal "/proj/lib/mb.txt")
-      (expect (projectile-replace--match-line m) :to-equal 3)
-      ;; byte offset 6 -> character column 5
-      (expect (projectile-replace--match-column m) :to-equal 5)
-      (expect (projectile-replace--match-string m) :to-equal "foo")
-      ;; the trailing newline is stripped from the context line
-      (expect (projectile-replace--match-context m) :to-equal "café foo bar")
+      ;; byte offset 6 -> character column 5; the trailing newline is stripped
+      ;; from the context line
+      (expect m :to-be-a-match-with '(:file "/proj/lib/mb.txt" :line 3 :column 
5
+                                      :string "foo" :context "café foo bar"))
       ;; write-back-only fields stay nil (search never uses them)
       (expect (projectile-replace--match-beg m) :to-be nil)
       (expect (projectile-replace--match-end m) :to-be nil)
@@ -579,17 +576,10 @@ REGEXP-P selects `projectile-search-regexp-review'."
           (expect projectile-replace--scanning :to-be nil)
           (expect (projectile-search-review-test--files buf)
                   :to-equal '("mb.txt" "plain.txt"))
-          (let ((mb (cl-find-if
-                     (lambda (m) (string-suffix-p
-                                  "mb.txt" (projectile-replace--match-file m)))
-                     projectile-replace--matches)))
-            (expect mb :not :to-be nil)
-            (expect (projectile-replace--match-line mb) :to-equal 1)
-            ;; character column 5 ("café " is 5 chars), NOT the byte offset 6
-            (expect (projectile-replace--match-column mb) :to-equal 5)
-            (expect (projectile-replace--match-string mb) :to-equal "foo")
-            (expect (projectile-replace--match-context mb)
-                    :to-equal "café foo bar"))))))
+          ;; character column 5 ("café " is 5 chars), NOT the byte offset 6
+          (expect (projectile-test-find-match projectile-replace--matches 
"mb.txt")
+                  :to-be-a-match-with '(:line 1 :column 5 :string "foo"
+                                        :context "café foo bar"))))))
 
   (it "honors projectile-replace-max-matches over a real ripgrep stream"
     (assume (executable-find "rg") "ripgrep is not installed")
diff --git a/test/projectile-session-test.el b/test/projectile-session-test.el
index 242e061aeb..d414e96366 100644
--- a/test/projectile-session-test.el
+++ b/test/projectile-session-test.el
@@ -492,18 +492,15 @@
         (delete-file tmp))))
 
   (it "skips a session file from an incompatible version with a message"
-    (let ((file (make-temp-file "projectile-session-ver" nil ".eld")))
-      (unwind-protect
-          (progn
-            (projectile-serialize
-             (list :projectile-session-version
-                   (1+ projectile-session--format-version)
-                   :buffers nil)
-             file)
-            (spy-on 'message)
-            (expect (projectile-session--read-file file) :to-be nil)
-            (expect 'message :to-have-been-called))
-        (delete-file file))))
+    (projectile-test-with-temp-files ((file ".eld"))
+      (projectile-serialize
+       (list :projectile-session-version
+             (1+ projectile-session--format-version)
+             :buffers nil)
+       file)
+      (spy-on 'message)
+      (expect (projectile-session--read-file file) :to-be nil)
+      (expect 'message :to-have-been-called)))
 
   (it "does not error restoring a layout whose file is gone"
     (let ((tmp (make-temp-file "projectile-session-gone" nil ".txt")))
diff --git a/test/projectile-test-helpers.el b/test/projectile-test-helpers.el
index ab0bfdc05a..e6165b91b6 100644
--- a/test/projectile-test-helpers.el
+++ b/test/projectile-test-helpers.el
@@ -178,6 +178,32 @@ macro-expansion time and must therefore be literals, not 
variables."
           "/tmp/temporary-file-" (format "%d" (random))
           ".eld"))
 
+(defmacro projectile-test-with-temp-files (bindings &rest body)
+  "Bind each VAR in BINDINGS to a fresh temp file (or directory) for BODY.
+Each binding is (VAR) or (VAR SUFFIX) for an empty temp file with the
+optional SUFFIX (e.g. \".txt\"), or (VAR :dir) for a temp directory.
+Every created path is removed when BODY exits, normally or via error -
+files with `delete-file', directories recursively - so a test doesn't
+have to hand-roll the `unwind-protect'/`delete-file' dance."
+  (declare (indent 1) (debug (sexp &rest form)))
+  (let ((vars (mapcar #'car bindings)))
+    `(let ,(mapcar
+            (lambda (b)
+              (let ((var (car b)) (arg (cadr b)))
+                (cond
+                 ((eq arg :dir) `(,var (file-name-as-directory
+                                        (make-temp-file "projectile-test-" 
t))))
+                 (arg           `(,var (make-temp-file "projectile-test-" nil 
,arg)))
+                 (t             `(,var (make-temp-file "projectile-test-"))))))
+            bindings)
+       (unwind-protect
+           (progn ,@body)
+         (dolist (projectile-test--tmp (list ,@vars))
+           (ignore-errors
+             (if (file-directory-p projectile-test--tmp)
+                 (delete-directory projectile-test--tmp t)
+               (delete-file projectile-test--tmp))))))))
+
 (defun projectile-test-wait-for (predicate &optional timeout)
   "Block until PREDICATE returns non-nil or TIMEOUT seconds elapse.
 Pumps the event loop with `accept-process-output' so asynchronous
@@ -205,8 +231,44 @@ message reports exactly which items are missing and which 
are extra."
           (format "Expected %S to have the same items as %S (missing: %S, 
extra: %S)"
                   actual expected missing extra))))
 
+(defconst projectile-test--match-accessors
+  '(:file projectile-replace--match-file
+    :buffer projectile-replace--match-buffer
+    :line projectile-replace--match-line
+    :column projectile-replace--match-column
+    :string projectile-replace--match-string
+    :context projectile-replace--match-context
+    :enabled projectile-replace--match-enabled)
+  "Map of field keyword to accessor for `:to-be-a-match-with'.")
+
+(buttercup-define-matcher :to-be-a-match-with (match plist)
+  "Match when MATCH's `projectile-replace--match' fields equal PLIST.
+PLIST maps field keywords (`:file' `:line' `:column' `:string' `:context'
+`:buffer' `:enabled') to their expected values; only the listed fields
+are checked.  Collapses a stack of per-field `expect' forms into one, and
+the failure message names every field that didn't match."
+  (let* ((m (funcall match))
+         (plist (funcall plist))
+         (mismatches nil))
+    (if (null m)
+        (cons nil "Expected a match struct but got nil")
+      (cl-loop for (k v) on plist by #'cddr
+               for acc = (plist-get projectile-test--match-accessors k)
+               for actual = (funcall acc m)
+               unless (equal actual v)
+               do (push (format "%s: expected %S, got %S" k v actual) 
mismatches))
+      (cons (null mismatches)
+            (format "Expected match to have %S but %s" plist
+                    (string-join (nreverse mismatches) "; "))))))
+
 ;;; Match helpers (reviewable search/replace)
 
+(defun projectile-test-find-match (matches file-suffix)
+  "Return the match in MATCHES whose file name ends with FILE-SUFFIX."
+  (cl-find-if (lambda (m)
+                (string-suffix-p file-suffix (projectile-replace--match-file 
m)))
+              matches))
+
 (defun projectile-test-match-sig (matches root)
   "Return a stable, comparable signature for MATCHES relative to ROOT.
 Each `projectile-replace--match' becomes (RELPATH LINE COLUMN STRING);

Reply via email to