|
15 | 15 | [cljs.pprint :as pp]
|
16 | 16 | [cljs.spec :as s]
|
17 | 17 | [cljs.spec.impl.gen :as gen]
|
18 |
| - [clojure.test.check] |
| 18 | + [clojure.test.check :as stc] |
19 | 19 | [clojure.test.check.properties]))
|
20 | 20 |
|
21 | 21 | (defn distinct-by
|
@@ -172,85 +172,196 @@ that can be instrumented."
|
172 | 172 | (:stub opts)
|
173 | 173 | (keys (:replace opts))])))
|
174 | 174 |
|
175 |
| -;; wrap and unwrap spec failure data in an exception so that |
176 |
| -;; quick-check will treat it as a failure. |
177 |
| -(defn- wrap-failing |
178 |
| - [explain-data step] |
179 |
| - (ex-info "Wrapper" {::check-call (assoc explain-data :failed-on step)})) |
| 175 | +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; testing ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
180 | 176 |
|
181 |
| -(defn- unwrap-failing |
182 |
| - [ret] |
183 |
| - (let [ret (if-let [explain (-> ret :result ex-data ::check-call)] |
184 |
| - (assoc ret :result explain) |
185 |
| - ret)] |
186 |
| - (if-let [shrunk-explain (-> ret :shrunk :result ex-data ::check-call)] |
187 |
| - (assoc-in ret [:shrunk :result] shrunk-explain) |
188 |
| - ret))) |
| 177 | +(defn- explain-check |
| 178 | + [args spec v role] |
| 179 | + (ex-info |
| 180 | + "Specification-based check failed" |
| 181 | + (when-not (s/valid? spec v nil) |
| 182 | + (assoc (s/explain-data* spec [role] [] [] v) |
| 183 | + ::args args |
| 184 | + ::val v |
| 185 | + ::s/failure :check-failed)))) |
189 | 186 |
|
190 | 187 | (defn- check-call
|
191 | 188 | "Returns true if call passes specs, otherwise *returns* an exception
|
192 |
| -with explain-data plus a :failed-on key under ::check-call." |
| 189 | +with explain-data + ::s/failure." |
193 | 190 | [f specs args]
|
194 | 191 | (let [cargs (when (:args specs) (s/conform (:args specs) args))]
|
195 | 192 | (if (= cargs ::s/invalid)
|
196 |
| - (wrap-failing (explain-data* (:args specs) args) :args) |
| 193 | + (explain-check args (:args specs) args :args) |
197 | 194 | (let [ret (apply f args)
|
198 | 195 | cret (when (:ret specs) (s/conform (:ret specs) ret))]
|
199 | 196 | (if (= cret ::s/invalid)
|
200 |
| - (wrap-failing (explain-data* (:ret specs) ret) :ret) |
| 197 | + (explain-check args (:ret specs) ret :ret) |
201 | 198 | (if (and (:args specs) (:ret specs) (:fn specs))
|
202 | 199 | (if (s/valid? (:fn specs) {:args cargs :ret cret})
|
203 | 200 | true
|
204 |
| - (wrap-failing (explain-data* (:fn specs) {:args cargs :ret cret}) :fn)) |
| 201 | + (explain-check args (:fn specs) {:args cargs :ret cret} :fn)) |
205 | 202 | true))))))
|
206 | 203 |
|
| 204 | +(defn- quick-check |
| 205 | + [f specs {gen :gen opts ::stc/opts}] |
| 206 | + (let [{:keys [num-tests] :or {num-tests 1000}} opts |
| 207 | + g (try (s/gen (:args specs) gen) (catch js/Error t t))] |
| 208 | + (if (instance? js/Error g) |
| 209 | + {:result g} |
| 210 | + (let [prop (gen/for-all* [g] #(check-call f specs %))] |
| 211 | + (apply gen/quick-check num-tests prop (mapcat identity opts)))))) |
| 212 | + |
| 213 | +(defn- make-check-result |
| 214 | + "Builds spec result map." |
| 215 | + [check-sym spec test-check-ret] |
| 216 | + (merge {:spec spec |
| 217 | + ::stc/ret test-check-ret} |
| 218 | + (when check-sym |
| 219 | + {:sym check-sym}) |
| 220 | + (when-let [result (-> test-check-ret :result)] |
| 221 | + (when-not (true? result) {:failure result})) |
| 222 | + (when-let [shrunk (-> test-check-ret :shrunk)] |
| 223 | + {:failure (:result shrunk)}))) |
| 224 | + |
| 225 | +(defn- check-1 |
| 226 | + [{:keys [s f v spec]} opts] |
| 227 | + (let [re-inst? (and v (seq (unstrument s)) true) |
| 228 | + f (or f (when v @v))] |
| 229 | + (try |
| 230 | + (cond |
| 231 | + (nil? f) |
| 232 | + {:failure (ex-info "No fn to spec" {::s/failure :no-fn}) |
| 233 | + :sym s :spec spec} |
| 234 | + |
| 235 | + (:args spec) |
| 236 | + (let [tcret (quick-check f spec opts)] |
| 237 | + (make-check-result s spec tcret)) |
| 238 | + |
| 239 | + :default |
| 240 | + {:failure (ex-info "No :args spec" {::s/failure :no-args-spec}) |
| 241 | + :sym s :spec spec}) |
| 242 | + (finally |
| 243 | + (when re-inst? (instrument s)))))) |
| 244 | + |
| 245 | +(defn- sym->check-map |
| 246 | + [s] |
| 247 | + (let [v (resolve s)] |
| 248 | + {:s s |
| 249 | + :v v |
| 250 | + :spec (when v (s/get-spec v))})) |
| 251 | + |
| 252 | +(defn- validate-check-opts |
| 253 | + [opts] |
| 254 | + (assert (every? ident? (keys (:gen opts))) "check :gen expects ident keys")) |
| 255 | + |
207 | 256 | (defn check-fn
|
208 |
| - "Check a function using provided specs and test.check. |
209 |
| -Same options and return as check-var" |
210 |
| - [f specs |
211 |
| - & {:keys [num-tests seed max-size reporter-fn] |
212 |
| - :or {num-tests 100 max-size 200 reporter-fn (constantly nil)}}] |
213 |
| - (let [g (s/gen (:args specs)) |
214 |
| - prop (gen/for-all* [g] #(check-call f specs %))] |
215 |
| - (let [ret (gen/quick-check num-tests prop :seed seed :max-size max-size :reporter-fn reporter-fn)] |
216 |
| - (if-let [[smallest] (-> ret :shrunk :smallest)] |
217 |
| - (unwrap-failing ret) |
218 |
| - ret)))) |
219 |
| - |
220 |
| -(defn check-var |
221 |
| - "Checks a var's specs using test.check. Optional args are |
222 |
| -passed through to test.check/quick-check: |
223 |
| -
|
224 |
| - num-tests number of tests to run, default 100 |
225 |
| - seed random seed |
226 |
| - max-size how large an input to generate, max 200 |
227 |
| - reporter-fn reporting fn |
228 |
| -
|
229 |
| -Returns a map as quick-check, with :explain-data added if |
230 |
| -:result is false." |
231 |
| - [v & opts] |
232 |
| - (let [fnspec (s/get-spec v)] |
233 |
| - (if (:args fnspec) |
234 |
| - (apply check-fn @v fnspec opts) |
235 |
| - (throw (js/Error. (str "No :args spec for " v)))))) |
236 |
| - |
237 |
| -(defn- run-var-tests |
238 |
| - "Helper for run-tests, run-all-tests." |
239 |
| - [vs] |
240 |
| - (let [reporter-fn println] |
241 |
| - (reduce |
242 |
| - (fn [totals v] |
243 |
| - (let [_ (println "Checking" v) |
244 |
| - ret (check-var v :reporter-fn reporter-fn)] |
245 |
| - (prn ret) |
246 |
| - (cond-> totals |
247 |
| - true (update :test inc) |
248 |
| - (true? (:result ret)) (update :pass inc) |
249 |
| - (::s/problems (:result ret)) (update :fail inc) |
250 |
| - (instance? js/Error (:result ret)) (update :error inc)))) |
251 |
| - {:test 0, :pass 0, :fail 0, :error 0} |
252 |
| - vs))) |
| 257 | + "Runs generative tests for fn f using spec and opts. See |
| 258 | +'check' for options and return." |
| 259 | + ([f spec] (check-fn f spec nil)) |
| 260 | + ([f spec opts] |
| 261 | + (validate-check-opts opts) |
| 262 | + (check-1 {:f f :spec spec} opts))) |
| 263 | + |
| 264 | +(defn checkable-syms |
| 265 | + "Given an opts map as per check, returns the set of syms that |
| 266 | +can be checked." |
| 267 | + ([] (checkable-syms nil)) |
| 268 | + ([opts] |
| 269 | + (validate-check-opts opts) |
| 270 | + (reduce into #{} [(filter fn-spec-name? (keys (s/registry))) |
| 271 | + (keys (:spec opts))]))) |
| 272 | + |
| 273 | +(defn check |
| 274 | + "Run generative tests for spec conformance on vars named by |
| 275 | +sym-or-syms, a symbol or collection of symbols. If sym-or-syms |
| 276 | +is not specified, check all checkable vars. |
| 277 | +
|
| 278 | +The opts map includes the following optional keys, where stc |
| 279 | +aliases clojure.spec.test.check: |
| 280 | +
|
| 281 | +::stc/opts opts to flow through test.check/quick-check |
| 282 | +:gen map from spec names to generator overrides |
| 283 | +
|
| 284 | +The ::stc/opts include :num-tests in addition to the keys |
| 285 | +documented by test.check. Generator overrides are passed to |
| 286 | +spec/gen when generating function args. |
| 287 | +
|
| 288 | +Returns a lazy sequence of check result maps with the following |
| 289 | +keys |
| 290 | +
|
| 291 | +:spec the spec tested |
| 292 | +:sym optional symbol naming the var tested |
| 293 | +:failure optional test failure |
| 294 | +::stc/ret optional value returned by test.check/quick-check |
| 295 | +
|
| 296 | +The value for :failure can be any exception. Exceptions thrown by |
| 297 | +spec itself will have an ::s/failure value in ex-data: |
| 298 | +
|
| 299 | +:check-failed at least one checked return did not conform |
| 300 | +:no-args-spec no :args spec provided |
| 301 | +:no-fn no fn provided |
| 302 | +:no-fspec no fspec provided |
| 303 | +:no-gen unable to generate :args |
| 304 | +:instrument invalid args detected by instrument |
| 305 | +" |
| 306 | + ([] (check (checkable-syms))) |
| 307 | + ([sym-or-syms] (check sym-or-syms nil)) |
| 308 | + ([sym-or-syms opts] |
| 309 | + (->> (collectionize sym-or-syms) |
| 310 | + (filter (checkable-syms opts)) |
| 311 | + (map |
| 312 | + #(check-1 (sym->check-map %) opts))))) |
| 313 | + |
| 314 | +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; check reporting ;;;;;;;;;;;;;;;;;;;;;;;; |
| 315 | + |
| 316 | +(defn- failure-type |
| 317 | + [x] |
| 318 | + (::s/failure (ex-data x))) |
253 | 319 |
|
| 320 | +(defn- unwrap-failure |
| 321 | + [x] |
| 322 | + (if (failure-type x) |
| 323 | + (ex-data x) |
| 324 | + x)) |
| 325 | + |
| 326 | +(defn- result-type |
| 327 | + "Returns the type of the check result. This can be any of the |
| 328 | +::s/failure keywords documented in 'check', or: |
| 329 | +
|
| 330 | + :check-passed all checked fn returns conformed |
| 331 | + :check-threw checked fn threw an exception" |
| 332 | + [ret] |
| 333 | + (let [failure (:failure ret)] |
| 334 | + (cond |
| 335 | + (nil? failure) :check-passed |
| 336 | + (failure-type failure) (failure-type failure) |
| 337 | + :default :check-threw))) |
| 338 | + |
| 339 | +(defn abbrev-result |
| 340 | + "Given a check result, returns an abbreviated version |
| 341 | +suitable for summary use." |
| 342 | + [x] |
| 343 | + (if (:failure x) |
| 344 | + (-> (dissoc x ::stc/ret) |
| 345 | + (update :spec s/describe) |
| 346 | + (update :failure unwrap-failure)) |
| 347 | + (dissoc x :spec ::stc/ret))) |
| 348 | + |
| 349 | +(defn summarize-results |
| 350 | + "Given a collection of check-results, e.g. from 'check', pretty |
| 351 | +prints the summary-result (default abbrev-result) of each. |
| 352 | +
|
| 353 | +Returns a map with :total, the total number of results, plus a |
| 354 | +key with a count for each different :type of result." |
| 355 | + ([check-results] (summarize-results check-results abbrev-result)) |
| 356 | + ([check-results summary-result] |
| 357 | + (reduce |
| 358 | + (fn [summary result] |
| 359 | + (pp/pprint (summary-result result)) |
| 360 | + (-> summary |
| 361 | + (update :total inc) |
| 362 | + (update (result-type result) (fnil inc 0)))) |
| 363 | + {:total 0} |
| 364 | + check-results))) |
254 | 365 |
|
255 | 366 | (comment
|
256 | 367 | (require
|
|
0 commit comments