diff --git a/.gitignore b/.gitignore index ef84011f..62647301 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ pubspec.lock lib/cljd-out/* test/cljd-out/* +**/lib/cljd-out/ +**/test/cljd-out/ +**/pubspec.lock +**/.dart_tool/ *-e .* !.github diff --git a/clj/src/cljd/compiler.cljc b/clj/src/cljd/compiler.cljc index b734f511..a7314ea3 100644 --- a/clj/src/cljd/compiler.cljc +++ b/clj/src/cljd/compiler.cljc @@ -1404,6 +1404,7 @@ nil)] (cond (env f) form + (= 'def f) form (or (= 'cljd.core/defprotocol f) (= 'defprotocol f)) (apply expand-defprotocol args) (or (= 'cljd.core/extend-type f) (= 'extend-type f)) (apply expand-extend-type args) @@ -2599,7 +2600,12 @@ name (when (symbol? (first bodies)) (first bodies)) bodies (cond-> bodies name next) [body & more-bodies :as bodies] (ensure-bodies bodies) - fn-type (if (or more-bodies (variadic? body)) :ifn :native) + ;; ^:cljd.spec.alpha/redef defs need an IFn body so the field + ;; can also hold an instrumented IFn wrapper without a type + ;; clash. + fn-type (if (or more-bodies (variadic? body) + (:cljd.spec.alpha/redef (meta var-name))) + :ifn :native) env (cond-> env name (assoc name (vary-meta (dart-local name env) #(assoc % @@ -3157,7 +3163,7 @@ (list (list 'dart/fn () :positional () false (list 'dart/let bindings dart-expr))) dart-expr)) -(declare write-top-dartfn write-top-field write-dynamic-var-top-field write-annotations) +(declare write-top-dartfn write-top-field write-dynamic-var-top-field write-redef-top-field write-annotations) (defn dart-qualify [dartname] (with-meta (symbol (str (dart-alias-for-ns *current-ns*) "." dartname)) @@ -3372,10 +3378,17 @@ (throw (ex-info "Too many arguments to def" {}))) sym (vary-meta sym assoc :doc doc-string) expr (macroexpand env expr) - kind (case (:dart/fn-type (dart-meta sym env)) - :native :dart - :ifn :clj - (fn-kind expr)) + kind (cond + ;; ^:cljd.spec.alpha/redef defs are stored as IFn-typed + ;; cells so that instrument can swap a wrapper (also an + ;; IFn) into the field. Forcing :clj here ensures both + ;; the original body and the wrapper share the IFn + ;; calling convention. + (:cljd.spec.alpha/redef (meta sym)) :clj + :else (case (:dart/fn-type (dart-meta sym env)) + :native :dart + :ifn :clj + (fn-kind expr))) sym (cond-> sym kind (vary-meta assoc kind true) (:macro (meta sym)) (vary-meta assoc :macro-host-fn expr) @@ -3401,6 +3414,7 @@ expr)) sub-type (cond (:dynamic (meta sym)) :dynamic ; dynamic as in dynamic var, not dc.dynamic + (:cljd.spec.alpha/redef (meta sym)) :cljd.spec.alpha/redef (:no-reload (meta sym)) :defonce dart-type :fn :else :field) @@ -3413,10 +3427,16 @@ ; predecl so that the def is visible in recursive defs (when-not (:cljd/contrib (meta sym)) ; don't predecl if the def is a contribution, since it must have been created first - (swap! nses do-def sym {:dart/qname dartqname - :dart/name dartname - :type :field - :dart/type dart-type})) + (swap! nses do-def sym (cond-> {:dart/qname dartqname + :dart/name dartname + :type :field + :dart/type dart-type} + (= sub-type :cljd.spec.alpha/redef) + (assoc :cljd.spec.alpha/redef? true) + ;; Cache the macroexpanded body so a later + ;; macro (e.g. s/fdef) can re-emit this def + ;; with different metadata. + (some? expr) (assoc :source-expr expr)))) (emit expr env)) dart-annotations (when-not *host-eval* (into [] (map #(emit % env)) (-> form meta :annotations))) @@ -3428,6 +3448,8 @@ (:dynamic (meta sym)) (let [k (symbol (name *current-ns*) (name sym))] (write-dynamic-var-top-field k dartname dart-fn)) + (:cljd.spec.alpha/redef (meta sym)) + (write-redef-top-field dartname dart-fn) (and (seq? expr) (= 'fn* (first expr)) (not (symbol? (second expr)))) (write-top-dartfn dartname (or @@ -4048,6 +4070,29 @@ (write (emit `(cljd.core/set-dynamic-binding! '~k ~'v) '{v v}) expr-locus {}) (dart-print ";\n"))) +(defn write-redef-top-field + "Emits a mutable backing field plus a getter and setter for + ^:cljd.spec.alpha/redef defs. Unlike ^:dynamic, no Zone-binding + indirection is involved: the setter writes the backing field + directly so callers see the new value persistently and from any + namespace." + [dart-sym x] + (let [root-sym (symbol (str dart-sym "$root")) + type (-> dart-sym meta :dart/type (or dc-dynamic))] + (write-top-field root-sym x) + (write-type type) + (dart-print " get ") + (dart-print dart-sym) + (dart-print " => ") + (dart-print (str root-sym)) + (dart-print ";\nset ") + (dart-print dart-sym) + (dart-print "(") + (write-type type) + (dart-print " v) => ") + (dart-print (str root-sym)) + (dart-print " = v;\n"))) + (defn- write-args [args aliases] (let [[positionals nameds] (split-with (complement keyword?) args)] (dart-print "(") diff --git a/clj/src/cljd/core.cljd b/clj/src/cljd/core.cljd index 51867f0a..de668e19 100644 --- a/clj/src/cljd/core.cljd +++ b/clj/src/cljd/core.cljd @@ -902,6 +902,12 @@ (int? n) (dart/is? n BigInt))) +(defn rational? + "Return true if n is a rational number. ClojureDart has no ratio + type, so this is currently the same as integer?." + [n] + (integer? n)) + (defn ^int int "Coerce to int" [x] @@ -9539,6 +9545,17 @@ specified. `(do ~@body) triples))) +(defonce ^:private -multimethods (atom {})) + +(defn methods + "Returns the dispatch-value to method map for a ClojureDart + multimethod, or nil when unavailable." + [mm] + (or (some-> @-multimethods (get mm) :table) + (some (fn [[_ {:keys [table] f :fn}]] + (when (identical? f mm) table)) + @-multimethods))) + (defn -mk-multimethod [mm-name dispatch table default] (let [resolve (fn [dv] @@ -9548,12 +9565,14 @@ specified. (throw (ArgumentError. (str "No method in multimethod `" mm-name "` for dispatch value: " (pr-str dv))))))] - (fn - ([] ((resolve (dispatch)))) - ([a] ((resolve (dispatch a)) a)) - ([a b] ((resolve (dispatch a b)) a b)) - ([a b c] ((resolve (dispatch a b c)) a b c)) - ([a b c d] ((resolve (dispatch a b c d)) a b c d)) - ([a b c d & rest] - (apply (resolve (apply dispatch a b c d rest)) - a b c d rest))))) + (let [f (fn + ([] ((resolve (dispatch)))) + ([a] ((resolve (dispatch a)) a)) + ([a b] ((resolve (dispatch a b)) a b)) + ([a b c] ((resolve (dispatch a b c)) a b c)) + ([a b c d] ((resolve (dispatch a b c d)) a b c d)) + ([a b c d & rest] + (apply (resolve (apply dispatch a b c d rest)) + a b c d rest)))] + (swap! -multimethods assoc mm-name {:fn f :table table :default default}) + f))) diff --git a/clj/src/cljd/spec/alpha.cljd b/clj/src/cljd/spec/alpha.cljd new file mode 100644 index 00000000..07880342 --- /dev/null +++ b/clj/src/cljd/spec/alpha.cljd @@ -0,0 +1,751 @@ +; 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. + +;;; Public API for cljd.spec.alpha. Runtime lives in cljd.spec.alpha.impl. +;;; +;;; ClojureScript-compatible macro names preserve source forms. Runtime +;;; helper fns with `*` suffixes remain below for implementation use and +;;; for true value-style construction where no source form exists. + +(ns cljd.spec.alpha + (:require [cljd.spec.alpha.impl :as impl] + [cljd.spec.gen.alpha :as gen])) + +;; Re-exports as defn wrappers (avoid cljd's top-level IFn lifting). + +(def ^:dynamic *recursion-limit* 4) +(def ^:dynamic *fspec-iterations* 21) +(def ^:dynamic *coll-check-limit* 101) +(def ^:dynamic *coll-error-limit* 20) + +(def ^:macro-support ^:dynamic *compile-asserts* + "If true (the default), s/assert macro expands to a runtime check; + otherwise it expands to its value argument and the spec is dropped + entirely. Bind to false at macro-expansion time to compile asserts + out of production builds." + true) + +(def ^:skip-wiki Spec impl/Spec) +(def ^:skip-wiki Specize impl/Specize) + +(defn ^:skip-wiki conform* [spec x] (impl/conform* spec x)) +(defn ^:skip-wiki unform* [spec y] (impl/unform* spec y)) +(defn ^:skip-wiki explain* [spec path via in x] (impl/explain* spec path via in x)) +(defn ^:skip-wiki gen* [spec overrides path rm] (impl/gen* spec overrides path rm)) +(defn ^:skip-wiki with-gen* [spec gfn] (impl/with-gen* spec gfn)) +(defn ^:skip-wiki describe* [spec] (impl/describe* spec)) +(defn ^:skip-wiki specize* + ([x] (impl/specize* x)) + ([x form] (impl/specize* x form))) + +(defn spec? [x] (impl/spec? x)) +(defn regex? [x] (impl/regex? x)) +(defn abbrev [form] (impl/abbrev form)) +(defn invalid? [ret] (impl/invalid? ret)) +(defn conform [spec x] (impl/conform spec x)) +(defn unform [spec x] (impl/unform spec x)) +(defn form [spec] (impl/form spec)) +(defn describe [spec] (impl/describe spec)) +(defn ^:skip-wiki explain-data* + [spec path via in x] + (impl/explain-data* spec path via in x)) +(defn explain-data [spec x] (impl/explain-data spec x)) +(defn valid? + ([spec x] (impl/valid? spec x)) + ([spec x form] (impl/valid? spec x form))) +(defn explain-printer [ed] (impl/explain-out ed)) +(def ^:dynamic *explain-out* explain-printer) +(defn explain-out [ed] (*explain-out* ed)) +(defn explain [spec x] (explain-out (explain-data spec x))) +(defn explain-str [spec x] (with-out-str (explain spec x))) +(defn with-gen [spec f] (impl/with-gen spec f)) +(defn gen + ([spec] (impl/gen spec)) + ([spec overrides] (impl/gen spec overrides))) +(defn exercise + ([spec] (impl/exercise spec)) + ([spec n] (impl/exercise spec n)) + ([spec n overrides] (impl/exercise spec n overrides))) +(defn ^:skip-wiki exercise-fn* + "Runtime form of `exercise-fn`. Takes a function value (not a + symbol), an optional sample count, and an explicit fspec. Use when + the call site has a fn value rather than a symbol resolvable at + macro-expansion time." + ([f fspec] (impl/exercise-fn f 10 fspec)) + ([f n fspec] (impl/exercise-fn f n fspec))) + +(defn exercise-fn-runtime + "Runtime variant of `exercise-fn` that takes a fully-qualified + symbol naming a ^:cljd.spec.alpha/redef def. Looks up the current + fn value via the redef registry and the fspec via `get-spec`, so + this works from a non-macro context — including for symbols held in + runtime-bound locals." + ([sym] (exercise-fn-runtime sym 10)) + ([sym n] (exercise-fn-runtime sym n nil)) + ([sym n fspec] + (let [getter (impl/redef-getter sym) + f (when getter (getter)) + spec (clojure.core/or fspec (get-spec sym))] + (when (nil? f) + (throw (Exception. + (str "exercise-fn-runtime: no ^:cljd.spec.alpha/redef " + "registration for " sym + " — only fdef'd defs are reachable at runtime")))) + (when (nil? spec) + (throw (Exception. + (str "exercise-fn-runtime: no fspec for " sym)))) + (impl/exercise-fn f n spec)))) +(defn check-asserts? [] (impl/check-asserts?)) +(defn check-asserts [flag] (impl/check-asserts flag)) +(defn assert* [spec x] + (if (valid? spec x) + x + (let [ed (assoc (explain-data spec x) + :cljd.spec.alpha/failure :assertion-failed)] + (throw (ex-info (str "Spec assertion failed\n" + (with-out-str (explain-out ed))) + ed))))) + +(declare ensure-internal-specs!) + +(defn registry [] + (ensure-internal-specs!) + (impl/registry)) + +(defn get-spec [k] + (ensure-internal-specs!) + (impl/get-spec k)) + +(defn ^:skip-wiki kvs->map + "Helper that converts a sequence of {:cljd.spec.alpha/k k :cljd.spec.alpha/v v} + maps (as produced by `s/keys*`'s inner regex) into a single map of + k->v. Exposed so that the form `(cljd.spec.alpha/conformer cljd.spec.alpha/kvs->map)` + emitted by `keys*` resolves to a real fn." + [xs] + (into {} + (map (fn [m] + [(:cljd.spec.alpha/k m) + (:cljd.spec.alpha/v m)])) + xs)) + +(defn- map->kvs [m] + (map (fn [[k v]] + {:cljd.spec.alpha/k k + :cljd.spec.alpha/v v}) + m)) + +(defonce ^:private kvs->map-spec-registration + (impl/def-impl :cljd.spec.alpha/kvs->map + '(cljd.spec.alpha/conformer cljd.spec.alpha/kvs->map) + (impl/spec-impl + '(cljd.spec.alpha/conformer cljd.spec.alpha/kvs->map) + kvs->map + nil + true + map->kvs))) + +(defn- ensure-internal-specs! [] + (when (nil? kvs->map-spec-registration) + nil) + nil) + +(defonce ^:private ^:macro-support speced-vars-ref (atom #{})) + +(defn- ^:macro-support collectionize [x] + (if (symbol? x) (list x) x)) + +(defn ^:skip-wiki ^:macro-support speced-vars + "Returns symbols that have been registered with s/def or s/fdef in + compiled code. In ClojureDart this is populated by macros so + cljd.spec.test.alpha can discover instrumentable vars." + ([] @speced-vars-ref) + ([ns-sym-or-syms] + (let [nses (set (collectionize ns-sym-or-syms))] + (into #{} + (filter #(when-let [ns-name (namespace %)] + (contains? nses (symbol ns-name)))) + @speced-vars-ref)))) + +(defn- ^:macro-support remember-speced-var! [sym fdef-ns] + (swap! speced-vars-ref conj (vary-meta sym assoc :fdef-ns fdef-ns)) + sym) + +(defn- ^:macro-support qualify-symbol [env sym] + (if (namespace sym) + sym + (if-let [current-ns (get-in env [:nses :current-ns])] + (symbol (str current-ns) (name sym)) + sym))) + +(defn- ^:macro-support resolve-form-symbol [env sym] + (let [sym-ns (namespace sym) + nses (:nses env) + current-ns-sym (:current-ns nses) + current-ns (nses current-ns-sym)] + (cond + (clojure.core/and sym-ns (not= sym-ns (str current-ns-sym))) + (let [lib (get-in current-ns [:clj-aliases sym-ns]) + target-ns (get-in nses [:libs lib :ns])] + (if target-ns + (symbol (str target-ns) (name sym)) + sym)) + + sym-ns + sym + + (env sym) + sym + + :else + (clojure.core/or (get-in current-ns [:mappings sym]) + (when (contains? current-ns sym) + (symbol (str current-ns-sym) (name sym))) + sym)))) + +(defn- ^:macro-support qualified-form [env form] + (cond + (symbol? form) + (resolve-form-symbol env form) + + (seq? form) + (let [op (first form)] + (if (#{'quote 'fn 'fn* 'cljd.core/fn 'cljd.core/fn*} op) + form + (apply list (map #(qualified-form env %) form)))) + + (vector? form) + (vec (map #(qualified-form env %) form)) + + (map? form) + (into {} (map (fn [[k v]] + [(qualified-form env k) (qualified-form env v)]) + form)) + + (set? form) + (set (map #(qualified-form env %) form)) + + :else + form)) + +(defn- ^:macro-support qualified-forms [env forms] + (mapv #(qualified-form env %) forms)) + +(defn- ^:macro-support qualified-opts [env opts] + (mapv #(qualified-form env %) opts)) + +(defn- ^:macro-support def-form [env k spec-form] + (let [current-ns (get-in env [:nses :current-ns]) + k (if (symbol? k) + (remember-speced-var! (qualify-symbol env k) current-ns) + k)] + `(impl/def-impl '~k '~(qualified-form env spec-form) ~spec-form))) + +(defn- ^:macro-support def-call-form [env k spec-form] + (let [current-ns (get-in env [:nses :current-ns]) + sym? (symbol? k) + k (if sym? + (remember-speced-var! (qualify-symbol env k) current-ns) + k)] + (if sym? + `(do + (swap! speced-vars-ref conj '~k) + (impl/def-impl '~k '~(qualified-form env spec-form) ~spec-form)) + `(impl/def-impl '~k '~(qualified-form env spec-form) ~spec-form)))) + +;; Form-preserving macros. + +(defmacro ^:skip-wiki def-spec + "ClojureDart-only synonym for `s/def`. Note: top-level (s/def-spec ...) + calls do not auto-initialize at module load on Dart — call from a + function body (e.g. main) until cljd grows eager top-level init." + [k spec-form] + (def-form &env k spec-form)) + +(defmacro ^:skip-wiki and-spec + "ClojureDart-only synonym for `s/and`." + [& pred-forms] + `(impl/and-spec-impl '~(qualified-forms &env pred-forms) ~(vec pred-forms) nil)) + +(defmacro ^:skip-wiki or-spec + "ClojureDart-only synonym for `s/or`." + [& key-pred-forms] + (let [pairs (partition 2 key-pred-forms) + ks (mapv first pairs) + preds (mapv second pairs) + forms (qualified-forms &env preds)] + (clojure.core/assert (clojure.core/and (even? (count key-pred-forms)) + (every? keyword? ks)) + "or-spec expects k1 p1 k2 p2..., where ks are keywords") + `(impl/or-spec-impl ~ks '~forms ~preds nil))) + +(defmacro ^:skip-wiki keys-spec + "ClojureDart-only synonym for `s/keys`." + [& opts] + (let [gfn (:gen (apply hash-map opts))] + `(impl/map-spec-impl (impl/keys-args '~opts ~gfn)))) + +(defmacro keys + "Creates and returns a map-validating spec for :req/:opt/:req-un/:opt-un." + [& opts] + (let [gfn (:gen (apply hash-map opts))] + `(impl/map-spec-impl (impl/keys-args '~opts ~gfn)))) + +(defmacro spec + "Takes a single predicate form and returns a spec. Supports + `:gen generator-fn`." + ([form] + `(impl/spec-impl '~(qualified-form &env form) ~form nil nil)) + ([form gen-key gen-fn] + (assert (= :gen gen-key) "spec only supports :gen") + `(impl/spec-impl '~(qualified-form &env form) ~form ~gen-fn nil))) + +(defmacro conformer + "Takes a conforming function, and optionally an unformer, and returns + a spec that treats :cljd.spec.alpha/invalid as failure." + ([f] + `(impl/spec-impl '~(list 'cljd.spec.alpha/conformer (qualified-form &env f)) ~f nil true)) + ([f unf] + `(impl/spec-impl '~(list 'cljd.spec.alpha/conformer + (qualified-form &env f) + (qualified-form &env unf)) + ~f nil true ~unf))) + +(declare ^:macro-support resolve-def-info) + +(defn- ^:macro-support resolve-def-info + "Looks up a (possibly qualified) symbol's def info in the analyzer + state attached to env. Returns the info map or nil." + [env sym] + (let [nses (:nses env) + current-ns-sym (:current-ns nses) + current-ns (nses current-ns-sym) + mappings (:mappings current-ns)] + (when-not (env sym) + (clojure.core/or (current-ns sym) + (if-some [v (mappings sym)] + (resolve-def-info env v) + (let [sym-ns (namespace sym)] + (some-> sym-ns symbol nses (get (symbol (name sym)))))))))) + +(defmacro multi-spec + "Takes the name of a spec/predicate-returning multimethod and a + tag-restoring keyword or fn." + [mm retag] + (let [form (if (symbol? mm) (qualify-symbol &env mm) mm)] + `(impl/multi-spec-impl '~form ~mm ~retag '~retag nil))) + +(defmacro merge + "Takes map-validating specs and returns a spec that merges their + conformed maps." + [& pred-forms] + `(impl/merge-spec-impl '~(qualified-forms &env pred-forms) ~(vec pred-forms) nil)) + +(defn int-in-range? + "Return true if start <= val, val < end and val is an integer." + [start end val] + (impl/int-in-range? start end val)) + +(defmacro int-in + "Returns a spec for integers in the range start <= x < end." + [start end] + `(impl/spec-impl (list 'cljd.spec.alpha/int-in ~start ~end) + (fn [x#] (impl/int-in-range? ~start ~end x#)) + (fn [] (gen/large-integer* {:min ~start :max (dec ~end)})) + nil)) + +(defn inst-in-range? + "Return true if inst at or after start and before end." + [start end inst] + (impl/inst-in-range? start end inst)) + +(defmacro inst-in + "Returns a spec for instants in the range start <= x < end." + [start end] + `(impl/spec-impl (list 'cljd.spec.alpha/inst-in ~start ~end) + (fn [x#] (impl/inst-in-range? ~start ~end x#)) + (fn [] + (gen/fmap #(DateTime/fromMillisecondsSinceEpoch %) + (gen/large-integer* + {:min (.-millisecondsSinceEpoch ^DateTime ~start) + :max (dec (.-millisecondsSinceEpoch ^DateTime ~end))}))) + nil)) + +(defmacro double-in + "Returns a spec for doubles constrained by :min/:max and + :infinite?/:NaN? options." + [& opts] + `(let [opts# (hash-map ~@opts)] + (impl/spec-impl (cons 'cljd.spec.alpha/double-in '~opts) + (fn [x#] (impl/double-in-range? opts# x#)) + (fn [] (gen/double* opts#)) + nil))) + +(defmacro * + "Returns a regex op that matches zero or more values matching pred." + [pred-form] + `(impl/rep-impl '~(qualified-form &env pred-form) ~pred-form)) + +(defmacro + + "Returns a regex op that matches one or more values matching pred." + [pred-form] + `(impl/rep+impl '~(qualified-form &env pred-form) ~pred-form)) + +(defmacro ? + "Returns a regex op that matches zero or one value matching pred." + [pred-form] + `(impl/maybe-impl ~pred-form '~(qualified-form &env pred-form))) + +(defmacro alt + "Regex alternation over key/predicate pairs." + [& key-pred-forms] + (let [pairs (partition 2 key-pred-forms) + ks (mapv first pairs) + preds (mapv second pairs) + forms (qualified-forms &env preds)] + (clojure.core/assert (clojure.core/and (even? (count key-pred-forms)) + (every? keyword? ks)) + "alt expects k1 p1 k2 p2..., where ks are keywords") + `(impl/alt-impl ~ks ~preds '~forms))) + +(defmacro cat + "Regex concatenation over key/predicate pairs." + [& key-pred-forms] + (let [pairs (partition 2 key-pred-forms) + ks (mapv first pairs) + preds (mapv second pairs) + forms (qualified-forms &env preds)] + (clojure.core/assert (clojure.core/and (even? (count key-pred-forms)) + (every? keyword? ks)) + "cat expects k1 p1 k2 p2..., where ks are keywords") + `(impl/cat-impl ~ks ~preds '~forms))) + +(defmacro & + "Applies additional predicates to a regex op's conformed value." + [re & preds] + `(impl/amp-impl ~re '~(qualified-form &env re) + ~(vec preds) '~(qualified-forms &env preds))) + +(defmacro fspec + "Creates a function spec." + [& specs] + (let [m (apply hash-map specs) + args (:args m) + ret (if (contains? m :ret) (:ret m) 'any?) + fnspec (:fn m) + gen (:gen m)] + `(impl/fspec-impl ~(when args `(spec ~args)) '~args + (spec ~ret) '~ret + ~(when fnspec `(spec ~fnspec)) '~fnspec + ~gen))) + +(defmacro fdef + "Registers an fspec under a function symbol. If a defn for fn-sym + has already been compiled in the current namespace, fdef also + re-emits it as ^:cljd.spec.alpha/redef so + cljd.spec.test.alpha/instrument can install a checking wrapper + without the user having to mark the defn explicitly. When the def + is (or becomes) ^:cljd.spec.alpha/redef and is lexically reachable + from the fdef call site, fdef also registers a getter/setter pair in + the runtime registry so instrument can dispatch by symbol — + including for runtime-bound collections." + [fn-sym & specs] + (let [fn-sym (if (symbol? fn-sym) (qualify-symbol &env fn-sym) fn-sym) + form (cons 'cljd.spec.alpha/fspec specs) + info (when (symbol? fn-sym) (resolve-def-info &env fn-sym)) + current-ns-sym (get-in &env [:nses :current-ns]) + _ (when (symbol? fn-sym) + (remember-speced-var! fn-sym current-ns-sym)) + same-ns? (clojure.core/and current-ns-sym + (= (namespace fn-sym) (str current-ns-sym))) + ;; Only re-emit when the def is in the same compilation unit + ;; and is not already a ^:cljd.spec.alpha/redef def. + source-expr (when (clojure.core/and same-ns? info + (not (:cljd.spec.alpha/redef? info))) + (:source-expr info)) + ;; A def is redef-able after this expansion if we're about to + ;; promote it (source-expr is set) or if it was already redef. + will-be-redef? (clojure.core/or source-expr + (:cljd.spec.alpha/redef? info)) + register-setter (when will-be-redef? + `(impl/register-redef-setter! + '~fn-sym + (fn [] ~fn-sym) + (fn [v#] (set! ~fn-sym v#)))) + register-spec `(do + (swap! speced-vars-ref conj '~fn-sym) + (impl/def-impl '~fn-sym '~form (fspec ~@specs)))] + (if source-expr + `(do + (def ~(with-meta (symbol (name fn-sym)) + {:cljd.spec.alpha/redef true}) + ~source-expr) + ~register-setter + ~register-spec) + (if register-setter + `(do ~register-setter ~register-spec) + register-spec)))) + +(defmacro keys* + "Regex op that consumes key/value pairs, builds a map, and conforms + it with a corresponding keys spec." + [& kspecs] + `(let [mspec# (impl/map-spec-impl (impl/keys-args '~kspecs nil))] + (impl/amp-impl + (impl/rep-impl + '(cljd.spec.alpha/cat :cljd.spec.alpha/k cljd.core/keyword? + :cljd.spec.alpha/v cljd.core/any?) + (impl/cat-impl [:cljd.spec.alpha/k :cljd.spec.alpha/v] + [keyword? any?] + '[cljd.core/keyword? cljd.core/any?])) + '(cljd.spec.alpha/* (cljd.spec.alpha/cat + :cljd.spec.alpha/k cljd.core/keyword? + :cljd.spec.alpha/v cljd.core/any?)) + [(impl/spec-impl + '(cljd.spec.alpha/conformer cljd.spec.alpha/kvs->map) + cljd.spec.alpha/kvs->map + nil + true + (fn [m#] + (map (fn [[k# v#]] + {:cljd.spec.alpha/k k# + :cljd.spec.alpha/v v#}) + m#))) + mspec#] + ['(cljd.spec.alpha/conformer cljd.spec.alpha/kvs->map) + (impl/form mspec#)]))) + +(defmacro assert + "Returns x when spec assertions are disabled or x conforms to spec; + otherwise throws an explain-backed exception. Expansion is elided + entirely when cljd.spec.alpha/*compile-asserts* is false at + macro-expansion time." + [spec x] + (if *compile-asserts* + `(if (impl/check-asserts?) + (assert* ~spec ~x) + ~x) + x)) + +(defmacro exercise-fn + "Exercises the fn named by sym by applying it to generated samples + from its :args spec. Returns [args ret] tuples." + ([sym] + `(exercise-fn ~sym 10)) + ([sym n] + `(exercise-fn ~sym ~n nil)) + ([sym n fspec] + (let [raw-sym (if (clojure.core/and (seq? sym) (= 'quote (first sym))) + (second sym) + sym) + qualified-sym (if (symbol? raw-sym) + (qualify-symbol &env raw-sym) + raw-sym)] + (if (symbol? raw-sym) + `(let [fspec# ~(if fspec fspec `(impl/get-spec '~qualified-sym))] + (impl/exercise-fn '~qualified-sym ~n fspec#)) + (let [f-sym (gensym "f")] + `(let [~f-sym ~sym + fspec# ~(if fspec + fspec + `(when (symbol? ~f-sym) (impl/get-spec ~f-sym)))] + (impl/exercise-fn ~f-sym ~n fspec#))))))) + +(defn- runtime-form [pred] + (cond + (ident? pred) pred + (impl/spec? pred) (impl/form pred) + :else :cljd.spec.alpha/unknown)) + +(defn nonconforming [spec] + (impl/nonconforming-impl (runtime-form spec) spec)) + +(defn- and* [& preds] + (impl/and-spec-impl (mapv runtime-form preds) (vec preds) nil)) + +(defn- or* [& key-pred-forms] + (let [pairs (partition 2 key-pred-forms) + ks (mapv first pairs) + preds (mapv second pairs)] + (clojure.core/assert (clojure.core/and (even? (count key-pred-forms)) + (every? keyword? ks)) + "or expects k1 p1 k2 p2..., where ks are keywords") + (impl/or-spec-impl ks (mapv runtime-form preds) preds nil))) + +(defn- nilable* [pred] + (impl/nilable-impl (runtime-form pred) pred nil)) + +(defn- tuple* [& preds] + (clojure.core/assert (not (empty? preds)) "tuple expects at least one predicate") + (impl/tuple-impl (mapv runtime-form preds) (vec preds))) + +(defn- opts-map [opts] + (apply hash-map opts)) + +(defn- coll-opts [op pred opts] + (let [m (opts-map opts) + form (runtime-form pred)] + (assoc m + :opts opts + :kind-form (when (:kind m) (runtime-form (:kind m))) + :describe (cons op (cons form opts))))) + +(defn- every* [pred & opts] + (let [m (coll-opts 'cljd.spec.alpha/every pred opts)] + (impl/every-impl (runtime-form pred) pred m (:gen m)))) + +(defn- coll-of* [pred & opts] + (let [m (assoc (coll-opts 'cljd.spec.alpha/coll-of pred opts) + :conform-all true)] + (impl/every-impl (runtime-form pred) pred m (:gen m)))) + +(defn- every-kv* [kpred vpred & opts] + (let [entry-spec (tuple* kpred vpred) + entry-form (list 'cljd.spec.alpha/tuple (runtime-form kpred) (runtime-form vpred)) + m (assoc (opts-map opts) + :opts opts + :kind (clojure.core/or (:kind (opts-map opts)) map?) + :kind-form (clojure.core/or (when (:kind (opts-map opts)) (runtime-form (:kind (opts-map opts)))) + 'cljd.core/map?) + :kfn (fn [_ e] (nth e 0)) + :describe (cons 'cljd.spec.alpha/every-kv + (cons (runtime-form kpred) + (cons (runtime-form vpred) opts))))] + (impl/every-impl entry-form entry-spec m (:gen m)))) + +(defn- map-of* [kpred vpred & opts] + (let [entry-spec (tuple* kpred vpred) + entry-form (list 'cljd.spec.alpha/tuple (runtime-form kpred) (runtime-form vpred)) + m (assoc (opts-map opts) + :opts opts + :kind (clojure.core/or (:kind (opts-map opts)) map?) + :kind-form (clojure.core/or (when (:kind (opts-map opts)) (runtime-form (:kind (opts-map opts)))) + 'cljd.core/map?) + :kfn (fn [_ e] (nth e 0)) + :conform-all true + :describe (cons 'cljd.spec.alpha/map-of + (cons (runtime-form kpred) + (cons (runtime-form vpred) opts))))] + (impl/every-impl entry-form entry-spec m (:gen m)))) + +(defn- ^:macro-support coll-opts-form [env op pred-form opts] + (let [m (apply hash-map opts) + form (qualified-form env pred-form) + qopts (qualified-opts env opts) + kind-form (when (:kind m) (qualified-form env (:kind m)))] + `(assoc (hash-map ~@opts) + :opts '~qopts + :kind-form '~kind-form + :describe '~(cons op (cons form qopts))))) + +(defn- ^:macro-support map-coll-opts-form [env op kpred-form vpred-form opts conform-all] + (let [m (apply hash-map opts) + kform (qualified-form env kpred-form) + vform (qualified-form env vpred-form) + qopts (qualified-opts env opts) + kind-form (clojure.core/or (when (:kind m) (qualified-form env (:kind m))) + 'cljd.core/map?)] + `(let [m# (hash-map ~@opts)] + (assoc m# + :opts '~qopts + :kind (clojure.core/or (:kind m#) map?) + :kind-form '~kind-form + :kfn (fn [_# e#] (nth e# 0)) + ~@(when conform-all [:conform-all true]) + :describe '~(cons op (cons kform (cons vform qopts))))))) + +(defmacro and + "And-combinator." + [& pred-forms] + `(impl/and-spec-impl '~(qualified-forms &env pred-forms) ~(vec pred-forms) nil)) + +(defmacro or + "Or-combinator with key/pred pairs." + [& key-pred-forms] + (let [pairs (partition 2 key-pred-forms) + ks (mapv first pairs) + preds (mapv second pairs) + forms (qualified-forms &env preds)] + (clojure.core/assert (clojure.core/and (even? (count key-pred-forms)) + (every? keyword? ks)) + "or expects k1 p1 k2 p2..., where ks are keywords") + `(impl/or-spec-impl ~ks '~forms ~preds nil))) + +(defmacro nilable + "Returns a spec that allows nil in addition to pred." + [pred-form] + `(impl/nilable-impl '~(qualified-form &env pred-form) ~pred-form nil)) + +(defmacro tuple + "Returns a spec for a vector whose elements conform to the + corresponding predicates." + [& pred-forms] + (clojure.core/assert (not (empty? pred-forms)) "tuple expects at least one predicate") + `(impl/tuple-impl '~(qualified-forms &env pred-forms) ~(vec pred-forms))) + +(defmacro every + "Returns a collection spec that samples elements while conforming." + [pred-form & opts] + `(let [opts# ~(coll-opts-form &env 'cljd.spec.alpha/every pred-form opts)] + (impl/every-impl '~(qualified-form &env pred-form) ~pred-form opts# (:gen opts#)))) + +(defmacro coll-of + "Returns a collection spec that conforms all elements." + [pred-form & opts] + `(let [opts# ~(coll-opts-form &env 'cljd.spec.alpha/coll-of pred-form opts)] + (impl/every-impl '~(qualified-form &env pred-form) + ~pred-form + (assoc opts# :conform-all true) + (:gen opts#)))) + +(defmacro every-kv + "Returns a map entry spec that samples key/value entries while conforming." + [kpred-form vpred-form & opts] + (let [entry-form (list 'cljd.spec.alpha/tuple + (qualified-form &env kpred-form) + (qualified-form &env vpred-form))] + `(let [entry-spec# (impl/tuple-impl '~(vec (rest entry-form)) + [~kpred-form ~vpred-form]) + opts# ~(map-coll-opts-form &env 'cljd.spec.alpha/every-kv + kpred-form vpred-form opts false)] + (impl/every-impl '~entry-form entry-spec# opts# (:gen opts#))))) + +(defmacro map-of + "Returns a map spec that conforms keys and values." + [kpred-form vpred-form & opts] + (let [entry-form (list 'cljd.spec.alpha/tuple + (qualified-form &env kpred-form) + (qualified-form &env vpred-form))] + `(let [entry-spec# (impl/tuple-impl '~(vec (rest entry-form)) + [~kpred-form ~vpred-form]) + opts# ~(map-coll-opts-form &env 'cljd.spec.alpha/map-of + kpred-form vpred-form opts true)] + (impl/every-impl '~entry-form entry-spec# opts# (:gen opts#))))) + +(defn ^:skip-wiki def* [k spec-form] + (impl/def-impl k (runtime-form spec-form) spec-form)) + +#?(:cljd/clj-host + (defn ^:macro-support def-macro + [&form &env k spec-form] + (def-call-form &env k spec-form))) + +#?(:cljd/clj-host + (def + ^{:macro true + :doc "Given a namespace-qualified keyword or resolvable symbol k, and a + spec, spec-name, predicate or regex-op, registers k in the spec + registry. Symbol keys are namespace-qualified and tracked for + speced-vars discovery." + :arglists '([k spec-form])} + def + def-macro) + :cljd + (def def + "Runtime value for `cljd.spec.alpha/def`; call sites are expanded + by the host macro above so symbol keys can be qualified and tracked." + def*)) diff --git a/clj/src/cljd/spec/alpha/impl.cljd b/clj/src/cljd/spec/alpha/impl.cljd new file mode 100644 index 00000000..0a57b2cc --- /dev/null +++ b/clj/src/cljd/spec/alpha/impl.cljd @@ -0,0 +1,1558 @@ +; 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. + +;;; Runtime implementation for cljd.spec.alpha. +;;; +;;; Lives in its own namespace (without :refer-clojure :exclude) so that +;;; cljd's defprotocol/defn expansions — which emit unqualified `or` and +;;; `and` — keep resolving against clojure.core. The cljd.spec.alpha +;;; namespace re-exports the public functions and adds ClojureScript-shaped +;;; entry points. + +(ns cljd.spec.alpha.impl + (:require [cljd.spec.gen.alpha :as gen] + [cljd.spec.gen.alpha.test-check :as gen-tc] + [cljd.walk :as walk])) + +(def ^:dynamic *recursion-limit* 4) +(def ^:dynamic *fspec-iterations* 21) +(def ^:dynamic *coll-check-limit* 101) +(def ^:dynamic *coll-error-limit* 20) + +(defn- public-dynamic-binding [sym root] + (get-dynamic-binding sym root)) + +(defn- recursion-limit [] + (public-dynamic-binding 'cljd.spec.alpha/*recursion-limit* *recursion-limit*)) + +(defn- fspec-iterations [] + (public-dynamic-binding 'cljd.spec.alpha/*fspec-iterations* *fspec-iterations*)) + +(defn- coll-check-limit [] + (public-dynamic-binding 'cljd.spec.alpha/*coll-check-limit* *coll-check-limit*)) + +(defn- coll-error-limit [] + (public-dynamic-binding 'cljd.spec.alpha/*coll-error-limit* *coll-error-limit*)) + +(defprotocol Spec + (conform* [spec x]) + (unform* [spec y]) + (explain* [spec path via in x]) + (gen* [spec overrides path rmap]) + (with-gen* [spec gfn]) + (describe* [spec])) + +(defonce ^:private registry-ref (atom {})) +(defonce ^:private runtime-asserts-ref (atom false)) +(defonce ^:private redef-setters-ref (atom {})) + +(defn ^:skip-wiki register-redef-setter! + "Records a getter/setter pair for a ^:cljd.spec.alpha/redef def + under its fully-qualified symbol. Called by s/fdef expansions in the + same lexical scope as the def so the setter has direct write access + to the dartname. Once registered, cljd.spec.test.alpha/instrument + can swap the value at runtime — including for runtime-bound + collections of syms — without the macro needing to introspect the + call site." + [sym getter setter] + (swap! redef-setters-ref assoc sym {:get getter :set setter}) + nil) + +(defn ^:skip-wiki redef-setter [sym] + (:set (get @redef-setters-ref sym))) + +(defn ^:skip-wiki redef-getter [sym] + (:get (get @redef-setters-ref sym))) + +(defn ^:skip-wiki redef-registered? [sym] + (contains? @redef-setters-ref sym)) + +(defn ^:skip-wiki redef-registered-syms [] + (set (keys @redef-setters-ref))) + +(defn- deep-resolve [reg k] + (loop [spec k] + (if (ident? spec) + (recur (get reg spec)) + spec))) + +(defn- reg-resolve + "returns the spec/regex at end of alias chain starting with k, nil if not found, k if k not ident" + [k] + (if (ident? k) + (let [reg @registry-ref + spec (get reg k)] + (if-not (ident? spec) + spec + (deep-resolve reg spec))) + k)) + +(defn- reg-resolve! + "returns the spec/regex at end of alias chain starting with k, throws if not found, k if k not ident" + [k] + (if (ident? k) + (or (reg-resolve k) + (throw (Exception. (str "Unable to resolve spec: " k)))) + k)) + +(defn spec? + "returns x if x is a spec object, else logical false" + [x] + (when (satisfies? Spec x) x)) + +(defn regex? + "returns x if x is a (cljd.spec.alpha) regex op, else logical false" + [x] + (and (:cljd.spec.alpha.impl/op x) x)) + +(defn- with-name [spec nm] + (cond + (ident? spec) spec + (regex? spec) (assoc spec :cljd.spec.alpha.impl/name nm) + (satisfies? IMeta spec) (with-meta spec (assoc (meta spec) :cljd.spec.alpha.impl/name nm)) + :else spec)) + +(defn- spec-name [spec] + (cond + (ident? spec) spec + (regex? spec) (:cljd.spec.alpha.impl/name spec) + (satisfies? IMeta spec) (-> (meta spec) :cljd.spec.alpha.impl/name))) + +(defn- recursion-depth [rmap nm] + (get-in rmap [:cljd.spec.alpha/recursion-depth nm] 0)) + +(defn- enter-recursion [rmap nm] + (if nm + (update-in rmap [:cljd.spec.alpha/recursion-depth nm] (fnil inc 0)) + rmap)) + +(declare spec-impl regex-spec-impl) + +(defn- maybe-spec + "spec-or-k must be a spec, regex or resolvable kw/sym, else returns nil." + [spec-or-k] + (let [s (or (and (ident? spec-or-k) (reg-resolve spec-or-k)) + (spec? spec-or-k) + (regex? spec-or-k) + nil)] + (if (regex? s) + (with-name (regex-spec-impl s nil) (spec-name s)) + s))) + +(defn- the-spec + "spec-or-k must be a spec, regex or kw/sym, else returns nil. Throws if unresolvable kw/sym" + [spec-or-k] + (or (maybe-spec spec-or-k) + (when (ident? spec-or-k) + (throw (Exception. (str "Unable to resolve spec: " spec-or-k)))))) + +(defprotocol Specize + (specize* [_] [_ form])) + +(extend-protocol Specize + cljd.core/Keyword + (specize* ([k] (specize* (reg-resolve! k))) + ([k _] (specize* (reg-resolve! k)))) + + cljd.core/Symbol + (specize* ([s] (specize* (reg-resolve! s))) + ([s _] (specize* (reg-resolve! s)))) + + cljd.core/PersistentHashSet + (specize* ([s] (spec-impl s s nil nil)) + ([s form] (spec-impl form s nil nil))) + + cljd.core/HashRankedWideTreapSet + (specize* ([s] (spec-impl s s nil nil)) + ([s form] (spec-impl form s nil nil))) + + fallback + (specize* + ([o] (spec-impl :cljd.spec.alpha/unknown o nil nil)) + ([o form] (spec-impl form o nil nil)))) + +(defn- specize + ([s] (or (spec? s) (when (regex? s) (regex-spec-impl s nil)) (specize* s))) + ([s form] (or (spec? s) (when (regex? s) (regex-spec-impl s nil)) (specize* s form)))) + +(defn invalid? + "tests the validity of a conform return value" + [ret] + (identical? :cljd.spec.alpha/invalid ret)) + +(defn conform + "Given a spec and a value, returns ::invalid if value does not match + spec, else the (possibly destructured) value." + [spec x] + (conform* (specize spec) x)) + +(defn unform + "Given a spec and a conformed value, returns a value with all conform + destructuring undone." + [spec x] + (unform* (specize spec) x)) + +(defn form + "returns the spec as data" + [spec] + (describe* (specize spec))) + +(defn abbrev [form] + (cond + (seq? form) + (walk/postwalk (fn [form] + (cond + (and (symbol? form) (namespace form)) + (-> form name symbol) + + (and (seq? form) (= 'fn (first form)) (= '[%] (second form))) + (last form) + + :else form)) + form) + + (and (symbol? form) (namespace form)) + (-> form name symbol) + + :else form)) + +(defn describe [spec] (abbrev (form spec))) + +(defn with-gen [spec gen-fn] + (let [spec (reg-resolve spec)] + (if (regex? spec) + (assoc spec :cljd.spec.alpha.impl/gfn gen-fn) + (with-gen* (specize spec) gen-fn)))) + +(defn explain-data* [spec path via in x] + (when-let [probs (explain* (specize spec) path via in x)] + (when-not (empty? probs) + {:cljd.spec.alpha/problems probs + :cljd.spec.alpha/spec spec + :cljd.spec.alpha/value x}))) + +(defn explain-data [spec x] + (explain-data* spec [] (if-let [nm (spec-name spec)] [nm] []) [] x)) + +(defn explain-out [ed] + (if ed + (doseq [{:keys [path pred val reason via in] :as prob} + (->> (:cljd.spec.alpha/problems ed) + (sort-by #(- (count (:in %)))) + (sort-by #(- (count (:path %)))))] + (pr val) + (print " - failed: ") + (if reason + (print reason) + (pr (abbrev pred))) + (when-not (empty? in) (print " in: " (pr-str in))) + (when-not (empty? path) (print " at: " (pr-str path))) + (when-not (empty? via) (print " spec: " (pr-str (last via)))) + (doseq [[k v] prob] + (when-not (#{:path :pred :val :reason :via :in} k) + (print "\n\t" (pr-str k) " ") + (pr v))) + (println)) + (println "Success!"))) + +(defn explain [spec x] (explain-out (explain-data spec x))) + +(defn explain-str [spec x] (with-out-str (explain spec x))) + +(declare valid?) + +(defn- gensub + [spec overrides path rmap form] + (let [nm (when (ident? spec) spec) + spec (specize spec) + nm (or nm (spec-name spec)) + limit (:cljd.spec.alpha/recursion-limit rmap) + depth (recursion-depth rmap nm) + _ (when (and nm limit (> depth limit)) + (throw (Exception. (str "Recursion limit reached for: " nm)))) + rmap (enter-recursion rmap nm) + g (gen/ensure-generator + ;; Override lookup precedence: kw/sym name preserved across + ;; specize, then the post-specize spec-name (set on regex + ;; ops via with-name), then the spec object itself, then + ;; the current path. Without `nm` here, kw-keyed overrides + ;; never match for registered deftype-backed specs because + ;; they don't carry IMeta. + (or (when-let [gfn (or (get overrides nm) + (get overrides (or (spec-name spec) spec)) + (get overrides path))] + (gfn)) + (gen* spec overrides path rmap)))] + (if g + (gen/such-that #(valid? spec %) g 100) + (throw (Exception. (str "Unable to construct gen at: " + path " for: " (abbrev form))))))) + +(defn- try-gensub + [spec overrides path rmap form] + (try + (gensub spec overrides path rmap form) + (catch Exception _ nil) + (catch Error _ nil))) + +(defn gen + "Given a spec, returns the generator for it, or throws if none can + be constructed. Optionally an overrides map can be provided which + maps spec names or paths to no-arg generator-returning fns." + ([spec] (gen spec nil)) + ([spec overrides] + (gensub spec overrides [] {:cljd.spec.alpha/recursion-limit (recursion-limit)} spec))) + +(defn check-asserts? [] + @runtime-asserts-ref) + +(defn check-asserts [flag] + (reset! runtime-asserts-ref flag)) + +(defn ^:skip-wiki def-impl [k form spec] + (assert (and (ident? k) (namespace k)) "k must be namespaced keyword or resolvable symbol") + (if (nil? spec) + (do (swap! registry-ref dissoc k) k) + (let [spec (if (or (spec? spec) (regex? spec) (get @registry-ref spec)) + spec + (spec-impl form spec nil nil))] + (swap! registry-ref assoc k (with-name spec k)) + k))) + +(defn registry [] @registry-ref) + +(defn get-spec [k] (get (registry) k)) + +(defn int-in-range? + [start end val] + (and (integer? val) (<= start val) (< val end))) + +(defn inst-in-range? + [start end inst] + (and (inst? inst) + (let [t (.-millisecondsSinceEpoch ^DateTime inst) + st (.-millisecondsSinceEpoch ^DateTime start) + et (.-millisecondsSinceEpoch ^DateTime end)] + (and (<= st t) (< t et))))) + +(defn double-in-range? + [opts val] + (let [allow-infinite? (if (contains? opts :infinite?) (:infinite? opts) true) + allow-NaN? (if (contains? opts :NaN?) (:NaN? opts) true)] + (and (double? val) + (or allow-infinite? (not (infinite? val))) + (or allow-NaN? (not (NaN? val))) + (or (not (contains? opts :min)) (<= (:min opts) val)) + (or (not (contains? opts :max)) (<= val (:max opts)))))) + +(defn- dt + ([pred x form] (dt pred x form nil)) + ([pred x form cpred?] + (if pred + (if-let [spec (the-spec pred)] + (conform spec x) + (if (ifn? pred) + (if cpred? + (pred x) + (if (pred x) x :cljd.spec.alpha/invalid)) + (throw (Exception. (str (pr-str form) " is not a fn, expected predicate fn"))))) + x))) + +(defn valid? + ([spec x] + (let [spec (specize spec)] + (not (invalid? (conform* spec x))))) + ([spec x form] + (let [spec (specize spec form)] + (not (invalid? (conform* spec x)))))) + +(defn- explain-1 [form pred path via in v] + (let [pred (maybe-spec pred)] + (if (spec? pred) + (explain* pred path (if-let [nm (spec-name pred)] (conj via nm) via) in v) + [{:path path :pred form :val v :via via :in in}]))) + +(defn- and-preds [x preds forms] + (loop [ret x + [pred & preds] preds + [form & forms] forms] + (if pred + (let [nret (dt pred ret form)] + (if (invalid? nret) :cljd.spec.alpha/invalid (recur nret preds forms))) + ret))) + +(defn- explain-pred-list [forms preds path via in x] + (loop [ret x + [form & forms] forms + [pred & preds] preds] + (when pred + (let [nret (dt pred ret form)] + (if (invalid? nret) + (explain-1 form pred path via in ret) + (recur nret forms preds)))))) + +(defn- tagged-ret [tag ret] [tag ret]) + +(declare ->PredSpec ->AndSpec ->OrSpec) + +(deftype PredSpec [form pred gfn cpred? unc] + Spec + (conform* [_ x] + (let [ret (pred x)] + (if cpred? ret (if ret x :cljd.spec.alpha/invalid)))) + (unform* [_ x] + (if cpred? + (if unc (unc x) (throw (Exception. "no unform fn for conformer"))) + x)) + (explain* [this path via in x] + (when (invalid? (dt pred x form cpred?)) + [{:path path :pred form :val x :via via :in in}])) + (gen* [_ _ _ _] + (if gfn + (gfn) + (gen/gen-for-pred pred))) + (with-gen* [_ gfn] (->PredSpec form pred gfn cpred? unc)) + (describe* [_] form)) + +(defn ^:skip-wiki spec-impl + ([form pred gfn cpred?] (spec-impl form pred gfn cpred? nil)) + ([form pred gfn cpred? unc] + (cond + (spec? pred) (cond-> pred gfn (with-gen gfn)) + (regex? pred) (regex-spec-impl pred gfn) + (ident? pred) (cond-> (the-spec pred) gfn (with-gen gfn)) + :else (->PredSpec form pred gfn cpred? unc)))) + +(defn- multi-spec-dispatch-value [retag x] + (cond + (and (keyword? retag) (map? x)) (get x retag) + (keyword? retag) nil + :else :cljd.spec.alpha/dispatch)) + +(defn- multi-spec-retag [retag dispatch-val x] + (if (and (keyword? retag) (map? x)) + (assoc x retag dispatch-val) + x)) + +(defn- selected-multi-spec [mm x] + (try + (mm x) + (catch ArgumentError _ nil))) + +(deftype MultiSpec [form mm retag retag-form gfn] + Spec + (conform* [_ x] + (if-let [pred (selected-multi-spec mm x)] + (dt pred x form) + :cljd.spec.alpha/invalid)) + (unform* [_ x] + (if-let [pred (selected-multi-spec mm x)] + (unform pred x) + (throw (Exception. (str "No method of: " form " for dispatch value: " + (multi-spec-dispatch-value retag x)))))) + (explain* [_ path via in x] + (let [dv (multi-spec-dispatch-value retag x) + path (conj path dv)] + (if-let [pred (selected-multi-spec mm x)] + (explain-1 form pred path via in x) + [{:path path :pred form :val x :reason "no method" :via via :in in}]))) + (gen* [_ overrides path rmap] + (if gfn + (gfn) + (let [gs (keep (fn [[dispatch-val method-fn]] + (try + (gen/fmap #(multi-spec-retag retag dispatch-val %) + (gensub (method-fn dispatch-val) + overrides + (conj path dispatch-val) + rmap + form)) + (catch Exception _ nil) + (catch Error _ nil))) + (methods mm))] + (when-not (empty? gs) + (gen/one-of (vec gs)))))) + (with-gen* [_ gfn] (->MultiSpec form mm retag retag-form gfn)) + (describe* [_] (list 'cljd.spec.alpha/multi-spec form retag-form))) + +(defn ^:skip-wiki multi-spec-impl + ([form mm retag] (multi-spec-impl form mm retag retag nil)) + ([form mm retag gfn] (multi-spec-impl form mm retag retag gfn)) + ([form mm retag retag-form gfn] + (->MultiSpec form mm retag retag-form gfn))) + +(deftype AndSpec [forms preds gfn specs-delay] + Spec + (conform* [_ x] (and-preds x @specs-delay forms)) + (unform* [_ x] (reduce #(unform %2 %1) x (reverse @specs-delay))) + (explain* [_ path via in x] (explain-pred-list forms @specs-delay path via in x)) + (gen* [_ overrides path rmap] + (if gfn + (gfn) + (gensub (first preds) overrides path rmap (first forms)))) + (with-gen* [_ gfn] + (->AndSpec forms preds gfn (delay (mapv specize preds forms)))) + (describe* [_] (cons 'cljd.spec.alpha/and forms))) + +(defn ^:skip-wiki and-spec-impl [forms preds gfn] + (->AndSpec forms preds gfn (delay (mapv specize preds forms)))) + +(deftype OrSpec [ks forms preds gfn specs-delay] + Spec + (conform* [_ x] + (loop [i 0] + (if (< i (count @specs-delay)) + (let [spec (nth @specs-delay i) + ret (conform* spec x)] + (if (invalid? ret) + (recur (inc i)) + (tagged-ret (nth ks i) ret))) + :cljd.spec.alpha/invalid))) + (unform* [_ [k x]] + (let [idx (.indexOf ^List ks k)] + (unform (nth preds idx) x))) + (explain* [this path via in x] + (when-not (valid? this x) + (apply concat + (map (fn [k form pred] + (when-not (valid? pred x form) + (explain-1 form pred (conj path k) via in x))) + ks forms preds)))) + (gen* [_ overrides path rmap] + (if gfn + (gfn) + (let [gs (remove nil? + (map (fn [k pred form] + (try-gensub pred overrides (conj path k) rmap form)) + ks preds forms))] + (when-not (empty? gs) + (gen/one-of gs))))) + (with-gen* [_ gfn] + (->OrSpec ks forms preds gfn (delay (mapv specize preds forms)))) + (describe* [_] (cons 'cljd.spec.alpha/or (mapcat vector ks forms)))) + +(defn ^:skip-wiki or-spec-impl [ks forms preds gfn] + (->OrSpec ks forms preds gfn (delay (mapv specize preds forms)))) + +(defn- unqual-key [k] + (keyword (name k))) + +(defn- key-op? [op n] + (and (symbol? op) (= (name op) n))) + +(defn- key-expr-valid? [m expr key-fn] + (cond + (keyword? expr) (contains? m (key-fn expr)) + + (seq? expr) + (let [op (first expr) + exprs (rest expr)] + (cond + (key-op? op "or") (boolean (some #(key-expr-valid? m % key-fn) exprs)) + (key-op? op "and") (every? #(key-expr-valid? m % key-fn) exprs) + :else false)) + + :else false)) + +(defn- key-expr-form [expr key-fn] + (cond + (keyword? expr) (list 'cljd.core/contains? '% (key-fn expr)) + + (seq? expr) + (let [op (first expr)] + (cons (symbol "cljd.core" (name op)) + (map #(key-expr-form % key-fn) (rest expr)))) + + :else expr)) + +(defn- key-expr-keywords [expr] + (cond + (keyword? expr) [expr] + (seq? expr) (mapcat key-expr-keywords (rest expr)) + :else [])) + +(declare key-expr-gen) + +(defn- subsets [xs] + (if (empty? xs) + [[]] + (let [x (first xs) + more (subsets (rest xs))] + (concat more (map #(cons x %) more))))) + +(defn- concat-entry-gens [gs] + (if (empty? gs) + (gen/return []) + (gen/fmap #(apply concat %) (apply gen/tuple gs)))) + +(defn- try-key-expr-gen [expr key-fn overrides path rmap] + (try + (key-expr-gen expr key-fn overrides path rmap) + (catch Exception _ nil) + (catch Error _ nil))) + +(defn- key-expr-gen [expr key-fn overrides path rmap] + (cond + (keyword? expr) + (let [k (key-fn expr)] + (gen/fmap (fn [v] [[k v]]) + (gensub expr overrides (conj path k) rmap expr))) + + (seq? expr) + (let [op (first expr) + exprs (vec (rest expr))] + (cond + (key-op? op "or") + (let [gs (keep #(try-key-expr-gen % key-fn overrides path rmap) exprs)] + (when-not (empty? gs) + (gen/one-of (vec (map concat-entry-gens + (remove empty? (subsets gs))))))) + + (key-op? op "and") + (let [gs (mapv #(key-expr-gen % key-fn overrides path rmap) exprs)] + (concat-entry-gens gs)) + + :else nil)) + + :else nil)) + +(defn- opt-key-expr-gen [expr key-fn overrides path rmap] + (if-let [g (try-key-expr-gen expr key-fn overrides path rmap)] + (gen/one-of [(gen/return []) g]) + (gen/return []))) + +(defn ^:skip-wiki keys-args [opts gfn] + (let [m (apply hash-map opts) + req (:req m) + req-un (:req-un m) + opt (:opt m) + opt-un (:opt-un m) + req-keys (vec (mapcat key-expr-keywords req)) + req-un-specs (vec (mapcat key-expr-keywords req-un)) + opt-keys (vec (mapcat key-expr-keywords opt)) + opt-un-specs (vec (mapcat key-expr-keywords opt-un))] + (assert (every? #(and (keyword? %) (namespace %)) + (concat req-keys req-un-specs opt-keys opt-un-specs)) + "all keys must be namespace-qualified keywords") + {:req req + :opt opt + :req-un req-un + :opt-un opt-un + :req-keys (into req-keys (map unqual-key req-un-specs)) + :req-specs (into req-keys req-un-specs) + :opt-keys (into opt-keys (map unqual-key opt-un-specs)) + :opt-specs (into opt-keys opt-un-specs) + :gfn (or gfn (:gen m))})) + +(deftype MapSpec [argm] + Spec + (conform* [_ m] + (let [{:keys [req req-un req-keys req-specs opt-keys opt-specs]} argm] + (if (and (map? m) + (every? #(key-expr-valid? m % identity) req) + (every? #(key-expr-valid? m % unqual-key) req-un)) + (let [reg (registry) + k->s (zipmap (concat req-keys opt-keys) (concat req-specs opt-specs))] + (loop [ret m + [[k v] & ks :as entries] (seq m)] + (if entries + (let [sname (or (get k->s k) k)] + (if-let [s (get reg sname)] + (let [cv (conform s v)] + (if (invalid? cv) + :cljd.spec.alpha/invalid + (recur (if (identical? cv v) ret (assoc ret k cv)) ks))) + (recur ret ks))) + ret))) + :cljd.spec.alpha/invalid))) + (unform* [_ m] + (let [{:keys [req-keys req-specs opt-keys opt-specs]} argm + reg (registry) + k->s (zipmap (concat req-keys opt-keys) (concat req-specs opt-specs))] + (loop [ret m + [k & ks :as keys] (seq (keys m))] + (if keys + (let [sname (or (get k->s k) k)] + (if (contains? reg sname) + (let [cv (get m k) + v (unform sname cv)] + (recur (if (identical? cv v) ret (assoc ret k v)) ks)) + (recur ret ks))) + ret)))) + (explain* [this path via in x] + (let [{:keys [req req-un req-keys req-specs opt-keys opt-specs]} argm] + (if-not (map? x) + [{:path path :pred 'cljd.core/map? :val x :via via :in in}] + (let [reg (registry) + k->s (zipmap (concat req-keys opt-keys) (concat req-specs opt-specs))] + (apply concat + (concat + (keep (fn [expr] + (when-not (key-expr-valid? x expr identity) + [{:path path :pred (key-expr-form expr identity) :val x :via via :in in}])) + req) + (keep (fn [expr] + (when-not (key-expr-valid? x expr unqual-key) + [{:path path :pred (key-expr-form expr unqual-key) :val x :via via :in in}])) + req-un) + (keep (fn [[k v]] + (let [sname (or (get k->s k) k)] + (when-not (or (not (contains? reg sname)) + (valid? sname v sname)) + (explain-1 sname sname (conj path k) via (conj in k) v)))) + (seq x)))))))) + (gen* [_ overrides path rmap] + (if-let [gfn (:gfn argm)] + (gfn) + (let [{:keys [req req-un opt opt-un]} argm + req-gen (apply gen/tuple + (concat + (mapv #(key-expr-gen % identity overrides path rmap) req) + (mapv #(key-expr-gen % unqual-key overrides path rmap) req-un))) + opt-gen (apply gen/tuple + (concat + (mapv #(opt-key-expr-gen % identity overrides path rmap) opt) + (mapv #(opt-key-expr-gen % unqual-key overrides path rmap) opt-un)))] + (gen/fmap (fn [[req-entries opt-entries]] + (into {} (apply concat (concat req-entries opt-entries)))) + (gen/tuple req-gen opt-gen))))) + (with-gen* [_ gfn] (->MapSpec (assoc argm :gfn gfn))) + (describe* [_] + (let [{:keys [req opt req-un opt-un]} argm] + (cons 'cljd.spec.alpha/keys + (cond-> [] + req (conj :req req) + opt (conj :opt opt) + req-un (conj :req-un req-un) + opt-un (conj :opt-un opt-un)))))) + +(defn ^:skip-wiki map-spec-impl [argm] + (->MapSpec argm)) + +(deftype MergeSpec [forms preds gfn] + Spec + (conform* [_ x] + (let [ms (map #(dt %1 x %2) preds forms)] + (if (some invalid? ms) + :cljd.spec.alpha/invalid + (apply merge ms)))) + (unform* [_ x] + (apply merge (map #(unform % x) (reverse preds)))) + (explain* [_ path via in x] + (apply concat + (map #(explain-1 %1 %2 path via in x) + forms preds))) + (gen* [_ overrides path rmap] + (if gfn + (gfn) + (let [gs (mapv (fn [pred form] + (gensub pred overrides path rmap form)) + preds forms)] + (gen/fmap #(apply merge %) (apply gen/tuple gs))))) + (with-gen* [_ gfn] (->MergeSpec forms preds gfn)) + (describe* [_] (cons 'cljd.spec.alpha/merge forms))) + +(defn ^:skip-wiki merge-spec-impl [forms preds gfn] + (->MergeSpec forms preds gfn)) + +(deftype TupleSpec [forms preds gfn specs-delay] + Spec + (conform* [_ x] + (let [cnt (count preds)] + (if-not (and (vector? x) (= (count x) cnt)) + :cljd.spec.alpha/invalid + (loop [ret x + i 0] + (if (= i cnt) + ret + (let [v (nth x i) + cv (conform* (nth @specs-delay i) v)] + (if (invalid? cv) + :cljd.spec.alpha/invalid + (recur (if (identical? cv v) ret (assoc ret i cv)) + (inc i))))))))) + (unform* [_ x] + (loop [ret x + i 0] + (if (= i (count preds)) + ret + (let [cv (nth x i) + v (unform (nth preds i) cv)] + (recur (if (identical? cv v) ret (assoc ret i v)) + (inc i)))))) + (explain* [_ path via in x] + (cond + (not (vector? x)) + [{:path path :pred 'cljd.core/vector? :val x :via via :in in}] + + (not= (count x) (count preds)) + [{:path path :pred (list 'cljd.core/= (list 'cljd.core/count '%) (count preds)) :val x :via via :in in}] + + :else + (apply concat + (map (fn [i form pred] + (let [v (nth x i)] + (when-not (valid? pred v form) + (explain-1 form pred (conj path i) via (conj in i) v)))) + (range (count preds)) forms preds)))) + (gen* [_ overrides path rmap] + (if gfn + (gfn) + (apply gen/tuple + (map (fn [i pred form] + (gensub pred overrides (conj path i) rmap form)) + (range (count preds)) preds forms)))) + (with-gen* [_ gfn] (->TupleSpec forms preds gfn (delay (mapv specize preds forms)))) + (describe* [_] (cons 'cljd.spec.alpha/tuple forms))) + +(defn ^:skip-wiki tuple-impl + ([forms preds] (tuple-impl forms preds nil)) + ([forms preds gfn] + (->TupleSpec forms preds gfn (delay (mapv specize preds forms))))) + +(defn- conform-map [spec x conform-keys] + (loop [ret {} + [e & es :as entries] (seq x)] + (if entries + (let [cv (conform* spec e)] + (if (invalid? cv) + :cljd.spec.alpha/invalid + (let [k (nth e 0) + ck (if conform-keys (nth cv 0) k) + cv (nth cv 1)] + (recur (assoc ret ck cv) es)))) + ret))) + +(defn- conform-coll [spec x] + (cond + (map? x) (conform-map spec x false) + + (vector? x) + (loop [ret x + i 0] + (if (= i (count x)) + ret + (let [v (nth x i) + cv (conform* spec v)] + (if (invalid? cv) + :cljd.spec.alpha/invalid + (recur (if (identical? cv v) ret (assoc ret i cv)) + (inc i)))))) + + (set? x) + (loop [ret #{} + [v & vs :as xs] (seq x)] + (if xs + (let [cv (conform* spec v)] + (if (invalid? cv) + :cljd.spec.alpha/invalid + (recur (conj ret cv) vs))) + ret)) + + (list? x) + (loop [ret [] + [v & vs :as xs] (seq x)] + (if xs + (let [cv (conform* spec v)] + (if (invalid? cv) + :cljd.spec.alpha/invalid + (recur (conj ret cv) vs))) + (into () (reverse ret)))) + + :else + (loop [ret [] + [v & vs :as xs] (seq x)] + (if xs + (let [cv (conform* spec v)] + (if (invalid? cv) + :cljd.spec.alpha/invalid + (recur (conj ret cv) vs))) + ret)))) + +(defn- unform-coll [spec x] + (cond + (vector? x) + (loop [ret x + i 0] + (if (= i (count x)) + ret + (let [cv (nth x i) + v (unform* spec cv)] + (recur (if (identical? cv v) ret (assoc ret i v)) + (inc i))))) + + (set? x) + (loop [ret #{} + [v & vs :as xs] (seq x)] + (if xs + (recur (conj ret (unform* spec v)) vs) + ret)) + + (list? x) + (loop [ret [] + [v & vs :as xs] (seq x)] + (if xs + (recur (conj ret (unform* spec v)) vs) + (into () (reverse ret)))) + + :else + (loop [ret [] + [v & vs :as xs] (seq x)] + (if xs + (recur (conj ret (unform* spec v)) vs) + ret)))) + +(defn- coll-problems [x kind kind-form count* min-count max-count distinct* path via in] + (cond + (not ((or kind coll?) x)) + (explain-1 (or kind-form 'cljd.core/coll?) (or kind coll?) path via in x) + + (and count* (not= count* (bounded-count count* x))) + [{:path path :pred (list 'cljd.core/= count* (list 'cljd.core/count '%)) :val x :via via :in in}] + + (and (or min-count max-count) + (not (<= (or min-count 0) + (bounded-count (if max-count (inc max-count) min-count) x) + (or max-count 9007199254740991)))) + [{:path path :pred (list 'cljd.core/<= (or min-count 0) (list 'cljd.core/count '%) (or max-count 9007199254740991)) :val x :via via :in in}] + + (and distinct* (not (empty? x)) (not (apply distinct? x))) + [{:path path :pred 'cljd.core/distinct? :val x :via via :in in}])) + +(defn- gen-coll-count [{:keys [count min-count max-count gen-max]}] + (cond + count count + (or min-count max-count) + (+ (or min-count 0) + (rand-int (inc (- (or max-count (max (or gen-max 20) + (* 2 (or min-count 0)))) + (or min-count 0))))) + :else (rand-int (inc (or gen-max 20))))) + +(defn- pair-seq? [xs] + (every? #(and (sequential? %) (= 2 (count %))) xs)) + +(defn- generated-coll [opts xs] + (cond + (contains? opts :into) + (into (:into opts) xs) + + (contains? opts :gen-into) + (into (:gen-into opts) xs) + + :else + (let [{:keys [kind count min-count max-count distinct]} opts + map-candidate (when (pair-seq? xs) (into {} xs)) + candidates (cond-> [(vec xs) (into #{} xs) (apply list xs)] + map-candidate (conj map-candidate))] + (or (first (filter #(not (coll-problems % kind (:kind-form opts) + count min-count max-count distinct + [] [] [])) + candidates)) + (vec xs))))) + +(deftype EverySpec [form pred opts gfn spec-delay] + Spec + (conform* [_ x] + (let [{:keys [kind count min-count max-count distinct conform-all conform-keys]} opts + spec @spec-delay] + (if (coll-problems x kind (:kind-form opts) count min-count max-count distinct [] [] []) + :cljd.spec.alpha/invalid + (if conform-all + (if (and (map? x) conform-keys) + (conform-map spec x true) + (conform-coll spec x)) + (loop [i 0 + [v & vs :as xs] (seq x)] + (cond + (or (nil? xs) (= i (coll-check-limit))) x + (valid? spec v) (recur (inc i) vs) + :else :cljd.spec.alpha/invalid)))))) + (unform* [_ x] + (let [{:keys [conform-all conform-keys]} opts + spec @spec-delay] + (if-not conform-all + x + (cond + (map? x) + (loop [ret {} + [e & es :as entries] (seq x)] + (if entries + (let [ue (unform* spec e) + k (if conform-keys (nth ue 0) (nth e 0)) + v (nth ue 1)] + (recur (assoc ret k v) es)) + ret)) + + :else + (unform-coll spec x))))) + (explain* [_ path via in x] + (let [{:keys [kind count min-count max-count distinct conform-all]} opts + kfn (or (:kfn opts) (fn [i _] i)) + spec @spec-delay] + (or (coll-problems x kind (:kind-form opts) count min-count max-count distinct path via in) + (apply concat + ((if conform-all identity (partial take (coll-error-limit))) + (keep identity + (map (fn [i v] + (when-not (valid? spec v) + (explain-1 form pred path via (conj in (kfn i v)) v))) + (range) x))))))) + (gen* [_ overrides path rmap] + (if gfn + (gfn) + (let [{:keys [count min-count max-count distinct gen-max]} opts + pgen (gensub pred overrides path rmap form) + ;; Element count: prefer explicit :count, then a bounded + ;; range, else 0..gen-max (default 20). + n-gen (cond + count (gen/return count) + (or min-count max-count) + (gen/choose (or min-count 0) + (or max-count + (max (or gen-max 20) + (* 2 (or min-count 0))))) + :else (gen/choose 0 (or gen-max 20))) + ;; Collection-of-elements gen with rose-tree shrinks. + vec-gen (if distinct + (gen/vector-distinct + pgen + (cond + count {:num-elements count :max-tries 100} + :else {:min-elements (or min-count 0) + :max-elements (or max-count + (max (or gen-max 20) + (* 2 (or min-count 0)))) + :max-tries 100})) + (gen/bind n-gen (fn [n] (gen/vector pgen n))))] + (gen/fmap #(generated-coll opts %) vec-gen)))) + (with-gen* [_ gfn] (->EverySpec form pred opts gfn spec-delay)) + (describe* [_] (or (:describe opts) + (cons 'cljd.spec.alpha/every (cons form (:opts opts)))))) + +(defn ^:skip-wiki every-impl + ([form pred opts] (every-impl form pred opts nil)) + ([form pred opts gfn] + (->EverySpec form pred opts gfn (delay (specize pred form))))) + +(deftype NilableSpec [form pred gfn spec-delay] + Spec + (conform* [_ x] (if (nil? x) nil (conform* @spec-delay x))) + (unform* [_ x] (if (nil? x) nil (unform* @spec-delay x))) + (explain* [_ path via in x] + (when-not (or (nil? x) (valid? @spec-delay x)) + (conj + (explain-1 form pred (conj path :cljd.spec.alpha/pred) via in x) + {:path (conj path :cljd.spec.alpha/nil) :pred 'cljd.core/nil? :val x :via via :in in}))) + (gen* [_ overrides path rmap] + (if gfn + (gfn) + (if-let [g (try-gensub pred overrides (conj path :cljd.spec.alpha/pred) rmap form)] + (gen/frequency + [[1 (gen/return nil)] + [9 g]]) + (gen/return nil)))) + (with-gen* [_ gfn] (->NilableSpec form pred gfn spec-delay)) + (describe* [_] (list 'cljd.spec.alpha/nilable form))) + +(defn ^:skip-wiki nilable-impl [form pred gfn] + (->NilableSpec form pred gfn (delay (specize pred form)))) + +(deftype NonconformingSpec [form pred spec-delay] + Spec + (conform* [_ x] + (let [ret (conform* @spec-delay x)] + (if (invalid? ret) ret x))) + (unform* [_ x] (unform* @spec-delay x)) + (explain* [_ path via in x] (explain* @spec-delay path via in x)) + (gen* [_ overrides path rmap] (gen* @spec-delay overrides path rmap)) + (with-gen* [_ gfn] (->NonconformingSpec form (with-gen* @spec-delay gfn) (delay (with-gen* @spec-delay gfn)))) + (describe* [_] (list 'cljd.spec.alpha/nonconforming form))) + +(defn nonconforming-impl [form pred] + (->NonconformingSpec form pred (delay (specize pred form)))) + +(defn- regex-op [op m] + (assoc m :cljd.spec.alpha.impl/op op)) + +(defn ^:skip-wiki cat-impl [ks preds forms] + (regex-op :cljd.spec.alpha.impl/cat + {:ks ks :preds preds :forms forms})) + +(defn ^:skip-wiki alt-impl [ks preds forms] + (regex-op :cljd.spec.alpha.impl/alt + {:ks ks :preds preds :forms forms})) + +(defn ^:skip-wiki rep-impl [form pred] + (regex-op :cljd.spec.alpha.impl/rep + {:form form :pred pred})) + +(defn ^:skip-wiki rep+impl [form pred] + (regex-op :cljd.spec.alpha.impl/rep+ + {:form form :pred pred})) + +(defn ^:skip-wiki maybe-impl [pred form] + (regex-op :cljd.spec.alpha.impl/maybe + {:form form :pred pred})) + +(defn ^:skip-wiki amp-impl [re re-form preds pred-forms] + (regex-op :cljd.spec.alpha.impl/amp + {:re re :re-form re-form :preds preds :forms pred-forms})) + +(declare match-regex-results op-unform op-describe) + +(def ^:private no-ret :cljd.spec.alpha.impl/no-ret) + +(defn- match-result [ret input] + {:ret ret :input input}) + +(defn- no-ret? [x] + (identical? no-ret x)) + +(defn- include-cat-ret? [pred ret] + (let [pred (reg-resolve pred)] + (not (or (no-ret? ret) + (and (= (:cljd.spec.alpha.impl/op pred) :cljd.spec.alpha.impl/rep) + (empty? ret)))))) + +(defn- match-pred-results [form pred input] + (let [pred (reg-resolve pred)] + (if (regex? pred) + (match-regex-results pred input) + (when-let [xs (seq input)] + (let [x (first xs) + ret (dt pred x form)] + (when-not (invalid? ret) + [(match-result ret (rest xs))])))))) + +(defn- match-repeat-results [form pred input min-count] + (letfn [(step [ret input n] + (concat + (when (<= min-count n) + [(match-result ret input)]) + (apply concat + (map (fn [{cv :ret more :input}] + (when-not (= (count input) (count more)) + (step (conj ret cv) more (inc n)))) + (match-pred-results form pred input)))))] + (step [] input 0))) + +(defn- match-cat-results [ks preds forms input] + (letfn [(step [ret ks preds forms input] + (if-let [pred (first preds)] + (apply concat + (map (fn [{cv :ret more :input}] + (step (if (include-cat-ret? pred cv) + (assoc ret (first ks) cv) + ret) + (rest ks) + (rest preds) + (rest forms) + more)) + (match-pred-results (first forms) pred input))) + [(match-result ret input)]))] + (step {} ks preds forms input))) + +(defn- match-alt-results [ks preds forms input] + (apply concat + (map (fn [k pred form] + (map (fn [{cv :ret more :input}] + (match-result (tagged-ret k cv) more)) + (match-pred-results form pred input))) + ks preds forms))) + +(defn- match-regex-results [re input] + (let [r (reg-resolve re) + ks (:ks r) + preds (:preds r) + forms (:forms r) + form (:form r) + pred (:pred r) + inner-re (:re r) + re-form (:re-form r) + op (:cljd.spec.alpha.impl/op r)] + (case op + :cljd.spec.alpha.impl/cat + (match-cat-results ks preds forms input) + + :cljd.spec.alpha.impl/alt + (match-alt-results ks preds forms input) + + :cljd.spec.alpha.impl/rep + (match-repeat-results form pred input 0) + + :cljd.spec.alpha.impl/rep+ + (match-repeat-results form pred input 1) + + :cljd.spec.alpha.impl/maybe + (cons (match-result no-ret input) + (match-pred-results form pred input)) + + :cljd.spec.alpha.impl/amp + (keep (fn [{cv :ret more :input}] + (let [ret (and-preds cv preds forms)] + (when-not (invalid? ret) + (match-result ret more)))) + (match-pred-results re-form inner-re input))))) + +(defn- op-unform [re x] + (let [r (reg-resolve re) + ks (:ks r) + preds (:preds r) + pred (:pred r) + inner-re (:re r) + op (:cljd.spec.alpha.impl/op r) + k->p (zipmap ks preds)] + (case op + :cljd.spec.alpha.impl/cat + (apply concat + (keep (fn [k] + (when (contains? x k) + (op-unform (get k->p k) (get x k)))) + ks)) + + :cljd.spec.alpha.impl/alt + (let [[k v] x] + (op-unform (get k->p k) v)) + + :cljd.spec.alpha.impl/rep + (apply concat (map #(op-unform pred %) x)) + + :cljd.spec.alpha.impl/rep+ + (apply concat (map #(op-unform pred %) x)) + + :cljd.spec.alpha.impl/maybe + ;; nil/no-ret denote "matched nothing" (paired with the same + ;; rewrite at the top of RegexSpec.conform*). If a (s/? nil?) + ;; conformed to nil, we collapse it to [] too — same asymmetry as + ;; upstream clojure.spec.alpha. + (if (or (no-ret? x) (nil? x)) + [] + (op-unform pred x)) + + :cljd.spec.alpha.impl/amp + (let [px (reduce #(unform %2 %1) x (reverse preds))] + (op-unform inner-re px)) + + nil + [(unform re x)]))) + +(defn- op-describe [re] + (let [r (reg-resolve re) + ks (:ks r) + forms (:forms r) + form (:form r) + re-form (:re-form r) + op (:cljd.spec.alpha.impl/op r)] + (case op + :cljd.spec.alpha.impl/cat + (cons 'cljd.spec.alpha/cat (mapcat vector ks forms)) + + :cljd.spec.alpha.impl/alt + (cons 'cljd.spec.alpha/alt (mapcat vector ks forms)) + + :cljd.spec.alpha.impl/rep + (list 'cljd.spec.alpha/* form) + + :cljd.spec.alpha.impl/rep+ + (list 'cljd.spec.alpha/+ form) + + :cljd.spec.alpha.impl/maybe + (list 'cljd.spec.alpha/? form) + + :cljd.spec.alpha.impl/amp + (cons 'cljd.spec.alpha/& (cons re-form forms)) + + nil + re))) + +(declare op-gen) + +(defn- atomic-op-gen [pred overrides path rmap form] + (when-let [g (gensub pred overrides path rmap form)] + (gen/fmap vector g))) + +(defn- op-gen [re overrides path rmap form] + (let [r (reg-resolve re) + ks (:ks r) + preds (:preds r) + forms (:forms r) + pred (:pred r) + inner-re (:re r) + re-form (:re-form r) + op (:cljd.spec.alpha.impl/op r)] + (or (when-let [gfn (:cljd.spec.alpha.impl/gfn r)] + (gfn)) + (case op + :cljd.spec.alpha.impl/cat + (let [gs (map (fn [k pred form] + (op-gen pred overrides (conj path k) rmap form)) + ks preds forms)] + (when (every? identity gs) + (apply gen/cat gs))) + + :cljd.spec.alpha.impl/alt + (let [gs (remove nil? + (map (fn [k pred form] + (try + (op-gen pred overrides (conj path k) rmap form) + (catch Exception _ nil) + (catch Error _ nil))) + ks preds forms))] + (when-not (empty? gs) + (gen/one-of gs))) + + :cljd.spec.alpha.impl/rep + (if-let [g (try + (op-gen pred overrides path rmap (:form r)) + (catch Exception _ nil) + (catch Error _ nil))] + (gen/fmap #(apply concat %) (gen/vector g 0 4)) + (gen/return [])) + + :cljd.spec.alpha.impl/rep+ + (when-let [g (try + (op-gen pred overrides path rmap (:form r)) + (catch Exception _ nil) + (catch Error _ nil))] + (gen/fmap #(apply concat %) (gen/vector g 1 4))) + + :cljd.spec.alpha.impl/maybe + (if-let [g (try + (op-gen pred overrides path rmap (:form r)) + (catch Exception _ nil) + (catch Error _ nil))] + (gen/one-of [(gen/return []) g]) + (gen/return [])) + + :cljd.spec.alpha.impl/amp + ;; Inner regex must satisfy the amp's predicates after the + ;; conform; filter generated values via such-that against + ;; the same and-pred path conform* uses. + (when-let [g (op-gen inner-re overrides path rmap re-form)] + (gen/such-that #(not (invalid? (and-preds % preds forms))) + g + 100)) + + nil + (atomic-op-gen re overrides path rmap form))))) + +(deftype RegexSpec [re gfn] + Spec + (conform* [_ x] + (if (or (nil? x) (sequential? x)) + (if-let [{ret :ret input :input} + (first (filter #(empty? (:input %)) + (match-regex-results re (seq x))))] + (if (empty? input) + ;; An empty maybe yields the internal no-ret sentinel; expose + ;; it as nil at the user-facing boundary so callers can pattern + ;; on a stable value. op-unform handles nil symmetrically. + (if (no-ret? ret) nil ret) + :cljd.spec.alpha/invalid) + :cljd.spec.alpha/invalid) + :cljd.spec.alpha/invalid)) + (unform* [_ x] (vec (op-unform re x))) + (explain* [this path via in x] + (cond + (not (or (nil? x) (sequential? x))) + [{:path path + :pred '(fn [%] (cljd.core/or (cljd.core/nil? %) (cljd.core/sequential? %))) + :val x + :via via + :in in}] + + (valid? this x) + nil + + :else + [{:path path :pred (op-describe re) :val x :via via :in in}])) + (gen* [_ overrides path rmap] + (if gfn + (gfn) + (op-gen re overrides path rmap (op-describe re)))) + (with-gen* [_ gfn] (->RegexSpec re gfn)) + (describe* [_] (op-describe re))) + +(defn ^:skip-wiki regex-spec-impl [re gfn] + (->RegexSpec re gfn)) + +(defn- call-valid? + [f specs args] + (try + (let [cargs (when (:args specs) (conform (:args specs) args))] + (if (invalid? cargs) + false + (let [ret (apply f args) + cret (when (:ret specs) (conform (:ret specs) ret))] + (and (not (invalid? cret)) + (if (:fn specs) + (valid? (:fn specs) {:args cargs :ret cret}) + true))))) + (catch Exception _ false) + (catch Error _ false))) + +(defn- assoc-fn-failure-context [probs args role] + (map #(assoc % + :cljd.spec.alpha/args args + :cljd.spec.alpha/role role) + probs)) + +(defn- call-problems + [f specs args path via in] + (let [argspec (:args specs) + retspec (:ret specs) + fnspec (:fn specs) + cargs (when argspec (conform argspec args))] + (if (invalid? cargs) + (assoc-fn-failure-context + (explain* argspec (conj path :args) via in args) + args + :args) + (try + (let [retv (apply f args) + cret (when retspec (conform retspec retv))] + (cond + (invalid? cret) + (assoc-fn-failure-context + (explain* retspec (conj path :ret) via in retv) + args + :ret) + + (and argspec retspec fnspec + (not (valid? fnspec {:args cargs :ret cret}))) + (assoc-fn-failure-context + (explain* fnspec (conj path :fn) via in {:args cargs :ret cret}) + args + :fn))) + (catch Exception e + [{:path path + :pred 'cljd.core/ifn? + :val f + :reason "exception thrown during generative check" + :via via + :in in + :cljd.spec.alpha/args args + :cljd.spec.alpha/failure e}]) + (catch Error e + [{:path path + :pred 'cljd.core/ifn? + :val f + :reason "error thrown during generative check" + :via via + :in in + :cljd.spec.alpha/args args + :cljd.spec.alpha/failure e}]))))) + +(defn- fn-problems + "Walks up to `iters` argument samples looking for the first set of + call-problems. When found, descends into that sample's rose tree to + find a strictly smaller still-failing input via depth-first walk — + the resulting problems describe a minimized counterexample, not the + raw first failure." + [f specs iters path via in] + (if-let [argspec (:args specs)] + (let [g (gen argspec) + rng (gen-tc/make-rng nil) + fails? (fn [args] (seq (call-problems f specs args path via in))) + first-rose (loop [i 0] + (when (< i iters) + (let [size (clojure.core/min 200 i) + rose (gen-tc/run-gen g rng size)] + (if (fails? (gen-tc/rose-val rose)) + rose + (recur (inc i))))))] + (when first-rose + (let [shrunk (loop [r first-rose] + (let [child (some (fn [c] + (when (fails? (gen-tc/rose-val c)) c)) + (gen-tc/rose-children r))] + (if child (recur child) r)))] + (call-problems f specs (gen-tc/rose-val shrunk) path via in)))) + (throw (Exception. "No :args spec found, can't generate")))) + +(defn- validate-fn? + [f specs iters] + (if-let [argspec (:args specs)] + (let [g (gen argspec)] + (loop [i 0] + (cond + (= i iters) true + (call-valid? f specs (gen/generate g)) (recur (inc i)) + :else false))) + (throw (Exception. "No :args spec found, can't generate")))) + +(deftype FSpec [argspec aform retspec rform fnspec fform gfn specs] + cljd.core/ILookup + (-lookup [_ k] (get specs k)) + (-lookup [_ k not-found] (get specs k not-found)) + (-contains-key? [_ k] (contains? specs k)) + + Spec + (conform* [_ f] + (if (and (ifn? f) (validate-fn? f specs (fspec-iterations))) + f + :cljd.spec.alpha/invalid)) + (unform* [_ f] f) + (explain* [this path via in f] + (cond + (not (ifn? f)) + [{:path path :pred 'cljd.core/ifn? :val f :via via :in in}] + + :else + (when-let [probs (seq (fn-problems f specs 100 path via in))] + (map #(cond-> % + (nil? (:reason %)) + (assoc :reason "function failed generative check")) + probs)))) + (gen* [_ overrides _ _] + (if gfn + (gfn) + (gen/return + (fn [& args] + (when argspec + (assert (valid? argspec args) (with-out-str (explain argspec args)))) + (gen/generate (gen retspec overrides)))))) + (with-gen* [_ gfn] (->FSpec argspec aform retspec rform fnspec fform gfn specs)) + (describe* [_] + (let [ret (cond-> ['cljd.spec.alpha/fspec] + argspec (conj :args aform) + retspec (conj :ret rform) + fnspec (conj :fn fform))] + ret))) + +(defn ^:skip-wiki fspec-impl [argspec aform retspec rform fnspec fform gfn] + (let [specs (cond-> {} + argspec (assoc :args argspec) + retspec (assoc :ret retspec) + fnspec (assoc :fn fnspec))] + (->FSpec argspec aform retspec rform fnspec fform gfn specs))) + +(defn exercise + "Generates n values compatible with spec and returns [val conformed-val] + tuples. Defaults to 10 samples." + ([spec] (exercise spec 10)) + ([spec n] (exercise spec n nil)) + ([spec n overrides] + (map #(vector % (conform spec %)) + (gen/sample (gen spec overrides) n)))) + +(defn exercise-fn + "Exercises f by applying it to generated samples from fspec's :args spec." + ([f fspec] (exercise-fn f 10 fspec)) + ([f n fspec] + (let [fspec (or fspec (when (symbol? f) (get-spec f))) + f (if (symbol? f) + (if-let [getter (redef-getter f)] + (getter) + (throw (Exception. (str "No fn registered for symbol: " f)))) + f)] + (if-let [arg-spec (:args fspec)] + (map (fn [args] [args (apply f args)]) + (gen/sample (gen arg-spec) n)) + (throw (Exception. "No :args spec found, can't generate")))))) diff --git a/clj/src/cljd/spec/gen/alpha.cljd b/clj/src/cljd/spec/gen/alpha.cljd new file mode 100644 index 00000000..8510212c --- /dev/null +++ b/clj/src/cljd/spec/gen/alpha.cljd @@ -0,0 +1,375 @@ +; 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 ^{:doc "ClojureDart port of clojure.spec.gen.alpha. Public API + matches clojure.spec.gen.alpha; internally everything delegates to + cljd.spec.gen.alpha.test-check so generators carry rose-tree shrink data + through to s/check. + + The legacy `(generator no-arg-fn)` shape from earlier ClojureDart + releases is still accepted: `generator` adapts a 0-arity fn into a + tc Generator with no shrinks. New code should prefer building + generators with the combinators here (return, fmap, bind, etc.) so + shrinks compose." + :author "Rich Hickey"} + cljd.spec.gen.alpha + (:refer-clojure :exclude [boolean bytes cat char double int keyword + long map not-empty set string symbol vector + list shuffle]) + (:require [cljd.spec.gen.alpha.test-check :as tc])) + +;; --------------------------------------------------------------------------- +;; Adapter: tc Generator coercion + +(defn ensure-generator + "Coerces g into a cljd.spec.gen.alpha.test-check Generator. Returns + g unchanged if it's already one; wraps a no-arg fn as a no-shrink + Generator; passes nil through. The single entry point for adapting + legacy `(fn [] x)`-shaped generators (the pre-test_check ClojureDart + convention) into shrink-aware ones — used at the spec.alpha gensub + boundary and as the public `generator` constructor." + [g] + (cond + (nil? g) nil + (tc/generator? g) g + (clojure.core/ifn? g) (tc/generator (fn [_ _] (tc/pure (g)))) + :else g)) + +(defn generator + "Coerces its argument into a tc Generator (alias of `ensure-generator`). + Existing call sites that passed `(fn [] ...)` continue to work; the + resulting generator produces values with no shrinks. New code should + prefer the named combinators below so shrinks compose." + [f-or-g] + (ensure-generator f-or-g)) + +(defn generator? + "Returns x if x is a tc Generator, else nil." + [x] + (tc/generator? x)) + +(defn generate + "Generate a single value from generator g." + ([g] (tc/generate g)) + ([g size] (tc/generate g size)) + ([g size seed] (tc/generate g size seed))) + +(defn sample + "Returns n generated values from g (default 10)." + ([g] (tc/sample g)) + ([g n] (tc/sample g n)) + ([g n seed] (tc/sample g n seed))) + +;; --------------------------------------------------------------------------- +;; Core combinators + +(defn return [x] (tc/return x)) +(defn fmap [f g] (tc/fmap f g)) +(defn bind [g f] (tc/bind g f)) +(defn sized [f] (tc/sized f)) +(defn resize [n g] (tc/resize n g)) + +(defn delay-impl + "Returns a generator that derefs gfnd at generation time. Used by + the `delay` macro." + [gfnd] + (tc/generator (fn [rng size] (tc/run-gen @gfnd rng size)))) + +(defmacro delay + "Delays construction of a generator until generation time. Useful + for self-referential / mutually recursive specs." + [expr] + `(delay-impl (clojure.core/delay ~expr))) + +(defn choose [lo hi] (tc/choose lo hi)) +(defn elements [coll] (tc/elements coll)) +(defn one-of [gens] (tc/one-of gens)) +(defn frequency [pairs] (tc/frequency pairs)) + +(defn tuple [& gens] (apply tc/tuple gens)) + +(defn cat + "Concatenates the sequential values produced by `gens` into a + single seq, preserving order." + [& gens] + (tc/fmap #(apply concat %) (apply tc/tuple gens))) + +(defn such-that + "Returns a generator that retries g until pred holds, up to + max-tries (default 100)." + ([pred g] (tc/such-that pred g)) + ([pred g max-tries] (tc/such-that pred g max-tries))) + +(defn vector + "Returns a generator of vectors of values from g." + ([g] (tc/vector g)) + ([g n] (tc/vector g n)) + ([g min max] (tc/vector g min max))) + +(defn bytes + "Returns a generator of byte vectors represented as immutable + List values with entries in [0,255]." + [] + (fmap int-array (vector (choose 0 255)))) + +(defn list + "Returns a generator of lists of values from g." + ([g] (tc/list g)) + ([g n] (tc/list g n)) + ([g min max] (tc/list g min max))) + +(defn set + "Returns a generator of sets of values from g." + ([g] (tc/set g)) + ([g n] (tc/set g n)) + ([g min max] (tc/set g min max))) + +(defn map + "Returns a generator of maps from kg/vg generators." + ([kg vg] (tc/map kg vg)) + ([kg vg n] (tc/map kg vg n)) + ([kg vg min max] (tc/map kg vg min max))) + +(defn hash-map + "Returns a generator of maps from alternating key/value generators." + [& kvs] + (apply tc/hash-map kvs)) + +(defn vector-distinct + "Returns a generator of vectors with distinct elements. Opts: + :num-elements, :min-elements, :max-elements, :max-tries." + ([g] (tc/vector-distinct g)) + ([g opts] (tc/vector-distinct g opts))) + +(defn not-empty + "Returns a generator producing a non-empty collection from g." + [g] + (tc/not-empty g)) + +(defn ^{:deprecated "0.1" + :doc "Returns a generator of shuffled coll. DEPRECATED: the + signature diverges from clojure.test.check.generators/shuffle, which + takes a generator-of-coll. Prefer `(fmap clojure.core/shuffle gen)` + where `gen` produces the collection to shuffle. This will be removed + or rebased on the test.check signature in a future release."} + shuffle + [coll] + (tc/generator (fn [_ _] (tc/pure (clojure.core/shuffle coll))))) + +;; --------------------------------------------------------------------------- +;; Numeric / character / string + +(defn large-integer* [opts] (tc/large-integer* opts)) +(defn large-integer [] (tc/large-integer)) +(defn int [] (tc/int)) +(defn double* [opts] (tc/double* opts)) +(defn double [] (tc/double)) +(defn boolean [] (tc/boolean)) + +(defn char [] (tc/char)) +(defn char-ascii [] (tc/char-ascii)) +(defn char-alpha [] (tc/char-alpha)) +(defn char-alphanumeric [] (tc/char-alphanumeric)) + +(defn string [] (tc/string)) +(defn string-ascii [] (tc/string-ascii)) +(defn string-alphanumeric [] (tc/string-alphanumeric)) + +;; --------------------------------------------------------------------------- +;; Idents, UUIDs, instants + +(defn- ident-name [] + (let [s (generate (string-alphanumeric))] + (if (empty? s) "x" s))) + +(defn keyword + "Returns a generator of simple keywords." + [] + (tc/generator (fn [_ _] (tc/pure (clojure.core/keyword (ident-name)))))) + +(defn keyword-ns + "Returns a generator of namespace-qualified keywords." + [] + (tc/generator + (fn [_ _] + (tc/pure (clojure.core/keyword (str "gen" (rand-int 16)) (ident-name)))))) + +(defn symbol + "Returns a generator of simple symbols." + [] + (tc/generator (fn [_ _] (tc/pure (clojure.core/symbol (ident-name)))))) + +(defn symbol-ns + "Returns a generator of namespace-qualified symbols." + [] + (tc/generator + (fn [_ _] + (tc/pure (clojure.core/symbol (str "gen" (rand-int 16)) (ident-name)))))) + +(defn uuid + "Returns a generator of UUIDs." + [] + (letfn [(quad-hex [rng] + (subs (.toRadixString (+ 65536 (tc/rng-int rng 65536)) 16) 1))] + (tc/generator + (fn [rng _] + (let [ver-triple-hex (.toRadixString + (bit-or 0x4000 + (bit-and 0x0fff (tc/rng-int rng 65536))) + 16) + res-triple-hex (.toRadixString + (bit-or 0x8000 + (bit-and 0x3fff (tc/rng-int rng 65536))) + 16)] + (tc/pure + (clojure.core/uuid + (str (quad-hex rng) (quad-hex rng) "-" (quad-hex rng) "-" + ver-triple-hex "-" res-triple-hex "-" + (quad-hex rng) (quad-hex rng) (quad-hex rng))))))))) + +(defn ratio + "Returns a generator of ratios, represented as ClojureDart numbers." + [] + (fmap #(/ % (inc (rand-int 99))) (large-integer))) + +;; --------------------------------------------------------------------------- +;; simple / any composites + +(defn simple-type-printable + "Returns a generator of simple printable values." + [] + (one-of [(return nil) + (boolean) + (large-integer) + (double) + (string-alphanumeric) + (keyword) + (symbol) + (char)])) + +(defn simple-type + "Returns a generator of simple values." + [] + (simple-type-printable)) + +(defn- any-value [depth printable?] + (if (zero? depth) + (generate (if printable? (simple-type-printable) (simple-type))) + (generate + (one-of + [(if printable? (simple-type-printable) (simple-type)) + (fmap vec (vector (generator #(any-value (dec depth) printable?)) 0 4)) + (fmap #(apply clojure.core/list %) + (vector (generator #(any-value (dec depth) printable?)) 0 4)) + (fmap #(into #{} %) + (vector (generator #(any-value (dec depth) printable?)) 0 4)) + (map (generator #(any-value (dec depth) printable?)) + (generator #(any-value (dec depth) printable?)) + 0 4)])))) + +(defn any + "Returns a generator of arbitrary values." + [] + (generator (fn [] (any-value 2 false)))) + +(defn any-printable + "Returns a generator of arbitrary printable values." + [] + (generator (fn [] (any-value 2 true)))) + +;; --------------------------------------------------------------------------- +;; Property runner — delegates entirely to tc + +(defn for-all* + "Bundles gens + pred into a property map for `quick-check`. The + predicate is called with generated args spread (apply pred args). + Returns a map {:gen ... :pred ...}." + [gens pred] + (tc/for-all* gens pred)) + +(defn quick-check + "Runs `prop` (built by `for-all*`) up to `num-tests` times. See + cljd.spec.gen.alpha.test-check/quick-check for the result-map shape." + ([num-tests prop] (tc/quick-check num-tests prop)) + ([num-tests prop opts] (tc/quick-check num-tests prop opts)) + ;; Variadic call form historically used by callers that pass + ;; alternating k/v opts. Convert to a map and delegate. + ([num-tests prop k v & more] + (tc/quick-check num-tests prop (apply clojure.core/hash-map k v more)))) + +;; --------------------------------------------------------------------------- +;; Predicate-keyed generator registry + +(defn- qualified? [ident] + (not (nil? (namespace ident)))) + +(def ^:private gen-builtins + (clojure.core/delay + (let [simple (simple-type-printable)] + {any? (one-of [(return nil) (any-printable)]) + some? (such-that some? (any-printable)) + number? (one-of [(large-integer) (double)]) + integer? (large-integer) + rational? (large-integer) + int? (large-integer) + pos-int? (large-integer* {:min 1}) + neg-int? (large-integer* {:max -1}) + nat-int? (large-integer* {:min 0}) + float? (double) + double? (double) + string? (string-alphanumeric) + ident? (one-of [(keyword-ns) (symbol-ns)]) + simple-ident? (one-of [(keyword) (symbol)]) + qualified-ident? (such-that qualified? (one-of [(keyword-ns) (symbol-ns)])) + keyword? (keyword-ns) + simple-keyword? (keyword) + qualified-keyword? (such-that qualified? (keyword-ns)) + symbol? (symbol-ns) + simple-symbol? (symbol) + qualified-symbol? (such-that qualified? (symbol-ns)) + uuid? (uuid) + uri? (fmap #(Uri/parse (str "http://" % ".example")) (string-alphanumeric)) + inst? (fmap #(DateTime/fromMillisecondsSinceEpoch %) (large-integer)) + seqable? (one-of [(return nil) + (list simple) + (vector simple) + (map simple simple) + (set simple) + (string-alphanumeric)]) + indexed? (vector simple) + map? (map simple simple) + vector? (vector simple) + list? (list simple) + seq? (list simple) + char? (char) + set? (set simple) + nil? (return nil) + false? (return false) + true? (return true) + boolean? (boolean) + zero? (return 0) + coll? (one-of [(map simple simple) + (list simple) + (vector simple) + (set simple)]) + empty? (elements [nil '() [] {} #{}]) + associative? (one-of [(map simple simple) (vector simple)]) + sequential? (one-of [(list simple) (vector simple)]) + fn? (elements [identity (fn [_] nil) (fn [_ _] nil)]) + ifn? (one-of [(elements [identity (fn [_] nil)]) + (keyword) + (set simple)]) + reduced? (fmap reduced simple) + volatile? (fmap volatile! simple)}))) + +(defn gen-for-pred + "Given a predicate, returns a built-in generator if one exists." + [pred] + (if (clojure.core/set? pred) + (elements pred) + (get @gen-builtins pred))) diff --git a/clj/src/cljd/spec/gen/alpha/test_check.cljd b/clj/src/cljd/spec/gen/alpha/test_check.cljd new file mode 100644 index 00000000..279eb9e0 --- /dev/null +++ b/clj/src/cljd/spec/gen/alpha/test_check.cljd @@ -0,0 +1,640 @@ +; Copyright (c) Rich Hickey, Reid Draper. 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 ^{:doc "Minimal port of clojure.test.check.generators for cljd.spec.alpha. + + Provides shrink-aware property-based generators backed by: + - a rose tree for shrink representation + - a seedable PRNG (dart:math.Random with an integer seed) + - a Generator type wrapping (fn [rng size] -> Rose) + + Pragmatic simplifications relative to test.check: + - The PRNG is not splittable; child generators reseed from the + parent's next int. + - Sized recursion uses a single int `size` parameter; there is no + full Size machinery. + - bind shrinking does not coordinate the bound value with the + inner generator's shrinks (we shrink the inner first, then walk + outer shrinks)." + :author "Rich Hickey, Reid Draper"} + cljd.spec.gen.alpha.test-check + (:refer-clojure :exclude [boolean cat char double int keyword list map + not-empty set string symbol vector]) + (:require ["dart:math" :as math])) + +;; --------------------------------------------------------------------------- +;; Rose tree +;; +;; A Rose is { :val a :children (Seq Rose) }. The children +;; represent strictly smaller candidate values reachable by shrinking +;; the current value. + +(deftype Rose [val children] + Object + (^String toString [_] (str "#Rose<" val ">"))) + +(defn rose + "Build a rose tree from a value and a (possibly lazy) seq of children + rose trees representing shrink candidates." + [val children] + (->Rose val children)) + +(defn pure + "A rose tree with no shrinks." + [v] + (rose v ())) + +(defn rose-val + "Returns the value at the root of a rose." + [^Rose r] + (.-val r)) + +(defn rose-children + "Returns the children seq of a rose." + [^Rose r] + (.-children r)) + +(defn rose-fmap + "Maps f across the value at every node of the rose tree." + [f r] + (rose (f (rose-val r)) + (clojure.core/map #(rose-fmap f %) (rose-children r)))) + +(defn rose-filter + "Returns a rose with `pred` applied recursively, dropping any + subtrees whose root value fails pred. Returns nil if the root fails." + [pred r] + (when (pred (rose-val r)) + (rose (rose-val r) + (keep #(rose-filter pred %) (rose-children r))))) + +(defn rose-join + "Flattens a Rose> into a Rose by splicing the inner + rose's children together with the outer rose's shrinks." + [r] + (let [inner (rose-val r) + outer-shrinks (clojure.core/map rose-join (rose-children r))] + (rose (rose-val inner) + (concat (rose-children inner) outer-shrinks)))) + +;; --------------------------------------------------------------------------- +;; Sized seedable PRNG + +(deftype Rng [r] + Object + (^String toString [_] "#Rng")) + +(defn make-rng + "Returns a new Rng. With no arg or nil, seeds non-deterministically; + with an int, seeds reproducibly." + ([] (->Rng (math/Random.))) + ([seed] + (->Rng (if (nil? seed) + (math/Random.) + (math/Random. seed))))) + +(defn rng? + "Returns x if x is an Rng instance, else nil." + [x] + (when (instance? Rng x) x)) + +(defn rng-int + "Returns a uniformly random int in [0, max). Dart's Random.nextInt + is capped at 2^32; for larger spans we compose two 32-bit draws and + take modulo, accepting a small bias acceptable for generator use." + [^Rng rng max] + (if (<= max 4294967296) + (.nextInt (.-r rng) max) + (let [hi (.nextInt (.-r rng) 2147483648) ; 2^31 — keeps 64-bit result positive + lo (.nextInt (.-r rng) 4294967296) ; 2^32 + v (+ (* hi 4294967296) lo)] + (mod v max)))) + +(defn rng-double + "Returns a uniformly random double in [0.0, 1.0)." + [^Rng rng] + (.nextDouble (.-r rng))) + +(defn split-rng + "Returns a fresh Rng seeded from rng's next int. Used to give + child generators an independent stream while remaining reproducible + given the parent seed." + [rng] + (make-rng (rng-int rng 2147483647))) + +;; --------------------------------------------------------------------------- +;; Generator type + +(deftype Generator [run-fn] + Object + (^String toString [_] "#Gen")) + +(defn generator? + "Returns x if x is a Generator, else nil." + [x] + (when (instance? Generator x) x)) + +(defn generator + "Wraps a (fn [rng size] -> Rose) into a Generator." + [f] + (->Generator f)) + +(defn run-gen + "Evaluates a Generator and returns the resulting rose tree." + [g ^Rng rng size] + ((.-run-fn ^Generator g) rng size)) + +(defn generate + "Generates a single value using `g`. Optional `size` (default 30) + and `seed` (an int for reproducibility, or nil)." + ([g] (generate g 30 nil)) + ([g size] (generate g size nil)) + ([g size seed] + (rose-val (run-gen g (make-rng seed) size)))) + +(defn sample-rose + "Generates n rose trees from g; size grows with index." + ([g] (sample-rose g 10 nil)) + ([g n] (sample-rose g n nil)) + ([g n seed] + (let [rng (make-rng seed)] + (mapv #(run-gen g rng %) (range n))))) + +(defn sample + "Generates n values from g; size grows with index." + ([g] (sample g 10 nil)) + ([g n] (sample g n nil)) + ([g n seed] + (mapv rose-val (sample-rose g n seed)))) + +;; --------------------------------------------------------------------------- +;; Integer shrinking helpers + +(defn- halves + "Returns a seq halving x toward 0: x, x/2, x/4, ..., 1 (or -1)." + [x] + (->> (iterate #(quot % 2) x) + (take-while #(not= 0 %)))) + +(defn- shrink-int-toward + "Returns a seq of ints that shrink src toward dest. Always begins + with dest and excludes src itself." + [dest src] + (let [delta (- src dest)] + (cond + (= dest src) () + :else + (->> (clojure.core/cons dest + (clojure.core/map #(- src %) (halves delta))) + distinct + (filter #(not= % src)))))) + +(declare int-rose) + +(defn- int-rose + "Builds a rose tree for an int that shrinks toward 0." + [v] + (rose v (clojure.core/map int-rose (shrink-int-toward 0 v)))) + +;; --------------------------------------------------------------------------- +;; Core combinators + +(defn return + "Returns a generator that always produces v with no shrinks." + [v] + (generator (fn [_ _] (pure v)))) + +(defn fmap + "Returns a generator whose values are (f x) where x comes from g. + Shrinks of x are mapped through f." + [f g] + (generator (fn [rng size] (rose-fmap f (run-gen g rng size))))) + +(defn bind + "Threads a generated value of g through f to obtain a new generator, + whose result becomes the bind's output. Shrinks try the bound value's + shrinks first, re-running f with a stable inner seed, then try the + inner generator's own shrinks." + [g f] + (generator (fn [rng size] + (let [r (run-gen g rng size) + inner-seed (rng-int rng 2147483647)] + (letfn [(bind-rose [outer] + (let [inner (run-gen (f (rose-val outer)) + (make-rng inner-seed) + size)] + (rose (rose-val inner) + (concat + (clojure.core/map bind-rose + (rose-children outer)) + (rose-children inner)))))] + (bind-rose r)))))) + +(defn sized + "Calls f with the current generation size and runs the returned + generator at that same size." + [f] + (generator (fn [rng size] + (run-gen (f size) rng size)))) + +(defn resize + "Runs generator g with size fixed to n, ignoring the ambient size." + [n g] + (generator (fn [rng _] + (run-gen g rng n)))) + +(defn choose + "Returns a generator for ints in [lo hi] inclusive, shrinking toward lo." + [lo hi] + (let [lo (clojure.core/int lo) + hi (clojure.core/int hi) + span (inc (- hi lo))] + (generator (fn [rng _] + (let [v (if (pos? span) (+ lo (rng-int rng span)) lo)] + (rose v + (clojure.core/map int-rose + (shrink-int-toward lo v)))))))) + +(defn elements + "Returns a generator that picks an element from coll uniformly, + shrinking toward the first element." + [coll] + (when (empty? coll) + (throw (Exception. "elements requires a non-empty collection"))) + (let [xs (vec coll)] + (fmap #(nth xs %) (choose 0 (dec (count xs)))))) + +(defn one-of + "Returns a generator that picks one of `gens` and runs it. Shrinks + via the chosen generator's shrinks plus alternative-generator + shrinks." + [gens] + (let [gs (vec gens)] + (when (empty? gs) + (throw (Exception. "one-of requires a non-empty collection of generators"))) + (bind (choose 0 (dec (count gs))) + #(nth gs %)))) + +(defn frequency + "Returns a generator choosing among [weight gen] pairs, weighted by + the integer weights. Shrinks via the chosen generator." + [pairs] + (let [pairs (vec pairs) + total (reduce + (clojure.core/map first pairs))] + (when (or (empty? pairs) (not (pos? total))) + (throw (Exception. "frequency requires positive weights"))) + (bind (choose 0 (dec total)) + (fn [n] + (loop [n n + [[w g] & more] pairs] + (if (< n w) g (recur (- n w) more))))))) + +(defn such-that + "Returns a generator that retries g until pred holds, up to + max-tries. Shrinks are filtered to only those satisfying pred." + ([pred g] (such-that pred g 100)) + ([pred g max-tries] + (generator (fn [rng size] + (loop [i 0 + last-r nil] + (if (< i max-tries) + (let [r (run-gen g rng size)] + (if (pred (rose-val r)) + (or (rose-filter pred r) r) + (recur (inc i) r))) + (throw (Exception. + (str "Couldn't satisfy such-that predicate after " + max-tries " tries. Last generated value: " + (pr-str (some-> last-r rose-val))))))))))) + +;; --------------------------------------------------------------------------- +;; Tuple + +(declare tuple-rose) + +(defn- tuple-rose + "Given a vec of roses, returns a rose-of-vec whose shrinks are + vecs with one component shrunk." + [roses] + (let [n (count roses) + vs (mapv rose-val roses) + children (mapcat (fn [i] + (clojure.core/map (fn [child] + (tuple-rose (assoc roses i child))) + (rose-children (nth roses i)))) + (range n))] + (rose vs children))) + +(defn tuple + "Returns a generator producing vectors of values, one from each + argument generator. Shrinks each component independently." + [& gens] + (let [gs (vec gens)] + (generator (fn [rng size] + (tuple-rose (mapv #(run-gen % rng size) gs)))))) + +;; --------------------------------------------------------------------------- +;; Sized collections + +(declare vec-rose) + +(defn- vec-rose + "Given a vec of roses, builds a rose-of-vec whose shrinks include: + - drop one element (at each position) + - shrink one element (at each position)" + [roses] + (let [roses (vec roses) + n (count roses) + drop-shrinks (clojure.core/map (fn [i] + (vec-rose + (into (subvec roses 0 i) + (subvec roses (inc i))))) + (range n)) + elem-shrinks (mapcat (fn [i] + (clojure.core/map + (fn [child] (vec-rose (assoc roses i child))) + (rose-children (nth roses i)))) + (range n))] + (rose (mapv rose-val roses) + (concat drop-shrinks elem-shrinks)))) + +(defn- bounded-int [rng lo hi] + (let [lo (clojure.core/int lo) + hi (clojure.core/int hi)] + (if (> lo hi) + lo + (+ lo (rng-int rng (inc (- hi lo))))))) + +(defn vector + "Returns a generator of vectors of values from g. Sizes default to + [0..size]; alternatively a fixed n or [min..max] is allowed." + ([g] + (generator (fn [rng size] + (let [n (if (zero? size) 0 (rng-int rng (inc size))) + roses (vec (for [_ (range n)] (run-gen g rng size)))] + (vec-rose roses))))) + ([g n] + (generator (fn [rng size] + (let [roses (vec (for [_ (range n)] (run-gen g rng size)))] + (vec-rose roses))))) + ([g min-n max-n] + (generator (fn [rng size] + (let [n (bounded-int rng min-n max-n) + roses (vec (for [_ (range n)] (run-gen g rng size)))] + (vec-rose roses)))))) + +(defn list + "Returns a generator of lists from g (vector + reverse-into-list)." + ([g] (fmap #(apply clojure.core/list %) (vector g))) + ([g n] (fmap #(apply clojure.core/list %) (vector g n))) + ([g min-n max-n] (fmap #(apply clojure.core/list %) (vector g min-n max-n)))) + +(defn set + "Returns a generator of sets from g." + ([g] (fmap clojure.core/set (vector g))) + ([g n] (fmap clojure.core/set (vector g n))) + ([g min-n max-n] (fmap clojure.core/set (vector g min-n max-n)))) + +(defn vector-distinct + "Returns a generator of vectors with distinct elements. Accepts + optional opts: :num-elements, :min-elements, :max-elements, + :max-tries." + ([g] (vector-distinct g nil)) + ([g {:keys [num-elements min-elements max-elements max-tries] + :or {min-elements 0 max-tries 100}}] + (generator + (fn [rng size] + (let [target (cond + num-elements num-elements + max-elements (bounded-int rng min-elements max-elements) + :else (rng-int rng (inc (max 3 min-elements))))] + (loop [seen #{} + roses [] + tries 0] + (cond + (= (count roses) target) + (vec-rose roses) + + (= tries max-tries) + (vec-rose roses) + + :else + (let [r (run-gen g rng size) + v (rose-val r)] + (if (contains? seen v) + (recur seen roses (inc tries)) + (recur (conj seen v) (conj roses r) tries)))))))))) + +(defn map + "Returns a generator of maps from key/value generators. Sizes follow + the same conventions as `vector`." + ([kg vg] + (fmap (fn [pairs] (into {} pairs)) (vector (tuple kg vg)))) + ([kg vg n] + (fmap (fn [pairs] (into {} pairs)) (vector (tuple kg vg) n))) + ([kg vg min-n max-n] + (fmap (fn [pairs] (into {} pairs)) (vector (tuple kg vg) min-n max-n)))) + +(defn hash-map + "Returns a generator of maps from alternating key/value generators." + [& kvs] + (let [pairs (partition 2 kvs)] + (fmap (fn [vs] (into {} (clojure.core/map vector + (clojure.core/map first pairs) + vs))) + (apply tuple (clojure.core/map second pairs))))) + +(defn not-empty + "Returns a generator producing a non-empty collection from g." + [g] + (such-that clojure.core/not-empty g)) + +;; --------------------------------------------------------------------------- +;; Numeric / character / string generators + +(defn large-integer* + "Returns a generator of ints constrained by optional :min/:max. + Shrinks toward zero (clamped into [min,max])." + [{:keys [min max]}] + (let [lo (clojure.core/or min -2147483648) + hi (clojure.core/or max 2147483647) + target (cond + (and min (pos? min)) min + (and max (neg? max)) max + :else 0)] + (generator + (fn [rng _] + (let [v (bounded-int rng lo hi)] + (rose v + (clojure.core/map int-rose + (shrink-int-toward target v)))))))) + +(defn large-integer [] + (large-integer* {})) + +(defn int [] + (large-integer* {})) + +(defn double* + "Returns a generator of doubles constrained by :min/:max and optional + :NaN?/:infinite?. Shrinks toward 0 by halving the integer part." + [{:keys [min max NaN? infinite?]}] + (let [min-given? (some? min) + max-given? (some? max) + lo (clojure.core/or min -1000.0) + hi (clojure.core/or max 1000.0) + allow-NaN? (and NaN? (not min-given?) (not max-given?)) + allow-pos-inf? (and infinite? (not max-given?)) + allow-neg-inf? (and infinite? (not min-given?))] + (generator + (fn [rng _] + (let [pick (rng-int rng 100)] + (cond + (and allow-NaN? (= pick 0)) (pure ##NaN) + (and allow-pos-inf? (= pick 1)) (pure ##Inf) + (and allow-neg-inf? (= pick 2)) (pure ##-Inf) + :else + (let [d (+ lo (* (rng-double rng) (- hi lo))) + iv (.toInt d)] + (rose d + (clojure.core/map (fn [shrunk] (pure (.toDouble shrunk))) + (shrink-int-toward 0 iv)))))))))) + +(defn double [] + (double* {})) + +(defn boolean [] + (elements [false true])) + +(defn char + "Returns a generator of printable ASCII characters." + [] + (fmap #(String/fromCharCode %) (choose 32 126))) + +(defn char-ascii [] (char)) +(defn char-alpha [] + (fmap #(String/fromCharCode %) (one-of [(choose 65 90) (choose 97 122)]))) +(defn char-alphanumeric [] + (fmap #(String/fromCharCode %) + (one-of [(choose 48 57) (choose 65 90) (choose 97 122)]))) + +(defn- string-from-char-gen [g] + (fmap #(apply str %) (vector g 0 12))) + +(defn string [] (string-from-char-gen (char))) +(defn string-ascii [] (string-from-char-gen (char-ascii))) +(defn string-alphanumeric [] (string-from-char-gen (char-alphanumeric))) + +(defn from-no-arg-gen + "Adapts a cljd.spec.gen.alpha-style generator (a no-arg fn) into a + tc Generator that produces values with no shrinks. Use to bridge + spec.alpha's existing per-Spec gen* implementations into tc-style + property runs — you keep reproducible seeds and the result-map + shape, but you do not get input shrinking." + [old-g] + (generator (fn [_ _] (pure (old-g))))) + +;; --------------------------------------------------------------------------- +;; Property runner + +(defn- run-pred + "Calls (apply pred args), catching exceptions/errors and returning + them in place of true/false." + [pred args] + (try (apply pred args) + (catch Exception e e) + (catch Error e e))) + +(defn for-all* + "Bundles a vector of generators and a predicate into a Property + map: {:gen :pred }. Pass to `quick-check`. The + predicate is called with the generated args spread (apply pred + args). The property is satisfied when `pred` returns truthy." + [gens pred] + {:gen (apply tuple gens) + :pred pred}) + +(defn- shrink-loop + "Given a rose-of-args that is currently failing pred, walks the + rose tree depth-first looking for a strictly smaller still-failing + args. Stops when no child fails. Returns the test.check-shaped + shrink result map." + [pred rose] + (loop [r rose + depth 0 + total 0] + (let [children (rose-children r) + failing + (reduce (fn [_ child] + (let [args (rose-val child) + result (run-pred pred args)] + (if (true? result) + nil + (reduced {:rose child :result result})))) + nil + children)] + (if failing + (recur (:rose failing) (inc depth) (inc total)) + {:total-nodes-visited (+ total (count children)) + :depth depth + :pass? false + :result (run-pred pred (rose-val r)) + :smallest (rose-val r)})))) + +(defn quick-check + "Runs `prop` (built by `for-all*`) up to `num-tests` times. Size + grows from 0 up to `:max-size` (default 200). Reproducible when + `:seed` is supplied. On failure, walks the rose tree to find the + smallest counterexample. + + Returns a result map matching test.check's shape: + + - `:result` true on success, otherwise the failure value (boolean + false, an exception, or whatever `pred` returned) + - `:num-tests` how many tests were run before stopping + - `:seed` the seed used (echoed for reproducibility) + - `:pass?` true on success + - on failure: `:failing-args` (the first failing input) and + `:shrunk` `{:smallest :result :pass? :total-nodes-visited :depth}`." + ([num-tests prop] (quick-check num-tests prop nil)) + ([num-tests prop {:keys [seed max-size reporter-fn] + :or {max-size 200}}] + (let [{gen :gen pred :pred} prop + seed (if (some? seed) + seed + (.nextInt (math/Random.) 2147483647)) + rng (make-rng seed) + report! (when reporter-fn + (fn [m] (try (reporter-fn m) (catch Exception _ nil))))] + (loop [i 0] + (if (= i num-tests) + (let [r {:result true :num-tests i :seed seed :pass? true}] + (when report! (report! (assoc r :type :complete))) + r) + (let [size (clojure.core/min max-size i) + rose (run-gen gen rng size) + args (rose-val rose) + result (run-pred pred args)] + (if (true? result) + (do + (when report! + (report! {:type :trial + :so-far (inc i) + :num-tests num-tests + :args args})) + (recur (inc i))) + (let [shrunk (shrink-loop pred rose) + r {:result result + :seed seed + :num-tests (inc i) + :pass? false + :failing-args args + :shrunk shrunk}] + (when report! + (report! (assoc r :type :failure))) + r)))))))) diff --git a/clj/src/cljd/spec/test/alpha.cljd b/clj/src/cljd/spec/test/alpha.cljd new file mode 100644 index 00000000..1cf06f19 --- /dev/null +++ b/clj/src/cljd/spec/test/alpha.cljd @@ -0,0 +1,496 @@ +; 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 ^{:doc "ClojureDart port of clojure.spec.test.alpha." + :author "Rich Hickey"} + cljd.spec.test.alpha + (:require [cljd.spec.alpha :as s] + [cljd.spec.alpha.impl :as impl] + [cljd.spec.gen.alpha :as gen] + [cljd.spec.gen.alpha.test-check :as gen-tc])) + +(def ^:private ^:dynamic *instrument-enabled* + "If false, instrumented fns call straight through." + true) + +(defonce ^:private instrumented-vars (atom {})) + +(defn distinct-by + ([f coll] + (letfn [(step [xs seen] + (lazy-seq + (when-let [s (seq xs)] + (let [x (first s) + v (f x)] + (if (contains? seen v) + (step (rest s) seen) + (cons x (step (rest s) (conj seen v))))))))] + (step coll #{})))) + +(defn- ^:macro-support collectionize [x] + (if (symbol? x) (list x) x)) + +(defn- fn-spec? + "Fn-spec must include at least :args or :ret specs." + [m] + (or (:args m) (:ret m))) + +(defn- fn-spec-name? [x] + (symbol? x)) + +(defn- explain-check [args spec v role] + (ex-info + "Specification-based check failed" + (when-not (s/valid? spec v) + (assoc (s/explain-data spec v) + :cljd.spec.alpha/args args + :cljd.spec.alpha/val v + :cljd.spec.alpha/failure :check-failed + :cljd.spec.test.alpha/role role)))) + +(defn- no-fspec [sym spec] + (ex-info (str "Fn at " sym " is not spec'ed.") + {:var sym :spec spec :cljd.spec.alpha/failure :no-fspec})) + +(defn- conform! [sym spec data args] + (let [conformed (s/conform spec data)] + (if (s/invalid? conformed) + (throw + (ex-info (str "Call to " sym " did not conform to spec.") + (assoc (s/explain-data spec data) + :cljd.spec.alpha/fn sym + :cljd.spec.alpha/args args + :cljd.spec.alpha/failure :instrument))) + conformed))) + +(defmacro with-instrument-disabled + "Disables instrument's checking of calls within body." + [& body] + `(binding [*instrument-enabled* nil] + ~@body)) + +(defn- spec-checking-fn [sym f fn-spec] + (let [fn-spec (or fn-spec (throw (no-fspec sym fn-spec))) + args-spec (:args fn-spec)] + (if args-spec + (fn [& args] + (if *instrument-enabled* + (with-instrument-disabled + (conform! sym args-spec args args) + (binding [*instrument-enabled* true] + (apply f args))) + (apply f args))) + f))) + +(defn- instrument-choose-fn + [f spec sym {:keys [gen stub replace]}] + (cond + (contains? (set stub) sym) + (gen/generate (s/gen spec gen)) + + (contains? replace sym) + (get replace sym) + + :else f)) + +(defn- instrument-choose-spec + [spec sym {overrides :spec}] + (or (get overrides sym) spec)) + +(defn ^:skip-wiki instrument-1* + "Runtime helper for the instrument macro. Builds the checking + wrapper and stores it in `instrumented-vars` keyed by sym, then + returns sym (matching CLJS). Callers that need the wrapper read it + back via `(:wrapped (get @instrumented-vars sym))`." + [sym f opts] + (let [spec (s/get-spec sym)] + (when (or spec (get-in opts [:spec sym])) + (let [{:keys [raw wrapped]} (get @instrumented-vars sym) + to-wrap (or raw f) + ospec (or (instrument-choose-spec spec sym opts) + (throw (no-fspec sym spec))) + ofn (instrument-choose-fn to-wrap ospec sym opts) + checked (spec-checking-fn sym ofn ospec)] + (swap! instrumented-vars assoc sym {:raw to-wrap :wrapped checked}) + sym)))) + +(defn ^:skip-wiki unstrument-1* + "Runtime helper for the unstrument macro." + [sym] + (when-let [{:keys [raw]} (get @instrumented-vars sym)] + (swap! instrumented-vars dissoc sym) + raw)) + +(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))]))) + +(defn instrumentable-redef-syms + "Subset of instrumentable-syms whose ^:cljd.spec.alpha/redef + getter/setter pair has been registered. Only these are reachable by + the runtime instrument fn — typically because s/fdef ran for them in + some compilation unit." + ([] (instrumentable-redef-syms nil)) + ([opts] + (let [syms (instrumentable-syms opts)] + (into #{} (filter #(impl/redef-registered? %)) syms)))) + +(defn- collectionize-syms + "Normalizes input to a flat seq of syms. A namespace symbol (no + name part) expands at runtime to every redef-registered sym in that + namespace." + [sym-or-syms] + (cond + (nil? sym-or-syms) nil + (symbol? sym-or-syms) + (if (namespace sym-or-syms) + [sym-or-syms] + (let [nm (name sym-or-syms)] + (filter #(= nm (namespace %)) (impl/redef-registered-syms)))) + :else + (mapcat collectionize-syms sym-or-syms))) + +(defn- instrument-target-syms [sym-or-syms opts] + (let [explicit (collectionize-syms sym-or-syms) + opt-syms (concat (:stub opts) (keys (:replace opts)))] + (if (nil? sym-or-syms) + (instrumentable-redef-syms opts) + (distinct (concat explicit opt-syms))))) + +(defn instrument-runtime + "Runtime variant of `instrument`. Accepts a qualified symbol, a + namespace symbol, or a collection thereof. Looks up each sym's + registered ^:cljd.spec.alpha/redef setter in + cljd.spec.alpha.impl/redef-setters-ref and installs a checking + wrapper. Symbols without a registered setter — i.e. no s/fdef has + expanded for them in any loaded compilation unit — are silently + skipped. Returns a vector of qualified symbols actually + instrumented." + ([] (instrument-runtime (instrumentable-redef-syms) nil)) + ([sym-or-syms] (instrument-runtime sym-or-syms nil)) + ([sym-or-syms opts] + (assert (every? ident? (keys (:gen opts))) "instrument :gen expects ident keys") + (vec (keep (fn [sym] + (when-let [getter (impl/redef-getter sym)] + (let [setter (impl/redef-setter sym) + f (getter) + result (instrument-1* sym f opts)] + (when result + (setter (:wrapped (get @instrumented-vars sym))) + result)))) + (instrument-target-syms sym-or-syms opts))))) + +(defn unstrument-runtime + "Runtime variant of `unstrument`. Accepts a qualified symbol, a + namespace symbol, or a collection thereof. Restores the original + value via the registered setter for each currently-instrumented sym + and returns a vector of qualified symbols whose instrumentation was + removed." + ([] (unstrument-runtime (set (keys @instrumented-vars)))) + ([sym-or-syms] + (vec (keep (fn [sym] + (when-let [setter (impl/redef-setter sym)] + (when-let [raw (unstrument-1* sym)] + (setter raw) + sym))) + (collectionize-syms sym-or-syms))))) + +(defn- ^:macro-support quoted-form [form] + (if (and (seq? form) (= 'quote (first form))) + (second form) + form)) + +(defn- ^:macro-support qualify-symbol [env sym] + (if (namespace sym) + sym + (if-let [current-ns (get-in env [:nses :current-ns])] + (symbol (str current-ns) (name sym)) + sym))) + +(defn- ^:macro-support namespace-sym? [env sym] + (and (symbol? sym) + (nil? (namespace sym)) + (contains? (:nses env) sym))) + +(defn- ^:macro-support namespace-def-syms [env ns-sym] + (into [] + (keep (fn [[name-sym info]] + (when (and (symbol? name-sym) + (map? info) + (= ns-sym (:ns info))) + (symbol (name ns-sym) (name name-sym))))) + (get-in env [:nses ns-sym]))) + +(defn- ^:macro-support enumerate-namespace* [env ns-sym-or-syms] + (into #{} + (mapcat #(namespace-def-syms env %)) + (collectionize ns-sym-or-syms))) + +(defn- ^:macro-support speced-syms [] + (set (map #(with-meta % nil) (s/speced-vars)))) + +(defn- ^:macro-support accessible-sym? [env sym] + (let [sym-ns (some-> (namespace sym) symbol) + current-ns (get-in env [:nses :current-ns]) + current-ns-map (get-in env [:nses current-ns]) + sym-lib (get-in env [:nses sym-ns :lib])] + (or (= sym-ns current-ns) + (and sym-lib (contains? (:imports current-ns-map) sym-lib))))) + +(defn- ^:macro-support accessible-speced-syms [env] + (vec (sort-by str (filter #(accessible-sym? env %) (speced-syms))))) + +(defn- ^:macro-support expand-form-sym [env sym] + (if (namespace-sym? env sym) + (namespace-def-syms env sym) + [(qualify-symbol env sym)])) + +(defn- ^:macro-support resolve-def-info + "Looks up a (possibly qualified) symbol's def info in the analyzer + state attached to env. Returns the info map or nil." + [env sym] + (let [{:keys [libs] :as nses} (:nses env) + current-ns-sym (:current-ns nses) + {:keys [mappings] :as current-ns} (nses current-ns-sym)] + (when-not (env sym) + (or (current-ns sym) + (if-some [v (mappings sym)] + (recur env v) + (let [sym-ns (namespace sym)] + (some-> sym-ns symbol nses (get (symbol (name sym)))))))))) + +(defn- ^:macro-support redef? [env sym] + (-> (resolve-def-info env sym) :cljd.spec.alpha/redef? boolean)) + +(defn- ^:macro-support form-syms [env form] + (let [x (quoted-form form) + xs (if (symbol? x) [x] (vec x))] + (vec (distinct (mapcat #(expand-form-sym env %) xs))))) + +(defn- ^:macro-support explicit-syms + "Returns only the syms explicitly named at the call site — any + namespace-symbol entries in `form` are dropped. Used so that the + instrument/unstrument warning fires for explicit non-redef syms only, + not for namespace expansions where silent skipping is expected." + [env form] + (let [x (quoted-form form) + xs (if (symbol? x) [x] (vec x))] + (vec (distinct (keep (fn [sym] + (when-not (namespace-sym? env sym) + (qualify-symbol env sym))) + xs))))) + +(defn- ^:macro-support check-form-syms [env form] + (let [x (quoted-form form) + xs (if (symbol? x) [x] (vec x)) + speced (speced-syms)] + (vec + (distinct + (mapcat (fn [sym] + (if (namespace-sym? env sym) + (filter #(contains? speced %) (namespace-def-syms env sym)) + [(qualify-symbol env sym)])) + xs))))) + +(defmacro instrument + "Instruments the redef-registered ^:cljd.spec.alpha/redef defs named + by sym-or-syms. Accepts a qualified symbol, a namespace symbol (no + name part) which expands to every redef-registered sym in that ns, + or a collection of those — including a runtime-bound one. A def is + reachable here once an s/fdef for it has expanded in some loaded + compilation unit. Returns a vector of qualified symbols + instrumented." + ([] `(instrument-runtime)) + ([sym-or-syms] `(instrument-runtime ~sym-or-syms)) + ([sym-or-syms opts] `(instrument-runtime ~sym-or-syms ~opts))) + +(defmacro unstrument + "Removes instrumentation from the named symbols. See instrument + for accepted shapes." + ([] `(unstrument-runtime)) + ([sym-or-syms] `(unstrument-runtime ~sym-or-syms))) + +(defn- check-call + "Returns true if call passes specs, otherwise returns an exception + with explain-data." + [f specs args] + (let [cargs (when (:args specs) (s/conform (:args specs) args))] + (if (s/invalid? cargs) + (explain-check args (:args specs) args :args) + (try + (let [ret (apply f args) + cret (when (:ret specs) (s/conform (:ret specs) ret))] + (if (s/invalid? cret) + (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))) + (catch Exception e e) + (catch Error e e))))) + +(defn- quick-check + "Drives cljd.spec.gen.alpha.test-check/quick-check using the spec's :args + generator. Reproducible when :seed is supplied via + :clojure.spec.test.check/opts. With per-Spec gen* impls returning tc + Generators (or wrapped via gen/ensure-generator), shrinking + propagates automatically when the underlying generator carries + shrink data." + [f specs {overrides :gen opts :clojure.spec.test.check/opts old-opts :clojure.test.check/opts}] + (let [opts (or opts old-opts) + num-tests (or (:num-tests opts) 1000) + tc-opts (cond-> {} + (contains? opts :seed) (assoc :seed (:seed opts)) + (contains? opts :max-size) (assoc :max-size (:max-size opts)) + (contains? opts :reporter-fn) (assoc :reporter-fn (:reporter-fn opts))) + g (try (s/gen (:args specs) overrides) + (catch Exception e e) + (catch Error e e))] + (if (or (instance? Exception g) (instance? Error g)) + {:result g} + (let [prop (gen-tc/for-all* [g] #(check-call f specs %))] + (gen-tc/quick-check num-tests prop tc-opts))))) + +(defn- make-check-result + [check-sym spec test-check-ret tc-ret-key] + (merge {:spec spec + tc-ret-key test-check-ret} + (when check-sym + {:sym check-sym}) + (when-let [result (:result test-check-ret)] + (when-not (true? result) + {:failure result})) + (when-let [shrunk (:shrunk test-check-ret)] + {:failure (:result shrunk)}))) + +(defn validate-check-opts [opts] + (assert (every? ident? (keys (:gen opts))) "check :gen expects ident keys")) + +(defn ^:skip-wiki check-1* + [sym f spec opts] + (let [spec (or spec (when sym (s/get-spec sym))) + f (or f (when sym (throw (ex-info "No fn expression supplied by check macro" + {:sym sym :cljd.spec.alpha/failure :no-fn})))) + old-key :clojure.test.check/opts + tc-ret-key (if (contains? opts old-key) + :clojure.test.check/ret + :clojure.spec.test.check/ret)] + (cond + (nil? f) + {:failure (ex-info "No fn to spec" {:cljd.spec.alpha/failure :no-fn}) + :sym sym :spec spec} + + (nil? spec) + {:failure (ex-info "No spec" {:cljd.spec.alpha/failure :no-fspec}) + :sym sym :spec spec} + + (:args spec) + (make-check-result sym spec (quick-check f spec opts) tc-ret-key) + + :else + {:failure (ex-info "No :args spec" {:cljd.spec.alpha/failure :no-args-spec}) + :sym sym :spec spec}))) + +(defn checkable-syms + "Given an opts map as per check, returns the set of syms that can be + checked." + ([] (checkable-syms nil)) + ([opts] + (validate-check-opts opts) + (reduce into #{} + [(filter fn-spec-name? (keys (s/registry))) + (keys (:spec opts))]))) + +(defmacro check + "Run generative tests for spec conformance on explicit vars named by + sym-or-syms. No-arg check expands over s/fdef symbols known at + macro-expansion time." + ([] + `(check '~(accessible-speced-syms &env))) + ([sym-or-syms] + `(check ~sym-or-syms nil)) + ([sym-or-syms opts] + (let [syms (check-form-syms &env sym-or-syms) + opts-sym (gensym "opts")] + `(let [~opts-sym ~opts] + (do + (validate-check-opts ~opts-sym) + [~@(map (fn [sym] + `(check-1* '~sym ~sym nil ~opts-sym)) + syms)]))))) + +(defn check-fn + "Runs generative tests for fn f using fspec and opts." + ([f fspec] (check-fn f fspec nil)) + ([f fspec opts] + (validate-check-opts opts) + (check-1* nil f fspec opts))) + +(defmacro enumerate-namespace + "Given a symbol naming an ns, or a collection of such symbols, + returns the set of all symbols naming defs in those nses known to + the ClojureDart analyzer at macro-expansion time." + [ns-sym-or-syms] + `'~(enumerate-namespace* &env (quoted-form ns-sym-or-syms))) + +(defn enumerate-namespace-runtime + "Runtime variant of enumerate-namespace backed by the redef registry. + Returns loaded symbols in the given namespace(s) that can be reached + by runtime instrumentation." + [ns-sym-or-syms] + (let [nses (set (collectionize ns-sym-or-syms))] + (into #{} + (filter #(when-let [ns-name (namespace %)] + (contains? nses (symbol ns-name)))) + (impl/redef-registered-syms)))) + +(defn- failure-type [x] + (:cljd.spec.alpha/failure (ex-data x))) + +(defn- unwrap-failure [x] + (if (failure-type x) (ex-data x) x)) + +(defn- result-type [ret] + (let [failure (:failure ret)] + (cond + (nil? failure) :check-passed + (failure-type failure) (failure-type failure) + :else :check-threw))) + +(defn abbrev-result + "Given a check result, returns an abbreviated version suitable for + summary use." + [x] + (if (:failure x) + (-> (dissoc x :clojure.spec.test.check/ret) + (update :spec s/describe) + (update :failure unwrap-failure)) + (dissoc x :spec :clojure.spec.test.check/opts))) + +(defn summarize-results + "Prints each summarized check result and returns counts by result + type." + ([check-results] (summarize-results check-results abbrev-result)) + ([check-results summary-result] + (reduce + (fn [summary result] + (prn (summary-result result)) + (-> summary + (update :total inc) + (update (result-type result) (fnil inc 0)))) + {:total 0} + check-results))) diff --git a/clj/test/cljd/test_clojure/spec_alpha.cljd b/clj/test/cljd/test_clojure/spec_alpha.cljd new file mode 100644 index 00000000..2d1f331f --- /dev/null +++ b/clj/test/cljd/test_clojure/spec_alpha.cljd @@ -0,0 +1,3438 @@ +(ns cljd.test-clojure.spec-alpha + (:require [cljd.spec.alpha :as s] + [cljd.spec.gen.alpha :as gen] + [cljd.spec.test.alpha :as st]) + (:use [clojure.test :only [deftest is testing use-fixtures]])) + +(defmulti shape-spec :shape) +(defmethod shape-spec :circle [_] (s/keys :req-un [::shape ::radius])) +(defmethod shape-spec :rect [_] (s/keys :req-un [::shape ::width ::height])) + +(defn add1 [x] (inc x)) + +;; No ^:cljd.spec.alpha/redef marker — s/fdef must auto-redef this +;; for instrument to work. +(defn add1-instr [x] (inc x)) + +(defn stub-target [] 1) +(defn replace-target [x] (inc x)) +(def symbol-registered 0) +(defn bad-ret [_] "bad") + +;; Explicit ^:cljd.spec.alpha/redef metadata — instrument should +;; install a wrapper without relying on s/fdef's same-compilation-unit +;; auto-promotion (the def already comes back with :cljd.spec.alpha/redef? +;; set in the analyzer). +(defn ^:cljd.spec.alpha/redef explicit-redef-fn [x] (inc x)) + +(defn register-specs! [] + (s/def ::even-int (s/and int? even?)) + (s/def ::pos-or-tiny (s/or :pos pos-int? :tiny #(< % 10))) + (s/def ::shape keyword?) + (s/def ::radius number?) + (s/def ::width number?) + (s/def ::height number?) + (s/def ::score int?) + (s/def ::a int?) + (s/def ::b int?) + (s/def ::small-int (s/int-in 1 4)) + (s/def ::shape-map (s/keys :req-un [::shape] :opt-un [::score])) + (s/def ::req-un-or (s/keys :req-un [(or ::a ::b)])) + (s/def ::opt-un-or (s/keys :opt-un [(or ::a ::b)])) + (s/def ::opt-un-and (s/keys :opt-un [(and ::a ::b)])) + (s/def ::ints-into-list (s/coll-of int? :into () :count 3)) + (s/def ::tree (s/or :leaf int? + :node (s/coll-of ::tree :kind vector? :into [] :gen-max 2))) + (s/def ::shape-kvs (s/keys* :req-un [::shape])) + (s/def ::shape-value (s/multi-spec shape-spec :shape)) + (s/def ::plain-int int?) + (s/def ::form-and (s/and int? pos?)) + (s/def ::form-or (s/or :i int? :s string?)) + (s/def ::form-nilable (s/nilable int?)) + (s/def ::form-every (s/every int?)) + (s/def ::form-coll-of (s/coll-of int? :kind vector?)) + (s/def ::form-every-kv (s/every-kv keyword? int?)) + (s/def ::form-map-of (s/map-of keyword? int?)) + (s/def ::maybe-then-int (s/cat :maybe (s/? int?) :value int?)) + (s/def ::prefix-tail (s/cat :prefix (s/* any?) :tail any?)) + (s/def symbol-registered int?) + (s/def ::wide-and (s/and int? pos? #(not= % 1) #(< % 10) #(not= % 7))) + (s/def ::wide-and-spec (s/and-spec int? pos? #(not= % 1) #(< % 10) #(not= % 7))) + (s/def ::wide-or (s/or :s string? :k keyword? :i int? :b boolean? :n nil?)) + (s/def ::wide-or-spec (s/or-spec :s string? :k keyword? :i int? :b boolean? :n nil?)) + (s/def ::wide-cat (s/cat :i int? :s string? :k keyword? :b boolean? :n nil?)) + (s/def ::wide-alt (s/alt :s string? :k keyword? :i int? :b boolean? :n nil?)) + (s/def ::wide-tuple (s/tuple int? string? keyword? boolean? nil?)) + (s/def ::form-tuple (s/tuple int? string? keyword? boolean? nil?)) + (s/fdef add1 :args (s/cat :x int?) :ret int?) + (s/fdef add1-instr :args (s/cat :x int?) :ret int?) + (s/fdef stub-target :args (s/cat) :ret #{42}) + (s/fdef replace-target :args (s/cat :x int?) :ret int?) + (s/fdef explicit-redef-fn :args (s/cat :x int?) :ret int?)) + +(defn spec-fixture + ([] (register-specs!)) + ([_] nil)) + +(use-fixtures :each spec-fixture) + +(deftest valid?-predicate + (is (true? (s/valid? int? 1))) + (is (false? (s/valid? int? "x"))) + (is (true? (s/valid? int? 1 'int?))) + (is (false? (s/valid? int? "x" 'int?))) + (is (true? (s/valid? ::even-int 4))) + (is (false? (s/valid? ::even-int 5))) + (is (false? (s/valid? ::even-int :foo)))) + +(deftest public-protocol-aliases + (let [spec (s/spec int?)] + (is (satisfies? s/Spec spec)) + (is (= 1 (s/conform* spec 1))) + (is (s/invalid? (s/conform* spec "x"))) + (is (= 'cljd.core/int? (s/describe* spec)))) + (is (satisfies? s/Specize ::even-int)) + (is (s/spec? (s/specize* ::even-int)))) + +(deftest conform-predicate + (is (= 4 (s/conform ::even-int 4))) + (is (s/invalid? (s/conform ::even-int 5))) + (is (s/invalid? (s/conform ::even-int :foo)))) + +(deftest unform-and-roundtrip + (let [c (s/conform ::even-int 4)] + (is (= 4 (s/unform ::even-int c))))) + +(deftest or-tagged-conform + (is (= [:pos 3] (s/conform ::pos-or-tiny 3))) + (is (= [:tiny -1] (s/conform ::pos-or-tiny -1))) + (is (= [:pos 100] (s/conform ::pos-or-tiny 100))) + (is (s/invalid? (s/conform ::pos-or-tiny 10.5)))) + +(deftest or-unform + (is (= 3 (s/unform ::pos-or-tiny [:pos 3]))) + (is (= -1 (s/unform ::pos-or-tiny [:tiny -1])))) + +(deftest explain-data-shape + (let [ed (s/explain-data ::even-int 5)] + (is (some? ed)) + (is (contains? ed :cljd.spec.alpha/problems)) + (is (= 5 (:cljd.spec.alpha/value ed)))) + (is (= 5 (:cljd.spec.alpha/value + (s/explain-data* ::even-int [] [] [] 5)))) + (is (nil? (s/explain-data ::even-int 4)))) + +(deftest explain-out-is-rebindable + (let [ed (s/explain-data ::even-int 5)] + (is (= (with-out-str (s/explain-printer ed)) + (s/explain-str ::even-int 5))) + (is (= "custom explain: 5\n" + (binding [s/*explain-out* (fn [ed] + (println "custom explain:" + (:cljd.spec.alpha/value ed)))] + (s/explain-str ::even-int 5)))))) + +(deftest registry-roundtrip + (is (some? (s/get-spec ::even-int))) + (is (some? (s/get-spec 'cljd.test-clojure.spec-alpha/symbol-registered))) + (is (some? (s/get-spec (var symbol-registered)))) + (is (contains? (s/registry) ::even-int))) + +(deftest public-range-predicates + (testing "int-in-range? mirrors the public upstream helper" + (is (true? (s/int-in-range? 1 4 1))) + (is (true? (s/int-in-range? 1 4 3))) + (is (false? (s/int-in-range? 1 4 4))) + (is (false? (s/int-in-range? 1 4 1.5)))) + (testing "inst-in-range? mirrors the public upstream helper" + (let [start #inst "2020-01-01T00:00:00.000-00:00" + mid #inst "2020-06-01T00:00:00.000-00:00" + end #inst "2021-01-01T00:00:00.000-00:00"] + (is (true? (s/inst-in-range? start end start))) + (is (true? (s/inst-in-range? start end mid))) + (is (false? (s/inst-in-range? start end end))) + (is (false? (s/inst-in-range? start end "2020")))))) + +(deftest cljs-style-constructors-preserve-forms + (is (= 'cljd.core/int? (s/form ::plain-int))) + (is (= '(cljd.spec.alpha/and cljd.core/int? cljd.core/pos?) + (s/form ::form-and))) + (is (= '(cljd.spec.alpha/or :i cljd.core/int? :s cljd.core/string?) + (s/form ::form-or))) + (is (= '(cljd.spec.alpha/nilable cljd.core/int?) + (s/form ::form-nilable))) + (is (= '(cljd.spec.alpha/tuple cljd.core/int? cljd.core/string? + cljd.core/keyword? cljd.core/boolean? + cljd.core/nil?) + (s/form ::form-tuple))) + (is (= '(cljd.spec.alpha/every cljd.core/int?) + (s/form ::form-every))) + (is (= '(cljd.spec.alpha/coll-of cljd.core/int? :kind cljd.core/vector?) + (s/form ::form-coll-of))) + (is (= '(cljd.spec.alpha/every-kv cljd.core/keyword? cljd.core/int?) + (s/form ::form-every-kv))) + (is (= '(cljd.spec.alpha/map-of cljd.core/keyword? cljd.core/int?) + (s/form ::form-map-of)))) + +(deftest public-dynamic-vars-route-to-impl + (let [sampled (s/every int?) + fsp (s/fspec :args (s/cat :x int?) :ret int?)] + (is (false? (s/valid? sampled [1 "x"]))) + (is (true? (binding [s/*coll-check-limit* 1] + (s/valid? sampled [1 "x"])))) + (is (= 1 + (count (:cljd.spec.alpha/problems + (binding [s/*coll-error-limit* 1] + (s/explain-data sampled ["x" "y" "z"])))))) + (is (true? (binding [s/*fspec-iterations* 0] + (s/valid? fsp bad-ret)))) + (is (false? (binding [s/*fspec-iterations* 1] + (s/valid? fsp bad-ret)))))) + +(deftest multi-spec-conform + (is (= {:shape :circle :radius 2} + (s/conform ::shape-value {:shape :circle :radius 2}))) + (is (= {:shape :rect :width 3 :height 4} + (s/conform ::shape-value {:shape :rect :width 3 :height 4}))) + (is (s/invalid? (s/conform ::shape-value {:shape :circle}))) + (is (s/invalid? (s/conform ::shape-value {:shape :triangle :side 3})))) + +(deftest multi-spec-form-and-explain + (is (= '(cljd.spec.alpha/multi-spec cljd.test-clojure.spec-alpha/shape-spec :shape) + (s/form ::shape-value))) + (is (= "no method" + (-> (s/explain-data ::shape-value {:shape :triangle :side 3}) + :cljd.spec.alpha/problems + first + :reason))) + (is (.contains ^String (s/explain-str ::shape-value {:shape :triangle :side 3}) + "no method"))) + +(deftest regex-backtracking-and-empty-entries + (is (= {:value 1} + (s/conform ::maybe-then-int [1]))) + (is (= {:tail 1} + (s/conform ::prefix-tail [1]))) + (is (= {:prefix [1] :tail 2} + (s/conform ::prefix-tail [1 2])))) + +(deftest keys-star-conforms-key-value-seqs + (is (= {:shape :circle} + (s/conform ::shape-kvs [:shape :circle])))) + +(deftest variadic-combinators-exceed-old-caps + (testing "runtime and/or aliases accept more than the old fixed arities" + (is (true? (s/valid? ::wide-and 2))) + (is (false? (s/valid? ::wide-and 7))) + (is (= [:b true] (s/conform ::wide-or true))) + (is (= [:n nil] (s/conform ::wide-or nil)))) + (testing "form-preserving and-spec/or-spec macros are variadic" + (is (true? (s/valid? ::wide-and-spec 2))) + (is (false? (s/valid? ::wide-and-spec 1))) + (is (= [:b false] (s/conform ::wide-or-spec false))) + (is (= [:n nil] (s/conform ::wide-or-spec nil)))) + (testing "tuple and regex cat/alt handle more than four predicates" + (is (= [1 "x" :k true nil] + (s/conform ::wide-tuple [1 "x" :k true nil]))) + (is (s/invalid? (s/conform ::wide-tuple [1 "x" :k true]))) + (is (= {:i 1 :s "x" :k :k :b true :n nil} + (s/conform ::wide-cat [1 "x" :k true nil]))) + (is (= [:b true] (s/conform ::wide-alt [true]))) + (is (= [:n nil] (s/conform ::wide-alt [nil]))))) + +(deftest fdef-qualifies-current-ns-symbol + (is (some? (s/get-spec 'cljd.test-clojure.spec-alpha/add1))) + (is (true? (s/valid? 'cljd.test-clojure.spec-alpha/add1 add1)))) + +(deftest built-in-and-range-generators + (is (boolean? (gen/generate (s/gen boolean?)))) + (is (integer? (gen/generate (s/gen rational?) 10 1))) + (doseq [[x conformed] (s/exercise ::small-int 10)] + (is (s/valid? ::small-int x)) + (is (= x conformed)))) + +(deftest composed-spec-generators + (doseq [[x conformed] (s/exercise ::shape-map 10)] + (is (s/valid? ::shape-map x)) + (is (= x conformed))) + (doseq [[x conformed] (s/exercise ::prefix-tail 10)] + (is (s/valid? ::prefix-tail x)) + (is (not (s/invalid? conformed))))) + +(deftest multi-spec-generates-from-methods + (doseq [[x conformed] (s/exercise ::shape-value 10)] + (is (#{:circle :rect} (:shape x))) + (is (s/valid? ::shape-value x)) + (is (= x conformed)))) + +(deftest map-generator-preserves-grouped-optional-key-shapes + (let [req-or-samples (gen/sample (s/gen ::req-un-or) 100 1) + opt-or-samples (gen/sample (s/gen ::opt-un-or) 100 1)] + (doseq [x req-or-samples] + (is (or (contains? x :a) (contains? x :b))) + (is (s/valid? ::req-un-or x))) + (testing "canonical or-k-gen may include multiple alternatives" + (is (some #(and (contains? % :a) (contains? % :b)) + req-or-samples)) + (is (some #(and (contains? % :a) (contains? % :b)) + opt-or-samples)))) + (doseq [[x _] (s/exercise ::opt-un-and 25)] + (is (or (not (or (contains? x :a) (contains? x :b))) + (and (contains? x :a) (contains? x :b)))))) + +(deftest every-generator-honors-into + (let [x (gen/generate (s/gen ::ints-into-list) 10 1)] + (is (list? x)) + (is (= 3 (count x)))) + (testing "cljs's generation-only :gen-into option is accepted" + (let [x (gen/generate (s/gen (s/coll-of int? :gen-into () :count 3)) 10 1)] + (is (list? x)) + (is (= 3 (count x)))))) + +(deftest recursive-generators-respect-recursion-limit + (let [g (binding [s/*recursion-limit* 0] + (s/gen ::tree)) + samples (gen/sample g 20 1)] + (is (every? int? samples)))) + +(deftest exercise-fn-uses-fdef-args + (doseq [[args ret] (s/exercise-fn add1 10)] + (is (= (inc (first args)) ret))) + (testing "runtime symbol expressions resolve via the redef registry" + (doseq [[args ret] (s/exercise-fn (identity `add1) 3)] + (is (= (inc (first args)) ret))))) + +(deftest assert-throws-ex-info-with-explain-data + (try + (s/check-asserts true) + (try + (s/assert ::even-int 5) + (is false "expected assertion failure") + (catch Object e + (is (= :assertion-failed + (:cljd.spec.alpha/failure (ex-data e)))) + (is (seq (:cljd.spec.alpha/problems (ex-data e)))))) + (finally + (s/check-asserts false)))) + +(deftest fspec-without-args-throws-during-conformance + (let [fsp (s/fspec :ret int?)] + (is (= :thrown + (try + (s/valid? fsp (fn [] 1)) + :not-thrown + (catch Object _ :thrown)))))) + +(deftest fspec-explain-reruns-to-report-failing-input + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?) + problem (-> (s/explain-data fsp bad-ret) + :cljd.spec.alpha/problems + first)] + (is (= :ret (:cljd.spec.alpha/role problem))) + (is (sequential? (:cljd.spec.alpha/args problem))) + (is (= "bad" (:val problem))) + (is (= "function failed generative check" (:reason problem))))) + +(deftest spec-test-check-explicit-fn + (let [ret (first (st/check `add1 {:clojure.spec.test.check/opts {:num-tests 5}}))] + (is (= 'cljd.test-clojure.spec-alpha/add1 (:sym ret))) + (is (nil? (:failure ret))))) + +(defn- instrument-failure + "Calls f, returning the :cljd.spec.alpha/failure value of any + ex-info raised, else nil." + [f] + (try (f) nil + (catch Object e (-> e ex-data :cljd.spec.alpha/failure)))) + +(deftest analyzer-backed-spec-discovery + (let [add1-sym 'cljd.test-clojure.spec-alpha/add1 + add1-instr-sym 'cljd.test-clojure.spec-alpha/add1-instr] + (testing "s/def and s/fdef record speced vars for macro-time and runtime discovery" + (is (contains? (s/speced-vars) add1-sym)) + (is (contains? (s/speced-vars) add1-instr-sym)) + (is (contains? (s/speced-vars) + 'cljd.test-clojure.spec-alpha/symbol-registered))) + (testing "enumerate-namespace uses analyzer defs" + (is (contains? (st/enumerate-namespace 'cljd.test-clojure.spec-alpha) + add1-sym))) + (testing "enumerate-namespace-runtime uses loaded redef registrations" + (is (contains? (st/enumerate-namespace-runtime + (identity 'cljd.test-clojure.spec-alpha)) + add1-sym)) + (is (contains? (st/enumerate-namespace-runtime + ['cljd.test-clojure.spec-alpha]) + add1-instr-sym))) + (testing "no-arg check expands over discovered fdefs" + (let [checked (set (map :sym (st/check)))] + (is (contains? checked add1-sym)) + (is (contains? checked add1-instr-sym)))) + (testing "namespace-scope instrument expands over analyzer defs" + (try + (let [instrumented (set (st/instrument 'cljd.test-clojure.spec-alpha))] + (is (contains? instrumented add1-sym)) + (is (contains? instrumented add1-instr-sym)) + (is (= :instrument (instrument-failure #(add1-instr "no"))))) + (finally + (st/unstrument 'cljd.test-clojure.spec-alpha)))))) + +(deftest instrument-stub-and-replace-opts-route-through-auto-redef + (testing ":stub target is instrumented even when it appears only in opts" + (try + (is (= ['cljd.test-clojure.spec-alpha/stub-target] + (st/instrument [] {:stub #{`stub-target}}))) + (is (= 42 (stub-target))) + (finally + (st/unstrument `stub-target)))) + (testing ":replace target is instrumented even when it appears only in opts" + (try + (is (= ['cljd.test-clojure.spec-alpha/replace-target] + (st/instrument [] {:replace {`replace-target (fn [x] (* x 10))}}))) + (is (= 30 (replace-target 3))) + (finally + (st/unstrument `replace-target)))) + (testing ":stub replaces the auto-redef call site with a generated fn" + (try + (is (= ['cljd.test-clojure.spec-alpha/stub-target] + (st/instrument `stub-target {:stub #{`stub-target}}))) + (is (= 42 (stub-target))) + (finally + (st/unstrument `stub-target)))) + (testing ":replace installs the replacement while retaining arg checks" + (try + (is (= ['cljd.test-clojure.spec-alpha/replace-target] + (st/instrument `replace-target + {:replace {`replace-target (fn [x] (* x 10))}}))) + (is (= 30 (replace-target 3))) + (is (= :instrument (instrument-failure #(replace-target "bad")))) + (finally + (st/unstrument `replace-target)))) + (is (= 4 (replace-target 3)))) + +(deftest instrument-redef-roundtrip + (testing "instrument returns the qualified sym for fdef'd defs" + (is (= ['cljd.test-clojure.spec-alpha/add1-instr] + (st/instrument `add1-instr)))) + (testing "instrumented call passes spec when args conform" + (is (= 6 (add1-instr 5)))) + (testing "instrumented call raises an :instrument failure for bad args" + (is (= :instrument (instrument-failure #(add1-instr "no"))))) + (testing "unstrument restores original behavior" + (is (= ['cljd.test-clojure.spec-alpha/add1-instr] + (st/unstrument `add1-instr))) + (is (= 6 (add1-instr 5))) + ;; bad args now reach the body and trigger a Dart-level type error + ;; from inc; the spec wrapper is gone so failure is no longer + ;; tagged :instrument. + (is (nil? (instrument-failure #(add1-instr "no")))))) + +(defn unspec-fn [x] (inc x)) + +(deftest instrument-skips-without-fdef + (testing "fns without s/fdef stay non-redef and are silently skipped" + (is (= [] (st/instrument `unspec-fn))) + (is (= 6 (unspec-fn 5))))) + +(deftest regex?-distinguishes-regex-and-spec-values + (testing "regex ops register as regex?" + (is (some? (s/regex? (s/get-spec ::maybe-then-int)))) + (is (some? (s/regex? (s/get-spec ::wide-cat)))) + (is (some? (s/regex? (s/get-spec ::wide-alt))))) + (testing "non-regex specs and predicates are not regex?" + (is (not (s/regex? (s/get-spec ::even-int)))) + (is (not (s/regex? (s/get-spec ::form-or)))) + (is (not (s/regex? int?))) + (is (not (s/regex? :a/keyword))) + (is (not (s/regex? nil))))) + +(deftest abbrev-strips-namespaces-and-fn-wrappers + (testing "qualified symbols collapse to their name" + (is (= 'int? (s/abbrev 'cljd.core/int?)))) + (testing "(fn [%] body) collapses to body" + (is (= 'body (s/abbrev '(fn [%] body))))) + (testing "abbrev walks nested forms" + (is (= '(or :i int? :s string?) + (s/abbrev '(cljd.spec.alpha/or :i cljd.core/int? :s cljd.core/string?))))) + (testing "non-symbol non-seq values pass through unchanged" + (is (= 42 (s/abbrev 42))) + (is (= :keyword (s/abbrev :keyword))))) + +(deftest kvs->map-builds-map-from-tagged-pairs + (testing "explicit conversion of keys*'s inner regex output" + (is (= {:a 1 :b 2} + (s/kvs->map [{:cljd.spec.alpha/k :a :cljd.spec.alpha/v 1} + {:cljd.spec.alpha/k :b :cljd.spec.alpha/v 2}])))) + (testing "empty input yields empty map" + (is (= {} (s/kvs->map [])))) + (testing "the upstream ::kvs->map spec is registered" + (let [spec (s/get-spec :cljd.spec.alpha/kvs->map)] + (is (some? spec)) + (is (= {:a 1} + (s/conform spec [{:cljd.spec.alpha/k :a + :cljd.spec.alpha/v 1}]))) + (is (= [{:cljd.spec.alpha/k :a + :cljd.spec.alpha/v 1}] + (vec (s/unform spec {:a 1})))))) + (testing "form of a keys*-built spec references the registered fn" + (let [f (s/form ::shape-kvs)] + (is (some #{'cljd.spec.alpha/kvs->map} (tree-seq seqable? seq f)))))) + +(deftest explicit-redef-metadata-instruments-without-auto-redef + (testing "explicit ^:cljd.spec.alpha/redef metadata is honored" + (try + (is (= ['cljd.test-clojure.spec-alpha/explicit-redef-fn] + (st/instrument `explicit-redef-fn))) + (testing "instrumented call passes spec when args conform" + (is (= 6 (explicit-redef-fn 5)))) + (testing "instrumented call raises :instrument failure for bad args" + (is (= :instrument (instrument-failure #(explicit-redef-fn "no"))))) + (finally + (st/unstrument `explicit-redef-fn)))) + (testing "after unstrument, explicit redef def behaves like normal" + (is (= 6 (explicit-redef-fn 5))))) + +(deftest dynamic-recursion-limit-bridges-impl + (testing "rebinding s/*recursion-limit* propagates into impl's gen pipeline" + (let [g (s/gen ::even-int) + base (s/gen ::even-int)] + ;; The bridge is observable via `gen` returning a generator that + ;; respects the binding for any recursive subgen. `::even-int` + ;; isn't recursive, but the binding must at least not throw and + ;; must produce a working generator. + (is (some? g)) + (is (some? (binding [s/*recursion-limit* 0] + (s/gen ::even-int))))))) + +(deftest spec-check-shrinks-failing-input + (testing "s/check on a deliberately-broken fn shrinks toward the boundary" + ;; Define an fdef that requires its arg < 1000 and a fn that + ;; rejects values >= 5. Most generated ints will fail; the shrunk + ;; smallest should be near the boundary 5. + (s/def ::small-arg (s/and int? #(< % 1000))) + (s/fdef bad-ret :args (s/cat :x ::small-arg) :ret #(< % 5)) + (let [opts {:clojure.spec.test.check/opts {:num-tests 200 :seed 1}} + ret (first (st/check `bad-ret opts))] + (is (some? (:failure ret))) + (let [tc-ret (or (:clojure.spec.test.check/ret ret) + (:clojure.test.check/ret ret))] + (when (and tc-ret (:shrunk tc-ret)) + (testing ":shrunk records :smallest with rose-tree shrink data" + (is (some? (:smallest (:shrunk tc-ret)))))))))) + +;; ---------------------------------------------------------------------------- +;; NilableSpec +;; ---------------------------------------------------------------------------- + +(deftest nilable-conform-and-unform + (s/def ::nilable-int (s/nilable int?)) + (testing "nil conforms to nil" + (is (nil? (s/conform ::nilable-int nil)))) + (testing "valid value conforms to itself" + (is (= 42 (s/conform ::nilable-int 42)))) + (testing "value failing the inner pred is invalid" + (is (s/invalid? (s/conform ::nilable-int "x")))) + (testing "unform passes nil through and is identity on valid values" + (is (nil? (s/unform ::nilable-int nil))) + (is (= 42 (s/unform ::nilable-int 42))))) + +(deftest nilable-explain-reports-both-nil-and-pred-paths + (s/def ::nilable-int (s/nilable int?)) + (let [probs (:cljd.spec.alpha/problems + (s/explain-data ::nilable-int "x"))] + (is (= 2 (count probs))) + (is (some #(= [:cljd.spec.alpha/pred] (:path %)) probs)) + (is (some #(= [:cljd.spec.alpha/nil] (:path %)) probs)) + (is (some #(= 'cljd.core/nil? (:pred %)) probs))) + (is (nil? (s/explain-data ::nilable-int nil))) + (is (nil? (s/explain-data ::nilable-int 42)))) + +(deftest nilable-generator-produces-both-nil-and-values + (let [samples (gen/sample (s/gen (s/nilable int?)) 200 1)] + (is (some nil? samples)) + (is (some int? samples)) + (is (every? #(or (nil? %) (int? %)) samples)))) + +;; ---------------------------------------------------------------------------- +;; s/& (amp) +;; ---------------------------------------------------------------------------- + +(deftest amp-conform-applies-extra-predicate + (s/def ::amp-even-count (s/& (s/* int?) #(even? (count %)))) + (is (= [1 2 3 4] (s/conform ::amp-even-count [1 2 3 4]))) + (is (= [] (s/conform ::amp-even-count []))) + (is (s/invalid? (s/conform ::amp-even-count [1 2 3])))) + +(deftest amp-explain-points-at-regex-form + (s/def ::amp-even-count (s/& (s/* int?) #(even? (count %)))) + (let [ed (s/explain-data ::amp-even-count [1 2 3])] + (is (some? ed)) + (is (seq (:cljd.spec.alpha/problems ed))))) + +(deftest amp-generator-only-emits-passing-values + (s/def ::amp-even-count (s/& (s/* int?) #(even? (count %)))) + (doseq [x (gen/sample (s/gen ::amp-even-count) 20 1)] + (is (even? (count x))) + (is (every? int? x)))) + +(deftest amp-unform-roundtrips-through-inner-regex + (s/def ::amp-even-count (s/& (s/* int?) #(even? (count %)))) + (let [v [1 2 3 4]] + (is (= v (s/unform ::amp-even-count (s/conform ::amp-even-count v)))))) + +;; ---------------------------------------------------------------------------- +;; s/merge +;; ---------------------------------------------------------------------------- + +(deftest merge-conform-merges-component-maps + (s/def ::ma int?) + (s/def ::mb string?) + (s/def ::map-a (s/keys :req-un [::ma])) + (s/def ::map-b (s/keys :req-un [::mb])) + (s/def ::merged (s/merge ::map-a ::map-b)) + (is (= {:ma 1 :mb "x"} (s/conform ::merged {:ma 1 :mb "x"}))) + (is (s/invalid? (s/conform ::merged {:ma 1})))) + +(deftest merge-unform-roundtrip + (s/def ::ma int?) + (s/def ::mb string?) + (s/def ::map-a (s/keys :req-un [::ma])) + (s/def ::map-b (s/keys :req-un [::mb])) + (s/def ::merged (s/merge ::map-a ::map-b)) + (let [m {:ma 1 :mb "x"}] + (is (= m (s/unform ::merged (s/conform ::merged m)))))) + +(deftest merge-form-preserves-components + (s/def ::ma int?) + (s/def ::mb string?) + (s/def ::map-a (s/keys :req-un [::ma])) + (s/def ::map-b (s/keys :req-un [::mb])) + (s/def ::merged (s/merge ::map-a ::map-b)) + (is (= '(cljd.spec.alpha/merge + :cljd.test-clojure.spec-alpha/map-a + :cljd.test-clojure.spec-alpha/map-b) + (s/form ::merged)))) + +(deftest merge-generator-produces-merged-maps + (s/def ::ma int?) + (s/def ::mb string?) + (s/def ::map-a (s/keys :req-un [::ma])) + (s/def ::map-b (s/keys :req-un [::mb])) + (s/def ::merged (s/merge ::map-a ::map-b)) + (doseq [x (gen/sample (s/gen ::merged) 10 1)] + (is (contains? x :ma)) + (is (contains? x :mb)) + (is (s/valid? ::merged x)))) + +;; ---------------------------------------------------------------------------- +;; s/nonconforming +;; ---------------------------------------------------------------------------- + +(deftest nonconforming-returns-original-on-success + (s/def ::nc-source (s/or :i int? :s string?)) + (let [nc (s/nonconforming ::nc-source)] + (testing "underlying spec would tag, nonconforming drops the tag" + (is (= [:i 1] (s/conform ::nc-source 1))) + (is (= 1 (s/conform nc 1))) + (is (= "x" (s/conform nc "x")))) + (testing "invalid stays invalid" + (is (s/invalid? (s/conform nc :k)))))) + +(deftest nonconforming-unform-delegates + ;; nonconforming delegates unform* to the inner spec — so the inner + ;; spec's unform shape (tagged value for or, plain for pred, etc.) + ;; is what unform expects. + (s/def ::nc-int int?) + (let [nc (s/nonconforming ::nc-int)] + (is (= 1 (s/unform nc 1)))) + (testing "for an or-source, unform still requires the tag" + (s/def ::nc-or (s/or :i int? :s string?)) + (let [nc (s/nonconforming ::nc-or)] + (is (= 1 (s/unform nc [:i 1])))))) + +(deftest nonconforming-form-and-describe + (s/def ::nc-source (s/or :i int? :s string?)) + (let [nc (s/nonconforming ::nc-source)] + (is (= '(cljd.spec.alpha/nonconforming + :cljd.test-clojure.spec-alpha/nc-source) + (s/form nc))))) + +(deftest nonconforming-explain-delegates-to-inner-spec + (s/def ::nc-source (s/or :i int? :s string?)) + (let [nc (s/nonconforming ::nc-source) + ed (s/explain-data nc :k)] + (is (some? ed)) + (is (seq (:cljd.spec.alpha/problems ed))))) + +;; ---------------------------------------------------------------------------- +;; double-in +;; ---------------------------------------------------------------------------- + +(deftest double-in-validates-and-generates + (s/def ::unit-double + (s/double-in :min 0.0 :max 1.0 :infinite? false :NaN? false)) + (testing "validates within range" + (is (true? (s/valid? ::unit-double 0.5))) + (is (true? (s/valid? ::unit-double 0.0))) + (is (true? (s/valid? ::unit-double 1.0)))) + (testing "rejects out-of-range / Inf / NaN" + (is (false? (s/valid? ::unit-double 2.0))) + (is (false? (s/valid? ::unit-double -0.5))) + (is (false? (s/valid? ::unit-double ##NaN))) + (is (false? (s/valid? ::unit-double ##Inf)))) + (testing "non-double values fail" + (is (false? (s/valid? ::unit-double 1))) + (is (false? (s/valid? ::unit-double "x")))) + (testing "generator stays in the configured band" + (doseq [x (gen/sample (s/gen ::unit-double) 20 1)] + (is (<= 0.0 x)) + (is (<= x 1.0))))) + +(deftest double-in-allows-inf-and-nan-by-default + (s/def ::any-double (s/double-in)) + (is (true? (s/valid? ::any-double 1.5))) + (is (true? (s/valid? ::any-double ##NaN))) + (is (true? (s/valid? ::any-double ##Inf))) + (is (true? (s/valid? ::any-double ##-Inf))) + (is (false? (s/valid? ::any-double "x")))) + +;; ---------------------------------------------------------------------------- +;; s/inst-in macro +;; ---------------------------------------------------------------------------- + +(deftest inst-in-macro-validates-and-generates + (s/def ::y2k + (s/inst-in #inst "2000-01-01T00:00:00.000-00:00" + #inst "2001-01-01T00:00:00.000-00:00")) + (testing "values inside the half-open range conform" + (is (true? (s/valid? ::y2k #inst "2000-06-01T00:00:00.000-00:00"))) + (is (true? (s/valid? ::y2k #inst "2000-01-01T00:00:00.000-00:00")))) + (testing "values at-or-after end are rejected" + (is (false? (s/valid? ::y2k #inst "2001-01-01T00:00:00.000-00:00"))) + (is (false? (s/valid? ::y2k #inst "2001-06-01T00:00:00.000-00:00")))) + (testing "generator handles year-scale ranges (rng-int composes 64-bit draws)" + (doseq [x (gen/sample (s/gen ::y2k) 10 1)] + (is (s/valid? ::y2k x))))) + +;; ---------------------------------------------------------------------------- +;; s/conformer with unformer +;; ---------------------------------------------------------------------------- + +(defn- to-double-conformer [x] + (if (number? x) (double x) :cljd.spec.alpha/invalid)) + +(defn- to-double-unformer [x] + (cond + (double? x) (.toInt ^double x) + (int? x) x + :else x)) + +(deftest conformer-with-unformer-roundtrips + (s/def ::int->double (s/conformer to-double-conformer to-double-unformer)) + (testing "conformer applies its fn" + (is (= 1.0 (s/conform ::int->double 1))) + (is (= 2.5 (s/conform ::int->double 2.5)))) + (testing ":cljd.spec.alpha/invalid sentinel is honored" + (is (s/invalid? (s/conform ::int->double "x")))) + (testing "unformer reverses the conformer" + (is (= 1 (s/unform ::int->double 1.0)))) + (testing "form includes the conformer wrapper symbol" + (is (some #{'cljd.spec.alpha/conformer} + (tree-seq seqable? seq (s/form ::int->double)))))) + +(deftest conformer-without-unformer-throws-on-unform + (s/def ::just-conformer + (s/conformer (fn [x] (if (number? x) x :cljd.spec.alpha/invalid)))) + (is (= 1 (s/conform ::just-conformer 1))) + (is (= :thrown + (try (s/unform ::just-conformer 1) + :not-thrown + (catch Object _ :thrown))))) + +;; ---------------------------------------------------------------------------- +;; Set as a spec (Specize path for PersistentHashSet) +;; ---------------------------------------------------------------------------- + +(deftest set-spec-membership-conform + (s/def ::colors #{:red :green :blue}) + (is (= :red (s/conform ::colors :red))) + (is (= :green (s/conform ::colors :green))) + (is (s/invalid? (s/conform ::colors :purple)))) + +(deftest set-spec-explain-records-value + (s/def ::colors #{:red :green :blue}) + (let [prob (-> (s/explain-data ::colors :purple) + :cljd.spec.alpha/problems + first)] + (is (= :purple (:val prob))))) + +(deftest set-spec-generator-samples-membership + (s/def ::colors #{:red :green :blue}) + (let [samples (set (gen/sample (s/gen ::colors) 50 1))] + (is (every? #{:red :green :blue} samples)) + (is (>= (count samples) 2)))) + +;; ---------------------------------------------------------------------------- +;; reg-resolve alias chain (deep-resolve) +;; ---------------------------------------------------------------------------- + +(deftest reg-resolve-walks-kw-alias-chain + (s/def ::alias-base int?) + (s/def ::alias-layer-1 ::alias-base) + (s/def ::alias-layer-2 ::alias-layer-1) + (testing "conform reaches the terminal spec through nested aliases" + (is (= 1 (s/conform ::alias-layer-2 1))) + (is (s/invalid? (s/conform ::alias-layer-2 "x")))) + (testing "valid? works through the chain" + (is (true? (s/valid? ::alias-layer-2 1))) + (is (false? (s/valid? ::alias-layer-2 "x"))))) + +;; ---------------------------------------------------------------------------- +;; gen overrides +;; ---------------------------------------------------------------------------- + +(deftest gen-overrides-by-spec-name + ;; gensub preserves the originating kw across specize and uses it for + ;; the override lookup, so an override map keyed by the registered + ;; spec name takes precedence over the default generator. + (s/def ::override-int int?) + (let [overrides {::override-int #(gen/return 42)} + samples (gen/sample (s/gen ::override-int overrides) 5 1)] + (is (every? #(= 42 %) samples)))) + +(deftest gen-overrides-by-spec-object-fallback + ;; The spec-object key still works for callers that look up the spec + ;; via get-spec — the lookup chain tries kw name first, then the spec + ;; itself, then the path. + (s/def ::override-int-obj int?) + (let [spec-obj (s/get-spec ::override-int-obj) + overrides {spec-obj #(gen/return 42)} + samples (gen/sample (s/gen ::override-int-obj overrides) 5 1)] + (is (every? #(= 42 %) samples)))) + +(deftest gen-overrides-at-top-level-path + (s/def ::override-int2 int?) + (let [overrides {[] #(gen/return 42)} + samples (gen/sample (s/gen ::override-int2 overrides) 5 1)] + (is (every? #(= 42 %) samples)))) + +(deftest gen-overrides-by-nested-path + (s/def ::override-tuple (s/tuple int? int?)) + (let [overrides {[1] #(gen/return 99)} + samples (gen/sample (s/gen ::override-tuple overrides) 5 1)] + (is (every? #(= 99 (nth % 1)) samples)) + (testing "non-overridden positions still validate" + (is (every? #(int? (nth % 0)) samples))))) + +;; ---------------------------------------------------------------------------- +;; s/spec with :gen +;; ---------------------------------------------------------------------------- + +(deftest spec-macro-accepts-gen-override + (let [sp (s/spec int? :gen #(gen/return 7))] + (testing "validates the predicate" + (is (true? (s/valid? sp 1))) + (is (false? (s/valid? sp "x")))) + (testing "uses the supplied generator" + (is (every? #(= 7 %) (gen/sample (s/gen sp) 5 1)))))) + +;; ---------------------------------------------------------------------------- +;; Unform roundtrips across remaining spec types +;; ---------------------------------------------------------------------------- + +(deftest unform-roundtrip-tuple + (s/def ::unform-tup (s/tuple int? string? (s/or :k keyword? :i int?))) + (let [v [1 "x" :k] + c (s/conform ::unform-tup v)] + (is (= [1 "x" [:k :k]] c)) + (is (= v (s/unform ::unform-tup c))))) + +(deftest unform-roundtrip-coll-of + (s/def ::unform-xs (s/coll-of (s/or :i int? :s string?) :kind vector?)) + (let [v [1 "x" 2] + c (s/conform ::unform-xs v)] + (is (= [[:i 1] [:s "x"] [:i 2]] c)) + (is (= v (s/unform ::unform-xs c))))) + +(deftest unform-roundtrip-map-of + (s/def ::unform-m (s/map-of keyword? (s/or :i int? :s string?))) + (let [v {:a 1 :b "x"} + c (s/conform ::unform-m v)] + (is (= {:a [:i 1] :b [:s "x"]} c)) + (is (= v (s/unform ::unform-m c))))) + +(deftest unform-roundtrip-keys + (s/def ::ua int?) + (s/def ::ub string?) + (s/def ::unform-km (s/keys :req-un [::ua ::ub])) + (let [v {:ua 1 :ub "x"}] + (is (= v (s/unform ::unform-km (s/conform ::unform-km v)))))) + +(deftest unform-roundtrip-regex-cat-alt + (s/def ::unform-cat (s/cat :i int? :tail (s/alt :s string? :k keyword?))) + (let [v [1 "x"] + c (s/conform ::unform-cat v)] + (is (= {:i 1 :tail [:s "x"]} c)) + (is (= v (s/unform ::unform-cat c))))) + +(deftest unform-roundtrip-regex-star-plus-maybe + (s/def ::unform-star (s/* int?)) + (s/def ::unform-plus (s/+ int?)) + (s/def ::unform-maybe (s/? int?)) + (testing "star roundtrip" + (is (= [1 2 3] (s/unform ::unform-star (s/conform ::unform-star [1 2 3]))))) + (testing "star roundtrip on empty input" + (is (= [] (s/unform ::unform-star (s/conform ::unform-star []))))) + (testing "plus roundtrip" + (is (= [1] (s/unform ::unform-plus (s/conform ::unform-plus [1]))))) + (testing "maybe roundtrip with a value" + (is (= [1] (s/unform ::unform-maybe (s/conform ::unform-maybe [1]))))) + (testing "empty maybe roundtrip via nil" + (is (= [] (s/unform ::unform-maybe (s/conform ::unform-maybe [])))))) + +(deftest maybe-conform-on-empty-returns-nil + ;; (s/? ...) on empty input matches the upstream contract: conform + ;; collapses the internal no-ret sentinel to nil and unform of nil + ;; produces [] — symmetric, modulo the (s/? nil?) asymmetry shared + ;; with upstream clojure.spec.alpha. + (s/def ::maybe-int (s/? int?)) + (is (nil? (s/conform ::maybe-int []))) + (is (= [] (s/unform ::maybe-int nil)))) + +;; ---------------------------------------------------------------------------- +;; EverySpec options +;; ---------------------------------------------------------------------------- + +(deftest coll-of-distinct-rejects-duplicates + (s/def ::distinct-vec (s/coll-of int? :distinct true)) + (is (true? (s/valid? ::distinct-vec [1 2 3]))) + (is (true? (s/valid? ::distinct-vec []))) + (is (false? (s/valid? ::distinct-vec [1 2 1])))) + +(deftest coll-of-min-max-count + (s/def ::bounded (s/coll-of int? :min-count 2 :max-count 4)) + (is (false? (s/valid? ::bounded [1]))) + (is (true? (s/valid? ::bounded [1 2]))) + (is (true? (s/valid? ::bounded [1 2 3 4]))) + (is (false? (s/valid? ::bounded [1 2 3 4 5])))) + +(deftest coll-of-count-exact + (s/def ::triple (s/coll-of int? :count 3)) + (is (true? (s/valid? ::triple [1 2 3]))) + (is (false? (s/valid? ::triple [1 2]))) + (is (false? (s/valid? ::triple [1 2 3 4])))) + +(deftest map-of-conform-keys-tags-keys + (s/def ::ckm (s/map-of (s/or :k keyword? :s string?) int? + :conform-keys true)) + (is (= {[:k :a] 1 [:s "b"] 2} + (s/conform ::ckm {:a 1 "b" 2})))) + +(deftest coll-of-distinct-generator + (s/def ::dgen (s/coll-of int? :distinct true :min-count 3 :max-count 5)) + (doseq [x (gen/sample (s/gen ::dgen) 10 1)] + (is (or (empty? x) (apply distinct? x))) + (is (<= 3 (count x) 5)))) + +;; ---------------------------------------------------------------------------- +;; coll-problems surfaces in explain-data +;; ---------------------------------------------------------------------------- + +(deftest coll-explain-kind-mismatch + (s/def ::vec-only (s/coll-of int? :kind vector?)) + (let [prob (-> (s/explain-data ::vec-only #{1 2 3}) + :cljd.spec.alpha/problems + first)] + (is (= 'cljd.core/vector? (:pred prob))) + (is (= #{1 2 3} (:val prob))))) + +(deftest coll-explain-count-mismatch + (s/def ::need-3 (s/coll-of int? :count 3)) + (let [prob (-> (s/explain-data ::need-3 [1 2]) + :cljd.spec.alpha/problems + first)] + (is (= [1 2] (:val prob))) + (is (= (list 'cljd.core/= 3 (list 'cljd.core/count '%)) + (:pred prob))))) + +(deftest coll-explain-distinct-violation + (s/def ::distinct-only (s/coll-of int? :distinct true)) + (let [prob (-> (s/explain-data ::distinct-only [1 1]) + :cljd.spec.alpha/problems + first)] + (is (= 'cljd.core/distinct? (:pred prob))) + (is (= [1 1] (:val prob))))) + +(deftest coll-explain-min-max-bound + (s/def ::bounded2 (s/coll-of int? :min-count 2 :max-count 3)) + (let [prob (-> (s/explain-data ::bounded2 [1]) + :cljd.spec.alpha/problems + first)] + (is (= [1] (:val prob))) + (testing "pred describes the bound check" + (is (seq? (:pred prob))) + (is (= 'cljd.core/<= (first (:pred prob))))))) + +;; ---------------------------------------------------------------------------- +;; s/every vs s/coll-of (conform semantics) +;; ---------------------------------------------------------------------------- + +(deftest every-checks-without-conforming-elements + ;; every only checks validity; the original collection is returned + ;; unchanged. coll-of, in contrast, applies element-level conform. + (s/def ::tagged (s/or :i int? :s string?)) + (s/def ::e-every (s/every ::tagged)) + (s/def ::e-coll-of (s/coll-of ::tagged)) + (let [xs [1 "x" 2]] + (testing "every returns the input verbatim" + (is (= xs (s/conform ::e-every xs)))) + (testing "coll-of applies element-level conform tagging" + (is (= [[:i 1] [:s "x"] [:i 2]] (s/conform ::e-coll-of xs)))))) + +(deftest every-respects-coll-check-limit + ;; every walks at most *coll-check-limit* elements before short- + ;; circuiting to "valid". With the limit dropped, a bad value past + ;; the cutoff isn't observed. + (s/def ::e-ints (s/every int?)) + (let [xs (into (vec (repeat 50 1)) ["bad"])] + (is (false? (s/valid? ::e-ints xs))) + (binding [s/*coll-check-limit* 5] + (is (true? (s/valid? ::e-ints xs)))))) + +;; ---------------------------------------------------------------------------- +;; s/every-kv vs s/map-of +;; ---------------------------------------------------------------------------- + +(deftest every-kv-checks-without-conforming-values + (s/def ::v-spec (s/or :i int? :s string?)) + (s/def ::ekv (s/every-kv keyword? ::v-spec)) + (s/def ::mof (s/map-of keyword? ::v-spec)) + (let [m {:a 1 :b "x"}] + (testing "every-kv returns the input map verbatim" + (is (= m (s/conform ::ekv m)))) + (testing "map-of applies value-level conform tagging" + (is (= {:a [:i 1] :b [:s "x"]} (s/conform ::mof m)))))) + +;; ---------------------------------------------------------------------------- +;; MultiSpec :default dispatch +;; ---------------------------------------------------------------------------- + +(defmulti shape-with-default-spec :shape) +(defmethod shape-with-default-spec :circle [_] (s/keys :req-un [::shape ::radius])) +(defmethod shape-with-default-spec :default [_] (s/keys :req-un [::shape])) + +(deftest multi-spec-uses-default-method-when-no-tag-matches + (s/def ::shape keyword?) + (s/def ::radius number?) + (s/def ::any-shape (s/multi-spec shape-with-default-spec :shape)) + (testing ":circle dispatch hits the specific method" + (is (= {:shape :circle :radius 2} + (s/conform ::any-shape {:shape :circle :radius 2}))) + (is (s/invalid? (s/conform ::any-shape {:shape :circle})))) + (testing "unknown tags fall through to :default" + (is (= {:shape :triangle} + (s/conform ::any-shape {:shape :triangle}))))) + +;; ---------------------------------------------------------------------------- +;; s/keys :req (qualified keys) +;; ---------------------------------------------------------------------------- + +(deftest keys-req-validates-qualified-keys + (s/def ::user-id int?) + (s/def ::user-name string?) + (s/def ::user-q (s/keys :req [::user-id ::user-name])) + (is (true? (s/valid? ::user-q {::user-id 1 ::user-name "a"}))) + (is (false? (s/valid? ::user-q {::user-id 1}))) + (testing "keys :req requires the fully-qualified key, not the unqualified one" + (is (false? (s/valid? ::user-q {:user-id 1 :user-name "a"}))))) + +(deftest keys-req-explain-uses-contains?-form + (s/def ::user-id int?) + (s/def ::user-q (s/keys :req [::user-id])) + (let [prob (-> (s/explain-data ::user-q {}) + :cljd.spec.alpha/problems + first)] + (is (= (list 'cljd.core/contains? '% ::user-id) (:pred prob))))) + +(deftest keys-or-and-key-expressions + (s/def ::x int?) + (s/def ::y int?) + (s/def ::z int?) + (s/def ::ka (s/keys :req-un [(or ::x (and ::y ::z))])) + (testing "or-branch satisfied by single key" + (is (true? (s/valid? ::ka {:x 1})))) + (testing "or-branch satisfied via the and-clause" + (is (true? (s/valid? ::ka {:y 1 :z 2})))) + (testing "missing both branches is invalid" + (is (false? (s/valid? ::ka {}))))) + +;; ---------------------------------------------------------------------------- +;; MapSpec non-qualified key assertion +;; ---------------------------------------------------------------------------- + +(deftest keys-rejects-non-qualified-key-at-construction + (testing "plain keyword in :req-un triggers the assert in keys-args" + (is (= :thrown + (try (s/keys :req-un [:plain-kw]) + :not-thrown + (catch Object _ :thrown))))) + (testing "plain keyword in :req also triggers the assert" + (is (= :thrown + (try (s/keys :req [:plain-kw]) + :not-thrown + (catch Object _ :thrown)))))) + +;; ---------------------------------------------------------------------------- +;; reg-resolve! throw path +;; ---------------------------------------------------------------------------- + +(deftest conform-on-unregistered-keyword-throws + ;; Specize -> Keyword extension calls reg-resolve!, which throws when + ;; the key isn't in the registry. + (is (= :thrown + (try (s/conform :cljd.test-clojure.spec-alpha/not-a-real-spec 1) + :not-thrown + (catch Object _ :thrown))))) + +;; ---------------------------------------------------------------------------- +;; gensub recursion-limit throw +;; ---------------------------------------------------------------------------- + +(deftest gensub-throws-when-recursion-limit-exceeded + ;; A directly-recursive coll-of has no non-recursive escape branch + ;; for gen, so with limit 0 the inner gensub call exceeds the limit + ;; and propagates the exception (coll-of's gen doesn't try/catch). + (s/def ::strict-recur (s/coll-of ::strict-recur)) + (is (= :thrown + (try + (binding [s/*recursion-limit* 0] + (gen/generate (s/gen ::strict-recur) 5 1)) + :not-thrown + (catch Object _ :thrown))))) + +;; ---------------------------------------------------------------------------- +;; s/with-gen runtime generator override +;; ---------------------------------------------------------------------------- + +(deftest with-gen-replaces-default-generator + (let [fixed (s/with-gen int? #(gen/return 100))] + (testing "predicate validity is preserved" + (is (true? (s/valid? fixed 100))) + (is (false? (s/valid? fixed "no")))) + (testing "samples use the override" + (is (every? #(= 100 %) (gen/sample (s/gen fixed) 5 1)))))) + +(deftest with-gen-on-registered-spec + (s/def ::wg-int int?) + (let [wrapped (s/with-gen ::wg-int #(gen/return 7))] + (is (every? #(= 7 %) (gen/sample (s/gen wrapped) 5 1))))) + +;; ---------------------------------------------------------------------------- +;; fspec :fn relationship +;; ---------------------------------------------------------------------------- + +(defn fn-inc [x] (inc x)) +(defn fn-id [x] x) + +(deftest fspec-fn-validates-arg-ret-relationship + (let [fsp (s/fspec :args (s/cat :x int?) + :ret int? + :fn #(> (:ret %) (-> % :args :x)))] + (testing "fn satisfying the relationship validates" + (is (true? (s/valid? fsp fn-inc)))) + (testing "fn violating the relationship is invalid" + (is (false? (s/valid? fsp fn-id)))))) + +(deftest fspec-explain-reports-fn-role-on-relationship-failure + (let [fsp (s/fspec :args (s/cat :x int?) + :ret int? + :fn #(> (:ret %) (-> % :args :x))) + ed (s/explain-data fsp fn-id)] + (is (some? ed)) + (is (some #(= :fn (:cljd.spec.alpha/role %)) + (:cljd.spec.alpha/problems ed))))) + +;; ---------------------------------------------------------------------------- +;; check-asserts toggle +;; ---------------------------------------------------------------------------- + +(deftest check-asserts-toggle-controls-assert + (s/def ::ca-int int?) + (try + (testing "with asserts off, s/assert is a no-op even for invalid" + (s/check-asserts false) + (is (false? (s/check-asserts?))) + (is (= "x" (s/assert ::ca-int "x")))) + (testing "with asserts on, s/assert throws ex-info for invalid" + (s/check-asserts true) + (is (true? (s/check-asserts?))) + (is (= :thrown + (try (s/assert ::ca-int "x") + :not-thrown + (catch Object _ :thrown))))) + (testing "valid values pass through regardless of toggle" + (is (= 1 (s/assert ::ca-int 1)))) + (finally + (s/check-asserts false)))) + +;; ---------------------------------------------------------------------------- +;; FSpec ILookup +;; ---------------------------------------------------------------------------- + +(deftest fspec-ilookup-exposes-component-specs + (let [argspec (s/cat :x int?) + retspec (s/spec int?) + fnspec (s/spec #(> (:ret %) (-> % :args :x))) + fsp (s/fspec :args argspec :ret retspec :fn fnspec)] + (testing "keyword access returns the registered component specs" + (is (some? (:args fsp))) + (is (some? (:ret fsp))) + (is (some? (:fn fsp)))) + (testing "missing key returns the not-found fallback" + (is (= ::nope (get fsp :missing ::nope)))) + (testing "contains? sees the component keys" + (is (contains? fsp :args)) + (is (contains? fsp :ret)) + (is (contains? fsp :fn)) + (is (not (contains? fsp :nope)))))) + +(deftest fspec-ilookup-omits-unspecified-components + ;; The fspec macro defaults :ret to any? when unspecified, so :ret is + ;; always present. Only :fn is truly omittable. + (let [fsp (s/fspec :args (s/cat :x int?))] + (is (some? (:args fsp))) + (is (some? (:ret fsp))) + (is (not (contains? fsp :fn))))) + +;; ---------------------------------------------------------------------------- +;; s/conformer invalid propagation +;; ---------------------------------------------------------------------------- + +(defn- positive-double-conformer [x] + (cond + (and (number? x) (pos? x)) (double x) + :else :cljd.spec.alpha/invalid)) + +(deftest conformer-invalid-propagates-through-or + (s/def ::pos-double (s/conformer positive-double-conformer)) + (s/def ::pos-or-string (s/or :p ::pos-double :s string?)) + (testing "positive number takes the conformer branch" + (is (= [:p 2.0] (s/conform ::pos-or-string 2)))) + (testing "negative falls through to the string branch (invalid here)" + (is (s/invalid? (s/conform ::pos-or-string -1)))) + (testing "string takes the string branch directly" + (is (= [:s "x"] (s/conform ::pos-or-string "x"))))) + +(deftest conformer-invalid-propagates-through-coll-of + (s/def ::pos-double (s/conformer positive-double-conformer)) + (s/def ::pos-vec (s/coll-of ::pos-double)) + (testing "all-positive vector conforms with each element converted" + (is (= [1.0 2.0 3.0] (s/conform ::pos-vec [1 2 3])))) + (testing "one invalid element makes the whole coll invalid" + (is (s/invalid? (s/conform ::pos-vec [1 -2 3]))))) + +;; ---------------------------------------------------------------------------- +;; explain-out extra-keys printing +;; ---------------------------------------------------------------------------- + +(deftest explain-out-prints-extra-problem-keys + ;; fspec failures attach :cljd.spec.alpha/args and :cljd.spec.alpha/role + ;; entries to each problem. explain-out's "extra keys" loop should + ;; render these alongside the standard fields. + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?) + s (s/explain-str fsp bad-ret)] + (is (.contains ^String s ":cljd.spec.alpha/role")) + (is (.contains ^String s ":cljd.spec.alpha/args")))) + +;; ---------------------------------------------------------------------------- +;; Empty s/cat +;; ---------------------------------------------------------------------------- + +(deftest empty-cat-conforms-empty-input + (s/def ::no-args (s/cat)) + (testing "empty input conforms to an empty arg map" + (is (= {} (s/conform ::no-args [])))) + (testing "non-empty input is invalid" + (is (s/invalid? (s/conform ::no-args [1]))))) + +;; ---------------------------------------------------------------------------- +;; coll-of :into type preservation +;; ---------------------------------------------------------------------------- + +(deftest coll-of-into-vector-set-list + (testing ":into [] yields a vector" + (let [x (gen/generate (s/gen (s/coll-of int? :into [] :count 3)) 10 1)] + (is (vector? x)) + (is (= 3 (count x))))) + (testing ":into #{} yields a set (duplicates collapsed)" + (let [x (gen/generate (s/gen (s/coll-of int? :into #{} :distinct true :count 3)) + 10 1)] + (is (set? x)) + (is (= 3 (count x))))) + (testing ":into () yields a list" + (let [x (gen/generate (s/gen (s/coll-of int? :into () :count 3)) 10 1)] + (is (list? x)) + (is (= 3 (count x)))))) + +;; ---------------------------------------------------------------------------- +;; MapSpec :opt (qualified-key optional) +;; ---------------------------------------------------------------------------- + +(deftest keys-opt-allows-missing-or-validates-present + (s/def ::opt-tag keyword?) + (s/def ::opt-source (s/keys :req [::opt-tag] :opt [::user-id])) + (s/def ::user-id int?) + (testing "missing :opt key is allowed" + (is (true? (s/valid? ::opt-source {::opt-tag :anon})))) + (testing "present :opt key is validated" + (is (true? (s/valid? ::opt-source {::opt-tag :anon ::user-id 1}))) + (is (false? (s/valid? ::opt-source {::opt-tag :anon ::user-id "bad"}))))) + +;; ---------------------------------------------------------------------------- +;; FSpec gen returns a function +;; ---------------------------------------------------------------------------- + +(deftest fspec-gen-returns-callable-fn + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?) + f (gen/generate (s/gen fsp) 10 1)] + (testing "the generated value is callable" + (is (fn? f))) + (testing "calling with conforming args yields a ret-spec value" + (is (int? (f 1)))))) + +;; ---------------------------------------------------------------------------- +;; exercise across spec types +;; ---------------------------------------------------------------------------- + +(deftest exercise-keys-spec + (s/def ::ex-a int?) + (s/def ::ex-b string?) + (s/def ::ex-map (s/keys :req-un [::ex-a ::ex-b])) + (doseq [[x conformed] (s/exercise ::ex-map 5)] + (is (s/valid? ::ex-map x)) + (is (= x conformed)))) + +(deftest exercise-coll-of-spec + (s/def ::ex-coll (s/coll-of int? :kind vector? :min-count 1 :max-count 5)) + (doseq [[x conformed] (s/exercise ::ex-coll 5)] + (is (s/valid? ::ex-coll x)) + (is (= x conformed)))) + +(deftest exercise-tuple-spec + (s/def ::ex-tup (s/tuple int? string?)) + (doseq [[x conformed] (s/exercise ::ex-tup 5)] + (is (s/valid? ::ex-tup x)) + (is (= x conformed)))) + +(deftest exercise-cat-spec + (s/def ::ex-cat (s/cat :i int? :s string?)) + (doseq [[x conformed] (s/exercise ::ex-cat 5)] + (is (s/valid? ::ex-cat x)) + (is (map? conformed)) + (is (contains? conformed :i)) + (is (contains? conformed :s)))) + +;; ---------------------------------------------------------------------------- +;; tuple single-pred +;; ---------------------------------------------------------------------------- + +(deftest tuple-with-single-pred + (s/def ::singleton-tup (s/tuple int?)) + (is (= [1] (s/conform ::singleton-tup [1]))) + (is (s/invalid? (s/conform ::singleton-tup []))) + (is (s/invalid? (s/conform ::singleton-tup [1 2]))) + (is (s/invalid? (s/conform ::singleton-tup ["x"])))) + +;; ---------------------------------------------------------------------------- +;; coll-of nil and non-coll input +;; ---------------------------------------------------------------------------- + +(deftest coll-of-rejects-non-coll + (s/def ::ints (s/coll-of int?)) + (is (s/invalid? (s/conform ::ints nil))) + (is (s/invalid? (s/conform ::ints "string"))) + (is (s/invalid? (s/conform ::ints 1)))) + +(deftest coll-of-explain-non-coll-uses-coll?-pred + (s/def ::ints2 (s/coll-of int?)) + (let [prob (-> (s/explain-data ::ints2 nil) + :cljd.spec.alpha/problems + first)] + (is (= 'cljd.core/coll? (:pred prob))) + (is (nil? (:val prob))))) + +;; ---------------------------------------------------------------------------- +;; MultiSpec gen falls through method errors +;; ---------------------------------------------------------------------------- + +(defmulti gen-fallback-spec :gtype) +(defmethod gen-fallback-spec :ok [_] (s/keys :req-un [::gtype])) +(defmethod gen-fallback-spec :broken [_] (throw (Exception. "broken method"))) + +(deftest multi-spec-gen-skips-throwing-methods + ;; MultiSpec.gen* invokes each method-fn under try/catch — a method + ;; that throws is silently dropped; one-of picks from the survivors. + (s/def ::gtype keyword?) + (s/def ::mg-spec (s/multi-spec gen-fallback-spec :gtype)) + (doseq [x (gen/sample (s/gen ::mg-spec) 10 1)] + (testing "only the working :ok method contributes generated values" + (is (= :ok (:gtype x)))))) + +;; ---------------------------------------------------------------------------- +;; every :gen-max bound +;; ---------------------------------------------------------------------------- + +(deftest coll-of-gen-max-bounds-generated-size + (s/def ::small-coll (s/coll-of int? :gen-max 3)) + (let [samples (gen/sample (s/gen ::small-coll) 25 1)] + (testing "no sample exceeds :gen-max" + (is (every? #(<= (count %) 3) samples))) + (testing "at least one sample uses the upper bound (statistical check)" + ;; Defensive — with seed 1 and 25 samples we expect to see 3. + (is (some #(= 3 (count %)) samples))))) + +;; ---------------------------------------------------------------------------- +;; fspec explain on non-fn +;; ---------------------------------------------------------------------------- + +(deftest fspec-explain-non-fn-reports-ifn? + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?) + prob (-> (s/explain-data fsp 42) + :cljd.spec.alpha/problems + first)] + (is (= 'cljd.core/ifn? (:pred prob))) + (is (= 42 (:val prob))))) + +;; ---------------------------------------------------------------------------- +;; exercise-fn-runtime error paths +;; ---------------------------------------------------------------------------- + +(deftest exercise-fn-runtime-throws-without-redef + (testing "no ^:cljd.spec.alpha/redef registration → explicit throw" + (is (= :thrown + (try (s/exercise-fn-runtime 'cljd.test-clojure.spec-alpha/no-such-sym) + :not-thrown + (catch Object _ :thrown)))))) + +;; ---------------------------------------------------------------------------- +;; coll-of :kind with custom predicate +;; ---------------------------------------------------------------------------- + +(defn non-empty-vec? [x] (and (vector? x) (not (empty? x)))) + +(deftest coll-of-kind-honors-arbitrary-predicate + (s/def ::ne-vec (s/coll-of int? :kind non-empty-vec?)) + (is (true? (s/valid? ::ne-vec [1 2]))) + (is (false? (s/valid? ::ne-vec []))) + (is (false? (s/valid? ::ne-vec '(1 2)))) + (is (false? (s/valid? ::ne-vec #{1 2})))) + +;; ---------------------------------------------------------------------------- +;; s/get-spec missing returns nil +;; ---------------------------------------------------------------------------- + +(deftest get-spec-returns-nil-for-unknown-keyword + (is (nil? (s/get-spec :cljd.test-clojure.spec-alpha/never-registered)))) + +;; ---------------------------------------------------------------------------- +;; maybe (s/? nil?) asymmetry +;; ---------------------------------------------------------------------------- + +(deftest maybe-nil-pred-shares-upstream-conform-asymmetry + ;; (s/? nil?) cannot distinguish "matched nil" from "matched nothing" + ;; at conform time — both surface as nil. unform of nil-conformed + ;; maybe collapses to []. Matches upstream clojure.spec.alpha. + (s/def ::maybe-nil (s/? nil?)) + (testing "matched-nil and no-match both conform to nil" + (is (nil? (s/conform ::maybe-nil [nil]))) + (is (nil? (s/conform ::maybe-nil [])))) + (testing "unform always collapses to []" + (is (= [] (s/unform ::maybe-nil nil))))) + +;; ---------------------------------------------------------------------------- +;; fspec :ret failure role tagging +;; ---------------------------------------------------------------------------- + +(deftest fspec-explain-ret-failure-tagged-as-ret-role + (let [fsp (s/fspec :args (s/cat :x int?) :ret string?) + prob (-> (s/explain-data fsp (fn [x] x)) + :cljd.spec.alpha/problems + first)] + (is (= :ret (:cljd.spec.alpha/role prob))) + (testing "args that triggered the ret failure are attached" + (is (sequential? (:cljd.spec.alpha/args prob)))))) + +;; ---------------------------------------------------------------------------- +;; exercise sample count +;; ---------------------------------------------------------------------------- + +(deftest exercise-returns-requested-number-of-samples + (s/def ::xs-int int?) + (testing "explicit count is honored" + (is (= 7 (count (s/exercise ::xs-int 7))))) + (testing "default n is 10" + (is (= 10 (count (s/exercise ::xs-int)))))) + +;; ---------------------------------------------------------------------------- +;; s/with-gen on a regex spec +;; ---------------------------------------------------------------------------- + +(deftest with-gen-on-regex-attaches-gfn-inside-regex-map + ;; with-gen on a regex stashes the gen-fn at :cljd.spec.alpha.impl/gfn + ;; inside the regex map; op-gen consults it before falling back to the + ;; structural generator. + (let [reg (s/cat :x int? :y int?) + wrapped (s/with-gen reg #(gen/return [42 99]))] + (testing "validity is preserved through the wrapped regex" + (is (true? (s/valid? wrapped [42 99])))) + (testing "samples use the override exclusively" + (is (every? #(= [42 99] %) (gen/sample (s/gen wrapped) 5 1)))))) + +;; ---------------------------------------------------------------------------- +;; s/every :into preserves type at gen +;; ---------------------------------------------------------------------------- + +(deftest every-into-preserves-target-collection-type + (s/def ::every-list (s/every int? :into () :count 3)) + (let [x (gen/generate (s/gen ::every-list) 10 1)] + (is (list? x)) + (is (= 3 (count x)))) + (s/def ::every-set (s/every int? :into #{} :distinct true :count 3)) + (let [x (gen/generate (s/gen ::every-set) 10 1)] + (is (set? x)) + (is (= 3 (count x))))) + +;; ---------------------------------------------------------------------------- +;; s/conform on a literal regex op (no s/def) +;; ---------------------------------------------------------------------------- + +(deftest conform-and-unform-on-literal-regex-value + (let [reg (s/cat :x int? :y string?)] + (testing "conform without registration" + (is (= {:x 1 :y "a"} (s/conform reg [1 "a"]))) + (is (s/invalid? (s/conform reg [1]))) + (is (s/invalid? (s/conform reg [1 "a" :extra])))) + (testing "unform roundtrips via the same regex value" + (is (= [1 "a"] (s/unform reg (s/conform reg [1 "a"]))))))) + +(deftest valid?-on-literal-regex-value + (let [reg (s/+ int?)] + (is (true? (s/valid? reg [1 2 3]))) + (is (false? (s/valid? reg []))))) + +;; ---------------------------------------------------------------------------- +;; s/keys :gen option +;; ---------------------------------------------------------------------------- + +(deftest keys-gen-option-overrides-default-map-generator + (s/def ::kg-a int?) + (s/def ::kg-b string?) + (s/def ::kg-map + (s/keys :req-un [::kg-a ::kg-b] + :gen #(gen/return {:kg-a 42 :kg-b "fixed"}))) + (testing "samples use the override" + (is (every? #(= {:kg-a 42 :kg-b "fixed"} %) + (gen/sample (s/gen ::kg-map) 5 1)))) + (testing "validity of the override-shaped sample" + (is (true? (s/valid? ::kg-map {:kg-a 42 :kg-b "fixed"}))))) + +;; ---------------------------------------------------------------------------- +;; s/coll-of :gen option +;; ---------------------------------------------------------------------------- + +(deftest coll-of-gen-option-overrides-default-coll-generator + (s/def ::cg-vec + (s/coll-of int? + :kind vector? + :gen #(gen/return [99 99 99]))) + (is (every? #(= [99 99 99] %) + (gen/sample (s/gen ::cg-vec) 5 1)))) + +;; ---------------------------------------------------------------------------- +;; s/regex? boundary behavior +;; ---------------------------------------------------------------------------- + +(deftest regex?-truthy-on-regex-maps-nil-on-wrappers + (let [reg (s/cat :x int?) + wrapped (s/spec reg)] + (testing "raw regex op is a regex" + (is (some? (s/regex? reg)))) + (testing "spec-wrapping the regex makes it a Spec, not a regex" + (is (nil? (s/regex? wrapped))) + (is (s/spec? wrapped)))) + (testing "non-regex values are not regex" + (is (nil? (s/regex? {}))) + (is (nil? (s/regex? [1 2]))) + (is (nil? (s/regex? :a/keyword))) + (is (nil? (s/regex? nil))) + (is (nil? (s/regex? int?))))) + +;; ---------------------------------------------------------------------------- +;; s/exercise across more spec types +;; ---------------------------------------------------------------------------- + +(deftest exercise-nilable-spec + (s/def ::ex-nilable (s/nilable int?)) + (let [pairs (s/exercise ::ex-nilable 20)] + (is (= 20 (count pairs))) + (doseq [[x conformed] pairs] + (is (or (nil? x) (int? x))) + (is (= x conformed))))) + +(deftest exercise-merge-spec + (s/def ::ex-ma int?) + (s/def ::ex-mb string?) + (s/def ::ex-merged (s/merge (s/keys :req-un [::ex-ma]) + (s/keys :req-un [::ex-mb]))) + (doseq [[x _] (s/exercise ::ex-merged 5)] + (is (contains? x :ex-ma)) + (is (contains? x :ex-mb)) + (is (s/valid? ::ex-merged x)))) + +;; ---------------------------------------------------------------------------- +;; coll-of with set input +;; ---------------------------------------------------------------------------- + +(deftest coll-of-conforms-set-input + ;; conform-coll has a dedicated branch for sets — it accumulates into + ;; a fresh set and short-circuits on element-level invalid. + (s/def ::set-ints (s/coll-of int?)) + (testing "all-int set conforms" + (is (= #{1 2 3} (s/conform ::set-ints #{1 2 3})))) + (testing "set containing a non-int is invalid" + (is (s/invalid? (s/conform ::set-ints #{1 "x" 2}))))) + +(deftest coll-of-list-input-preserves-list-shape + (s/def ::list-ints (s/coll-of int?)) + (let [c (s/conform ::list-ints '(1 2 3))] + (is (list? c)) + (is (= '(1 2 3) c)))) + +;; ---------------------------------------------------------------------------- +;; Spec protocol satisfaction across all deftype-backed specs +;; ---------------------------------------------------------------------------- + +(deftest all-spec-types-satisfy-Spec-protocol + (let [specs [(s/spec int?) + (s/and int? pos?) + (s/or :i int? :s string?) + (s/nilable int?) + (s/tuple int?) + (s/coll-of int?) + (s/every-kv keyword? int?) + (s/map-of keyword? int?) + (s/keys :req-un [::nilable-int]) + (s/merge (s/keys :req-un [::nilable-int]) + (s/keys :req-un [::nilable-int])) + (s/nonconforming (s/or :i int? :s string?)) + (s/fspec :args (s/cat :x int?) :ret int?) + (s/spec (s/cat :x int?)) ;; regex wrapped as Spec + ]] + (doseq [sp specs] + (is (satisfies? s/Spec sp))))) + +;; ---------------------------------------------------------------------------- +;; abbrev deep nesting +;; ---------------------------------------------------------------------------- + +(deftest abbrev-walks-deep-nested-forms + (testing "qualified syms inside nested seqs and vecs are stripped" + (is (= '(and int? (or pos? neg?)) + (s/abbrev '(cljd.spec.alpha/and cljd.core/int? + (cljd.spec.alpha/or cljd.core/pos? + cljd.core/neg?)))))) + (testing "(fn [%] body) inside larger forms collapses" + (is (= '(and int? body) + (s/abbrev '(cljd.spec.alpha/and cljd.core/int? (fn [%] body))))))) + +;; ---------------------------------------------------------------------------- +;; op-describe for each regex op +;; ---------------------------------------------------------------------------- + +(deftest op-describe-forms-for-each-regex-op + (testing "s/* describe" + (is (= '(cljd.spec.alpha/* cljd.core/int?) + (s/form (s/* int?))))) + (testing "s/+ describe" + (is (= '(cljd.spec.alpha/+ cljd.core/int?) + (s/form (s/+ int?))))) + (testing "s/? describe" + (is (= '(cljd.spec.alpha/? cljd.core/int?) + (s/form (s/? int?))))) + (testing "s/cat describe preserves k+form pairs" + (is (= '(cljd.spec.alpha/cat :i cljd.core/int? :s cljd.core/string?) + (s/form (s/cat :i int? :s string?))))) + (testing "s/alt describe preserves k+form pairs" + (is (= '(cljd.spec.alpha/alt :i cljd.core/int? :s cljd.core/string?) + (s/form (s/alt :i int? :s string?))))) + (testing "s/& describe leads with the &-form" + (let [reg (s/& (s/* int?) even?)] + (is (= 'cljd.spec.alpha/& (first (s/form reg))))))) + +;; ---------------------------------------------------------------------------- +;; s/alt unform via tagged key +;; ---------------------------------------------------------------------------- + +(deftest alt-unform-by-tagged-key + (s/def ::alt-spec (s/alt :i int? :s string?)) + (testing "unform of tagged int branch" + (is (= [1] (s/unform ::alt-spec [:i 1])))) + (testing "unform of tagged string branch" + (is (= ["x"] (s/unform ::alt-spec [:s "x"])))) + (testing "conform/unform roundtrips" + (is (= [1] (s/unform ::alt-spec (s/conform ::alt-spec [1])))) + (is (= ["x"] (s/unform ::alt-spec (s/conform ::alt-spec ["x"])))))) + +;; ---------------------------------------------------------------------------- +;; cat backtracking with optional members +;; ---------------------------------------------------------------------------- + +(deftest cat-with-optional-leading-and-trailing-via-types + ;; Distinct element predicates make backtracking unambiguous — each + ;; position is identifiable by its type even when ? regexes could + ;; slide values into adjacent positions. + (s/def ::back-cat (s/cat :a (s/? int?) :b string? :c (s/? int?))) + (testing "all three present" + (is (= {:a 1 :b "x" :c 2} (s/conform ::back-cat [1 "x" 2])))) + (testing "only the required b" + (is (= {:b "x"} (s/conform ::back-cat ["x"])))) + (testing "leading optional and required" + (is (= {:a 1 :b "x"} (s/conform ::back-cat [1 "x"])))) + (testing "required and trailing optional" + (is (= {:b "x" :c 2} (s/conform ::back-cat ["x" 2]))))) + +;; ---------------------------------------------------------------------------- +;; s/keys :req-un + :opt-un combined +;; ---------------------------------------------------------------------------- + +(deftest keys-req-un-and-opt-un-together + (s/def ::ko-a int?) + (s/def ::ko-b string?) + (s/def ::ko-map (s/keys :req-un [::ko-a] :opt-un [::ko-b])) + (testing "required present, optional absent — valid" + (is (true? (s/valid? ::ko-map {:ko-a 1})))) + (testing "both present and well-typed — valid" + (is (true? (s/valid? ::ko-map {:ko-a 1 :ko-b "x"})))) + (testing "required present, optional ill-typed — invalid" + (is (false? (s/valid? ::ko-map {:ko-a 1 :ko-b 99})))) + (testing "required absent — invalid" + (is (false? (s/valid? ::ko-map {:ko-b "x"}))))) + +;; ---------------------------------------------------------------------------- +;; s/keys (and ...) key-expression +;; ---------------------------------------------------------------------------- + +(deftest keys-and-only-key-expression + (s/def ::ka-x int?) + (s/def ::ka-y int?) + (s/def ::ka-spec (s/keys :req-un [(and ::ka-x ::ka-y)])) + (testing "both keys present and valid" + (is (true? (s/valid? ::ka-spec {:ka-x 1 :ka-y 2})))) + (testing "missing one of the and-branch keys is invalid" + (is (false? (s/valid? ::ka-spec {:ka-x 1}))) + (is (false? (s/valid? ::ka-spec {:ka-y 2})))) + (testing "explain reports the contains?-and form" + (let [prob (-> (s/explain-data ::ka-spec {}) + :cljd.spec.alpha/problems + first)] + (is (= 'cljd.core/and (first (:pred prob))))))) + +;; ---------------------------------------------------------------------------- +;; regex unform: alt-in-cat composition +;; ---------------------------------------------------------------------------- + +(deftest regex-unform-alt-in-cat-composition + (s/def ::ac (s/cat :a int? :b (s/alt :s string? :k keyword?))) + (testing "string branch roundtrip" + (let [v [1 "x"] + c (s/conform ::ac v)] + (is (= {:a 1 :b [:s "x"]} c)) + (is (= v (s/unform ::ac c))))) + (testing "keyword branch roundtrip" + (let [v [1 :k] + c (s/conform ::ac v)] + (is (= {:a 1 :b [:k :k]} c)) + (is (= v (s/unform ::ac c)))))) + +;; ---------------------------------------------------------------------------- +;; Empty input across regex op variants +;; ---------------------------------------------------------------------------- + +(deftest empty-input-across-regex-op-variants + (s/def ::eo-star (s/* int?)) + (s/def ::eo-plus (s/+ int?)) + (s/def ::eo-maybe (s/? int?)) + (testing "s/* accepts empty input" + (is (= [] (s/conform ::eo-star [])))) + (testing "s/+ rejects empty input" + (is (s/invalid? (s/conform ::eo-plus [])))) + (testing "s/? accepts empty input as nil" + (is (nil? (s/conform ::eo-maybe []))))) + +;; ---------------------------------------------------------------------------- +;; s/and chains conformed values through preds +;; ---------------------------------------------------------------------------- + +(defn- to-double-strict [x] + (if (number? x) (double x) :cljd.spec.alpha/invalid)) + +(deftest and-chains-conformed-values-through-preds + ;; The conformer transforms the value mid-chain; subsequent preds + ;; see the converted value, not the original. + (s/def ::ac-pos-double + (s/and number? (s/conformer to-double-strict) pos?)) + (testing "valid input passes through the conformer" + (is (= 3.0 (s/conform ::ac-pos-double 3)))) + (testing "downstream pred sees the converted (double) value" + (is (s/invalid? (s/conform ::ac-pos-double -3)))) + (testing "first pred failure short-circuits" + (is (s/invalid? (s/conform ::ac-pos-double "x"))))) + +;; ---------------------------------------------------------------------------- +;; map-of explain on key + value failures +;; ---------------------------------------------------------------------------- + +(deftest map-of-explain-shows-key-and-value-failures + (s/def ::ms-mo (s/map-of keyword? int?)) + (testing "bad key surfaces a problem" + (let [ed (s/explain-data ::ms-mo {"x" 1})] + (is (some? ed)) + (is (seq (:cljd.spec.alpha/problems ed))))) + (testing "bad value surfaces a problem" + (let [ed (s/explain-data ::ms-mo {:a "no"})] + (is (some? ed)) + (is (seq (:cljd.spec.alpha/problems ed)))))) + +;; ---------------------------------------------------------------------------- +;; s/keys skips unregistered keys at conform +;; ---------------------------------------------------------------------------- + +(deftest keys-conform-skips-unregistered-keys + ;; MapSpec.conform iterates every entry in the input but only looks + ;; up registered specs. Unregistered keys pass through unchanged. + (s/def ::kk-known int?) + (s/def ::kk-map (s/keys :req-un [::kk-known])) + (let [m {:kk-known 1 :unregistered "anything" :also-unknown 99}] + (is (= m (s/conform ::kk-map m))))) + +;; ---------------------------------------------------------------------------- +;; s/spec on a registered keyword +;; ---------------------------------------------------------------------------- + +(deftest spec-on-registered-keyword-resolves-via-registry + ;; spec-impl detects (ident? pred) and resolves via the-spec, so + ;; wrapping a registered kw in s/spec is essentially a passthrough. + (s/def ::sr-int int?) + (let [resolved (s/spec ::sr-int)] + (is (s/spec? resolved)) + (is (true? (s/valid? resolved 1))) + (is (false? (s/valid? resolved "x"))))) + +;; ---------------------------------------------------------------------------- +;; FSpec form shape +;; ---------------------------------------------------------------------------- + +(deftest fspec-form-and-describe-shapes + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?)] + (testing "form is a vector with the fspec head" + (is (vector? (s/form fsp))) + (is (= 'cljd.spec.alpha/fspec (first (s/form fsp)))) + (is (contains? (set (s/form fsp)) :args)) + (is (contains? (set (s/form fsp)) :ret))))) + +;; ---------------------------------------------------------------------------- +;; fspec captures exceptions thrown by the fn under check +;; ---------------------------------------------------------------------------- + +(defn- fn-that-throws [_] (throw (Exception. "boom from fn"))) + +(deftest fspec-explain-captures-fn-exception + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?) + prob (-> (s/explain-data fsp fn-that-throws) + :cljd.spec.alpha/problems + first)] + (is (some? prob)) + (is (= "exception thrown during generative check" (:reason prob))) + (is (some? (:cljd.spec.alpha/failure prob))))) + +;; ---------------------------------------------------------------------------- +;; s/conform on a direct fn predicate (no s/def, no s/spec) +;; ---------------------------------------------------------------------------- + +(deftest conform-direct-fn-predicate + ;; Specize fallback wraps the fn in a PredSpec on demand. The fn pred + ;; is its own form-data and explain reports it directly. + (testing "valid value passes through" + (is (= 1 (s/conform int? 1)))) + (testing "invalid value yields invalid sentinel" + (is (s/invalid? (s/conform int? "x")))) + (testing "explain points at the predicate" + (let [prob (-> (s/explain-data int? "x") + :cljd.spec.alpha/problems + first)] + (is (= "x" (:val prob)))))) + +;; ---------------------------------------------------------------------------- +;; valid? on a literal set spec without registration +;; ---------------------------------------------------------------------------- + +(deftest valid?-on-literal-set-without-def + (let [colors #{:r :g :b}] + (testing "membership-based validation" + (is (true? (s/valid? colors :r))) + (is (false? (s/valid? colors :purple)))) + (testing "set form roundtrips through describe" + (is (= colors (s/describe colors))) + (is (= colors (s/form colors)))))) + +;; ---------------------------------------------------------------------------- +;; s/keys with all four key kinds in one spec +;; ---------------------------------------------------------------------------- + +(deftest keys-all-four-kinds-together + (s/def ::ak-q1 int?) + (s/def ::ak-q2 int?) + (s/def ::ak-u1 string?) + (s/def ::ak-u2 string?) + (s/def ::ak-map + (s/keys :req [::ak-q1] + :opt [::ak-q2] + :req-un [::ak-u1] + :opt-un [::ak-u2])) + (testing "minimal req-only payload" + (is (true? (s/valid? ::ak-map {::ak-q1 1 :ak-u1 "x"})))) + (testing "fully-populated payload" + (is (true? (s/valid? ::ak-map {::ak-q1 1 :ak-u1 "x" + ::ak-q2 2 :ak-u2 "y"})))) + (testing "ill-typed opt :req fails" + (is (false? (s/valid? ::ak-map {::ak-q1 "no" :ak-u1 "x"})))) + (testing "missing :req-un fails" + (is (false? (s/valid? ::ak-map {::ak-q1 1})))) + (testing "missing :req fails" + (is (false? (s/valid? ::ak-map {:ak-u1 "x"}))))) + +;; ---------------------------------------------------------------------------- +;; explain on a spec-wrapped regex +;; ---------------------------------------------------------------------------- + +(deftest explain-on-spec-wrapped-regex + ;; (s/spec regex-value) wraps the regex in a RegexSpec; explain on + ;; bad input emits the regex-describe form as :pred. + (let [wrapped (s/spec (s/cat :i int? :s string?)) + prob (-> (s/explain-data wrapped [1 :not-string]) + :cljd.spec.alpha/problems + first)] + (is (some? prob)) + (is (= [1 :not-string] (:val prob))))) + +;; ---------------------------------------------------------------------------- +;; gen overrides reach a path inside a regex op +;; ---------------------------------------------------------------------------- + +(deftest gen-override-reaches-cat-sub-path + ;; Each cat alternative is generated under (conj path k); an override + ;; keyed by that path replaces only that position. + (s/def ::go-cat (s/cat :x int? :y int?)) + (let [overrides {[:x] #(gen/return 42)} + samples (gen/sample (s/gen ::go-cat overrides) 5 1)] + (testing "the overridden position is fixed" + (is (every? #(= 42 (first %)) samples))) + (testing "the non-overridden position is generated normally" + (is (every? #(int? (second %)) samples))))) + +;; ---------------------------------------------------------------------------- +;; s/coll-of :kfn drives :in path in explain +;; ---------------------------------------------------------------------------- + +(deftest coll-of-kfn-customizes-in-path + ;; map-of uses :kfn (fn [_ e] (nth e 0)) so the :in path reaches the + ;; failing entry by key rather than by integer index. + (s/def ::kfn-mo (s/map-of keyword? int?)) + (let [probs (->> (s/explain-data ::kfn-mo {:a "no"}) + :cljd.spec.alpha/problems + (filter #(= "no" (:val %))))] + (is (seq probs)) + (testing ":in path includes the entry's key (not just numeric index)" + (is (some #(some #{:a} (:in %)) probs))))) + +;; ---------------------------------------------------------------------------- +;; with-gen chaining: latest replaces, not composes +;; ---------------------------------------------------------------------------- + +(deftest with-gen-applied-twice-uses-the-latest + (let [sp (-> (s/spec int?) + (s/with-gen #(gen/return 1)) + (s/with-gen #(gen/return 2)))] + (is (every? #(= 2 %) (gen/sample (s/gen sp) 5 1))))) + +;; ---------------------------------------------------------------------------- +;; with-gen on each spec type +;; ---------------------------------------------------------------------------- + +(deftest with-gen-on-each-spec-type + (testing "PredSpec" + (let [sp (s/with-gen (s/spec int?) #(gen/return 100))] + (is (every? #(= 100 %) (gen/sample (s/gen sp) 3 1))))) + (testing "AndSpec" + (let [sp (s/with-gen (s/and int? pos?) #(gen/return 42))] + (is (every? #(= 42 %) (gen/sample (s/gen sp) 3 1))))) + (testing "OrSpec" + ;; with-gen produces inputs (which then conform to a tagged pair), + ;; not the conformed-shape itself. + (let [sp (s/with-gen (s/or :i int? :s string?) #(gen/return 7))] + (is (every? #(= 7 %) (gen/sample (s/gen sp) 3 1))) + (is (= [:i 7] (s/conform sp 7))))) + (testing "TupleSpec" + (let [sp (s/with-gen (s/tuple int? string?) #(gen/return [99 "y"]))] + (is (every? #(= [99 "y"] %) (gen/sample (s/gen sp) 3 1))))) + (testing "EverySpec / coll-of" + (let [sp (s/with-gen (s/coll-of int?) #(gen/return [1 1 1]))] + (is (every? #(= [1 1 1] %) (gen/sample (s/gen sp) 3 1))))) + (testing "NilableSpec" + (let [sp (s/with-gen (s/nilable int?) #(gen/return 0))] + (is (every? #(= 0 %) (gen/sample (s/gen sp) 3 1))))) + (testing "FSpec" + (let [fixed-fn (fn [_] 7) + sp (s/with-gen (s/fspec :args (s/cat :x int?) :ret int?) + #(gen/return fixed-fn)) + gend (gen/generate (s/gen sp) 3 1)] + (is (identical? fixed-fn gend)) + (is (= 7 (gend 0)))))) + +;; ---------------------------------------------------------------------------- +;; Empty s/and behaves as identity +;; ---------------------------------------------------------------------------- + +(deftest empty-and-acts-as-identity-conform + ;; AndSpec.conform* = (and-preds x @preds forms); with empty preds + ;; the loop returns x immediately. + (let [sp (s/and)] + (is (= 1 (s/conform sp 1))) + (is (= "x" (s/conform sp "x"))) + (is (nil? (s/conform sp nil))))) + +;; ---------------------------------------------------------------------------- +;; Nested s/and + s/or combinator +;; ---------------------------------------------------------------------------- + +(deftest and-with-nested-or-tags-inner-branch + (s/def ::ao-spec (s/and int? (s/or :even even? :three #(= % 3)))) + (testing "even branch wins for even ints" + (is (= [:even 4] (s/conform ::ao-spec 4)))) + (testing "three branch picks the 3 case (also odd)" + (is (= [:three 3] (s/conform ::ao-spec 3)))) + (testing "neither inner branch matches" + (is (s/invalid? (s/conform ::ao-spec 5)))) + (testing "outer pred (int?) fails first" + (is (s/invalid? (s/conform ::ao-spec "x"))))) + +;; ---------------------------------------------------------------------------- +;; specize* on an unresolvable kw throws +;; ---------------------------------------------------------------------------- + +(deftest specize*-throws-on-unregistered-keyword + ;; Goes through Keyword's Specize extension → reg-resolve! → throw. + (is (= :thrown + (try (s/specize* :no.such.ns/never-registered) + :not-thrown + (catch Object _ :thrown))))) + +;; ---------------------------------------------------------------------------- +;; s/exercise with overrides +;; ---------------------------------------------------------------------------- + +(deftest exercise-3-arg-honors-gen-overrides + (s/def ::ex-over int?) + (let [spec-obj (s/get-spec ::ex-over) + overrides {spec-obj #(gen/return 99)} + pairs (s/exercise ::ex-over 5 overrides)] + (is (= 5 (count pairs))) + (is (every? (fn [[x conformed]] (and (= 99 x) (= 99 conformed))) pairs)))) + +;; ---------------------------------------------------------------------------- +;; s/explain-data* direct invocation +;; ---------------------------------------------------------------------------- + +(deftest explain-data*-passes-path-via-in-through + (s/def ::ed-int int?) + (let [ed (s/explain-data* ::ed-int [:custom :path] ['some-via] [:in-key] "x") + prob (-> ed :cljd.spec.alpha/problems first)] + (is (some? ed)) + (is (= [:custom :path] (:path prob))) + (is (= ['some-via] (:via prob))) + (is (= [:in-key] (:in prob))) + (is (= "x" (:val prob))))) + +;; ---------------------------------------------------------------------------- +;; s/and / s/or with kw-alias preds +;; ---------------------------------------------------------------------------- + +(deftest and-resolves-kw-alias-preds + (s/def ::akw-int int?) + (s/def ::akw-pos pos?) + (s/def ::akw-spec (s/and ::akw-int ::akw-pos)) + (testing "all kw-aliased preds enforced" + (is (true? (s/valid? ::akw-spec 3))) + (is (false? (s/valid? ::akw-spec -1))) + (is (false? (s/valid? ::akw-spec "x"))))) + +(deftest or-resolves-kw-alias-preds + (s/def ::okw-int int?) + (s/def ::okw-str string?) + (s/def ::okw-spec (s/or :i ::okw-int :s ::okw-str)) + (testing "or branches resolve through kw aliases" + (is (= [:i 1] (s/conform ::okw-spec 1))) + (is (= [:s "x"] (s/conform ::okw-spec "x"))) + (is (s/invalid? (s/conform ::okw-spec :k))))) + +;; ---------------------------------------------------------------------------- +;; Mutually recursive specs (per-name recursion counters) +;; ---------------------------------------------------------------------------- + +(deftest mutually-recursive-or-specs-can-be-generated + ;; recursion-depth is keyed by spec-name, so mutual recursion through + ;; or-spec branches can proceed up to each name's individual limit. + ;; or-spec's try-gensub catches the recursion-limit throw on the + ;; recursive branch so the leaf branch always remains generatable. + (s/def ::mr-a (s/or :leaf int? + :node (s/coll-of ::mr-b :kind vector? + :max-count 1 :gen-max 1))) + (s/def ::mr-b (s/or :leaf string? + :node (s/coll-of ::mr-a :kind vector? + :max-count 1 :gen-max 1))) + (binding [s/*recursion-limit* 2] + (let [samples (gen/sample (s/gen ::mr-a) 10 1)] + (is (seq samples)) + (doseq [x samples] + (is (s/valid? ::mr-a x)))))) + +;; ---------------------------------------------------------------------------- +;; op-gen catch branches for alt / rep / maybe +;; ---------------------------------------------------------------------------- + +(deftest alt-gen-drops-throwing-branches + ;; op-gen for alt wraps each branch in try/catch — throws from a + ;; branch's gen are dropped, leaving the survivors to feed one-of. + (let [bad (s/with-gen int? (fn [] (throw (Exception. "bad gen")))) + good (s/with-gen string? #(gen/return "ok")) + sp (s/spec (s/alt :b bad :g good))] + (doseq [_ (range 5)] + (let [x (gen/generate (s/gen sp) 3 1)] + (is (= ["ok"] x)))))) + +(deftest rep-gen-with-throwing-inner-returns-empty-sequence + (let [bad (s/with-gen int? (fn [] (throw (Exception. "bad gen")))) + sp (s/spec (s/* bad))] + (is (= [] (gen/generate (s/gen sp) 3 1))))) + +(deftest maybe-gen-with-throwing-inner-returns-empty-sequence + (let [bad (s/with-gen int? (fn [] (throw (Exception. "bad gen")))) + sp (s/spec (s/? bad))] + (is (= [] (gen/generate (s/gen sp) 3 1))))) + +;; ---------------------------------------------------------------------------- +;; FSpec generated-fn arg assert +;; ---------------------------------------------------------------------------- + +(deftest fspec-default-generated-fn-asserts-on-bad-args + ;; FSpec.gen* without a gfn returns a fn whose body asserts args + ;; conform to argspec before generating a return value. + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?) + f (gen/generate (s/gen fsp) 10 1)] + (testing "good args produce a generated int" + (is (int? (f 1)))) + (testing "bad args trigger the internal assert" + (is (= :thrown + (try (f "not-int") + :not-thrown + (catch Object _ :thrown))))))) + +;; ---------------------------------------------------------------------------- +;; s/or explain reports all branches +;; ---------------------------------------------------------------------------- + +(deftest or-explain-reports-a-problem-per-branch + (s/def ::oe-spec (s/or :i int? :s string?)) + (let [probs (:cljd.spec.alpha/problems (s/explain-data ::oe-spec :k))] + (is (= 2 (count probs))) + (is (some #(= 'cljd.core/int? (:pred %)) probs)) + (is (some #(= 'cljd.core/string? (:pred %)) probs)) + (is (every? #(= :k (:val %)) probs)))) + +;; ---------------------------------------------------------------------------- +;; gen such-that retry exhaustion +;; ---------------------------------------------------------------------------- + +(deftest gen-such-that-throws-after-100-retries + ;; The override's gen always returns -1, but the predicate requires + ;; pos? — gensub's such-that wrapper exhausts its 100 retries and + ;; throws. + (let [impossible (s/with-gen pos? #(gen/return -1))] + (is (= :thrown + (try (gen/generate (s/gen impossible) 3 1) + :not-thrown + (catch Object _ :thrown)))))) + +;; ---------------------------------------------------------------------------- +;; s/coll-of deep nesting +;; ---------------------------------------------------------------------------- + +(deftest coll-of-of-coll-of-roundtrip + (s/def ::nested (s/coll-of (s/coll-of int? :kind vector?) :kind vector?)) + (let [v [[1 2] [3 4] [5]]] + (is (= v (s/conform ::nested v))) + (is (= v (s/unform ::nested (s/conform ::nested v)))))) + +;; ---------------------------------------------------------------------------- +;; explain-out on nil ed +;; ---------------------------------------------------------------------------- + +(deftest explain-out-on-nil-prints-success-line + ;; The Success! path is hit when explain-out is given nil (no + ;; problems). The newline comes from println. + (is (= "Success!\n" (with-out-str (s/explain-out nil))))) + +;; ---------------------------------------------------------------------------- +;; fspec without :args throws on explain +;; ---------------------------------------------------------------------------- + +(deftest fspec-without-args-throws-on-explain + ;; explain* → fn-problems → throws when (:args specs) is nil, just + ;; like the conform path. + (let [fsp (s/fspec :ret int?)] + (is (= :thrown + (try (s/explain-data fsp (fn [x] x)) + :not-thrown + (catch Object _ :thrown)))))) + +;; ---------------------------------------------------------------------------- +;; MapSpec :opt gen variation +;; ---------------------------------------------------------------------------- + +(deftest keys-opt-gen-may-include-or-omit-the-opt-key + (s/def ::ovg-r int?) + (s/def ::ovg-o string?) + (s/def ::ovg-spec (s/keys :req-un [::ovg-r] :opt-un [::ovg-o])) + (let [samples (gen/sample (s/gen ::ovg-spec) 50 1)] + (testing "required key always present" + (is (every? #(contains? % :ovg-r) samples))) + (testing "optional key appears in some samples" + (is (some #(contains? % :ovg-o) samples))) + (testing "optional key omitted in some samples" + (is (some #(not (contains? % :ovg-o)) samples))))) + +;; ---------------------------------------------------------------------------- +;; op-describe for amp with multiple inner preds +;; ---------------------------------------------------------------------------- + +(defn even-count-pred? [x] (even? (count x))) +(defn non-empty-pred? [x] (not (empty? x))) + +(deftest amp-form-with-multiple-predicates + (let [reg (s/& (s/* int?) even-count-pred? non-empty-pred?) + f (s/form reg)] + (testing "head is the & symbol" + (is (= 'cljd.spec.alpha/& (first f)))) + (testing "second element is the inner regex form" + (is (= '(cljd.spec.alpha/* cljd.core/int?) (second f)))) + (testing "subsequent elements are the qualified pred forms" + (is (some #{'cljd.test-clojure.spec-alpha/even-count-pred?} f)) + (is (some #{'cljd.test-clojure.spec-alpha/non-empty-pred?} f))))) + +;; ---------------------------------------------------------------------------- +;; FSpec with-gen preserves component specs +;; ---------------------------------------------------------------------------- + +(deftest fspec-with-gen-preserves-component-specs + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?) + wrapped (s/with-gen fsp #(gen/return (fn [_] 1)))] + (testing ":args / :ret survive the with-gen rewrap" + (is (some? (:args wrapped))) + (is (some? (:ret wrapped)))) + (testing "conform still validates against the preserved specs" + (is (true? (s/valid? wrapped (fn [_] 0)))) + (is (false? (s/valid? wrapped "not-a-fn")))))) + +;; ---------------------------------------------------------------------------- +;; NonconformingSpec with-gen +;; ---------------------------------------------------------------------------- + +(deftest nonconforming-with-gen-installs-override + (s/def ::ncwg-int int?) + (let [nc (s/nonconforming ::ncwg-int) + wrapped (s/with-gen nc #(gen/return 42))] + (testing "gen uses the override" + (is (every? #(= 42 %) (gen/sample (s/gen wrapped) 3 1)))) + (testing "conform still delegates to inner spec" + (is (= 1 (s/conform wrapped 1))) + (is (s/invalid? (s/conform wrapped "x")))))) + +;; ---------------------------------------------------------------------------- +;; MultiSpec with-gen preserves multimethod routing +;; ---------------------------------------------------------------------------- + +(defmulti wg-spec-multimethod :wg-mt) +(defmethod wg-spec-multimethod :a [_] (s/keys :req-un [::wg-mt])) + +(deftest multi-spec-with-gen-preserves-conform-routing + (s/def ::wg-mt keyword?) + (let [base (s/multi-spec wg-spec-multimethod :wg-mt) + wrapped (s/with-gen base #(gen/return {:wg-mt :a}))] + (testing "gen uses the override" + (is (= {:wg-mt :a} (gen/generate (s/gen wrapped) 3 1)))) + (testing "conform still routes through the multimethod" + (is (= {:wg-mt :a} (s/conform wrapped {:wg-mt :a})))))) + +;; ---------------------------------------------------------------------------- +;; s/spec 1-arg form preserves pred form +;; ---------------------------------------------------------------------------- + +(deftest spec-1-arg-form-preserves-qualified-pred-form + (is (= 'cljd.core/int? (s/form (s/spec int?)))) + (is (= 'cljd.core/string? (s/form (s/spec string?))))) + +;; ---------------------------------------------------------------------------- +;; :cljd.spec.alpha/kvs->map registry default behavior +;; ---------------------------------------------------------------------------- + +(deftest kvs->map-registered-spec-handles-empty-input + (let [spec (s/get-spec :cljd.spec.alpha/kvs->map)] + (is (some? spec)) + (testing "empty input conforms to empty map" + (is (= {} (s/conform spec [])))) + (testing "unform of empty map yields empty seq" + (is (empty? (s/unform spec {})))))) + +;; ---------------------------------------------------------------------------- +;; coll-of explain delegates to inner registered spec +;; ---------------------------------------------------------------------------- + +(deftest coll-of-explain-delegates-when-element-spec-is-registered + ;; explain-1 sees the inner registered spec via maybe-spec and + ;; delegates explain* to it. cljd's deftype specs don't implement + ;; IMeta, so the inner spec's name is lost from via (a known cljd + ;; gap relative to upstream). + (s/def ::ce-int int?) + (s/def ::ce-coll (s/coll-of ::ce-int :kind vector?)) + (let [probs (:cljd.spec.alpha/problems + (s/explain-data ::ce-coll [1 "no" 3]))] + (is (seq probs)) + (testing "problem points at the failing element value" + (is (some #(= "no" (:val %)) probs))) + (testing "outer spec name surfaces on via" + (is (some #(some #{::ce-coll} (:via %)) probs))))) + +;; ---------------------------------------------------------------------------- +;; explain-data via-path (top-level spec name) +;; ---------------------------------------------------------------------------- + +(deftest explain-data-via-includes-top-level-spec-name + (s/def ::vp-leaf int?) + (s/def ::vp-vec (s/coll-of ::vp-leaf :kind vector?)) + (let [probs (:cljd.spec.alpha/problems (s/explain-data ::vp-vec [1 "x" 3]))] + (testing "top-level spec name lands on via" + (let [via-set (set (mapcat :via probs))] + (is (contains? via-set ::vp-vec)))))) + +;; ---------------------------------------------------------------------------- +;; s/spec? on non-spec values +;; ---------------------------------------------------------------------------- + +(deftest spec?-only-truthy-on-Spec-instances + (testing "primitives, fns, kws, regex maps are not specs" + (is (nil? (s/spec? 1))) + (is (nil? (s/spec? "x"))) + (is (nil? (s/spec? int?))) + (is (nil? (s/spec? :a/kw))) + (is (nil? (s/spec? nil))) + (is (nil? (s/spec? (s/cat :x int?))))) + (testing "proper specs return truthy" + (is (some? (s/spec? (s/spec int?)))) + (is (some? (s/spec? (s/and int? pos?)))) + (is (some? (s/spec? (s/nilable int?)))))) + +;; ---------------------------------------------------------------------------- +;; coll-error-limit truncates non-conforming every explain output +;; ---------------------------------------------------------------------------- + +(deftest coll-error-limit-truncates-every-explain + ;; every (non-conforming) uses (take coll-error-limit) on the keep'd + ;; problem sequence. coll-of (conform-all) does NOT truncate. + (s/def ::cel-every (s/every int?)) + (let [xs (vec (repeat 50 "bad"))] + (binding [s/*coll-error-limit* 5] + (let [probs (:cljd.spec.alpha/problems (s/explain-data ::cel-every xs))] + (is (<= (count probs) 5)) + (testing "all reported problems are valid element failures" + (is (every? #(= "bad" (:val %)) probs))))))) + +;; ---------------------------------------------------------------------------- +;; s/regex? returns x when truthy +;; ---------------------------------------------------------------------------- + +(deftest regex?-returns-the-regex-map-when-truthy + (let [reg (s/cat :x int?)] + (is (= reg (s/regex? reg)))) + (let [star (s/* int?)] + (is (= star (s/regex? star))))) + +;; ---------------------------------------------------------------------------- +;; s/keys empty validates any map +;; ---------------------------------------------------------------------------- + +(deftest keys-empty-accepts-any-map + (s/def ::empty-keys-spec (s/keys)) + (testing "any map (including empty) is valid" + (is (true? (s/valid? ::empty-keys-spec {}))) + (is (true? (s/valid? ::empty-keys-spec {:a 1 :b 2})))) + (testing "non-map values are invalid" + (is (false? (s/valid? ::empty-keys-spec :not-a-map))) + (is (false? (s/valid? ::empty-keys-spec [1 2]))) + (is (false? (s/valid? ::empty-keys-spec nil))))) + +;; ---------------------------------------------------------------------------- +;; MapSpec on non-map input +;; ---------------------------------------------------------------------------- + +(deftest keys-non-map-input-explains-with-map?-pred + (s/def ::nmi-x int?) + (s/def ::nmi-spec (s/keys :req-un [::nmi-x])) + (let [prob (-> (s/explain-data ::nmi-spec [:not :a :map]) + :cljd.spec.alpha/problems + first)] + (is (= 'cljd.core/map? (:pred prob))) + (is (= [:not :a :map] (:val prob))))) + +;; ---------------------------------------------------------------------------- +;; Tuple wrong-length and non-vector explain +;; ---------------------------------------------------------------------------- + +(deftest tuple-explain-wrong-length + (s/def ::twl-pair (s/tuple int? int?)) + (let [prob (-> (s/explain-data ::twl-pair [1]) + :cljd.spec.alpha/problems + first)] + (is (= [1] (:val prob))) + (is (= (list 'cljd.core/= (list 'cljd.core/count '%) 2) (:pred prob))))) + +(deftest tuple-explain-non-vector-input + (s/def ::tnv (s/tuple int?)) + (let [prob (-> (s/explain-data ::tnv :not-vec) + :cljd.spec.alpha/problems + first)] + (is (= 'cljd.core/vector? (:pred prob))) + (is (= :not-vec (:val prob))))) + +;; ---------------------------------------------------------------------------- +;; cat-in-cat: inner regex consumes input +;; ---------------------------------------------------------------------------- + +(deftest cat-with-nested-cat-consumes-flat-input + (s/def ::cic-inner (s/cat :a int? :b int?)) + (s/def ::cic-outer (s/cat :head string? :tail ::cic-inner)) + (testing "inner cat consumes its share of the flat input sequence" + (is (= {:head "x" :tail {:a 1 :b 2}} + (s/conform ::cic-outer ["x" 1 2]))))) + +;; ---------------------------------------------------------------------------- +;; s/and with regex inner +;; ---------------------------------------------------------------------------- + +(deftest and-with-regex-inner-chains-conformed-map + ;; AndSpec.conform* chains conformed values; a regex inner conforms + ;; the sequential input into a map which the downstream pred receives. + (s/def ::ari (s/and (s/cat :x int? :y int?) #(< (:x %) (:y %)))) + (testing "downstream pred receives the conformed map" + (is (= {:x 1 :y 2} (s/conform ::ari [1 2])))) + (testing "pred failure invalidates the whole and" + (is (s/invalid? (s/conform ::ari [2 1]))))) + +;; ---------------------------------------------------------------------------- +;; s/coll-of on a map input +;; ---------------------------------------------------------------------------- + +(deftest coll-of-on-map-input-traverses-pairs + ;; conform-coll dispatches on (map? x) and treats the map as kv pairs + ;; conformed via the inner spec. Each entry is a 2-vector [k v]. + (s/def ::com (s/coll-of (s/tuple keyword? int?))) + (let [c (s/conform ::com {:a 1 :b 2})] + (testing "conform produces a map keyed by pair-first" + (is (map? c)) + (is (= 1 (get c :a))) + (is (= 2 (get c :b)))))) + +;; ---------------------------------------------------------------------------- +;; explain-data on a valid value returns nil +;; ---------------------------------------------------------------------------- + +(deftest explain-data-on-valid-returns-nil + (s/def ::edv-int int?) + (testing "nil when the value satisfies the spec" + (is (nil? (s/explain-data ::edv-int 1))) + (is (nil? (s/explain-data (s/coll-of int?) [1 2 3]))) + (is (nil? (s/explain-data (s/or :i int? :s string?) 42)))) + (testing "non-nil when the value fails" + (is (some? (s/explain-data ::edv-int "x"))))) + +;; ---------------------------------------------------------------------------- +;; MultiSpec.unform routes through the dispatch method +;; ---------------------------------------------------------------------------- + +(deftest multi-spec-unform-routes-through-dispatch + (let [v {:shape :circle :radius 2}] + (is (= v (s/unform ::shape-value v))))) + +(deftest multi-spec-unform-on-bad-dispatch-throws + ;; selected-multi-spec returns nil for an unknown :triangle dispatch + ;; (no method, no :default), and unform* throws an explicit error. + (is (= :thrown + (try (s/unform ::shape-value {:shape :triangle :side 3}) + :not-thrown + (catch Object _ :thrown))))) + +;; ---------------------------------------------------------------------------- +;; s/explain-str on a valid value +;; ---------------------------------------------------------------------------- + +(deftest explain-str-on-valid-returns-success-line + (is (= "Success!\n" (s/explain-str int? 1))) + (s/def ::esv-spec (s/and int? pos?)) + (is (= "Success!\n" (s/explain-str ::esv-spec 5)))) + +;; ---------------------------------------------------------------------------- +;; s/and with nilable inner +;; ---------------------------------------------------------------------------- + +(deftest and-with-nilable-inner + (s/def ::ani-spec (s/and any? (s/nilable int?))) + (testing "nil passes both preds" + (is (nil? (s/conform ::ani-spec nil)))) + (testing "int passes both preds" + (is (= 1 (s/conform ::ani-spec 1)))) + (testing "non-int non-nil fails the inner nilable" + (is (s/invalid? (s/conform ::ani-spec "x"))))) + +;; ---------------------------------------------------------------------------- +;; s/exercise for s/and and s/or +;; ---------------------------------------------------------------------------- + +(deftest exercise-and-spec + (s/def ::exa-spec (s/and int? pos?)) + (doseq [[x conformed] (s/exercise ::exa-spec 10)] + (is (s/valid? ::exa-spec x)) + (is (= x conformed)))) + +(deftest exercise-or-spec + (s/def ::exo-spec (s/or :i int? :s string?)) + (doseq [[x conformed] (s/exercise ::exo-spec 10)] + (is (or (int? x) (string? x))) + (is (vector? conformed)) + (is (= 2 (count conformed))) + (is (or (= :i (first conformed)) (= :s (first conformed)))))) + +;; ---------------------------------------------------------------------------- +;; conform of the ::invalid sentinel as input +;; ---------------------------------------------------------------------------- + +(deftest conform-with-invalid-sentinel-input + ;; The sentinel-as-input must not be confused with the sentinel-as- + ;; return. int? rejects it (it's a keyword), so conform returns the + ;; sentinel — but for the right reason (predicate failure). + (is (s/invalid? (s/conform int? :cljd.spec.alpha/invalid))) + ;; A keyword? spec, however, accepts it. + (is (= :cljd.spec.alpha/invalid + (s/conform keyword? :cljd.spec.alpha/invalid)))) + +;; ---------------------------------------------------------------------------- +;; describe of s/& strips namespaces +;; ---------------------------------------------------------------------------- + +(deftest amp-describe-strips-qualified-namespaces + (let [reg (s/& (s/* int?) even?) + d (s/describe reg)] + (is (= '(& (* int?) even?) d)))) + +;; ---------------------------------------------------------------------------- +;; coll-of :gen-into vs :into +;; ---------------------------------------------------------------------------- + +(deftest coll-of-gen-into-vs-into + (testing ":into affects both gen and conform output types" + (s/def ::ci-set (s/coll-of int? :into #{} :distinct true :count 3)) + (is (set? (gen/generate (s/gen ::ci-set) 10 1)))) + (testing ":gen-into only affects gen output type" + (s/def ::ci-gen-only (s/coll-of int? :gen-into #{} :distinct true :count 3)) + (is (set? (gen/generate (s/gen ::ci-gen-only) 10 1))) + (testing "conform of a vector input still yields a vector" + (let [c (s/conform ::ci-gen-only [1 2 3])] + (is (vector? c)))))) + +;; ---------------------------------------------------------------------------- +;; explain :reason via conformer that returns a sentinel +;; ---------------------------------------------------------------------------- + +(deftest explain-reason-from-pred-via-explain* + ;; The PredSpec emits a problem without :reason; explain consumers + ;; (like explain-out) print the pred form directly. Verify nothing + ;; in the pipeline silently injects a :reason for predicate fails. + (s/def ::no-reason int?) + (let [prob (-> (s/explain-data ::no-reason "x") + :cljd.spec.alpha/problems + first)] + (is (nil? (:reason prob))))) + +;; ---------------------------------------------------------------------------- +;; NonconformingSpec.unform delegates to inner +;; ---------------------------------------------------------------------------- + +(deftest nonconforming-unform-passes-through-inner-spec + ;; For a PredSpec inner, unform is identity. NonconformingSpec.unform* + ;; delegates straight through. + (s/def ::ncuf int?) + (let [nc (s/nonconforming ::ncuf)] + (is (= 42 (s/unform nc 42))))) + +;; ---------------------------------------------------------------------------- +;; conformer's cpred? path returns the fn's return value +;; ---------------------------------------------------------------------------- + +(defn- double-or-invalid [x] + (if (number? x) + (* x 2) + :cljd.spec.alpha/invalid)) + +(deftest conformer-cpred?-returns-fn-return-not-input + ;; spec-impl with cpred? true tells PredSpec.conform* to use the fn's + ;; return value directly (not the input). This is the conformer's + ;; whole point. + (let [c (s/conformer double-or-invalid)] + (is (= 6 (s/conform c 3))) + (is (= 0 (s/conform c 0))) + (is (s/invalid? (s/conform c "x"))))) + +;; ---------------------------------------------------------------------------- +;; s/exercise-fn with inline fn + explicit fspec +;; ---------------------------------------------------------------------------- + +(defn- inline-fn-target [x] (inc x)) + +(deftest exercise-fn-with-explicit-fspec-and-inline-fn + ;; Passing the fn via (identity ...) routes through the non-symbol + ;; branch of the macro, so we exercise the fn value directly without + ;; needing it fdef'd in the redef registry. + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?)] + (doseq [[args ret] (s/exercise-fn (identity inline-fn-target) 3 fsp)] + (is (sequential? args)) + (is (int? ret)) + (is (= (inc (first args)) ret))))) + +;; ---------------------------------------------------------------------------- +;; s/keys* form starts with the amp head +;; ---------------------------------------------------------------------------- + +(deftest keys-star-form-is-an-amp-regex + (s/def ::ks-x int?) + (s/def ::ks-spec (s/keys* :req-un [::ks-x])) + (let [f (s/form ::ks-spec)] + (is (= 'cljd.spec.alpha/& (first f))))) + +;; ---------------------------------------------------------------------------- +;; s/conformer form starts with the conformer head +;; ---------------------------------------------------------------------------- + +(deftest conformer-form-leads-with-conformer-symbol + (let [c (s/conformer double-or-invalid) + f (s/form c)] + (is (= 'cljd.spec.alpha/conformer (first f)))) + (testing "2-arg conformer form preserves both fns" + (let [c (s/conformer double-or-invalid double-or-invalid) + f (s/form c)] + (is (= 'cljd.spec.alpha/conformer (first f))) + (is (= 3 (count f)))))) + +;; ---------------------------------------------------------------------------- +;; s/exercise for fspec +;; ---------------------------------------------------------------------------- + +(deftest exercise-for-fspec-produces-valid-fns + (binding [s/*fspec-iterations* 3] + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?) + pairs (s/exercise fsp 2)] + (is (= 2 (count pairs))) + (doseq [[f _] pairs] + (is (fn? f)))))) + +;; ---------------------------------------------------------------------------- +;; explain-str text contains pred form and failing value +;; ---------------------------------------------------------------------------- + +(deftest explain-str-text-contains-pred-and-value + (s/def ::esx-int int?) + (let [s (s/explain-str ::esx-int "bad-input")] + (is (.contains ^String s "int?")) + (is (.contains ^String s "bad-input")))) + +;; ---------------------------------------------------------------------------- +;; s/and with a single pred +;; ---------------------------------------------------------------------------- + +(deftest and-single-pred-is-equivalent-to-that-pred + (s/def ::asp-spec (s/and int?)) + (testing "validity matches the inner pred" + (is (true? (s/valid? ::asp-spec 1))) + (is (false? (s/valid? ::asp-spec "x")))) + (testing "conform returns the input" + (is (= 1 (s/conform ::asp-spec 1))))) + +;; ---------------------------------------------------------------------------- +;; s/or with a single branch +;; ---------------------------------------------------------------------------- + +(deftest or-single-branch-tags-with-its-key + (s/def ::osb-spec (s/or :only int?)) + (testing "matching input takes the lone branch" + (is (= [:only 1] (s/conform ::osb-spec 1)))) + (testing "non-matching input is invalid" + (is (s/invalid? (s/conform ::osb-spec "x"))))) + +;; ---------------------------------------------------------------------------- +;; s/describe for coll-of and nilable strips namespaces +;; ---------------------------------------------------------------------------- + +(deftest describe-strips-ns-for-coll-of + (s/def ::dsc-spec (s/coll-of int? :kind vector?)) + (let [d (s/describe ::dsc-spec)] + (is (= '(coll-of int? :kind vector?) d)))) + +(deftest describe-strips-ns-for-nilable + (s/def ::dsn-spec (s/nilable int?)) + (let [d (s/describe ::dsn-spec)] + (is (= '(nilable int?) d)))) + +;; ---------------------------------------------------------------------------- +;; describe strips namespaces across remaining combinators +;; ---------------------------------------------------------------------------- + +(deftest describe-strips-ns-batch + (testing "s/and" + (is (= '(and int? pos?) (s/describe (s/and int? pos?))))) + (testing "s/or" + (is (= '(or :i int? :s string?) (s/describe (s/or :i int? :s string?))))) + (testing "s/tuple" + (is (= '(tuple int? string?) (s/describe (s/tuple int? string?))))) + (testing "s/cat" + (is (= '(cat :i int? :s string?) (s/describe (s/cat :i int? :s string?))))) + (testing "s/alt" + (is (= '(alt :i int? :s string?) (s/describe (s/alt :i int? :s string?))))) + (testing "s/*" + (is (= '(* int?) (s/describe (s/* int?))))) + (testing "s/+" + (is (= '(+ int?) (s/describe (s/+ int?))))) + (testing "s/?" + (is (= '(? int?) (s/describe (s/? int?))))) + (testing "s/map-of" + (is (= '(map-of keyword? int?) (s/describe (s/map-of keyword? int?)))))) + +(deftest describe-strips-ns-for-merge + (s/def ::dsm-a int?) + (s/def ::dsm-b string?) + (s/def ::dsm-mka (s/keys :req-un [::dsm-a])) + (s/def ::dsm-mkb (s/keys :req-un [::dsm-b])) + (s/def ::dsm-merged (s/merge ::dsm-mka ::dsm-mkb)) + ;; merge form contains the registered kw names (already short); describe + ;; doesn't transform kw namespaces but does strip the merge head's ns. + (let [d (s/describe ::dsm-merged)] + (is (= 'merge (first d))))) + +;; ---------------------------------------------------------------------------- +;; s/exercise for set spec +;; ---------------------------------------------------------------------------- + +(deftest exercise-for-set-spec + (s/def ::ex-color #{:red :green :blue}) + (let [pairs (s/exercise ::ex-color 20)] + (is (= 20 (count pairs))) + (doseq [[x conformed] pairs] + (is (#{:red :green :blue} x)) + (is (= x conformed))))) + +;; ---------------------------------------------------------------------------- +;; s/exercise-fn-runtime happy path +;; ---------------------------------------------------------------------------- + +(deftest exercise-fn-runtime-direct-symbol-happy-path + ;; add1 is fdef'd in register-specs!, which registers a redef setter. + ;; exercise-fn-runtime looks up the fn via redef-getter and the fspec + ;; via get-spec; samples come back as args+ret pairs. + (doseq [[args ret] (s/exercise-fn-runtime 'cljd.test-clojure.spec-alpha/add1 3)] + (is (sequential? args)) + (is (= (inc (first args)) ret)))) + +;; ---------------------------------------------------------------------------- +;; s/explain returns nil +;; ---------------------------------------------------------------------------- + +(deftest explain-returns-explain-out-result + ;; s/explain returns whatever *explain-out* yields. With the default + ;; printer that's nil (println side-effect). Rebinding to identity + ;; lets us observe the underlying explain-data passing through. + (s/def ::er-int int?) + (binding [s/*explain-out* identity] + (testing "valid path passes nil through" + (is (nil? (s/explain ::er-int 1)))) + (testing "invalid path passes explain-data through" + (let [ed (s/explain ::er-int "x")] + (is (some? ed)) + (is (contains? ed :cljd.spec.alpha/problems)))))) + +;; ---------------------------------------------------------------------------- +;; s/exercise with n=0 +;; ---------------------------------------------------------------------------- + +(deftest exercise-with-zero-count + (s/def ::ez-int int?) + (is (empty? (s/exercise ::ez-int 0)))) + +;; ---------------------------------------------------------------------------- +;; set spec rejects nil +;; ---------------------------------------------------------------------------- + +(deftest set-spec-rejects-nil + (s/def ::sn-set #{:a :b :c}) + (is (s/invalid? (s/conform ::sn-set nil))) + (is (false? (s/valid? ::sn-set nil)))) + +;; ---------------------------------------------------------------------------- +;; map-of generator produces valid maps +;; ---------------------------------------------------------------------------- + +(deftest map-of-generator-produces-valid-maps + (s/def ::mg-map (s/map-of keyword? int? :min-count 1 :max-count 3)) + (doseq [m (gen/sample (s/gen ::mg-map) 10 1)] + (is (map? m)) + (is (<= 1 (count m) 3)) + (is (every? keyword? (keys m))) + (is (every? int? (vals m))))) + +;; ---------------------------------------------------------------------------- +;; every-kv generator produces valid maps (non-conforming) +;; ---------------------------------------------------------------------------- + +(deftest every-kv-generator-produces-valid-maps + (s/def ::ekg-map (s/every-kv keyword? int? :min-count 1 :max-count 3)) + (doseq [m (gen/sample (s/gen ::ekg-map) 10 1)] + (is (map? m)) + (is (every? keyword? (keys m))) + (is (every? int? (vals m))))) + +;; ---------------------------------------------------------------------------- +;; tuple gen produces correct-length vectors +;; ---------------------------------------------------------------------------- + +(deftest tuple-generator-produces-correct-shape + (s/def ::tg-spec (s/tuple int? string? keyword?)) + (doseq [t (gen/sample (s/gen ::tg-spec) 10 1)] + (is (vector? t)) + (is (= 3 (count t))) + (is (int? (nth t 0))) + (is (string? (nth t 1))) + (is (keyword? (nth t 2))))) + +;; ---------------------------------------------------------------------------- +;; coll-of on a lazy seq input +;; ---------------------------------------------------------------------------- + +(deftest coll-of-conforms-lazy-seq-input + (s/def ::cls (s/coll-of int?)) + (let [c (s/conform ::cls (lazy-seq (list 1 2 3)))] + (is (or (sequential? c) (vector? c))) + (is (= '(1 2 3) (seq c))))) + +;; ---------------------------------------------------------------------------- +;; ClojureDart-only synonym macros +;; ---------------------------------------------------------------------------- + +(deftest def-spec-acts-as-s/def-synonym + (s/def-spec ::dsp-int int?) + (is (true? (s/valid? ::dsp-int 1))) + (is (false? (s/valid? ::dsp-int "x"))) + (is (some? (s/get-spec ::dsp-int)))) + +(deftest keys-spec-acts-as-s/keys-synonym + (s/def ::ksp-x int?) + (s/def ::ksp-map (s/keys-spec :req-un [::ksp-x])) + (is (true? (s/valid? ::ksp-map {:ksp-x 1}))) + (is (false? (s/valid? ::ksp-map {}))) + (is (false? (s/valid? ::ksp-map {:ksp-x "no"})))) + +(deftest and-spec-accepts-arity-greater-than-four + (s/def ::asp-big + (s/and-spec int? pos? #(< % 100) #(not= % 7) #(not= % 13) #(not= % 42))) + (testing "all-passing values are valid" + (is (true? (s/valid? ::asp-big 5))) + (is (true? (s/valid? ::asp-big 99)))) + (testing "any pred failure invalidates" + (is (false? (s/valid? ::asp-big -1))) + (is (false? (s/valid? ::asp-big 7))) + (is (false? (s/valid? ::asp-big 42))))) + +(deftest or-spec-single-branch-tags + (s/def ::osb-spec2 (s/or-spec :only int?)) + (testing "matching input takes the lone branch" + (is (= [:only 1] (s/conform ::osb-spec2 1)))) + (testing "non-matching input is invalid" + (is (s/invalid? (s/conform ::osb-spec2 "x"))))) + +;; ---------------------------------------------------------------------------- +;; s/exercise for regex spec +;; ---------------------------------------------------------------------------- + +(deftest exercise-for-regex-spec + (s/def ::erg-plus (s/+ int?)) + (doseq [[x conformed] (s/exercise ::erg-plus 5)] + (is (s/valid? ::erg-plus x)) + (is (vector? conformed)) + (is (every? int? conformed)))) + +;; ---------------------------------------------------------------------------- +;; coll-of with set predicate +;; ---------------------------------------------------------------------------- + +(deftest coll-of-with-set-predicate + ;; Inner pred is a set — Specize fallback wraps it in a PredSpec + ;; checking membership. + (s/def ::csp (s/coll-of #{:a :b :c})) + (testing "all-member sequence conforms" + (is (= [:a :b :c] (s/conform ::csp [:a :b :c])))) + (testing "non-member element invalidates" + (is (s/invalid? (s/conform ::csp [:a :z]))))) + +;; ---------------------------------------------------------------------------- +;; coll-of :gen + :into combined +;; ---------------------------------------------------------------------------- + +(deftest coll-of-gen-option-takes-precedence-over-into + ;; :gen wins over :into for gen — the override returns the literal + ;; collection regardless of :into. + (s/def ::cgi-spec + (s/coll-of int? :into #{} :gen #(gen/return [10 20 30]))) + (is (every? #(= [10 20 30] %) (gen/sample (s/gen ::cgi-spec) 3 1)))) + +;; ---------------------------------------------------------------------------- +;; s/spec wrapping a registered MapSpec +;; ---------------------------------------------------------------------------- + +(deftest spec-on-registered-keys-spec-preserves-conform + (s/def ::swk-a int?) + (s/def ::swk-spec (s/keys :req-un [::swk-a])) + (let [resolved (s/spec ::swk-spec)] + (testing "validity is preserved" + (is (true? (s/valid? resolved {:swk-a 1}))) + (is (false? (s/valid? resolved {}))) + (is (false? (s/valid? resolved {:swk-a "no"})))) + (testing "conform output matches the registered spec" + (is (= {:swk-a 1} (s/conform resolved {:swk-a 1})))))) + +;; ---------------------------------------------------------------------------- +;; coll-of with nested s/or pred +;; ---------------------------------------------------------------------------- + +(deftest coll-of-with-nested-or-tags-each-element + (s/def ::con-spec (s/coll-of (s/or :i int? :s string?))) + (let [v [1 "x" 2 "y"] + c (s/conform ::con-spec v)] + (is (= [[:i 1] [:s "x"] [:i 2] [:s "y"]] c)) + (testing "unform roundtrips through the tags" + (is (= v (s/unform ::con-spec c)))))) + +;; ---------------------------------------------------------------------------- +;; fspec :fn validates the args->ret relation +;; ---------------------------------------------------------------------------- + +(deftest fspec-fn-validates-relationship-on-conform + (let [fsp-inc (s/fspec :args (s/cat :x int?) :ret int? + :fn #(= (:ret %) (-> % :args :x inc))) + fsp-bad (s/fspec :args (s/cat :x int?) :ret int? + :fn #(= (:ret %) (-> % :args :x inc)))] + (testing "fn satisfying the relationship is valid" + (is (true? (s/valid? fsp-inc (fn [x] (inc x)))))) + (testing "fn violating the relationship is invalid" + (is (false? (s/valid? fsp-bad (fn [x] x))))))) + +;; ---------------------------------------------------------------------------- +;; s/and-spec form preserves pred list +;; ---------------------------------------------------------------------------- + +(deftest and-spec-form-preserves-qualified-preds + (s/def ::asf-spec (s/and-spec int? pos?)) + (is (= '(cljd.spec.alpha/and cljd.core/int? cljd.core/pos?) + (s/form ::asf-spec)))) + +;; ---------------------------------------------------------------------------- +;; s/or-spec form preserves branches +;; ---------------------------------------------------------------------------- + +(deftest or-spec-form-preserves-keys-and-preds + (s/def ::osf-spec (s/or-spec :i int? :s string?)) + (is (= '(cljd.spec.alpha/or :i cljd.core/int? :s cljd.core/string?) + (s/form ::osf-spec)))) + +;; ---------------------------------------------------------------------------- +;; s/explain with rebound *explain-out* +;; ---------------------------------------------------------------------------- + +(deftest explain-with-rebound-explain-out-passes-ed-through + (s/def ::eo-int int?) + (let [captured (atom nil)] + (binding [s/*explain-out* (fn [ed] (reset! captured ed) :captured)] + (let [result (s/explain ::eo-int "x")] + (testing "return value is what *explain-out* returns" + (is (= :captured result))) + (testing "the ed map flows through unchanged" + (is (some? @captured)) + (is (contains? @captured :cljd.spec.alpha/problems)) + (is (= "x" (:cljd.spec.alpha/value @captured)))))))) + +;; ---------------------------------------------------------------------------- +;; conformer returning the invalid sentinel +;; ---------------------------------------------------------------------------- + +(deftest conformer-returning-invalid-sentinel-fails-conform + (let [reject-all (s/conformer (constantly :cljd.spec.alpha/invalid))] + (testing "any input is rejected when conformer returns the sentinel" + (is (s/invalid? (s/conform reject-all 1))) + (is (s/invalid? (s/conform reject-all "x"))) + (is (s/invalid? (s/conform reject-all nil)))))) + +;; ---------------------------------------------------------------------------- +;; s/cat with s/+ inner +;; ---------------------------------------------------------------------------- + +(deftest cat-with-plus-inner-consumes-multiple + (s/def ::cwp (s/cat :nums (s/+ int?) :tail string?)) + (testing "1 int + tail" + (is (= {:nums [1] :tail "x"} (s/conform ::cwp [1 "x"])))) + (testing "many ints + tail" + (is (= {:nums [1 2 3] :tail "x"} (s/conform ::cwp [1 2 3 "x"])))) + (testing "zero ints fails s/+" + (is (s/invalid? (s/conform ::cwp ["x"]))))) + +;; ---------------------------------------------------------------------------- +;; s/cat with s/& (amp) inner +;; ---------------------------------------------------------------------------- + +(defn at-least-two? [x] (>= (count x) 2)) + +(deftest cat-with-amp-inner-constrains-prefix + (s/def ::cwa (s/cat :prefix (s/& (s/* int?) at-least-two?) + :tail string?)) + (testing "exactly 2 ints + tail passes the prefix predicate" + (is (s/valid? ::cwa [1 2 "x"]))) + (testing "3 ints + tail also passes" + (is (s/valid? ::cwa [1 2 3 "x"]))) + (testing "1 int + tail fails the >= 2 predicate" + (is (s/invalid? (s/conform ::cwa [1 "x"]))))) + +;; ---------------------------------------------------------------------------- +;; s/* with nil? pred handles nil-valued sequences +;; ---------------------------------------------------------------------------- + +(deftest star-nil?-conforms-sequences-of-nils + (s/def ::sn (s/* nil?)) + (testing "all-nil sequences conform" + (is (= [nil nil] (s/conform ::sn [nil nil])))) + (testing "empty sequence conforms to empty vector" + (is (= [] (s/conform ::sn [])))) + (testing "non-nil element invalidates" + (is (s/invalid? (s/conform ::sn [nil 1 nil]))))) + +;; ---------------------------------------------------------------------------- +;; coll-of with mixed valid + invalid elements +;; ---------------------------------------------------------------------------- + +(deftest coll-of-mixed-valid-invalid-fails + (s/def ::cmv (s/coll-of int?)) + (testing "first invalid element fails the whole coll" + (is (s/invalid? (s/conform ::cmv [1 2 "bad" 4])))) + (testing "explain reports the failing element" + (let [prob (-> (s/explain-data ::cmv [1 2 "bad" 4]) + :cljd.spec.alpha/problems + (->> (filter #(= "bad" (:val %)))) + first)] + (is (some? prob))))) + +;; ---------------------------------------------------------------------------- +;; s/+ with single-element input +;; ---------------------------------------------------------------------------- + +(deftest plus-conforms-single-element-vector + (s/def ::ps-single (s/+ int?)) + (testing "single element conforms to single-element vector" + (is (= [1] (s/conform ::ps-single [1])))) + (testing "empty input is invalid (s/+ requires at least one)" + (is (s/invalid? (s/conform ::ps-single []))))) + +;; ---------------------------------------------------------------------------- +;; coll-of :gen returning a list (non-vector) +;; ---------------------------------------------------------------------------- + +(deftest coll-of-gen-returning-list-passes-such-that + ;; coll? accepts lists; conform-coll has a dedicated list branch. + (s/def ::cgl-spec (s/coll-of int? :gen #(gen/return (list 1 2 3)))) + (doseq [x (gen/sample (s/gen ::cgl-spec) 3 1)] + (is (= '(1 2 3) x)) + (is (list? x)))) + +;; ---------------------------------------------------------------------------- +;; Re-defining a spec replaces the prior registration +;; ---------------------------------------------------------------------------- + +(deftest re-defining-spec-replaces-prior-entry + (s/def ::redef-target int?) + (testing "first definition is active" + (is (true? (s/valid? ::redef-target 1))) + (is (false? (s/valid? ::redef-target "x")))) + (s/def ::redef-target string?) + (testing "second definition replaces the first" + (is (false? (s/valid? ::redef-target 1))) + (is (true? (s/valid? ::redef-target "x"))))) + +;; ---------------------------------------------------------------------------- +;; s/spec :gen on a regex pred +;; ---------------------------------------------------------------------------- + +(deftest spec-3-arg-on-regex-pred-uses-gen-override + (let [sp (s/spec (s/cat :x int?) :gen #(gen/return [42]))] + (testing "samples come from the override" + (is (every? #(= [42] %) (gen/sample (s/gen sp) 3 1)))) + (testing "conform still validates input" + (is (= {:x 1} (s/conform sp [1])))))) + +;; ---------------------------------------------------------------------------- +;; coll-of :count 0 +;; ---------------------------------------------------------------------------- + +(deftest coll-of-count-zero-only-accepts-empty + (s/def ::empty-only (s/coll-of int? :count 0)) + (is (true? (s/valid? ::empty-only []))) + (is (false? (s/valid? ::empty-only [1]))) + (testing "gen produces empty" + (is (= [] (gen/generate (s/gen ::empty-only) 3 1))))) + +;; ---------------------------------------------------------------------------- +;; s/keys :gen returning invalid throws via such-that +;; ---------------------------------------------------------------------------- + +(deftest keys-gen-returning-invalid-exhausts-such-that + (s/def ::kgi-x int?) + (s/def ::kgi-bad (s/keys :req-un [::kgi-x] + :gen #(gen/return {}))) ;; missing :kgi-x + (is (= :thrown + (try (gen/generate (s/gen ::kgi-bad) 3 1) + :not-thrown + (catch Object _ :thrown))))) + +;; ---------------------------------------------------------------------------- +;; s/and-spec / s/or-spec single-pred forms +;; ---------------------------------------------------------------------------- + +(deftest and-spec-single-pred-form + (s/def ::asp1 (s/and-spec int?)) + (is (= '(cljd.spec.alpha/and cljd.core/int?) + (s/form ::asp1)))) + +(deftest or-spec-single-branch-form + (s/def ::osb1 (s/or-spec :only int?)) + (is (= '(cljd.spec.alpha/or :only cljd.core/int?) + (s/form ::osb1)))) + +;; ---------------------------------------------------------------------------- +;; s/regex? on registry-stored regex value +;; ---------------------------------------------------------------------------- + +(deftest regex?-on-registered-regex-value + (s/def ::rkr (s/cat :x int?)) + (let [v (s/get-spec ::rkr)] + (is (some? (s/regex? v))) + (is (= v (s/regex? v))))) + +;; ---------------------------------------------------------------------------- +;; s/keys :req-un with unregistered key +;; ---------------------------------------------------------------------------- + +(deftest keys-req-un-unregistered-key-still-requires-presence + ;; The key's spec doesn't need to be in the registry — MapSpec.conform + ;; checks presence (via key-expr-valid?) but skips value validation + ;; when no spec exists for the key. + (s/def ::kuk-spec (s/keys :req-un [:my.unregistered.ns/key1])) + (testing "missing the required key fails" + (is (false? (s/valid? ::kuk-spec {})))) + (testing "presence alone passes (no value spec applied)" + (is (true? (s/valid? ::kuk-spec {:key1 "anything"}))) + (is (true? (s/valid? ::kuk-spec {:key1 42}))))) + +;; ---------------------------------------------------------------------------- +;; coll-of :kind form preservation +;; ---------------------------------------------------------------------------- + +(deftest coll-of-kind-form-preserves-qualified-predicate + (s/def ::ckf-spec (s/coll-of int? :kind vector?)) + (let [f (s/form ::ckf-spec)] + (testing "form contains :kind followed by the qualified pred" + (is (some #{:kind} f)) + (is (some #{'cljd.core/vector?} f))))) + +;; ---------------------------------------------------------------------------- +;; s/+ with set predicate +;; ---------------------------------------------------------------------------- + +(deftest plus-with-set-pred-checks-membership + (s/def ::pwsp (s/+ #{:a :b :c})) + (testing "all members conform" + (is (= [:a :b] (s/conform ::pwsp [:a :b])))) + (testing "any non-member invalidates" + (is (s/invalid? (s/conform ::pwsp [:a :z]))))) + +;; ---------------------------------------------------------------------------- +;; explain-out includes "spec:" line when via is non-empty +;; ---------------------------------------------------------------------------- + +(deftest explain-out-prints-spec-line-when-via-present + (s/def ::eos-int int?) + (let [s (s/explain-str ::eos-int "x")] + (is (.contains ^String s "spec:")))) + +;; ---------------------------------------------------------------------------- +;; explain-out prints :reason line when present +;; ---------------------------------------------------------------------------- + +(deftest explain-out-prints-reason-line-when-present + ;; fspec attaches "function failed generative check" as the :reason + ;; on ret-spec failures; explain-out should render that text. + (let [fsp (s/fspec :args (s/cat :x int?) :ret int?) + s (s/explain-str fsp bad-ret)] + (is (.contains ^String s "function failed generative check")))) + +;; ---------------------------------------------------------------------------- +;; s/spec on a set preserves the set as form +;; ---------------------------------------------------------------------------- + +(deftest spec-with-set-pred-preserves-set-form + (let [sp (s/spec #{:a :b :c})] + (is (= #{:a :b :c} (s/form sp))))) + +;; ---------------------------------------------------------------------------- +;; s/with-gen on nilable +;; ---------------------------------------------------------------------------- + +(deftest with-gen-on-nilable-preserves-nil-acceptance + (let [sp (s/with-gen (s/nilable int?) #(gen/return 1))] + (testing "conform still accepts nil" + (is (nil? (s/conform sp nil)))) + (testing "conform still accepts int" + (is (= 5 (s/conform sp 5)))) + (testing "gen uses the override" + (is (every? #(= 1 %) (gen/sample (s/gen sp) 3 1)))))) + +;; ---------------------------------------------------------------------------- +;; s/explain-printer matches the default +;; ---------------------------------------------------------------------------- + +(deftest explain-printer-matches-default-explain-out + (s/def ::epm-int int?) + (let [ed (s/explain-data ::epm-int "x") + from-printer (with-out-str (s/explain-printer ed)) + from-explain (with-out-str (s/explain-out ed))] + (is (= from-printer from-explain)))) + +;; ---------------------------------------------------------------------------- +;; abbrev leaves vectors unchanged +;; ---------------------------------------------------------------------------- + +(deftest abbrev-does-not-walk-vectors + ;; abbrev only postwalks seqs; vectors fall through to the :else + ;; branch and are returned unchanged (qualified syms inside stay). + (is (= ['cljd.core/int? 'cljd.core/pos?] + (s/abbrev ['cljd.core/int? 'cljd.core/pos?]))) + (is (= [1 2 3] (s/abbrev [1 2 3])))) + +;; ---------------------------------------------------------------------------- +;; s/with-gen on map-of preserves conform +;; ---------------------------------------------------------------------------- + +(deftest with-gen-on-map-of-preserves-conform-shape + (let [sp (s/with-gen (s/map-of keyword? int?) + #(gen/return {:a 1 :b 2}))] + (testing "gen uses override" + (is (= {:a 1 :b 2} (gen/generate (s/gen sp) 3 1)))) + (testing "conform validates per-entry" + (is (= {:a 1 :b 2} (s/conform sp {:a 1 :b 2}))) + (is (s/invalid? (s/conform sp {:a "no"})))))) + +;; ---------------------------------------------------------------------------- +;; s/cat with nested cat unform produces flat sequence +;; ---------------------------------------------------------------------------- + +(deftest cat-with-nested-cat-unform-flattens + (s/def ::cncu-inner (s/cat :a int? :b int?)) + (s/def ::cncu-outer (s/cat :head string? :tail ::cncu-inner)) + (let [v ["x" 1 2] + c (s/conform ::cncu-outer v)] + (is (= {:head "x" :tail {:a 1 :b 2}} c)) + (testing "unform produces a flat input-shaped sequence" + (is (= v (s/unform ::cncu-outer c)))))) + +;; ---------------------------------------------------------------------------- +;; s/coll-of nested unform +;; ---------------------------------------------------------------------------- + +(deftest coll-of-nested-unform-preserves-shape + (s/def ::cnu-inner (s/coll-of int? :kind vector?)) + (s/def ::cnu-outer (s/coll-of ::cnu-inner :kind vector?)) + (let [v [[1 2] [3] [4 5 6]]] + (is (= v (s/unform ::cnu-outer (s/conform ::cnu-outer v)))))) + +;; ---------------------------------------------------------------------------- +;; default *explain-out* uses println → returns nil +;; ---------------------------------------------------------------------------- + +(deftest default-explain-out-returns-nil + ;; Capture both: the print side-effect (to stdout) and the return + ;; value (verified inside with-out-str so the stdout pollution is + ;; swallowed). + (s/def ::deo-int int?) + (with-out-str + (is (nil? (s/explain ::deo-int 1)))) + (with-out-str + (is (nil? (s/explain ::deo-int "x"))))) + +;; ---------------------------------------------------------------------------- +;; Nested or+and key expressions +;; ---------------------------------------------------------------------------- + +(deftest keys-or-of-and-key-expression + (s/def ::koka-x int?) + (s/def ::koka-y int?) + (s/def ::koka-z int?) + (s/def ::koka-spec + (s/keys :req-un [(or (and ::koka-x ::koka-y) ::koka-z)])) + (testing "and-branch satisfied via both x and y" + (is (true? (s/valid? ::koka-spec {:koka-x 1 :koka-y 2})))) + (testing "z-branch satisfied" + (is (true? (s/valid? ::koka-spec {:koka-z 3})))) + (testing "partial and-branch fails (x alone)" + (is (false? (s/valid? ::koka-spec {:koka-x 1})))) + (testing "all missing fails" + (is (false? (s/valid? ::koka-spec {}))))) + +;; ---------------------------------------------------------------------------- +;; s/cat with s/? wrapping a regex +;; ---------------------------------------------------------------------------- + +(deftest cat-with-maybe-regex-inner + (s/def ::cmri (s/cat :pair (s/? (s/cat :a int? :b int?)) + :tail string?)) + (testing "optional pair + tail consumed" + (is (= {:pair {:a 1 :b 2} :tail "x"} + (s/conform ::cmri [1 2 "x"])))) + (testing "tail-only input drops the absent pair" + (is (= {:tail "x"} (s/conform ::cmri ["x"]))))) + +;; ---------------------------------------------------------------------------- +;; s/keys with and(or) nested key-expression +;; ---------------------------------------------------------------------------- + +(deftest keys-and-of-or-key-expression + (s/def ::koa-x int?) + (s/def ::koa-y int?) + (s/def ::koa-z int?) + (s/def ::koa-spec + (s/keys :req [(and ::koa-x (or ::koa-y ::koa-z))])) + (testing "x + y satisfies" + (is (true? (s/valid? ::koa-spec {::koa-x 1 ::koa-y 2})))) + (testing "x + z satisfies" + (is (true? (s/valid? ::koa-spec {::koa-x 1 ::koa-z 3})))) + (testing "missing x fails (and-branch broken)" + (is (false? (s/valid? ::koa-spec {::koa-y 2})))) + (testing "x alone fails (needs y or z)" + (is (false? (s/valid? ::koa-spec {::koa-x 1}))))) + +;; ---------------------------------------------------------------------------- +;; s/def-spec form preservation +;; ---------------------------------------------------------------------------- + +(deftest def-spec-form-preserves-qualified-pred-form + (s/def-spec ::dsf-spec (s/and int? pos?)) + (is (= '(cljd.spec.alpha/and cljd.core/int? cljd.core/pos?) + (s/form ::dsf-spec)))) + +;; ---------------------------------------------------------------------------- +;; keys :opt absent key passes +;; ---------------------------------------------------------------------------- + +(deftest keys-opt-allows-absence + (s/def ::koak-a int?) + (s/def ::koak-spec (s/keys :opt [::koak-a])) + (testing "empty map valid" + (is (true? (s/valid? ::koak-spec {})))) + (testing "present + valid" + (is (true? (s/valid? ::koak-spec {::koak-a 1})))) + (testing "present + invalid value still fails" + (is (false? (s/valid? ::koak-spec {::koak-a "no"}))))) + +;; ---------------------------------------------------------------------------- +;; coll-of :distinct + :count 0 +;; ---------------------------------------------------------------------------- + +(deftest coll-of-distinct-and-count-zero + (s/def ::cdcz (s/coll-of int? :distinct true :count 0)) + (is (true? (s/valid? ::cdcz []))) + (is (false? (s/valid? ::cdcz [1])))) + +;; ---------------------------------------------------------------------------- +;; s/keys* roundtrip via kvs->map + map->kvs +;; ---------------------------------------------------------------------------- + +(deftest keys-star-conform-then-unform-roundtrips + ;; :req-un [::ksr-name] unqualifies to :ksr-name (not :name) — the + ;; input key must match the unqualified form. + (s/def ::ksr-name string?) + (s/def ::ksr-spec (s/keys* :req-un [::ksr-name])) + (let [v [:ksr-name "Alice"] + c (s/conform ::ksr-spec v)] + (is (= {:ksr-name "Alice"} c)) + (testing "unform produces a kv-seq" + (let [u (s/unform ::ksr-spec c)] + (is (= [:ksr-name "Alice"] (vec u))))))) + +;; ---------------------------------------------------------------------------- +;; s/and-spec empty preserved in form +;; ---------------------------------------------------------------------------- + +(deftest empty-and-spec-form + (s/def ::eas (s/and-spec)) + (is (= '(cljd.spec.alpha/and) (s/form ::eas))) + (testing "empty and-spec accepts anything (identity)" + (is (= 1 (s/conform ::eas 1))) + (is (= "x" (s/conform ::eas "x"))) + (is (nil? (s/conform ::eas nil))))) + +;; ---------------------------------------------------------------------------- +;; coll-of with :min-count only / :max-count only +;; ---------------------------------------------------------------------------- + +(deftest coll-of-min-count-only + (s/def ::cmco (s/coll-of int? :min-count 2)) + (testing "below min invalid" + (is (false? (s/valid? ::cmco [1])))) + (testing "at or above min valid" + (is (true? (s/valid? ::cmco [1 2]))) + (is (true? (s/valid? ::cmco (vec (range 100))))))) + +(deftest coll-of-max-count-only + (s/def ::cmxc (s/coll-of int? :max-count 3)) + (testing "0..max valid" + (is (true? (s/valid? ::cmxc []))) + (is (true? (s/valid? ::cmxc [1]))) + (is (true? (s/valid? ::cmxc [1 2 3])))) + (testing "above max invalid" + (is (false? (s/valid? ::cmxc [1 2 3 4]))))) diff --git a/clj/test/cljd/test_clojure/spec_alpha_cljs.cljd b/clj/test/cljd/test_clojure/spec_alpha_cljs.cljd new file mode 100644 index 00000000..8937e331 --- /dev/null +++ b/clj/test/cljd/test_clojure/spec_alpha_cljs.cljd @@ -0,0 +1,334 @@ +;; 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. + +;;; Port of clojurescript src/test/cljs/cljs/spec_test.cljs to ClojureDart. +;;; +;;; Tests that exercise spec features not yet ported to cljd.spec.alpha +;;; are wrapped in (comment ...) blocks so this file loads. They are +;;; preserved verbatim (with cljd namespace adaptations) so they can be +;;; un-commented incrementally as features land. Each block is annotated +;;; with the feature it depends on. + +(ns cljd.test-clojure.spec-alpha-cljs + (:require [cljd.spec.alpha :as s] + [cljd.spec.test.alpha :as st] + [cljd.spec.gen.alpha :as gen]) + (:use [clojure.test :only [deftest is are testing]])) + +(s/def ::even? (s/and number? even?)) +(s/def ::odd? (s/and number? odd?)) + +;; Needs: s/cat, s/+, s/keys, s/&, s/*, s/alt +(comment + (def s2 + (s/cat :forty-two #{42} + :odds (s/+ ::odd?) + :m (s/keys :req-un [::a ::b ::c]) + :oes (s/& (s/* (s/cat :o ::odd? :e ::even?)) #(< (count %) 3)) + :ex (s/* (s/alt :odd ::odd? :even ::even?)))) + + (deftest test-roundtrip + (let [xs [42 11 13 15 {:a 1 :b 2 :c 3} 1 2 3 42 43 44 11]] + (is (= xs (s/unform s2 (s/conform s2 xs)))))) + + (deftest test-conform-unform + (let [xs [42 11 13 15 {:a 1 :b 2 :c 3} 1 2 3 42 43 44 11]] + (is (= xs (s/unform s2 (s/conform s2 xs))))))) + +;; Needs: s/fdef, s/cat, s/?, st/instrument +(comment + (defn adder + ([a] a) + ([a b] (+ a b))) + + (s/fdef adder + :args (s/cat :a integer? :b (s/? integer?)) + :ret integer?) + + (deftest test-multi-arity-instrument + (is (= 1 (adder 1))) + (is (= 3 (adder 1 2))))) + +;; Needs: defmulti dispatch + s/fdef + st/instrument +(comment + (defmulti testmm :type) + (defmethod testmm :default [_]) + (defmethod testmm :good [_] "good") + + (s/fdef testmm :args (s/cat :m map?) :ret string?) + + (deftest test-multifn-instrument + (is (= "good" (testmm {:type :good}))))) + +;; Needs: s/int-in +(comment + (deftest int-in-test + (is (s/valid? (s/int-in 1 3) 2)) + (is (not (s/valid? (s/int-in 1 3) 0))))) + +;; Needs: s/inst-in (and #inst literals on Dart DateTime) +(comment + (deftest inst-in-test + (is (s/valid? (s/inst-in #inst "1999" #inst "2001") #inst "2000")) + (is (not (s/valid? (s/inst-in #inst "1999" #inst "2001") #inst "1492"))))) + +;; Needs: s/assert + s/check-asserts +(comment + (deftest test-assert + (s/def ::even-number (s/and number? even?)) + (is (= 42 (s/assert ::even-number 42))) + (s/check-asserts true) + (is (= 42 (s/assert ::even-number 42))) + (is (thrown? Exception (s/assert ::even-number 5))))) + +;; Needs: gen/generate, s/gen +(comment + (deftest test-cljs-1754 + (is (boolean? (gen/generate (s/gen boolean?)))))) + +;; Needs: s/fdef, s/cat, s/exercise-fn +(comment + (s/fdef cljs-1757-x :args (s/cat ::first number?) :ret #(= % 2)) + (defn cljs-1757-x [b] 2) + + (deftest test-cljs-1757 + (is (s/exercise-fn `cljs-1757-x)))) + +;; Needs: s/multi-spec +(comment + (deftest test-cljs-1788 + (defmulti mm :mm/type) + (s/def ::foo-1788 (s/multi-spec mm :mm/type)) + (is (= (s/form ::foo-1788) + '(cljd.spec.alpha/multi-spec cljd.test-clojure.spec-alpha-cljs/mm :mm/type)))) + + (def h-cljs-1790 (derive (make-hierarchy) :a :b)) + (defmulti spec-type-1790 identity :hierarchy #'h-cljs-1790) + (defmethod spec-type-1790 :b [_] + (s/spec (constantly true))) + + (deftest test-cljs-1790 + (s/def ::multi (s/multi-spec spec-type-1790 identity)) + (is (= :b (s/conform ::multi :b))) + (is (= :a (s/conform ::multi :a))))) + +;; Needs: s/exercise + s/coll-of +(comment + (deftest test-cljs-1944 + (is (not-empty (s/exercise (s/coll-of string? :kind set?)))))) + +;; Copied from Clojure spec tests + +(def even-count? #(even? (count %))) + +(defn submap? + "Is m1 a subset of m2?" + [m1 m2] + (if (clojure.core/and (map? m1) (map? m2)) + (every? (fn [[k v]] (clojure.core/and (contains? m2 k) + (submap? v (get m2 k)))) + m1) + (= m1 m2))) + +;; Needs: most of regex-ops, coll-of, map-of, conformer, int-in, inst-in, +;; double-in. The huge `are` table that exercises every spec form. +(comment + (deftest conform-explain + (let [a (s/and #(> % 5) #(< % 10)) + o (s/or :s string? :k keyword?) + c (s/cat :a string? :b keyword?) + either (s/alt :a string? :b keyword?) + star (s/* keyword?) + plus (s/+ keyword?) + opt (s/? keyword?) + andre (s/& (s/* keyword?) even-count?) + andre2 (s/& (s/* keyword?) #{[:a]}) + m (s/map-of keyword? string?) + mkeys (s/map-of (s/and keyword? (s/conformer name)) any?) + mkeys2 (s/map-of (s/and keyword? (s/conformer name)) any? :conform-keys true) + spec-list (s/coll-of (s/spec (s/cat :tag keyword? :val any?)) :kind list?) + v (s/coll-of keyword? :kind vector?) + coll (s/coll-of keyword?) + lrange (s/int-in 7 42) + drange (s/double-in :infinite? false :NaN? false :min 3.1 :max 3.2) + irange (s/inst-in #inst "1939" #inst "1946")] + (are [spec x conformed ed] + (let [co (s/conform spec x) + e (::s/problems (s/explain-data spec x))] + (clojure.core/and (= conformed co) (every? true? (map submap? ed e)))) + + keyword? :k :k nil + + a 6 6 nil + a 3 ::s/invalid '[{:pred (cljd.core/fn [%] (cljd.core/> % 5)), :val 3}] + + o "a" [:s "a"] nil + o :a [:k :a] nil + + coll [:a] [:a] nil)))) + +;; Needs: s/coll-of, s/map-of, s/every, s/every-kv, s/tuple +(comment + (deftest coll-form + (are [spec form] (= (s/form spec) form) + (s/map-of int? any?) + '(cljd.spec.alpha/map-of cljd.core/int? cljd.core/any?) + + (s/coll-of int?) + '(cljd.spec.alpha/coll-of cljd.core/int?) + + (s/every-kv int? int?) + '(cljd.spec.alpha/every-kv cljd.core/int? cljd.core/int?) + + (s/every int?) + '(cljd.spec.alpha/every cljd.core/int?) + + (s/coll-of (s/tuple (s/tuple int?))) + '(cljd.spec.alpha/coll-of (cljd.spec.alpha/tuple (cljd.spec.alpha/tuple cljd.core/int?))) + + (s/coll-of int? :kind vector?) + '(cljd.spec.alpha/coll-of cljd.core/int? :kind cljd.core/vector?) + + (s/coll-of int? :gen #(gen/return [1 2])) + '(cljd.spec.alpha/coll-of cljd.core/int? :gen (fn* [] (gen/return [1 2])))))) + +(defn check-conform-unform [spec vals expected-conforms] + (let [actual-conforms (map #(s/conform spec %) vals) + unforms (map #(s/unform spec %) actual-conforms)] + (is (= actual-conforms expected-conforms)) + (is (= vals unforms)))) + +;; Needs: s/coll-of, s/map-of, s/every, s/every-kv +(comment + (deftest coll-conform-unform + (check-conform-unform (s/coll-of (s/or :i int? :s string?)) + [[1 "x"]] + [[[:i 1] [:s "x"]]]) + (check-conform-unform (s/every (s/or :i int? :s string?)) + [[1 "x"]] + [[1 "x"]]) + (check-conform-unform (s/map-of int? (s/or :i int? :s string?)) + [{10 10 20 "x"}] + [{10 [:i 10] 20 [:s "x"]}]) + (check-conform-unform (s/map-of (s/or :i int? :s string?) int? :conform-keys true) + [{10 10 "x" 20}] + [{[:i 10] 10 [:s "x"] 20}]) + (check-conform-unform (s/every-kv int? (s/or :i int? :s string?)) + [{10 10 20 "x"}] + [{10 10 20 "x"}]))) + +;; Needs: s/& +(comment + (deftest &-explain-pred + (are [val expected] + (= expected (-> (s/explain-data (s/& int? even?) val) ::s/problems first :pred)) + [] 'cljd.core/int? + [0 2] '(cljd.spec.alpha/& cljd.core/int? cljd.core/even?)))) + +;; Needs: s/keys +(comment + (deftest keys-explain-pred + (is (= 'cljd.core/map? + (-> (s/explain-data (s/keys :req [::x]) :a) ::s/problems first :pred))))) + +;; Live: only depends on s/def returning the keyword and being able to remove. +(deftest remove-def + (is (= ::ABC (s/def ::ABC string?))) + (is (= ::ABC (s/def ::ABC nil))) + (is (nil? (s/get-spec ::ABC)))) + +;; Needs: s/exercise, s/keys +(comment + (deftest map-spec-generators + (s/def ::a nat-int?) + (s/def ::b boolean?) + (s/def ::c keyword?) + (s/def ::d double?) + (s/def ::e inst?) + (s/def ::f some?) + + (is (= #{[::a] [::a ::b] [::a ::b ::c] [::a ::c]} + (->> (s/exercise (s/keys :req [::a] :opt [::b ::c]) 100) + (map (comp sort keys first)) + (into #{})))) + + (is (every? some? (map #(-> % first) (s/exercise ::f 10)))))) + +;; Needs: s/tuple +(comment + (deftest tuple-explain-pred + (are [val expected] + (= expected (-> (s/explain-data (s/tuple int?) val) ::s/problems first :pred)) + :a 'cljd.core/vector? + [] '(cljd.core/= (cljd.core/count %) 1)))) + +;; Needs: s/fdef across an unrelated namespace +(comment + (s/fdef foo.bar/cljs-2275 + :args (s/cat :k keyword?) + :ret string?)) + +;; Needs: st/instrument (full block of variadic-arg instrument tests) +(comment + (defn foo-2793 [m & args] {:m m, :args args}) + (defn bar-2793 + ([x] {:x x}) + ([x y] {:x x, :y y}) + ([x y & m] {:x x, :y y, :m m})) + (defn baz-2793 [x & ys]) + (defn quux-2793 [& numbers]) + + (s/fdef foo-2793) + (s/fdef bar-2793) + (s/fdef baz-2793 :args (s/cat :x number? :ys (s/* number?))) + + (st/instrument `foo-2793) + (st/instrument `bar-2793) + (st/instrument `baz-2793) + (st/instrument `quux-2793) + + (deftest cljs-2793-test + (is (= {:m {:x 1 :y 2} :args nil} (foo-2793 {:x 1 :y 2}))) + (is (= {:m {:x 1 :y 2} :args [1]} (foo-2793 {:x 1 :y 2} 1))) + (is (= {:m {:x 1 :y 2} :args [1 2]} (foo-2793 {:x 1 :y 2} 1 2))) + (is (= {:x 1} (bar-2793 1))) + (is (= {:x 1 :y 2} (bar-2793 1 2))) + (is (= {:x 1 :y 2 :m [3]} (bar-2793 1 2 3))) + (is (= {:x 1 :y 2 :m [3 4]} (bar-2793 1 2 3 4))) + (is (nil? (baz-2793 1))) + (is (nil? (quux-2793))))) + +;; Needs: s/cat, s/nilable, recursive specs +(comment + (s/def ::cljs-2940-foo (s/cat :bar (s/nilable ::cljs-2940-foo)))) + +;; Live: describe / form for sets and predicates. +(deftest describing-evaled-specs + (let [sp #{1 2}] + (is (= (s/describe sp) (s/form sp) sp))) + (is (= (s/describe #(odd? %)) ::s/unknown)) + (is (= (s/form #(odd? %)) ::s/unknown))) + +;; Needs: s/fdef + st/instrument + variadic & optional args +(comment + (defn defk [key & [doc]] [key doc]) + (s/fdef defk :args (s/cat :key keyword? :doc (s/? string?))) + (st/instrument `defk) + (deftest cljs-2977-variadic-fn + (is (thrown? Exception (defk 1 1))) + (is (thrown? Exception (defk :foo 1))) + (is (= [:foo "bar"] (defk :foo "bar"))))) + +;; Needs: s/fspec, st/instrument +(comment + (s/def ::add-spec (s/fspec :args (s/cat :n pos?) :ret number?)) + (s/def add2 ::add-spec) + (defn add2 [n] (+ n 2)) + (st/instrument `add2) + (deftest cljs-3137 + (is (thrown? Exception (add2 0))))) \ No newline at end of file diff --git a/clj/test/cljd/test_clojure/spec_alpha_cross_ns.cljd b/clj/test/cljd/test_clojure/spec_alpha_cross_ns.cljd new file mode 100644 index 00000000..5978dc0a --- /dev/null +++ b/clj/test/cljd/test_clojure/spec_alpha_cross_ns.cljd @@ -0,0 +1,86 @@ +(ns cljd.test-clojure.spec-alpha-cross-ns + "Verifies that `cljd.spec.test.alpha/instrument` can install a + spec-checking wrapper on a ^:cljd.spec.alpha/redef def that lives in + a different namespace, via the top-level setter pair the compiler + emits. This exercises the only cross-namespace mechanism we have + without a runtime ns-interns table." + (:require [cljd.spec.alpha :as s] + [cljd.spec.test.alpha :as st] + [cljd.test-clojure.spec-alpha-redef-target :as t]) + (:use [clojure.test :only [deftest is testing use-fixtures]])) + +(defn- register-target-specs! [] + (s/fdef cljd.test-clojure.spec-alpha-redef-target/target-add1 + :args (s/cat :x int?) :ret int?) + (s/fdef cljd.test-clojure.spec-alpha-redef-target/target-double + :args (s/cat :x int?) :ret int?)) + +(use-fixtures :each (fn + ([] (register-target-specs!)) + ([_] nil))) + +(defn- instrument-failure [f] + (try (f) nil + (catch Object e (-> e ex-data :cljd.spec.alpha/failure)))) + +(deftest cross-ns-instrument-installs-wrapper + (try + (testing "instrument from a different namespace returns the qualified sym" + (is (= ['cljd.test-clojure.spec-alpha-redef-target/target-add1] + (st/instrument + 'cljd.test-clojure.spec-alpha-redef-target/target-add1)))) + (testing "wrapper is observed when calling through the import alias" + (is (= 6 (t/target-add1 5))) + (is (= :instrument (instrument-failure #(t/target-add1 "no"))))) + (finally + (st/unstrument + 'cljd.test-clojure.spec-alpha-redef-target/target-add1)))) + +(deftest cross-ns-instrument-handles-multiple-syms + ;; Literal quoted vector — passes through to the runtime fn which + ;; looks up each sym's setter in the redef-setter registry. + (try + (is (= #{'cljd.test-clojure.spec-alpha-redef-target/target-add1 + 'cljd.test-clojure.spec-alpha-redef-target/target-double} + (set (st/instrument + '[cljd.test-clojure.spec-alpha-redef-target/target-add1 + cljd.test-clojure.spec-alpha-redef-target/target-double])))) + (is (= :instrument (instrument-failure #(t/target-add1 "no")))) + (is (= :instrument (instrument-failure #(t/target-double "no")))) + (finally + (st/unstrument + '[cljd.test-clojure.spec-alpha-redef-target/target-add1 + cljd.test-clojure.spec-alpha-redef-target/target-double])))) + +(deftest cross-ns-instrument-accepts-runtime-bound-collection + ;; This is what the redef-setter registry buys us: a let-bound + ;; vector of qualified syms that the macro cannot introspect, but + ;; the runtime fn can dispatch on. + (let [syms ['cljd.test-clojure.spec-alpha-redef-target/target-add1 + 'cljd.test-clojure.spec-alpha-redef-target/target-double]] + (try + (is (= (set syms) (set (st/instrument syms)))) + (is (= :instrument (instrument-failure #(t/target-add1 "no")))) + (is (= :instrument (instrument-failure #(t/target-double "no")))) + (finally + (st/unstrument syms))))) + +(deftest cross-ns-instrument-accepts-namespace-symbol + ;; A namespace-only symbol (no name part) expands at runtime to + ;; every redef-registered sym in that ns. + (try + (is (= #{'cljd.test-clojure.spec-alpha-redef-target/target-add1 + 'cljd.test-clojure.spec-alpha-redef-target/target-double} + (set (st/instrument 'cljd.test-clojure.spec-alpha-redef-target)))) + (is (= :instrument (instrument-failure #(t/target-add1 "no")))) + (is (= :instrument (instrument-failure #(t/target-double "no")))) + (finally + (st/unstrument 'cljd.test-clojure.spec-alpha-redef-target)))) + +(deftest cross-ns-unstrument-restores-original + (try + (st/instrument 'cljd.test-clojure.spec-alpha-redef-target/target-add1) + (finally + (st/unstrument 'cljd.test-clojure.spec-alpha-redef-target/target-add1))) + (is (= 6 (t/target-add1 5))) + (is (nil? (instrument-failure #(t/target-add1 "no"))))) diff --git a/clj/test/cljd/test_clojure/spec_alpha_redef_target.cljd b/clj/test/cljd/test_clojure/spec_alpha_redef_target.cljd new file mode 100644 index 00000000..9e1c8e9c --- /dev/null +++ b/clj/test/cljd/test_clojure/spec_alpha_redef_target.cljd @@ -0,0 +1,14 @@ +(ns cljd.test-clojure.spec-alpha-redef-target + "Target namespace for cross-namespace instrument verification. The + consumer namespace `cljd.test-clojure.spec-alpha-cross-ns` requires + this one and instruments these defs from outside, exercising the + cross-namespace setter path emitted by ^:cljd.spec.alpha/redef.") + +;; Explicit redef metadata so the def is emitted with the +;; getter/setter pair; no s/fdef in this namespace, so the auto-redef +;; path doesn't fire (we want to test the explicit path). +(defn ^:cljd.spec.alpha/redef target-add1 [x] (inc x)) + +;; A second target so we can exercise multiple syms in one instrument +;; call. +(defn ^:cljd.spec.alpha/redef target-double [x] (* 2 x)) diff --git a/clj/test/cljd/test_clojure/spec_alpha_test_alpha.cljd b/clj/test/cljd/test_clojure/spec_alpha_test_alpha.cljd new file mode 100644 index 00000000..9a2bdcdc --- /dev/null +++ b/clj/test/cljd/test_clojure/spec_alpha_test_alpha.cljd @@ -0,0 +1,179 @@ +;; 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. + +;;; Port of clojurescript src/test/cljs/cljs/spec/test_test.cljs. +;;; Every test in this file depends on cljd.spec.test.alpha (instrument, +;;; unstrument, check, stub, enumerate-namespace, with-instrument-disabled). +;;; None of those are ported yet, so the entire body is wrapped in +;;; (comment ...) until cljd.spec.test.alpha is implemented. + +(ns cljd.test-clojure.spec-alpha-test-alpha + (:require [cljd.spec.alpha :as s] + [cljd.spec.test.alpha :as stest]) + (:use [clojure.test :only [deftest testing is are]])) + +(comment + ;; Needs: s/fdef, s/alt, s/cat, st/check + (s/fdef clojure.core/symbol + :args (s/alt :separate (s/cat :ns string? :n string?) + :str string? + :sym symbol?) + :ret symbol?) + + (defn h-cljs-1812 [x] true) + (s/fdef h-cljs-1812 :args (s/cat :x int?) :ret true?) + + (deftest test-cljs-1812 + (is (= (stest/unstrument `h-cljs-1812) [])) + (stest/check `h-cljs-1812 {:clojure.spec.test.check/opts {:num-tests 1}}) + (h-cljs-1812 "foo")) + + (def ^:const pi 3.14159) + (defn area [r] (* pi r r)) + (s/fdef area :args (s/cat :r number?)) + + (deftest test-cljs-2142 + (is (= `[area] (stest/instrument `[pi area])))) + + (defn f-2391 [] 1) + (s/fdef f-2391 :args (s/cat) :ret #{2}) + + (deftest test-cljs-2391-a + (is (= 1 (f-2391)))) + + (deftest test-cljs-2391-b + (stest/instrument `f-2391 {:stub #{`f-2391}}) + (is (= 2 (f-2391)))) + + (deftest test-cljs-2391-c + (stest/unstrument `f-2391) + (is (= 1 (f-2391)))) + + (deftest test-cljs-2197 + (stest/instrument `symbol) + (is (thrown? Exception (symbol 3))) + (is (thrown? Exception (#'symbol 3))) + (is (thrown? Exception (apply symbol [3]))) + (stest/unstrument `symbol)) + + (defn arities + ([a] (inc a)) + ([a b] (+ a b)) + ([a b c] 0)) + + (s/fdef arities + :args (s/or :arity-1 (s/cat :a number?) + :arity-2 (s/cat :a number? :b number?) + :arity-3 (s/cat :a string? :b boolean? :c map?)) + :ret number?) + + (deftest test-2397 + (stest/instrument `arities) + (is (arities 1)) + (is (thrown? Exception (arities "bad"))) + (stest/unstrument `arities)) + + (defn foo [& args] args) + (s/fdef foo :args (s/cat :args (s/* int?))) + + (deftest test-2641 + (stest/instrument `foo) + (is (= [1 2 3] (foo 1 2 3))) + (is (thrown? Exception (foo 1 :hello))) + (stest/unstrument `foo)) + + (deftest test-2755 + (is (uri? (ffirst (s/exercise uri? 1))))) + + (deftest test-cljs-2665 + (is (= '#{ns1/x ns1/y ns2/z} + (stest/enumerate-namespace '[ns1 ns2])))) + + (defn fn-2953 [x] ::ret-val) + (s/fdef fn-2953 :args (s/cat :x int?)) + + (deftest test-cljs-2953 + (stest/instrument `fn-2953) + (is @#'stest/*instrument-enabled*) + (is (= ::ret-val (stest/with-instrument-disabled + (is (nil? @#'stest/*instrument-enabled*)) + (fn-2953 "abc")))) + (is @#'stest/*instrument-enabled*)) + + (defn foo-2955 [n] "ret") + (s/fdef foo-2955 :args (s/cat :n number?) :ret string?) + + (deftest test-cljs-2955 + (is (seq (stest/check `foo-2955)))) + + (s/fdef cljd.core/= :args (s/+ any?)) + + (deftest test-cljs-2956 + (is (= '[cljd.core/=] (stest/instrument `=))) + (is (true? (= 1))) + (is (thrown-with-msg? Exception #"did not conform to spec" (=))) + (is (= '[cljd.core/=] (stest/unstrument `=)))) + + (defn fn-2975 [x]) + + (deftest test-2975 + (testing "instrument and unstrument return empty coll when no fdef exists" + (is (empty? (stest/instrument `fn-2975))) + (is (empty? (stest/unstrument `fn-2975))))) + + (defn fn-2995 + ([] (fn-2995 0)) + ([a] (fn-2995 a 1)) + ([a b] [a b])) + + (s/fdef fn-2995 :args (s/cat :a (s/? number?) :b (s/? number?))) + + (deftest test-2995 + (stest/instrument `fn-2995) + (testing "instrumented self-calling multi-arity function works" + (is (= [0 1] (fn-2995 0 1))) + (is (= [0 1] (fn-2995 0))) + (is (= [0 1] (fn-2995 0))) + (is (thrown? Exception (fn-2995 "not a number"))))) + + (defn cljs-2964 [x] true) + (s/fdef cljs-2964 :args (s/cat :x int?) :ret true?) + + (deftest test-cljs-2964 + (let [check-res (stest/check `cljs-2964 {:clojure.spec.test.check/opts {:num-tests 1}})] + (is (seq check-res)) + (is (every? (fn [res] + (= 1 (-> res :clojure.spec.test.check/ret :num-tests))) + check-res)))) + + (defn cljs-3033 [x] true) + (s/fdef cljs-3033 :args (s/cat :x int?) :ret true?) + + (deftest test-cljs-3033 + (let [check-res (stest/check `cljs-3033 {:clojure.test.check/opts {:num-tests 1}})] + (is (seq check-res)) + (is (every? (fn [res] + (= 1 (-> res :clojure.test.check/ret :num-tests))) + check-res)))) + + (s/fdef cljd.core/next :args (s/cat :coll seqable?)) + + (deftest test-3023 + (is (= '[cljd.core/next] (stest/instrument `next))) + (is (= [2 3] (next [1 2 3]))) + (is (thrown-with-msg? Exception #"did not conform to spec" (next 1))) + (is (= '[cljd.core/next] (stest/unstrument `next)))) + + (defn cljs-3049 [x] x) + (deftest test-3049 + (s/fdef cljs-3049 :args (s/cat :x number?) :ret number?) + (testing "the spec'ed fn is checkable" + (is (contains? (stest/checkable-syms) `cljs-3049))) + (s/def cljs-3049 nil) + (testing "the spec'ed fn is not checkable anymore" + (is (not (contains? (stest/checkable-syms) `cljs-3049)))))) diff --git a/clj/test/cljd/test_clojure/spec_gen_tc.cljd b/clj/test/cljd/test_clojure/spec_gen_tc.cljd new file mode 100644 index 00000000..dd019389 --- /dev/null +++ b/clj/test/cljd/test_clojure/spec_gen_tc.cljd @@ -0,0 +1,182 @@ +(ns cljd.test-clojure.spec-gen-tc + "Smoke tests for cljd.spec.gen.alpha.test-check — exercises the + rose-tree generator algebra and basic shrink mechanics." + (:require [cljd.spec.gen.alpha :as gen] + [cljd.spec.gen.alpha.test-check :as tc]) + (:use [clojure.test :only [deftest is testing]])) + +(deftest generator-types + (is (tc/generator? (tc/return 42))) + (is (tc/generator? (tc/choose 0 10))) + (is (tc/generator? (tc/fmap inc (tc/choose 0 10)))) + (is (tc/generator? (tc/tuple (tc/return 1) (tc/return 2)))) + (is (nil? (tc/generator? 42))) + (is (nil? (tc/generator? nil)))) + +(deftest seeded-generation-is-deterministic + (testing "same seed produces same sample" + (let [g (tc/choose 0 1000000)] + (is (= (tc/sample g 5 42) + (tc/sample g 5 42))))) + (testing "different seeds produce different samples" + (let [g (tc/choose 0 1000000)] + (is (not= (tc/sample g 10 1) + (tc/sample g 10 2)))))) + +(deftest choose-produces-values-in-range + (let [vals (tc/sample (tc/choose 5 9) 50 7)] + (is (every? #(<= 5 % 9) vals)))) + +(deftest return-has-no-shrinks + (let [r (tc/run-gen (tc/return 42) (tc/make-rng 1) 10)] + (is (= 42 (tc/rose-val r))) + (is (empty? (tc/rose-children r))))) + +(deftest choose-shrinks-toward-low-bound + (let [r (tc/run-gen (tc/choose 0 100) (tc/make-rng 1) 10) + v (tc/rose-val r) + shrink-vals (mapv tc/rose-val (tc/rose-children r))] + (testing "shrink seq begins with the low bound" + (is (= 0 (first shrink-vals)))) + (testing "no shrink equals the original value" + (is (every? #(not= v %) shrink-vals))) + (testing "all shrinks are smaller in magnitude than v" + (when (pos? v) + (is (every? #(< % v) shrink-vals)))))) + +(deftest fmap-maps-values-and-shrinks + (let [g (tc/fmap #(* 2 %) (tc/choose 1 10)) + r (tc/run-gen g (tc/make-rng 1) 10)] + (is (even? (tc/rose-val r))) + (testing "shrink values are also mapped" + (is (every? even? (mapv tc/rose-val (tc/rose-children r))))))) + +(deftest tuple-shrinks-each-component + (let [g (tc/tuple (tc/choose 0 10) (tc/choose 0 10)) + r (tc/run-gen g (tc/make-rng 1) 10) + v (tc/rose-val r)] + (is (= 2 (count v))) + (is (every? #(<= 0 % 10) v)) + (testing "at least one shrink reduces the first component" + (let [first-comp (first v)] + (when (pos? first-comp) + (is (some #(< (first %) first-comp) + (mapv tc/rose-val (tc/rose-children r))))))))) + +(deftest vector-grows-and-shrinks + (testing "fixed-size vector" + (let [g (tc/vector (tc/choose 0 5) 3) + r (tc/run-gen g (tc/make-rng 1) 10)] + (is (= 3 (count (tc/rose-val r)))))) + (testing "shrinks include drop-element variants" + (let [g (tc/vector (tc/choose 1 5) 3) + r (tc/run-gen g (tc/make-rng 1) 10) + v (tc/rose-val r) + shrink-counts (mapv #(count (tc/rose-val %)) (tc/rose-children r))] + (is (some #(= 2 %) shrink-counts) "at least one drop-shrink reduces count to 2")))) + +(deftest bind-threads-generated-values + (let [g (tc/bind (tc/return 5) #(tc/choose % (+ % 3))) + v (tc/generate g 10 7)] + (is (<= 5 v 8)))) + +(deftest bind-shrinks-bound-value-before-inner-value + (let [g (tc/bind (tc/choose 0 100) + (fn [outer] + (tc/fmap (fn [inner] [outer inner]) + (tc/choose 0 100)))) + prop (tc/for-all* [g] (fn [[outer _]] (< outer 5))) + ret (tc/quick-check 200 prop {:seed 1})] + (is (false? (:pass? ret))) + (testing "the outer bound value reaches the minimal failing boundary" + (is (= 5 (ffirst (:smallest (:shrunk ret)))))))) + +(deftest sized-and-resize-thread-size + (let [g (tc/sized (fn [size] (tc/return size)))] + (is (= 7 (tc/generate g 7 1))) + (is (= 2 (tc/generate (tc/resize 2 g) 7 1))))) + +(deftest public-gen-alpha-exposes-bytes-and-seeded-uuid + (let [bs (gen/generate (gen/bytes) 10 1)] + (is (every? #(<= 0 % 255) bs))) + (testing "uuid generation is reproducible through the generator seed" + (is (= (gen/generate (gen/uuid) 30 123) + (gen/generate (gen/uuid) 30 123))))) + +(deftest one-of-picks-from-alternatives + (let [g (tc/one-of [(tc/return :a) (tc/return :b) (tc/return :c)]) + vals (set (tc/sample g 30 1))] + (is (every? #{:a :b :c} vals)))) + +(deftest large-integer-honors-bounds + (let [g (tc/large-integer* {:min 10 :max 20}) + vals (tc/sample g 50 1)] + (is (every? #(<= 10 % 20) vals)))) + +(deftest such-that-filters-shrinks + (let [g (tc/such-that pos? (tc/choose -10 10)) + vals (tc/sample g 20 1)] + (is (every? pos? vals)))) + +;; --------------------------------------------------------------------------- +;; quick-check + shrink + +(deftest quick-check-passes-trivially-true-property + (let [prop (tc/for-all* [(tc/choose 0 100)] (fn [_] true)) + ret (tc/quick-check 50 prop {:seed 42})] + (is (= true (:result ret))) + (is (true? (:pass? ret))) + (is (= 50 (:num-tests ret))) + (is (= 42 (:seed ret))) + (is (nil? (:shrunk ret))))) + +(deftest quick-check-detects-failing-property + ;; Property: every int is < 5. Must fail and shrink toward 5. + (let [prop (tc/for-all* [(tc/choose 0 100)] (fn [n] (< n 5))) + ret (tc/quick-check 100 prop {:seed 1})] + (is (false? (:pass? ret))) + (is (false? (:result ret))) + (is (some? (:failing-args ret))) + (is (some? (:shrunk ret))) + (testing "shrunk smallest is the boundary value 5" + (is (= [5] (:smallest (:shrunk ret))))))) + +(deftest quick-check-shrinks-vector-failures + ;; Property: vector of ints contains no 7. + (let [prop (tc/for-all* + [(tc/vector (tc/choose 0 9) 0 8)] + (fn [v] (not (some #{7} v)))) + ret (tc/quick-check 200 prop {:seed 3})] + (when-not (:pass? ret) + (testing "shrunk vector contains exactly one 7" + (let [v (first (:smallest (:shrunk ret)))] + (is (= [7] v))))))) + +(deftest quick-check-is-reproducible-with-seed + (let [prop (tc/for-all* [(tc/choose 0 1000)] (fn [n] (< n 800))) + a (tc/quick-check 200 prop {:seed 99}) + b (tc/quick-check 200 prop {:seed 99})] + (is (= (:result a) (:result b))) + (is (= (:num-tests a) (:num-tests b))) + (is (= (:failing-args a) (:failing-args b))) + (is (= (:smallest (:shrunk a)) (:smallest (:shrunk b)))))) + +(deftest quick-check-records-seed-when-not-given + (let [prop (tc/for-all* [(tc/return 0)] (fn [_] true)) + ret (tc/quick-check 5 prop)] + (is (integer? (:seed ret))))) + +(deftest quick-check-treats-thrown-exceptions-as-failure + (let [prop (tc/for-all* [(tc/choose 0 5)] + (fn [n] (when (= n 3) (throw (Exception. "boom"))) true)) + ret (tc/quick-check 100 prop {:seed 5})] + ;; The property eventually picks 3 — that throws. + (when-not (:pass? ret) + (is (instance? Exception (:result ret)))))) + +(deftest shrink-narrows-to-boundary + ;; Property: int <= 10. Generate up to 1000, shrunk smallest should be 11. + (let [prop (tc/for-all* [(tc/choose 0 1000)] (fn [n] (<= n 10))) + ret (tc/quick-check 100 prop {:seed 11})] + (when-not (:pass? ret) + (is (= [11] (:smallest (:shrunk ret))))))) diff --git a/run-tests b/run-tests index 9051f4ec..7cde1367 100755 --- a/run-tests +++ b/run-tests @@ -31,6 +31,10 @@ clojure -M -m cljd.build compile \ cljd.test-clojure.other-functions \ cljd.test-clojure.parse \ cljd.test-clojure.predicates \ + cljd.test-clojure.spec-alpha \ + cljd.test-clojure.spec-alpha-redef-target \ + cljd.test-clojure.spec-alpha-cross-ns \ + cljd.test-clojure.spec-gen-tc \ cljd.test-clojure.primitives-test \ cljd.test-clojure.string \ cljd.test-clojure.test-test \ diff --git a/samples/spec_alpha_demo/deps.edn b/samples/spec_alpha_demo/deps.edn new file mode 100644 index 00000000..fa12479c --- /dev/null +++ b/samples/spec_alpha_demo/deps.edn @@ -0,0 +1,5 @@ +{:paths ["src"] + :deps {tensegritics/clojuredart {:local/root "../../"}} + :aliases {:cljd {:main-opts ["-m" "cljd.build"]}} + :cljd/opts {:main sample.spec-alpha-demo + :kind :dart}} diff --git a/samples/spec_alpha_demo/pubspec.yaml b/samples/spec_alpha_demo/pubspec.yaml new file mode 100644 index 00000000..e1f87798 --- /dev/null +++ b/samples/spec_alpha_demo/pubspec.yaml @@ -0,0 +1,6 @@ +name: spec_alpha_demo +description: cljd.spec.alpha smoke test +version: 1.0.0 + +environment: + sdk: ^3.0.3 \ No newline at end of file diff --git a/samples/spec_alpha_demo/src/sample/spec_alpha_demo.cljd b/samples/spec_alpha_demo/src/sample/spec_alpha_demo.cljd new file mode 100644 index 00000000..8fc21291 --- /dev/null +++ b/samples/spec_alpha_demo/src/sample/spec_alpha_demo.cljd @@ -0,0 +1,84 @@ +(ns sample.spec-alpha-demo + (:require [cljd.spec.alpha :as s] + [cljd.spec.gen.alpha :as gen])) + +(defn add1 [x] (inc x)) + +(defn register-specs! [] + (s/def ::even-int (s/and int? even?)) + (s/def ::small (s/or :pos pos-int? :tiny #(< % 10))) + (s/def ::maybe-even (s/nilable ::even-int)) + (s/def ::pair (s/tuple keyword? int?)) + (s/def ::ints (s/coll-of int?)) + (s/def ::mixed (s/coll-of (s/or :i int? :s string?))) + (s/def ::plain-mixed (s/nonconforming ::mixed)) + (s/def ::scores (s/map-of keyword? int?)) + (s/def ::name string?) + (s/def ::score int?) + (s/def ::person (s/keys :req-un [::name] :opt-un [::score])) + (s/def ::person-kvs (s/keys* :req-un [::name])) + (s/def ::named-or-scored (s/keys :req-un [(or ::name ::score)])) + (s/def ::scored-person (s/merge (s/keys :req-un [::name]) + (s/keys :req-un [::score]))) + (s/def ::small-int (s/int-in 1 4)) + (s/def ::small-double (s/double-in :min 1.5 :max 2.5 :NaN? false :infinite? false)) + (s/def ::recent-ish (s/inst-in #inst "2020-01-01T00:00:00.000-00:00" + #inst "2030-01-01T00:00:00.000-00:00")) + (s/def ::event (s/cat :tag keyword? :value int?)) + (s/def ::maybe-event (s/cat :tag keyword? :value (s/? int?))) + (s/def ::maybe-then-int (s/cat :maybe (s/? int?) :value int?)) + (s/def ::prefix-tail (s/cat :prefix (s/* any?) :tail any?)) + (s/def ::tokens (s/+ keyword?)) + (s/def ::small-token-run (s/& (s/+ keyword?) #(< (count %) 3))) + (s/fdef add1 :args (s/cat :x int?) :ret int?)) + +(defn main [] + (register-specs!) + (println "valid? ::even-int 4 =>" (s/valid? ::even-int 4)) + (println "valid? ::even-int 5 =>" (s/valid? ::even-int 5)) + (println "conform ::even-int 4 =>" (s/conform ::even-int 4)) + (println "conform ::small 3 =>" (s/conform ::small 3)) + (println "conform ::small -1 =>" (s/conform ::small -1)) + (println "conform ::small 100 =>" (s/conform ::small 100)) + (println "conform ::maybe-even nil =>" (s/conform ::maybe-even nil)) + (println "conform ::pair [:a 1] =>" (s/conform ::pair [:a 1])) + (println "conform ::ints [1 2 3] =>" (s/conform ::ints [1 2 3])) + (println "unform ::mixed (conform [1 \"x\"]) =>" (s/unform ::mixed (s/conform ::mixed [1 "x"]))) + (println "conform ::plain-mixed [1 \"x\"] =>" (s/conform ::plain-mixed [1 "x"])) + (println "conform ::scores {:a 1 :b 2} =>" (s/conform ::scores {:a 1 :b 2})) + (println "conform ::person {:name \"Ada\" :score 7} =>" (s/conform ::person {:name "Ada" :score 7})) + (println "conform ::person-kvs [:name \"Ada\"] =>" (s/conform ::person-kvs [:name "Ada"])) + (println "valid? ::scored-person {:name \"Ada\" :score 7} =>" (s/valid? ::scored-person {:name "Ada" :score 7})) + (println "valid? ::scored-person {:name \"Ada\"} =>" (s/valid? ::scored-person {:name "Ada"})) + (println "valid? ::person {:score 7} =>" (s/valid? ::person {:score 7})) + (println "valid? ::person {:name \"Ada\" :score \"x\"} =>" (s/valid? ::person {:name "Ada" :score "x"})) + (println "valid? ::named-or-scored {:score 7} =>" (s/valid? ::named-or-scored {:score 7})) + (println "valid? ::named-or-scored {} =>" (s/valid? ::named-or-scored {})) + (println "valid? ::small-int 2 =>" (s/valid? ::small-int 2)) + (println "valid? ::small-int 4 =>" (s/valid? ::small-int 4)) + (println "valid? ::small-double 2.0 =>" (s/valid? ::small-double 2.0)) + (println "valid? ::small-double ##NaN =>" (s/valid? ::small-double ##NaN)) + (println "valid? ::recent-ish #inst 2026 =>" (s/valid? ::recent-ish #inst "2026-01-01T00:00:00.000-00:00")) + (println "conform ::event [:ok 1] =>" (s/conform ::event [:ok 1])) + (println "unform ::event (conform [:ok 1]) =>" (s/unform ::event (s/conform ::event [:ok 1]))) + (println "conform ::maybe-event [:ok] =>" (s/conform ::maybe-event [:ok])) + (println "conform ::maybe-then-int [1] =>" (s/conform ::maybe-then-int [1])) + (println "conform ::prefix-tail [1] =>" (s/conform ::prefix-tail [1])) + (println "conform ::prefix-tail [1 2] =>" (s/conform ::prefix-tail [1 2])) + (println "conform ::tokens [:a :b] =>" (s/conform ::tokens [:a :b])) + (println "valid? ::small-token-run [:a :b] =>" (s/valid? ::small-token-run [:a :b])) + (println "valid? ::small-token-run [:a :b :c] =>" (s/valid? ::small-token-run [:a :b :c])) + (println "valid? fdef add1 =>" (s/valid? 'sample.spec-alpha-demo/add1 add1)) + (println "fdef add1 args form =>" (s/form (:args (s/get-spec 'sample.spec-alpha-demo/add1)))) + (println "generate boolean? =>" (gen/generate (s/gen boolean?))) + (println "exercise ::small-int =>" (first (s/exercise ::small-int 1))) + (println "exercise ::person =>" (first (s/exercise ::person 1))) + (println "exercise ::event =>" (first (s/exercise ::event 1))) + (println "exercise-fn add1 =>" (first (s/exercise-fn add1 1))) + (println "check-asserts? =>" (s/check-asserts?)) + (s/check-asserts true) + (println "assert ::even-int 4 =>" (s/assert ::even-int 4)) + (println "check-asserts? after enable =>" (s/check-asserts?)) + (s/check-asserts false) + (println "explain-data ::person {:score 7} =>" (s/explain-data ::person {:score 7})) + (println "explain-data ::even-int 5 =>" (s/explain-data ::even-int 5)))