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

cljs.spec.test.alpha.cljc 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
    [cljs.analyzer :as ana]
    [cljs.analyzer.api :as ana-api]
    [clojure.string :as string]
    [cljs.spec.alpha :as s]
    [cljs.spec.gen.alpha :as gen]))

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

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

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

(defmacro with-instrument-disabled
  "Disables instrument's checking of calls, within a scope."
  [& body]
  `(binding [*instrument-enabled* nil]
     ~@body))

(defmacro instrument-1
  [[quote s] opts]
  (when-let [v (ana-api/resolve &env s)]
    (when (and (nil? (:const v))
               #?(:cljs (nil? (:macro v))))
      (swap! instrumented-vars conj (:name v))
      `(let [checked# (instrument-1* '~s (var ~s) ~opts)]
         (when checked# (set! ~s checked#))
         '~(:name v)))))

(defmacro unstrument-1
  [[quote s]]
  (when-let [v (ana-api/resolve &env s)]
    (when (@instrumented-vars (:name v))
      (swap! instrumented-vars disj (:name v))
      `(let [raw# (unstrument-1* ~s (var ~s))]
         (when raw# (set! ~s raw#))
         '~(:name v)))))

(defn- sym-or-syms->syms [sym-or-syms]
  (into []
    (mapcat
      (fn [sym]
        (if (and (string/includes? (str sym) ".")
                 (ana-api/find-ns sym))
          (->> (vals (ana-api/ns-interns sym))
            (filter #(not (:macro %)))
            (map :name)
            (map
              (fn [name-sym]
                (symbol (name sym) (name name-sym)))))
          [sym])))
    (collectionize sym-or-syms)))

(defmacro instrument
  "Instruments the vars named by sym-or-syms, a symbol or collection
of symbols, or all instrumentable vars if sym-or-syms is not
specified. If a symbol identifies a namespace then all symbols in that
namespace will be enumerated.

If a var has an :args fn-spec, sets the var's root binding to a
fn that checks arg conformance (throwing an exception on failure)
before delegating to the original fn.

The opts map can be used to override registered specs, and/or to
replace fn implementations entirely. Opts for symbols not included
in sym-or-syms are ignored. This facilitates sharing a common
options map across many different calls to instrument.

The opts map may have the following keys:

  :spec     a map from var-name symbols to override specs
  :stub     a set of var-name symbols to be replaced by stubs
  :gen      a map from spec names to generator overrides
  :replace  a map from var-name symbols to replacement fns

:spec overrides registered fn-specs with specs your provide. Use
:spec overrides to provide specs for libraries that do not have
them, or to constrain your own use of a fn to a subset of its
spec'ed contract.

:stub replaces a fn with a stub that checks :args, then uses the
:ret spec to generate a return value.

:gen overrides are used only for :stub generation.

:replace replaces a fn with a fn that checks args conformance, then
invokes the fn you provide, enabling arbitrary stubbing and mocking.

:spec can be used in combination with :stub or :replace.

Returns a collection of syms naming the vars instrumented."
  ([]
   `(instrument '[~@(#?(:clj  s/speced-vars
                        :cljs cljs.spec.alpha$macros/speced-vars))]))
  ([xs]
   `(instrument ~xs nil))
  ([sym-or-syms opts]
   (let [syms (sym-or-syms->syms (eval sym-or-syms))
         opts-sym (gensym "opts")]
     `(let [~opts-sym ~opts]
        (reduce
          (fn [ret# [_# f#]]
            (let [sym# (f#)]
              (cond-> ret# sym# (conj sym#))))
          []
          (->> (zipmap '~syms
                 [~@(map
                      (fn [sym]
                        `(fn [] (instrument-1 '~sym ~opts-sym)))
                      syms)])
            (filter #((instrumentable-syms ~opts-sym) (first %)))
            (distinct-by first)))))))

(defmacro unstrument
  "Undoes instrument on the vars named by sym-or-syms, specified
as in instrument. With no args, unstruments all instrumented vars.
Returns a collection of syms naming the vars unstrumented."
  ([]
   `(unstrument '[~@(deref instrumented-vars)]))
  ([sym-or-syms]
   (let [syms (sym-or-syms->syms (eval sym-or-syms))]
     `(reduce
        (fn [ret# f#]
          (let [sym# (f#)]
            (cond-> ret# sym# (conj sym#))))
        []
        [~@(->> syms
             (map
               (fn [sym]
                 (when (symbol? sym)
                   `(fn []
                      (unstrument-1 '~sym)))))
             (remove nil?))]))))

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

(defmacro check-1
  [[quote s :as qs] f spec opts]
  (let [{:keys [name] :as v} (when qs (ana-api/resolve &env s))]
    `(let [s#        '~name
           opts#     ~opts
           v#        ~(when v `(var ~name))
           spec#     (or ~spec ~(when v `(s/get-spec (var ~name))))
           re-inst?# (and v# (seq (unstrument '~name)) true)
           f#        (or ~f (when v# @v#))]
       (try
         (cond
           (nil? f#)
           {:failure (ex-info "No fn to spec" {::s/failure :no-fn})
            :sym     s# :spec spec#}

           (:args spec#)
           (let [tcret# (quick-check f# spec# opts#)]
             (make-check-result s# spec# tcret#))

           :default
           {:failure (ex-info "No :args spec" {::s/failure :no-args-spec})
            :sym     s# :spec spec#})
         (finally
           (when re-inst?# (instrument '~name)))))))

(defmacro check-fn
  "Runs generative tests for fn f using spec and opts. See
'check' for options and return."
  ([f spec]
   `(check-fn ~f ~spec nil))
  ([f spec opts]
   `(let [opts# ~opts]
      (validate-check-opts opts#)
      (check-1 nil ~f ~spec opts#))))

(defn checkable-syms*
  ([]
    (checkable-syms* nil))
  ([opts]
   (reduce into #{}
     [(filter fn-spec-name? (keys @s/registry-ref))
      (keys (:spec opts))])))

(defmacro checkable-syms
  "Given an opts map as per check, returns the set of syms that
can be checked."
  ([]
   `(checkable-syms nil))
  ([opts]
   `(let [opts# ~opts]
      (validate-check-opts opts#)
      (reduce conj #{}
        '[~@(filter fn-spec-name? (keys @s/registry-ref))
          ~@(keys (:spec opts))]))))

(defmacro check
  "Run generative tests for spec conformance on vars named by
sym-or-syms, a symbol or collection of symbols. If sym-or-syms
is not specified, check all checkable vars. If a symbol identifies a
namespace then all symbols in that namespace will be enumerated.

The opts map includes the following optional keys, where stc
aliases clojure.test.check:

::stc/opts  opts to flow through test.check/quick-check
:gen        map from spec names to generator overrides

The ::stc/opts include :num-tests in addition to the keys
documented by test.check. Generator overrides are passed to
spec/gen when generating function args.

Returns a lazy sequence of check result maps with the following
keys

:spec       the spec tested
:sym        optional symbol naming the var tested
:failure    optional test failure
::stc/ret   optional value returned by test.check/quick-check

The value for :failure can be any exception. Exceptions thrown by
spec itself will have an ::s/failure value in ex-data:

:check-failed   at least one checked return did not conform
:no-args-spec   no :args spec provided
:no-fn          no fn provided
:no-fspec       no fspec provided
:no-gen         unable to generate :args
:instrument     invalid args detected by instrument
"
  ([]
   `(check '~(checkable-syms*)))
  ([sym-or-syms]
   `(check ~sym-or-syms nil))
  ([sym-or-syms opts]
   (let [syms (sym-or-syms->syms (eval sym-or-syms))
         opts-sym (gensym "opts")]
     `(let [~opts-sym ~opts]
        [~@(->> syms
             (filter (checkable-syms* opts))
             (map
               (fn [sym]
                 (do `(check-1 '~sym nil nil ~opts-sym)))))]))))




© 2015 - 2025 Weber Informatics LLC | Privacy Policy