Skip to content

Commit 1a1ca2d

Browse files
committed
Adopt underused buttercup facilities in the test suite
An audit against the buttercup docs found features we weren't using: - assume: environment-dependent specs (libxml, cygwin paths) now report as skipped instead of silently asserting a fallback branch. - Custom matchers in cider-test-utils: :to-have-sent-op collapses the spy-and-pluck plumbing around nREPL request assertions and names the offending key on failure; :to-equal-dict compares nrepl-dicts order-insensitively and reports differing keys. - spy-calls-most-recent instead of indexed spy-calls-args-for, which breaks when a function gains an earlier call. The hacking guide documents the conventions.
1 parent c52f666 commit 1a1ca2d

7 files changed

Lines changed: 144 additions & 64 deletions

File tree

doc/modules/ROOT/pages/contributing/hacking.adoc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ the
8080
https://github.com/jorgenschaefer/emacs-buttercup/blob/master/docs/running-tests.md[buttercup documentation] for
8181
all the details.
8282

83+
A few suite conventions worth knowing:
84+
85+
* Specs that only make sense in certain environments (a specific OS, Emacs
86+
feature or library) should declare that with buttercup's `assume`, so they
87+
show up as skipped instead of silently passing over the interesting branch.
88+
* `test/utils/cider-test-utils.el` defines custom matchers for the common
89+
assertion patterns: `:to-have-sent-op` for checking the nREPL request a
90+
spied request function sent, and `:to-equal-dict` for order-insensitive
91+
nrepl-dict comparison with key-level failure messages. Prefer them over
92+
hand-rolled request/dict plumbing.
93+
* Prefer `spy-calls-most-recent` over indexed `spy-calls-args-for` - it
94+
doesn't break when a function gains an earlier internal call.
95+
8396
==== Running the tests in batch mode
8497

8598
If you prefer running all tests outside Emacs that's also an option.

test/cider-find-tests.el

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,13 @@
6363
(nrepl-dict "dest" (current-buffer) "dest-point" 1))
6464
(spy-on 'cider-jump-to)
6565
(cider-find-keyword '(4)) ; single C-u -> same window
66-
(expect (nth 2 (spy-calls-args-for 'cider-jump-to 0)) :to-be nil)
66+
(expect (nth 2 (spy-context-args (spy-calls-most-recent 'cider-jump-to))) :to-be nil)
6767
(spy-calls-reset 'cider-jump-to)
6868
(cider-find-keyword '(16)) ; double C-u -> other window
69-
(expect (nth 2 (spy-calls-args-for 'cider-jump-to 0)) :to-be-truthy)
69+
(expect (nth 2 (spy-context-args (spy-calls-most-recent 'cider-jump-to))) :to-be-truthy)
7070
(spy-calls-reset 'cider-jump-to)
7171
(cider-find-keyword '-) ; negative -> other window
72-
(expect (nth 2 (spy-calls-args-for 'cider-jump-to 0)) :to-be-truthy)))
72+
(expect (nth 2 (spy-context-args (spy-calls-most-recent 'cider-jump-to))) :to-be-truthy)))
7373

7474
(describe "cider--find-keyword-loc"
7575
(it "finds the given keyword, discarding false positives"

test/cider-interaction-tests.el

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,21 @@
4545

4646
(describe "cider-to-nrepl-filename-function"
4747
(it "translates file paths when running on cygwin systems"
48+
(assume (eq system-type 'cygwin) "not running on cygwin")
4849
(let ((windows-file-name "C:/foo/bar")
4950
(unix-file-name "/cygdrive/c/foo/bar"))
50-
(if (eq system-type 'cygwin)
51-
(progn
52-
(expect (funcall cider-from-nrepl-filename-function windows-file-name)
53-
:to-equal unix-file-name)
54-
(expect (funcall cider-to-nrepl-filename-function unix-file-name)
55-
:to-equal windows-file-name))
56-
(progn
57-
(expect (funcall cider-from-nrepl-filename-function unix-file-name)
58-
:to-equal unix-file-name)
59-
(expect (funcall cider-to-nrepl-filename-function unix-file-name)
60-
:to-equal unix-file-name)))))
51+
(expect (funcall cider-from-nrepl-filename-function windows-file-name)
52+
:to-equal unix-file-name)
53+
(expect (funcall cider-to-nrepl-filename-function unix-file-name)
54+
:to-equal windows-file-name)))
55+
56+
(it "leaves file paths alone on other systems"
57+
(assume (not (eq system-type 'cygwin)) "running on cygwin")
58+
(let ((unix-file-name "/cygdrive/c/foo/bar"))
59+
(expect (funcall cider-from-nrepl-filename-function unix-file-name)
60+
:to-equal unix-file-name)
61+
(expect (funcall cider-to-nrepl-filename-function unix-file-name)
62+
:to-equal unix-file-name)))
6163
(it "translates file paths from container/vm location to host location"
6264
(let* ((/docker/src (expand-file-name "/docker/src"))
6365
(/cygdrive/c/project/src (expand-file-name "/cygdrive/c/project/src"))

test/cider-log-tests.el

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
(require 'buttercup)
2929
(require 'cider-log)
30+
(require 'cider-test-utils)
3031

3132
;; Please, for each `describe', ensure there's an `it' block, so that its execution is visible in CI.
3233

@@ -200,10 +201,6 @@
200201
(expect (substring-no-properties (cider-log--description-clear-events-buffer))
201202
:to-equal "Clear *cider-log* buffer"))))
202203

203-
(defun cider-log-tests--request-get (request key)
204-
"Return the value following KEY in the nREPL REQUEST list."
205-
(cadr (member key request)))
206-
207204
(describe "cider-log nREPL request construction"
208205
:var (framework appender event)
209206
(before-each
@@ -219,24 +216,17 @@
219216
:and-return-value (nrepl-dict "cider/log-add-appender" appender))
220217
(expect (cider-sync-request:log-add-appender framework appender)
221218
:to-equal appender)
222-
(let ((request (car (spy-calls-args-for 'cider-nrepl-sync-request 0))))
223-
(expect (cider-log-tests--request-get request "op")
224-
:to-equal "cider/log-add-appender")
225-
(expect (cider-log-tests--request-get request "framework") :to-equal "logback")
226-
(expect (cider-log-tests--request-get request "appender") :to-equal "my-appender")
227-
(expect (cider-log-tests--request-get request "size") :to-equal 100)
228-
(expect (cider-log-tests--request-get request "threshold") :to-equal 10)
229-
(expect (nrepl-dict-get (cider-log-tests--request-get request "filters") "level")
230-
:to-equal "INFO")))
219+
(expect 'cider-nrepl-sync-request :to-have-sent-op "cider/log-add-appender"
220+
"framework" "logback" "appender" "my-appender"
221+
"size" 100 "threshold" 10)
222+
(expect (cider-request-get (cider-sent-request 'cider-nrepl-sync-request) "filters")
223+
:to-equal-dict (nrepl-dict "level" "INFO")))
231224

232225
(it "cider-sync-request:log-clear sends the clear-appender op"
233226
(spy-on 'cider-nrepl-sync-request :and-return-value (nrepl-dict))
234227
(cider-sync-request:log-clear framework appender)
235-
(let ((request (car (spy-calls-args-for 'cider-nrepl-sync-request 0))))
236-
(expect (cider-log-tests--request-get request "op")
237-
:to-equal "cider/log-clear-appender")
238-
(expect (cider-log-tests--request-get request "framework") :to-equal "logback")
239-
(expect (cider-log-tests--request-get request "appender") :to-equal "my-appender")))
228+
(expect 'cider-nrepl-sync-request :to-have-sent-op "cider/log-clear-appender"
229+
"framework" "logback" "appender" "my-appender"))
240230

241231
(it "cider-sync-request:log-search sends the filters and pagination"
242232
(spy-on 'cider-nrepl-sync-request
@@ -245,56 +235,42 @@
245235
(expect (cider-sync-request:log-search framework appender
246236
:filters filters :limit 25 :offset 5)
247237
:to-equal (list event))
248-
(let ((request (car (spy-calls-args-for 'cider-nrepl-sync-request 0))))
249-
(expect (cider-log-tests--request-get request "op")
250-
:to-equal "cider/log-search")
251-
(expect (cider-log-tests--request-get request "filters") :to-equal filters)
252-
(expect (cider-log-tests--request-get request "limit") :to-equal 25)
253-
(expect (cider-log-tests--request-get request "offset") :to-equal 5))))
238+
(expect 'cider-nrepl-sync-request :to-have-sent-op "cider/log-search"
239+
"filters" filters "limit" 25 "offset" 5)))
254240

255241
(it "cider-sync-request:log-inspect-event asks for the event by id"
256242
(spy-on 'cider-nrepl-sync-request
257243
:and-return-value (nrepl-dict "value" "inspected"))
258244
(expect (cider-sync-request:log-inspect-event framework appender event)
259245
:to-equal "inspected")
260-
(let ((request (car (spy-calls-args-for 'cider-nrepl-sync-request 0))))
261-
(expect (cider-log-tests--request-get request "op")
262-
:to-equal "cider/log-inspect-event")
263-
(expect (cider-log-tests--request-get request "event") :to-equal "ev-42")))
246+
(expect 'cider-nrepl-sync-request :to-have-sent-op "cider/log-inspect-event"
247+
"event" "ev-42"))
264248

265249
(it "cider-sync-request:log-format-event includes the print options"
266250
(spy-on 'cider-nrepl-sync-request
267251
:and-return-value (nrepl-dict "cider/log-format-event" "formatted"))
268252
(expect (cider-sync-request:log-format-event framework appender event)
269253
:to-equal "formatted")
270-
(let ((request (car (spy-calls-args-for 'cider-nrepl-sync-request 0))))
271-
(expect (cider-log-tests--request-get request "op")
272-
:to-equal "cider/log-format-event")
273-
(expect (cider-log-tests--request-get request "event") :to-equal "ev-42")
274-
(expect (cider-log-tests--request-get request "nrepl.middleware.print/stream?")
275-
:to-equal "1")))
254+
(expect 'cider-nrepl-sync-request :to-have-sent-op "cider/log-format-event"
255+
"event" "ev-42" "nrepl.middleware.print/stream?" "1"))
276256

277257
(it "cider-request:log-add-consumer sends the consumer's filters and the callback"
278258
(spy-on 'cider-nrepl-send-request)
279259
(let ((consumer (nrepl-dict "filters" (nrepl-dict "level" "INFO"))))
280260
(cider-request:log-add-consumer framework appender consumer #'ignore)
281-
(let ((args (spy-calls-args-for 'cider-nrepl-send-request 0)))
282-
(expect (cider-log-tests--request-get (car args) "op")
283-
:to-equal "cider/log-add-consumer")
284-
(expect (cider-log-tests--request-get (car args) "framework") :to-equal "logback")
285-
(expect (cider-log-tests--request-get (car args) "appender") :to-equal "my-appender")
286-
(expect (nrepl-dict-get (cider-log-tests--request-get (car args) "filters") "level")
261+
(expect 'cider-nrepl-send-request :to-have-sent-op "cider/log-add-consumer"
262+
"framework" "logback" "appender" "my-appender")
263+
(let ((args (spy-context-args (spy-calls-most-recent 'cider-nrepl-send-request))))
264+
(expect (nrepl-dict-get (cider-request-get (car args) "filters") "level")
287265
:to-equal "INFO")
288266
(expect (cadr args) :to-be #'ignore))))
289267

290268
(it "cider-sync-request:log-remove-consumer identifies the consumer by id"
291269
(spy-on 'cider-nrepl-sync-request :and-return-value (nrepl-dict))
292270
(cider-sync-request:log-remove-consumer
293271
framework appender (nrepl-dict "id" "my-consumer"))
294-
(let ((request (car (spy-calls-args-for 'cider-nrepl-sync-request 0))))
295-
(expect (cider-log-tests--request-get request "op")
296-
:to-equal "cider/log-remove-consumer")
297-
(expect (cider-log-tests--request-get request "consumer") :to-equal "my-consumer"))))
272+
(expect 'cider-nrepl-sync-request :to-have-sent-op "cider/log-remove-consumer"
273+
"consumer" "my-consumer")))
298274

299275
(describe "cider-log appender and consumer accessors"
300276
(it "reads the appender size, threshold, filters and consumers"
@@ -454,6 +430,6 @@
454430
(cider-log-appender-id "my-appender"))
455431
(cider-log-info)))
456432
(expect (substring-no-properties
457-
(apply #'format (spy-calls-args-for 'message 0)))
433+
(apply #'format (spy-context-args (spy-calls-most-recent 'message))))
458434
:to-equal
459435
"Buffer: *cider-log* Framework: Logback Appender: my-appender Consumer: c1")))

test/cider-stacktrace-tests.el

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@
282282
'line 12)))
283283
(cider-stacktrace-navigate button)
284284
(expect 'cider-var-info :not :to-have-been-called)
285-
(let ((info (car (car (spy-calls-all-args 'cider--jump-to-loc-from-info)))))
285+
(let ((info (car (spy-context-args (spy-calls-most-recent 'cider--jump-to-loc-from-info)))))
286286
(expect (nrepl-dict-get info "file") :to-equal "file:/tmp/repro.clj")
287287
(expect (nrepl-dict-get info "line") :to-equal 12)))))
288288

test/cider-util-tests.el

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,12 @@ and some other vars (like clojure.core/filter).
605605

606606
(describe "cider--render-html-string"
607607
(it "renders html to plain text when libxml is available"
608-
(if (and (fboundp 'libxml-available-p) (libxml-available-p))
609-
(expect (substring-no-properties (cider--render-html-string "<p><b>hello</b></p>"))
610-
:to-equal "hello")
611-
(expect (cider--render-html-string "<b>x</b>") :to-equal "<b>x</b>"))))
608+
(assume (and (fboundp 'libxml-available-p) (libxml-available-p))
609+
"libxml support not available")
610+
(expect (substring-no-properties (cider--render-html-string "<p><b>hello</b></p>"))
611+
:to-equal "hello"))
612+
613+
(it "returns the raw html without libxml support"
614+
(assume (not (and (fboundp 'libxml-available-p) (libxml-available-p)))
615+
"libxml support is available")
616+
(expect (cider--render-html-string "<b>x</b>") :to-equal "<b>x</b>")))

test/utils/cider-test-utils.el

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525

2626
;;; Code:
2727

28+
(require 'buttercup)
29+
(require 'cl-lib)
2830
(require 'clojure-mode)
31+
(require 'nrepl-dict)
32+
(require 'seq)
33+
(require 'subr-x)
2934

3035
(defun with-clojure-buffer--go-to-point ()
3136
"Move point to the `|' marker and delete it, if present."
@@ -46,6 +51,85 @@ buffer."
4651
(with-clojure-buffer--go-to-point)
4752
,@body))
4853

54+
55+
;;; nREPL request assertions
56+
57+
(defun cider-request-get (request key)
58+
"Return the value following KEY in the nREPL REQUEST list."
59+
(cadr (member key request)))
60+
61+
(defun cider-sent-request (spy)
62+
"Return the request (first argument) of the most recent call to SPY."
63+
(when-let* ((context (spy-calls-most-recent spy)))
64+
(car (spy-context-args context))))
65+
66+
(buttercup-define-matcher :to-have-sent-op (spy op &rest expected)
67+
"Match when SPY's most recent request carries the op OP.
68+
EXPECTED are additional key/value pairs the request must contain,
69+
compared with `equal'. Use it on a spied request function:
70+
71+
(expect \\='cider-nrepl-sync-request :to-have-sent-op \"cider/log-search\"
72+
\"framework\" \"logback\" \"limit\" 25)"
73+
(let* ((spy (funcall spy))
74+
(op (funcall op))
75+
(expected (mapcar #'funcall expected))
76+
(request (cider-sent-request spy)))
77+
(cond
78+
((null request)
79+
(cons nil (format "Expected `%s' to have sent op %S, but it was never called"
80+
spy op)))
81+
((not (equal op (cider-request-get request "op")))
82+
(cons nil (format "Expected `%s' to have sent op %S, but its last request was %S"
83+
spy op request)))
84+
(t
85+
(let (mismatches)
86+
(cl-loop for (key value) on expected by #'cddr
87+
for actual = (cider-request-get request key)
88+
unless (equal value actual)
89+
do (push (format "%S is %S (expected %S)" key actual value)
90+
mismatches))
91+
(if mismatches
92+
(cons nil (format "Expected `%s's %S request to match, but %s"
93+
spy op (string-join (nreverse mismatches) ", ")))
94+
(cons t (format "Expected `%s' not to have sent op %S matching %S, but it did"
95+
spy op expected))))))))
96+
97+
(defun cider-test-utils--dict-pairs (dict)
98+
"Return DICT's entries as an alist."
99+
(let (pairs)
100+
(nrepl-dict-map (lambda (k v) (push (cons k v) pairs)) dict)
101+
(nreverse pairs)))
102+
103+
(buttercup-define-matcher :to-equal-dict (a b)
104+
"Match when the nREPL dicts A and B contain `equal' entries.
105+
Unlike `:to-equal', key order is irrelevant and the failure message
106+
reports the differing keys instead of printing both dicts whole."
107+
(let* ((a (funcall a))
108+
(b (funcall b))
109+
(a-pairs (cider-test-utils--dict-pairs a))
110+
(b-pairs (cider-test-utils--dict-pairs b))
111+
(missing (seq-remove (lambda (pair) (assoc (car pair) a-pairs)) b-pairs))
112+
(extra (seq-remove (lambda (pair) (assoc (car pair) b-pairs)) a-pairs))
113+
(differing (seq-filter (lambda (pair)
114+
(let ((other (assoc (car pair) b-pairs)))
115+
(and other (not (equal (cdr pair) (cdr other))))))
116+
a-pairs)))
117+
(if (or missing extra differing)
118+
(cons nil (string-join
119+
(delq nil
120+
(list (when missing
121+
(format "missing keys: %S" (mapcar #'car missing)))
122+
(when extra
123+
(format "unexpected keys: %S" (mapcar #'car extra)))
124+
(when differing
125+
(mapconcat (lambda (pair)
126+
(format "%S is %S (expected %S)"
127+
(car pair) (cdr pair)
128+
(cdr (assoc (car pair) b-pairs))))
129+
differing ", "))))
130+
"; "))
131+
(cons t (format "Expected dicts to differ, but both equal %S" a)))))
132+
49133
(provide 'cider-test-utils)
50134

51135
;;; cider-test-utils.el ends here

0 commit comments

Comments
 (0)