-
Notifications
You must be signed in to change notification settings - Fork 177
/
Copy pathtest.clj
387 lines (346 loc) · 15.1 KB
/
test.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
(ns cider.nrepl.middleware.test
"Test execution, reporting, and inspection"
{:author "Jeff Valk"}
(:require
[cider.nrepl.middleware.stacktrace :as st]
[cider.nrepl.middleware.test.extensions :as extensions]
[cider.nrepl.middleware.util :as util]
[cider.nrepl.middleware.util.coerce :as util.coerce]
[clojure.pprint :as pp]
[clojure.test :as test]
[clojure.walk :as walk]
[nrepl.middleware.interruptible-eval :as ie]
[nrepl.middleware.print :as print]
[nrepl.misc :refer [response-for]]
[nrepl.transport :as t]
[orchard.misc :as misc]
[orchard.query :as query]))
;;; ## Overview
;;
;; This middleware provides test execution and reporting support for the
;; `clojure.test` machinery. In this model, the smallest unit of execution is
;; the var (as created by `deftest`); which runs all the tests defined within
;; that function.
;;
;; Report results are stored in the session, indexed by namespace, var, and
;; assertion index within the var. This enables individual test failures/errors
;; to be inspected on demand, including cause and stacktrace detail for errors.
;;
;; Non-passing tests may be re-run for any namespace using the `retest` op.
;; Whenever a var's tests are run, their previous results are overwritten, so
;; the session always holds the most recent test result for each var.
;;; ## Test Results
;;
;; `clojure.test` allows extensible test reporting by rebinding the `report`
;; function. The implementation below does this to capture report events in the
;; `current-report` atom.
(def current-report
"An atom holding the results of the test run in progress"
(atom nil))
(defn report-reset! []
(reset! current-report {:summary {:ns 0 :var 0 :test 0 :pass 0 :fail 0 :error 0}
:results {} :testing-ns nil :gen-input nil}))
;; In the case of test errors, line number is obtained by searching the
;; stacktrace for the originating function. The search target will be the
;; current test var's `:test` metadata (which holds the actual test function) if
;; present, or the deref'ed var function otherwise (i.e. test fixture errors).
;;
;; This approach is similar in use to `clojure.test/file-position`, but doesn't
;; assume a fixed position in the stacktrace, and therefore resolves the correct
;; frame when the error occurs outside of an `is` form.
(defn stack-frame
"Search the stacktrace of exception `e` for the function `f` and return info
describing the stack frame, including var, class, and line."
([^Exception e f]
(stack-frame (st/directory-namespaces) e f))
([namespaces ^Exception e f]
(when-let [class-name (some-> f class .getName)]
(->> e
.getStackTrace
(map (partial st/analyze-frame namespaces))
(filter #(= (:class %) class-name))
first))))
(defn- print-object
"Print `object` using println for matcher-combinators results and pprint
otherwise. The matcher-combinators library uses a custom print-method
which doesn't get picked up by pprint since it uses a different dispatch
mechanism."
[object]
(let [matcher-combinators-result? (= (:type (meta object))
:matcher-combinators.clj-test/mismatch)
print-fn (if matcher-combinators-result?
println
pp/pprint)]
(with-out-str (print-fn object))))
(def ^:dynamic *test-error-handler*
"A function you can override via `binding`, or safely via `alter-var-root`.
On test `:error`s, the related Throwable be invoked as the sole argument
passed to this var.
For example, you can use this to add an additional `println`,
for pretty-printing Spec failures. Remember to `flush` if doing so."
identity)
(def ^:private fallback-var-name
"The pseudo var name which will be used when no var name can be found
for a given test report."
::unknown)
(defn test-result
"Transform the result of a test assertion. Append ns, var, assertion index,
and 'testing' context. Retain any exception. Pretty-print expected/actual or
use its `print-method`, if applicable."
[ns v m]
(let [{:keys [actual diffs expected fault]
t :type} m
v-name (or (:name (meta v)) fallback-var-name)
c (when (seq test/*testing-contexts*) (test/testing-contexts-str))
i (count (get-in (:results @current-report {}) [ns v-name]))
gen-input (:gen-input @current-report)]
;; Errors outside assertions (faults) do not return an :expected value.
;; Type :fail returns :actual value. Type :error returns :error and :line.
(merge (dissoc m :expected :actual)
{:ns ns, :var v-name, :index i, :context c}
(when (and (#{:fail :error} t) (not fault))
{:expected (print-object expected)})
(when (and (#{:fail} t) gen-input)
{:gen-input (print-object gen-input)})
(when (#{:fail} t)
{:actual (print-object actual)})
(when diffs
{:diffs (extensions/diffs-result diffs)})
(when (#{:error} t)
(let [e actual
f (or (:test (meta v)) (some-> v deref))] ; test fn or deref'ed fixture
(*test-error-handler* e)
{:error e
:line (:line (stack-frame e f))})))))
;;; ## test.check integration
;;
;; `test.check` generates random test inputs for property testing. We make the
;; inputs part of the report by parsing the respective calls to `report`:
;; `test.chuck`'s `checking` creates events of type
;; `:com.gfredericks.test.chuck.clojure-test/shrunk` with the minimal failing
;; input as determined by `test.check`. `test.check`'s own `defspec` does report
;; minimal inputs in recent versions, but for compatibility we also parse events
;; of type `:clojure.test.check.clojure-test/shrinking`, which `defspec`
;; produces to report failing input before shrinking it.
(defmulti report
"Handle reporting for test events.
This takes a test event map as an argument and updates the `current-report`
atom to reflect test results and summary statistics."
:type)
(defmethod report :default [_m])
(defmethod report :begin-test-ns
[m]
(let [ns (ns-name (get m :ns (:testing-ns @current-report)))]
(swap! current-report
#(-> %
(assoc :testing-ns ns)
(update-in [:summary :ns] inc)))))
(defmethod report :begin-test-var
[_m]
(swap! current-report update-in [:summary :var] inc))
(defn- report-final-status
[{:keys [type] :as m}]
(let [ns (ns-name (get m :ns (:testing-ns @current-report)))
v (last test/*testing-vars*)]
(swap! current-report
#(-> %
(update-in [:summary :test] inc)
(update-in [:summary type] (fnil inc 0))
(update-in [:results ns (or (:name (meta v))
fallback-var-name)]
(fnil conj [])
(test-result ns v m))
(assoc :gen-input nil)))))
(defmethod report :pass
[m]
(report-final-status m))
(defmethod report :fail
[m]
(report-final-status m))
(defmethod report :error
[m]
(report-final-status m))
(defmethod report :com.gfredericks.test.chuck.clojure-test/shrunk
[m]
(swap! current-report assoc :gen-input (-> m :shrunk :smallest)))
(defn- report-shrinking
[{:keys [clojure.test.check.clojure-test/params]}]
(swap! current-report assoc :gen-input params))
(defmethod report :clojure.test.check.clojure-test/shrinking
[m]
(report-shrinking m))
(defmethod report :clojure.test.check.clojure-test/shrunk
[m]
(report-shrinking m))
(defmethod report :matcher-combinators/mismatch
[m]
(report-final-status (assoc m
:type :fail
:actual (:markup m))))
(defn report-fixture-error
"Delegate reporting for test fixture errors to the `report` function. This
finds the erring test fixture in the stacktrace and binds it as the current
test var. Test count is decremented to indicate that no tests were run."
[ns e]
(let [namespaces (st/directory-namespaces)
frame (->> (concat (:clojure.test/once-fixtures (meta ns))
(:clojure.test/each-fixtures (meta ns)))
(map (partial stack-frame namespaces e))
(filter identity)
(first))
fixture (resolve (symbol (:var frame)))]
(swap! current-report update-in [:summary :test] dec)
(binding [test/*testing-vars* (list fixture)]
(report {:type :error, :fault true, :expected nil, :actual e
:message "Uncaught exception in test fixture"}))))
;;; ## Test Execution
;;
;; These functions are based on the ones in `clojure.test`, updated to accept
;; a list of vars to test, use the report implementation above, and distinguish
;; between test errors and faults outside of assertions.
(defn test-var
"If var `v` has a function in its `:test` metadata, call that function,
with `clojure.test/*testing-vars*` bound to append `v`."
[v]
(when-let [t (:test (meta v))]
(binding [test/*testing-vars* (conj test/*testing-vars* v)]
(test/do-report {:type :begin-test-var :var v})
(test/inc-report-counter :test)
(try (t)
(catch Throwable e
(test/do-report {:type :error, :fault true, :expected nil, :actual e
:message "Uncaught exception, not in assertion"})))
(test/do-report {:type :end-test-var :var v}))))
(defn test-vars
"Call `test-var` on each var, with the fixtures defined for namespace object
`ns`."
[ns vars]
(let [once-fixture-fn (test/join-fixtures (::test/once-fixtures (meta ns)))
each-fixture-fn (test/join-fixtures (::test/each-fixtures (meta ns)))]
(try (once-fixture-fn
(fn []
(doseq [v vars]
(each-fixture-fn (fn [] (test-var v))))))
(catch Throwable e
(report-fixture-error ns e)))))
(defn test-ns
"If the namespace object defines a function named `test-ns-hook`, call that.
Otherwise, test the specified vars. On completion, return a map of test
results."
[ns vars]
(binding [test/report report]
(test/do-report {:type :begin-test-ns, :ns ns})
(if-let [test-hook (ns-resolve ns 'test-ns-hook)]
(test-hook)
(test-vars ns vars))
(test/do-report {:type :end-test-ns, :ns ns})
@current-report))
(defn test-var-query
"Call `test-ns` for each var found via var-query."
[var-query]
(report-reset!)
(doseq [[ns vars] (group-by
(comp :ns meta)
(query/vars var-query))]
(test-ns ns vars))
@current-report)
(defn test-nss
"Call `test-ns` for each entry in map `m`, in which keys are namespace
symbols and values are var symbols to be tested in that namespace (or `nil`
to test all vars). Symbols are first resolved to their corresponding
objects."
[m]
(report-reset!)
(doseq [[ns vars] m]
(->> (map (partial ns-resolve ns) vars)
(filter identity)
(test-ns (the-ns ns))))
@current-report)
;;; ## Middleware
(def results
"An atom holding results of test runs, indexed by namespace. This is used to
reference exception objects from erring tests, and to rerun tests (by
namespace) that did not pass previously. The var itself will be bound from
the nREPL session."
(atom {}))
(defn handle-test-var-query-op
[{:keys [var-query transport session id] :as msg}]
(let [{:keys [exec]} (meta session)]
(exec id
(fn []
(with-bindings (assoc @session #'ie/*msg* msg)
(try
(let [stringify-msg (fn [report]
(walk/postwalk (fn [x] (if (and (map? x)
(contains? x :message))
(update x :message str)
x))
report))
report (-> var-query
(assoc-in [:ns-query :has-tests?] true)
(assoc :test? true)
(util.coerce/var-query)
test-var-query
stringify-msg)]
(reset! results (:results report))
(t/send transport (response-for msg (util/transform-value report))))
(catch clojure.lang.ExceptionInfo e
(let [d (ex-data e)]
(if (::util.coerce/id d)
(case (::util.coerce/id d)
:namespace-not-found (t/send transport (response-for msg :status :namespace-not-found)))
(throw e)))))))
(fn []
(t/send transport (response-for msg {:status :done}))))))
(defn handle-test-op
[{:keys [ns tests include exclude] :as msg}]
(handle-test-var-query-op
(merge msg {:var-query {:ns-query {:exactly [ns]}
:include-meta-key include
:exclude-meta-key exclude
:exactly (map #(str ns "/" %) tests)}})))
(defn handle-test-all-op
[{:keys [load? include exclude] :as msg}]
(handle-test-var-query-op
(merge msg {:var-query {:ns-query {:project? true
:load-project-ns? load?}
:include-meta-key include
:exclude-meta-key exclude}})))
(defn handle-retest-op
[{:keys [transport session id] :as msg}]
(let [{:keys [exec]} (meta session)]
(exec id
(fn []
(with-bindings (assoc @session #'ie/*msg* msg)
(let [nss (reduce (fn [ret [ns tests]]
(let [problems (filter (comp #{:fail :error} :type)
(mapcat val tests))
vars (distinct (map :var problems))]
(if (seq vars) (assoc ret ns vars) ret)))
{} @results)
report (test-nss nss)]
(reset! results (:results report))
(t/send transport (response-for msg (util/transform-value report))))))
(fn []
(t/send transport (response-for msg :status :done))))))
(defn handle-stacktrace-op
[{:keys [ns var index transport session id ::print/print-fn] :as msg}]
(let [{:keys [exec]} (meta session)]
(exec id
(fn []
(with-bindings (assoc @session #'ie/*msg* msg)
(let [[ns var] (map misc/as-sym [ns var])]
(if-let [e (get-in @results [ns var index :error])]
(doseq [cause (st/analyze-causes e print-fn)]
(t/send transport (response-for msg cause)))
(t/send transport (response-for msg :status :no-error))))))
(fn []
(t/send transport (response-for msg :status :done))))))
(defn handle-test [handler msg & _configuration]
(case (:op msg)
"test-var-query" (handle-test-var-query-op msg)
"test" (handle-test-op msg)
"test-all" (handle-test-all-op msg)
"test-stacktrace" (handle-stacktrace-op msg)
"retest" (handle-retest-op msg)
(handler msg)))