|
| 1 | +; Copyright (c) Rich Hickey. All rights reserved. |
| 2 | +; The use and distribution terms for this software are covered by the |
| 3 | +; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) |
| 4 | +; which can be found in the file epl-v10.html at the root of this distribution. |
| 5 | +; By using this software in any fashion, you are agreeing to be bound by |
| 6 | +; the terms of this license. |
| 7 | +; You must not remove this notice, or any other, from this software. |
| 8 | + |
| 9 | +(ns cljs.spec.test |
| 10 | + (:require-macros [cljs.spec.test :as st]) |
| 11 | + (:require |
| 12 | + [cljs.spec :as spec] |
| 13 | + [cljs.spec.impl.gen :as gen])) |
| 14 | + |
| 15 | +;; wrap spec/explain-data until specs always return nil for ok data |
| 16 | +(defn- explain-data* |
| 17 | + [spec v] |
| 18 | + (when-not (spec/valid? spec v nil) |
| 19 | + (spec/explain-data spec v))) |
| 20 | + |
| 21 | +;; wrap and unwrap spec failure data in an exception so that |
| 22 | +;; quick-check will treat it as a failure. |
| 23 | +(defn- wrap-failing |
| 24 | + [explain-data step] |
| 25 | + (ex-info "Wrapper" {::check-call (assoc explain-data :failed-on step)})) |
| 26 | + |
| 27 | +(defn- unwrap-failing |
| 28 | + [ret] |
| 29 | + (let [ret (if-let [explain (-> ret :result ex-data ::check-call)] |
| 30 | + (assoc ret :result explain) |
| 31 | + ret)] |
| 32 | + (if-let [shrunk-explain (-> ret :shrunk :result ex-data ::check-call)] |
| 33 | + (assoc-in ret [:shrunk :result] shrunk-explain) |
| 34 | + ret))) |
| 35 | + |
| 36 | +(defn- check-call |
| 37 | + "Returns true if call passes specs, otherwise *returns* an exception |
| 38 | +with explain-data plus a :failed-on key under ::check-call." |
| 39 | + [f specs args] |
| 40 | + (let [cargs (when (:args specs) (spec/conform (:args specs) args))] |
| 41 | + (if (= cargs ::spec/invalid) |
| 42 | + (wrap-failing (explain-data* (:args specs) args) :args) |
| 43 | + (let [ret (apply f args) |
| 44 | + cret (when (:ret specs) (spec/conform (:ret specs) ret))] |
| 45 | + (if (= cret ::spec/invalid) |
| 46 | + (wrap-failing (explain-data* (:ret specs) ret) :ret) |
| 47 | + (if (and (:args specs) (:ret specs) (:fn specs)) |
| 48 | + (if (spec/valid? (:fn specs) {:args cargs :ret cret}) |
| 49 | + true |
| 50 | + (wrap-failing (explain-data* (:fn specs) {:args cargs :ret cret}) :fn)) |
| 51 | + true)))))) |
| 52 | + |
| 53 | +(defn check-fn |
| 54 | + "Check a function using provided specs and test.check. |
| 55 | +Same options and return as check-var" |
| 56 | + [f specs |
| 57 | + & {:keys [num-tests seed max-size reporter-fn] |
| 58 | + :or {num-tests 100 max-size 200 reporter-fn (constantly nil)}}] |
| 59 | + (let [g (spec/gen (:args specs)) |
| 60 | + prop (gen/for-all* [g] #(check-call f specs %))] |
| 61 | + (let [ret (gen/quick-check num-tests prop :seed seed :max-size max-size :reporter-fn reporter-fn)] |
| 62 | + (if-let [[smallest] (-> ret :shrunk :smallest)] |
| 63 | + (unwrap-failing ret) |
| 64 | + ret)))) |
| 65 | + |
| 66 | +(defn check-var |
| 67 | + "Checks a var's specs using test.check. Optional args are |
| 68 | +passed through to test.check/quick-check: |
| 69 | +
|
| 70 | + num-tests number of tests to run, default 100 |
| 71 | + seed random seed |
| 72 | + max-size how large an input to generate, max 200 |
| 73 | + reporter-fn reporting fn |
| 74 | +
|
| 75 | +Returns a map as quick-check, with :explain-data added if |
| 76 | +:result is false." |
| 77 | + [v & opts] |
| 78 | + (let [specs (spec/fn-specs v)] |
| 79 | + (if (:args specs) |
| 80 | + (apply check-fn @v specs opts) |
| 81 | + (throw (js/Error. (str "No :args spec for " v)))))) |
| 82 | + |
| 83 | +(defn- run-var-tests |
| 84 | + "Helper for run-tests, run-all-tests." |
| 85 | + [vs] |
| 86 | + (let [reporter-fn println] |
| 87 | + (reduce |
| 88 | + (fn [totals v] |
| 89 | + (let [_ (println "Checking" v) |
| 90 | + ret (check-var v :reporter-fn reporter-fn)] |
| 91 | + (prn ret) |
| 92 | + (cond-> totals |
| 93 | + true (update :test inc) |
| 94 | + (true? (:result ret)) (update :pass inc) |
| 95 | + (::spec/problems (:result ret)) (update :fail inc) |
| 96 | + (instance? js/Error (:result ret)) (update :error inc)))) |
| 97 | + {:test 0, :pass 0, :fail 0, :error 0} |
| 98 | + vs))) |
| 99 | + |
| 100 | + |
| 101 | +(comment |
| 102 | + (require '[cljs.pprint :as pp] |
| 103 | + '[cljs.spec :as s] |
| 104 | + '[cljs.spec.impl.gen :as gen] |
| 105 | + '[cljs.test :as ctest]) |
| 106 | + |
| 107 | + (require :reload '[cjls.spec.test :as test]) |
| 108 | + |
| 109 | + ;; discover speced vars for your own test runner |
| 110 | + (s/speced-vars) |
| 111 | + |
| 112 | + ;; check a single var |
| 113 | + (test/check-var #'-) |
| 114 | + (test/check-var #'+) |
| 115 | + (test/check-var #'clojure.spec.broken-specs/throwing-fn) |
| 116 | + |
| 117 | + ;; old style example tests |
| 118 | + (ctest/run-all-tests) |
| 119 | + |
| 120 | + (s/speced-vars 'clojure.spec.correct-specs) |
| 121 | + ;; new style spec tests return same kind of map |
| 122 | + (test/check-var #'subs) |
| 123 | + (cljs.spec.test/run-tests 'clojure.core) |
| 124 | + (test/run-all-tests) |
| 125 | + |
| 126 | + ) |
| 127 | + |
| 128 | + |
| 129 | + |
| 130 | + |
| 131 | + |
0 commit comments