All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cljs.spec.test.alpha.cljs Maven / Gradle / Ivy

;   Copyright (c) Rich Hickey. All rights reserved.
;   The use and distribution terms for this software are covered by the
;   Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;   which can be found in the file epl-v10.html at the root of this distribution.
;   By using this software in any fashion, you are agreeing to be bound by
;   the terms of this license.
;   You must not remove this notice, or any other, from this software.

(ns cljs.spec.test.alpha
  (:require-macros [cljs.spec.test.alpha :as m :refer [with-instrument-disabled]])
  (:require
    [goog.object :as gobj]
    [goog.userAgent.product :as product]
    [clojure.string :as string]
    [cljs.stacktrace :as st]
    [cljs.pprint :as pp]
    [cljs.spec.alpha :as s]
    [cljs.spec.gen.alpha :as gen]
    [clojure.test.check :as stc]
    [clojure.test.check.properties]))

(defn distinct-by
  ([f coll]
   (let [step (fn step [xs seen]
                (lazy-seq
                  ((fn [[x :as xs] seen]
                     (when-let [s (seq xs)]
                       (let [v (f x)]
                         (if (contains? seen v)
                           (recur (rest s) seen)
                           (cons x (step (rest s) (conj seen v)))))))
                    xs seen)))]
     (step coll #{}))))

(defn ->sym
  [x]
  (@#'s/->sym x))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; instrument ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def ^:private ^:dynamic *instrument-enabled*
  "if false, instrumented fns call straight through"
  true)

(defn get-host-port []
  (if (not= "browser" *target*)
    {}
    {:host (.. js/window -location -host)
     :port (.. js/window -location -port)}))

(defn get-ua-product []
  (if (not= "browser" *target*)
    (keyword *target*)
    (cond
      product/SAFARI :safari
      product/CHROME :chrome
      product/FIREFOX :firefox
      product/IE :ie)))

(defn get-env []
  {:ua-product (get-ua-product)})

(defn- fn-spec?
  "Fn-spec must include at least :args or :ret specs."
  [m]
  (or (:args m) (:ret m)))

;; wrap spec/explain-data until specs always return nil for ok data
(defn- explain-data*
  [spec v]
  (when-not (s/valid? spec v nil)
    (s/explain-data spec v)))

(defn- find-caller [st]
  (letfn [(search-spec-fn [frame]
            (when frame
              (let [s (:function frame)]
                (and (string? s) (not (string/blank? s))
                     (re-find #"cljs\.spec\.test\.spec_checking_fn" s)))))]
    (->> st
         (drop-while #(not (search-spec-fn %)))
         (drop-while search-spec-fn)
         first)))

;; TODO: check ::caller result in other browsers - David

(defn- spec-checking-fn
  [v f fn-spec]
  (let [fn-spec (@#'s/maybe-spec fn-spec)
        conform! (fn [v role spec data args]
                   (let [conformed (s/conform spec data)]
                     (if (= ::s/invalid conformed)
                       (let [caller (find-caller
                                      (st/parse-stacktrace
                                        (get-host-port)
                                        (.-stack (js/Error.))
                                        (get-env) nil))
                             ed (merge (assoc (s/explain-data* spec [role] [] [] data)
                                         ::s/args args
                                         ::s/failure :instrument)
                                  (when caller
                                    {::caller caller}))]
                         (throw (ex-info
                                  (str "Call to " v " did not conform to spec:\n" (with-out-str (s/explain-out ed)))
                                  ed)))
                       conformed)))]
    (doto (fn [& args]
            (if *instrument-enabled*
              (with-instrument-disabled
                (when (:args fn-spec) (conform! v :args (:args fn-spec) args args))
                (binding [*instrument-enabled* true]
                  (apply f args)))
              (apply f args)))
      (gobj/extend (MetaFn. (fn [& args]
                              (if *instrument-enabled*
                                (with-instrument-disabled
                                  (when (:args fn-spec) (conform! v :args (:args fn-spec) args args))
                                  (binding [*instrument-enabled* true]
                                    (apply f args)))
                                (apply f args))) nil)))))

(defn- no-fspec
  [v spec]
  (ex-info (str "Fn at " v " is not spec'ed.")
    {:var v :spec spec ::s/failure :no-fspec}))

(defonce ^:private instrumented-vars (atom {}))

(defn- instrument-choose-fn
  "Helper for instrument."
  [f spec sym {over :gen :keys [stub replace]}]
  (if (some #{sym} stub)
    (-> spec (s/gen over) gen/generate)
    (get replace sym f)))

(defn- instrument-choose-spec
  "Helper for instrument"
  [spec sym {overrides :spec}]
  (get overrides sym spec))

(defn- instrument-1*
  [s v opts]
  (let [spec (s/get-spec v)
        {:keys [raw wrapped]} (get @instrumented-vars v)
        current @v
        to-wrap (if (= wrapped current) raw current)
        ospec (or (instrument-choose-spec spec s opts)
                (throw (no-fspec v spec)))
        ofn (instrument-choose-fn to-wrap ospec s opts)
        checked (spec-checking-fn v ofn ospec)]
    (swap! instrumented-vars assoc v {:raw to-wrap :wrapped checked})
    checked))

(defn- unstrument-1*
  [s v]
  (when v
    (when-let [{:keys [raw wrapped]} (get @instrumented-vars v)]
      (swap! instrumented-vars dissoc v)
      (let [current @v]
        (when (= wrapped current)
          raw)))))

(defn- fn-spec-name?
  [s]
  (symbol? s))

(defn- collectionize
  [x]
  (if (symbol? x)
    (list x)
    x))

(defn instrumentable-syms
  "Given an opts map as per instrument, returns the set of syms
that can be instrumented."
  ([] (instrumentable-syms nil))
  ([opts]
   (assert (every? ident? (keys (:gen opts))) "instrument :gen expects ident keys")
   (reduce into #{} [(filter fn-spec-name? (keys (s/registry)))
                     (keys (:spec opts))
                     (:stub opts)
                     (keys (:replace opts))])))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; testing  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- explain-check
  [args spec v role]
  (ex-info
    "Specification-based check failed"
    (when-not (s/valid? spec v nil)
      (assoc (s/explain-data* spec [role] [] [] v)
        ::args args
        ::val v
        ::s/failure :check-failed))))

(defn- check-call
  "Returns true if call passes specs, otherwise *returns* an exception
with explain-data + ::s/failure."
  [f specs args]
  (let [cargs (when (:args specs) (s/conform (:args specs) args))]
    (if (= cargs ::s/invalid)
      (explain-check args (:args specs) args :args)
      (let [ret (apply f args)
            cret (when (:ret specs) (s/conform (:ret specs) ret))]
        (if (= cret ::s/invalid)
          (explain-check args (:ret specs) ret :ret)
          (if (and (:args specs) (:ret specs) (:fn specs))
            (if (s/valid? (:fn specs) {:args cargs :ret cret})
              true
              (explain-check args (:fn specs) {:args cargs :ret cret} :fn))
            true))))))

(defn- quick-check
  [f specs {gen :gen opts ::stc/opts}]
  (let [{:keys [num-tests] :or {num-tests 1000}} opts
        g (try (s/gen (:args specs) gen) (catch js/Error t t))]
    (if (instance? js/Error g)
      {:result g}
      (let [prop (gen/for-all* [g] #(check-call f specs %))]
        (apply gen/quick-check num-tests prop (mapcat identity opts))))))

(defn- make-check-result
  "Builds spec result map."
  [check-sym spec test-check-ret]
  (merge {:spec spec
          ::stc/ret test-check-ret}
    (when check-sym
      {:sym check-sym})
    (when-let [result (-> test-check-ret :result)]
      (when-not (true? result) {:failure result}))
    (when-let [shrunk (-> test-check-ret :shrunk)]
      {:failure (:result shrunk)})))

(defn- validate-check-opts
  [opts]
  (assert (every? ident? (keys (:gen opts))) "check :gen expects ident keys"))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; check reporting  ;;;;;;;;;;;;;;;;;;;;;;;;

(defn- failure-type
  [x]
  (::s/failure (ex-data x)))

(defn- unwrap-failure
  [x]
  (if (failure-type x)
    (ex-data x)
    x))

(defn- result-type
  "Returns the type of the check result. This can be any of the
::s/failure keywords documented in 'check', or:

  :check-passed   all checked fn returns conformed
  :check-threw    checked fn threw an exception"
  [ret]
  (let [failure (:failure ret)]
    (cond
      (nil? failure) :check-passed
      (failure-type failure) (failure-type failure)
      :default :check-threw)))

(defn abbrev-result
  "Given a check result, returns an abbreviated version
suitable for summary use."
  [x]
  (if (:failure x)
    (-> (dissoc x ::stc/ret)
      (update :spec s/describe)
      (update :failure unwrap-failure))
    (dissoc x :spec ::stc/ret)))

(defn summarize-results
  "Given a collection of check-results, e.g. from 'check', pretty
prints the summary-result (default abbrev-result) of each.

Returns a map with :total, the total number of results, plus a
key with a count for each different :type of result."
  ([check-results] (summarize-results check-results abbrev-result))
  ([check-results summary-result]
   (reduce
     (fn [summary result]
       (pp/pprint (summary-result result))
       (-> summary
         (update :total inc)
         (update (result-type result) (fnil inc 0))))
     {:total 0}
     check-results)))

(comment
  (require
    '[cljs.pprint :as pp]
    '[cljs.spec :as s]
    '[cljs.spec.gen :as gen]
    '[cljs.test :as ctest])

  (require :reload '[cljs.spec.test :as test])

  ;; discover speced vars for your own test runner
  (s/speced-vars)

  ;; check a single var
  (test/check-var #'-)
  (test/check-var #'+)
  (test/check-var #'clojure.spec.broken-specs/throwing-fn)

  ;; old style example tests
  (ctest/run-all-tests)

  (s/speced-vars 'clojure.spec.correct-specs)
  ;; new style spec tests return same kind of map
  (test/check-var #'subs)
  (cljs.spec.test/run-tests 'clojure.core)
  (test/run-all-tests)

  ;; example evaluation
  (defn ranged-rand
    "Returns random int in range start <= rand < end"
    [start end]
    (+ start (long (rand (- end start)))))

  (s/fdef ranged-rand
    :args (s/and (s/cat :start int? :end int?)
                 #(< (:start %) (:end %)))
    :ret  int?
    :fn   (s/and #(>= (:ret %) (-> % :args :start))
                 #(< (:ret %) (-> % :args :end))))

  (instrumentable-syms)

  (m/instrument-1 `ranged-rand {})
  (m/unstrument-1 `ranged-rand)

  (m/instrument)
  (m/instrument `ranged-rand)
  (m/instrument `[ranged-rand])

  (m/unstrument)
  (m/unstrument `ranged-rand)
  (m/unstrument `[ranged-rand])

  (ranged-rand 8 5)
  (defn foo
    ([a])
    ([a b]
     (ranged-rand 8 5)))
  (foo 1 2)
  (m/unstrument-1 `ranged-rand)

  (m/check-1 `ranged-rand nil nil {})

  (m/check-fn inc
    (s/fspec
      :args (s/cat :x int?)
      :ret  int?))

  (m/checkable-syms)

  (m/check `ranged-rand)
  )









© 2015 - 2025 Weber Informatics LLC | Privacy Policy