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);