babashka/test-resources/lib_tests/expound/alpha_test.cljc
Gabriel Horner 665ae4dd97
Finish up library tests (#1120)
* Add tests for markdown-clj and tools.namespace

See comment for why only one markdown test could be run.
Closes #1069 and #1064

* Convert 10 test libs using add-libtest

Also improved add-libtest to only require maven artifact
and rely on clojars for getting git-url most of the time

* Convert 8 more test libs using add-libtest

Also updated table and added comment for newline test

* Fix doric test

* Disable tools.namespace test that fails on windows

* Added dozen manual test libs and converted 2 test libs

add-libtest.clj supports manually-added and test-directories options

* Converts last tests to test namespaces and write libraries.csv

* Add a number of library tests from projects.md

Also add more docs around adding test libs and tweak add script

* Use :sha for gitlib and older clojure cli

* Revert "Use :sha for gitlib and older clojure cli"

This reverts commit c663ab8368.

* Fix and disable failing tests

Disabled tests that fail consistently and fixed windows one
2021-12-29 16:35:14 +01:00

4350 lines
114 KiB
Clojure

(ns expound.alpha-test
(:require #?@(:clj
;; just to include the specs
[[clojure.core.specs.alpha]
[ring.core.spec]
[onyx.spec]])
;; Deps for specs that generate specs, which are currently disabled
#_[clojure.test.check.random :as random]
#_[clojure.test.check.rose-tree :as rose]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
[clojure.string :as string]
[clojure.test :as ct :refer [is testing deftest use-fixtures]]
[clojure.test.check.generators :as gen]
[clojure.walk :as walk]
[com.gfredericks.test.chuck :as chuck]
[com.gfredericks.test.chuck.clojure-test :refer [checking]]
[expound.alpha :as expound]
[expound.ansi :as ansi]
[expound.printer :as printer]
[expound.problems :as problems]
[expound.spec-gen :as sg]
[expound.test-utils :as test-utils]
[spec-tools.data-spec :as ds]
#?(:clj [orchestra.spec.test :as orch.st]
:cljs [orchestra-cljs.spec.test :as orch.st])))
;;;; override specs and add generators
;;;; this allows us to load expound with babaska and spartan.spec
(s/def :expound.printer/value-str-fn (s/with-gen ifn?
#(gen/return (fn [_ _ _ _] "NOT IMPLEMENTED"))))
(s/def :expound.spec/spec (s/or
:set set?
:pred (s/with-gen ifn?
#(gen/elements [boolean? string? int? keyword? symbol?]))
:kw qualified-keyword?
:spec (s/with-gen s/spec?
#(gen/elements
(for [pr [boolean? string? int? keyword? symbol?]]
(s/spec pr))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def num-tests 5)
(use-fixtures :once
test-utils/check-spec-assertions
test-utils/instrument-all)
;; Missing onyx specs
(s/def :trigger/materialize any?)
(s/def :flow/short-circuit any?)
(defn pf
"Fixes platform-specific namespaces and also formats using printf syntax"
[s & args]
(apply printer/format
#?(:cljs (string/replace s "pf." "cljs.")
:clj (string/replace s "pf." "clojure."))
args))
(defn take-lines [n s]
(string/join "\n" (take n (string/split-lines s))))
(defn formatted-exception [printer-options f]
(let [printer (expound/custom-printer printer-options)
exception-data (binding [s/*explain-out* printer]
(try
(f)
(catch #?(:cljs :default :clj Exception)
e
#?(:cljs {:message (.-message e)
:data (.-data e)}
:clj (Throwable->map e)))))
ed #?(:cljs (-> exception-data :data)
:clj (-> exception-data :via last :data))
cause# (-> #?(:cljs (:message exception-data)
:clj (:cause exception-data))
(clojure.string/replace #"Call to (.*) did not conform to spec:"
"Call to #'$1 did not conform to spec."))]
(str cause#
(if (re-find #"Detected \d+ error" cause#)
""
(str "\n"
(with-out-str (printer ed)))))))
(defn orch-unstrument-test-fns [f]
(orch.st/unstrument [`results-str-fn1
`results-str-fn2
`results-str-fn4
`results-str-fn7])
(f))
(def inverted-ansi-codes
(reduce
(fn [m [k v]]
(assoc m (str v) k))
{}
ansi/sgr-code))
(defn readable-ansi [s]
(string/replace
s
#"\x1b\[([0-9]*)m"
#(str "<" (string/upper-case (name (get inverted-ansi-codes (second %)))) ">")))
;; https://github.com/bhb/expound/issues/8
(deftest expound-output-ends-in-newline
(is (= "\n" (str (last (expound/expound-str string? 1)))))
(is (= "\n" (str (last (expound/expound-str string? ""))))))
(deftest expound-prints-expound-str
(is (=
(expound/expound-str string? 1)
(with-out-str (expound/expound string? 1)))))
(deftest predicate-spec
(is (= (pf "-- Spec failed --------------------
1
should satisfy
string?
-------------------------
Detected 1 error\n")
(expound/expound-str string? 1))))
(s/def :simple-type-based-spec/str string?)
(deftest simple-type-based-spec
(testing "valid value"
(is (= "Success!\n"
(expound/expound-str :simple-type-based-spec/str ""))))
(testing "invalid value"
(is (=
(pf "-- Spec failed --------------------
1
should satisfy
string?
-- Relevant specs -------
:simple-type-based-spec/str:
pf.core/string?
-------------------------
Detected 1 error\n")
(expound/expound-str :simple-type-based-spec/str 1)))))
(s/def :set-based-spec/tag #{:foo :bar})
(s/def :set-based-spec/nilable-tag (s/nilable :set-based-spec/tag))
(s/def :set-based-spec/set-of-one #{:foobar})
(s/def :set-based-spec/one-or-two (s/or
:one (s/cat :a #{:one})
:two (s/cat :b #{:two})))
(deftest set-based-spec
(testing "prints valid options"
(is (= "-- Spec failed --------------------
:baz
should be one of: :bar, :foo
-- Relevant specs -------
:set-based-spec/tag:
#{:bar :foo}
-------------------------
Detected 1 error\n"
(expound/expound-str :set-based-spec/tag :baz))))
(testing "prints combined options for various specs"
(is (= (pf "-- Spec failed --------------------
[:three]
^^^^^^
should be one of: :one, :two
-- Relevant specs -------
:set-based-spec/one-or-two:
(pf.spec.alpha/or
:one
(pf.spec.alpha/cat :a #{:one})
:two
(pf.spec.alpha/cat :b #{:two}))
-------------------------
Detected 1 error\n")
(expound/expound-str :set-based-spec/one-or-two [:three]))))
(testing "nilable version"
(is (= (pf "-- Spec failed --------------------
:baz
should be one of: :bar, :foo
or
should satisfy
nil?
-- Relevant specs -------
:set-based-spec/tag:
#{:bar :foo}
:set-based-spec/nilable-tag:
(pf.spec.alpha/nilable :set-based-spec/tag)
-------------------------
Detected 1 error\n")
(expound/expound-str :set-based-spec/nilable-tag :baz))))
(testing "single element spec"
(is (= (pf "-- Spec failed --------------------
:baz
should be: :foobar
-- Relevant specs -------
:set-based-spec/set-of-one:
#{:foobar}
-------------------------
Detected 1 error\n")
(expound/expound-str :set-based-spec/set-of-one :baz)))))
(s/def :nested-type-based-spec/str string?)
(s/def :nested-type-based-spec/strs (s/coll-of :nested-type-based-spec/str))
(deftest nested-type-based-spec
(is (=
(pf "-- Spec failed --------------------
[... ... 33]
^^
should satisfy
string?
-- Relevant specs -------
:nested-type-based-spec/str:
pf.core/string?
:nested-type-based-spec/strs:
(pf.spec.alpha/coll-of :nested-type-based-spec/str)
-------------------------
Detected 1 error\n")
(expound/expound-str :nested-type-based-spec/strs ["one" "two" 33]))))
(s/def :nested-type-based-spec-special-summary-string/int int?)
(s/def :nested-type-based-spec-special-summary-string/ints (s/coll-of :nested-type-based-spec-special-summary-string/int))
(deftest nested-type-based-spec-special-summary-string
(is (=
(pf "-- Spec failed --------------------
[... ... \"...\"]
^^^^^
should satisfy
int?
-- Relevant specs -------
:nested-type-based-spec-special-summary-string/int:
pf.core/int?
:nested-type-based-spec-special-summary-string/ints:
(pf.spec.alpha/coll-of
:nested-type-based-spec-special-summary-string/int)
-------------------------
Detected 1 error\n")
(expound/expound-str :nested-type-based-spec-special-summary-string/ints [1 2 "..."]))))
(s/def :or-spec/str-or-int (s/or :int int? :str string?))
(s/def :or-spec/vals (s/coll-of :or-spec/str-or-int))
(s/def :or-spec/str string?)
(s/def :or-spec/int int?)
(s/def :or-spec/m-with-str (s/keys :req [:or-spec/str]))
(s/def :or-spec/m-with-int (s/keys :req [:or-spec/int]))
(s/def :or-spec/m-with-str-or-int (s/or :m-with-str :or-spec/m-with-str
:m-with-int :or-spec/m-with-int))
(deftest or-spec
(testing "simple value"
(is (= (pf "-- Spec failed --------------------
:kw
should satisfy
int?
or
string?
-- Relevant specs -------
:or-spec/str-or-int:
(pf.spec.alpha/or :int pf.core/int? :str pf.core/string?)
-------------------------
Detected 1 error\n")
(expound/expound-str :or-spec/str-or-int :kw))))
(testing "collection of values"
(is (= (pf "-- Spec failed --------------------
[... ... :kw ...]
^^^
should satisfy
int?
or
string?
-- Relevant specs -------
:or-spec/str-or-int:
(pf.spec.alpha/or :int pf.core/int? :str pf.core/string?)
:or-spec/vals:
(pf.spec.alpha/coll-of :or-spec/str-or-int)
-------------------------
Detected 1 error\n")
(expound/expound-str :or-spec/vals [0 "hi" :kw "bye"]))))
(is (= "-- Spec failed --------------------
50
should satisfy
coll?
-------------------------
Detected 1 error
"
(expound/expound-str (s/or
:strs (s/coll-of string?)
:ints (s/coll-of int?))
50)))
(is (= "-- Spec failed --------------------
50
should be one of: \"a\", \"b\", 1, 2
-------------------------
Detected 1 error
"
(expound/expound-str
(s/or
:letters #{"a" "b"}
:ints #{1 2})
50)))
(is (= (pf "-- Spec failed --------------------
{}
should contain keys: :or-spec/int, :or-spec/str
| key | spec |
|==============+=========|
| :or-spec/int | int? |
|--------------+---------|
| :or-spec/str | string? |
-- Relevant specs -------
:or-spec/m-with-int:
(pf.spec.alpha/keys :req [:or-spec/int])
:or-spec/m-with-str:
(pf.spec.alpha/keys :req [:or-spec/str])
:or-spec/m-with-str-or-int:
(pf.spec.alpha/or
:m-with-str
:or-spec/m-with-str
:m-with-int
:or-spec/m-with-int)
-------------------------
Detected 1 error
")
(expound/expound-str :or-spec/m-with-str-or-int {})))
(testing "de-dupes keys"
(is (= "-- Spec failed --------------------
{}
should contain keys: :or-spec/str
| key | spec |
|==============+=========|
| :or-spec/str | string? |
-------------------------
Detected 1 error
"
(expound/expound-str (s/or :m-with-str1 (s/keys :req [:or-spec/str])
:m-with-int2 (s/keys :req [:or-spec/str])) {})))))
(s/def :and-spec/name (s/and string? #(pos? (count %))))
(s/def :and-spec/names (s/coll-of :and-spec/name))
(deftest and-spec
(testing "simple value"
(is (= (pf "-- Spec failed --------------------
\"\"
should satisfy
(fn [%%] (pos? (count %%)))
-- Relevant specs -------
:and-spec/name:
(pf.spec.alpha/and
pf.core/string?
(pf.core/fn [%%] (pf.core/pos? (pf.core/count %%))))
-------------------------
Detected 1 error\n")
(expound/expound-str :and-spec/name ""))))
(testing "shows both failures in order"
(is (=
(pf "-- Spec failed --------------------
[... ... \"\" ...]
^^
should satisfy
%s
-- Relevant specs -------
:and-spec/name:
(pf.spec.alpha/and
pf.core/string?
(pf.core/fn [%%] (pf.core/pos? (pf.core/count %%))))
:and-spec/names:
(pf.spec.alpha/coll-of :and-spec/name)
-- Spec failed --------------------
[... ... ... 1]
^
should satisfy
string?
-- Relevant specs -------
:and-spec/name:
(pf.spec.alpha/and
pf.core/string?
(pf.core/fn [%%] (pf.core/pos? (pf.core/count %%))))
:and-spec/names:
(pf.spec.alpha/coll-of :and-spec/name)
-------------------------
Detected 2 errors\n"
#?(:cljs "(fn [%] (pos? (count %)))"
:clj "(fn [%] (pos? (count %)))"))
(expound/expound-str :and-spec/names ["bob" "sally" "" 1])))))
(s/def :coll-of-spec/big-int-coll (s/coll-of int? :min-count 10))
(deftest coll-of-spec
(testing "min count"
(is (=
(pf "-- Spec failed --------------------
[]
should satisfy
(<= 10 (count %%) %s)
-- Relevant specs -------
:coll-of-spec/big-int-coll:
(pf.spec.alpha/coll-of pf.core/int? :min-count 10)
-------------------------
Detected 1 error\n"
#?(:cljs "9007199254740991"
:clj "Integer/MAX_VALUE"))
(expound/expound-str :coll-of-spec/big-int-coll [])))))
(s/def :cat-spec/kw (s/cat :k keyword? :v any?))
(s/def :cat-spec/set (s/cat :type #{:foo :bar} :str string?))
(s/def :cat-spec/alt* (s/alt :s string? :i int?))
(s/def :cat-spec/alt (s/+ :cat-spec/alt*))
(s/def :cat-spec/alt-inline (s/+ (s/alt :s string? :i int?)))
(s/def :cat-spec/any (s/cat :x (s/+ any?))) ;; Not a useful spec, but worth testing
(deftest cat-spec
(testing "too few elements"
(is (= (pf "-- Syntax error -------------------
[]
should have additional elements. The next element \":k\" should satisfy
keyword?
-- Relevant specs -------
:cat-spec/kw:
(pf.spec.alpha/cat :k pf.core/keyword? :v pf.core/any?)
-------------------------
Detected 1 error\n")
(expound/expound-str :cat-spec/kw [])))
(is (= (pf "-- Syntax error -------------------
[]
should have additional elements. The next element \":type\" should be one of: :bar, :foo
-- Relevant specs -------
:cat-spec/set:
(pf.spec.alpha/cat :type #{:bar :foo} :str pf.core/string?)
-------------------------
Detected 1 error\n")
(expound/expound-str :cat-spec/set [])))
(is (= (pf "-- Syntax error -------------------
[:foo]
should have additional elements. The next element \":v\" should satisfy
any?
-- Relevant specs -------
:cat-spec/kw:
(pf.spec.alpha/cat :k pf.core/keyword? :v pf.core/any?)
-------------------------
Detected 1 error\n")
(expound/expound-str :cat-spec/kw [:foo])))
;; This isn't ideal, but requires a fix from clojure
;; https://clojure.atlassian.net/browse/CLJ-2364
(is (= (pf "-- Syntax error -------------------
[]
should have additional elements. The next element should satisfy
(pf.spec.alpha/alt :s string? :i int?)
-- Relevant specs -------
:cat-spec/alt*:
(pf.spec.alpha/alt :s pf.core/string? :i pf.core/int?)
:cat-spec/alt:
(pf.spec.alpha/+ :cat-spec/alt*)
-------------------------
Detected 1 error\n")
(expound/expound-str :cat-spec/alt [])))
(is (= (pf "-- Syntax error -------------------
[]
should have additional elements. The next element should satisfy
(pf.spec.alpha/alt :s string? :i int?)
-- Relevant specs -------
:cat-spec/alt-inline:
(pf.spec.alpha/+
(pf.spec.alpha/alt :s pf.core/string? :i pf.core/int?))
-------------------------
Detected 1 error\n")
(expound/expound-str :cat-spec/alt-inline [])))
(is (= (pf "-- Syntax error -------------------
[]
should have additional elements. The next element \":x\" should satisfy
any?
-- Relevant specs -------
:cat-spec/any:
(pf.spec.alpha/cat :x (pf.spec.alpha/+ pf.core/any?))
-------------------------
Detected 1 error\n")
(expound/expound-str :cat-spec/any []))))
(testing "too many elements"
(is (= (pf "-- Syntax error -------------------
[... ... :bar ...]
^^^^
has extra input
-- Relevant specs -------
:cat-spec/kw:
(pf.spec.alpha/cat :k pf.core/keyword? :v pf.core/any?)
-------------------------
Detected 1 error\n")
(expound/expound-str :cat-spec/kw [:foo 1 :bar :baz])))))
(s/def :keys-spec/name string?)
(s/def :keys-spec/age int?)
(s/def :keys-spec/user (s/keys :req [:keys-spec/name]
:req-un [:keys-spec/age]))
(s/def :key-spec/state string?)
(s/def :key-spec/city string?)
(s/def :key-spec/zip pos-int?)
(s/def :keys-spec/user2 (s/keys :req [(and :keys-spec/name
:keys-spec/age)]
:req-un [(or
:key-spec/zip
(and
:key-spec/state
:key-spec/city))]))
(s/def :keys-spec/user3 (s/keys :req-un [(or
:key-spec/zip
(and
:key-spec/state
:key-spec/city))]))
(s/def :keys-spec/user4 (s/keys :req []))
(defmulti key-spec-mspec :tag)
(defmethod key-spec-mspec :int [_] (s/keys :req-un [::tag ::i]))
(defmethod key-spec-mspec :string [_] (s/keys :req-un [::tag ::s]))
(deftest keys-spec
(testing "missing keys"
(is (= (pf "-- Spec failed --------------------
{}
should contain keys: :age, :keys-spec/name
| key | spec |
|=================+=========|
| :age | int? |
|-----------------+---------|
| :keys-spec/name | string? |
-- Relevant specs -------
:keys-spec/user:
%s
-------------------------
Detected 1 error\n"
#?(:cljs "(cljs.spec.alpha/keys :req [:keys-spec/name] :req-un [:keys-spec/age])"
:clj "(clojure.spec.alpha/keys\n :req\n [:keys-spec/name]\n :req-un\n [:keys-spec/age])"))
(expound/expound-str :keys-spec/user {}))))
(testing "missing compound keys"
(is (= (pf "-- Spec failed --------------------
{}
should contain keys:
(and (and :keys-spec/name :keys-spec/age) (or :zip (and :state :city)))
| key | spec |
|=================+==========|
| :city | string? |
|-----------------+----------|
| :state | string? |
|-----------------+----------|
| :zip | pos-int? |
|-----------------+----------|
| :keys-spec/age | int? |
|-----------------+----------|
| :keys-spec/name | string? |
-- Relevant specs -------
:keys-spec/user2:
(pf.spec.alpha/keys
:req
[(and :keys-spec/name :keys-spec/age)]
:req-un
[(or :key-spec/zip (and :key-spec/state :key-spec/city))])
-------------------------
Detected 1 error\n")
(expound/expound-str :keys-spec/user2 {})))
(is (= (pf "-- Spec failed --------------------
{}
should contain keys:
(or :zip (and :state :city))
| key | spec |
|========+==========|
| :city | string? |
|--------+----------|
| :state | string? |
|--------+----------|
| :zip | pos-int? |
-- Relevant specs -------
:keys-spec/user3:
(pf.spec.alpha/keys
:req-un
[(or :key-spec/zip (and :key-spec/state :key-spec/city))])
-------------------------
Detected 1 error\n")
(expound/expound-str :keys-spec/user3 {}))))
(testing "inline spec with req-un"
(is (= (pf "-- Spec failed --------------------
{}
should contain keys: :age, :name
| key | spec |
|=======+=========|
| :age | int? |
|-------+---------|
| :name | string? |
-------------------------
Detected 1 error\n"
#?(:cljs "(cljs.spec.alpha/keys :req [:keys-spec/name] :req-un [:keys-spec/age])"
:clj "(clojure.spec.alpha/keys\n :req\n [:keys-spec/name]\n :req-un\n [:keys-spec/age])"))
(expound/expound-str (s/keys :req-un [:keys-spec/name :keys-spec/age]) {})))
(s/def :key-spec/mspec (s/multi-spec key-spec-mspec :tag))
(s/def :key-spec/i int?)
(s/def :key-spec/s string?)
;; We can't inspect the contents of a multi-spec (to figure out
;; which spec we mean by :i), so this is the best we can do.
(is (= "-- Spec failed --------------------
{:tag :int}
should contain key: :i
| key | spec |
|=====+===================================================|
| :i | <can't find spec for unqualified spec identifier> |
-------------------------
Detected 1 error\n"
(expound/expound-str
:key-spec/mspec
{:tag :int}
{:print-specs? false}))))
(testing "invalid key"
(is (= (pf "-- Spec failed --------------------
{:age ..., :keys-spec/name :bob}
^^^^
should satisfy
string?
-- Relevant specs -------
:keys-spec/name:
pf.core/string?
:keys-spec/user:
%s
-------------------------
Detected 1 error\n"
#?(:cljs "(cljs.spec.alpha/keys :req [:keys-spec/name] :req-un [:keys-spec/age])"
:clj "(clojure.spec.alpha/keys\n :req\n [:keys-spec/name]\n :req-un\n [:keys-spec/age])"))
(expound/expound-str :keys-spec/user {:age 1 :keys-spec/name :bob}))))
(testing "contains compound specs"
(s/def :keys-spec/states (s/coll-of :key-spec/state :kind vector?))
(s/def :keys-spec/address (s/keys :req [:key-spec/city :key-space/state]))
(s/def :keys-spec/cities (s/coll-of :key-spec/city :kind set?))
(s/def :keys-spec/locations (s/keys :req-un [:keys-spec/states
:keys-spec/address
:keys-spec/locations]))
(is (=
"-- Spec failed --------------------
{}
should contain keys: :address, :locations, :states
| key | spec |
|============+===============================================================|
| :address | (keys :req [:key-spec/city :key-space/state]) |
|------------+---------------------------------------------------------------|
| :locations | (keys |
| | :req-un |
| | [:keys-spec/states :keys-spec/address :keys-spec/locations]) |
|------------+---------------------------------------------------------------|
| :states | (coll-of :key-spec/state :kind vector?) |
-------------------------
Detected 1 error
"
(expound/expound-str :keys-spec/locations {} {:print-specs? false})))))
(s/def :keys-spec/foo string?)
(s/def :keys-spec/bar string?)
(s/def :keys-spec/baz string?)
(s/def :keys-spec/qux (s/or :string string?
:int int?))
(s/def :keys-spec/child-1 (s/keys :req-un [:keys-spec/baz :keys-spec/qux]))
(s/def :keys-spec/child-2 (s/keys :req-un [:keys-spec/bar :keys-spec/child-1]))
(s/def :keys-spec/map-spec-1 (s/keys :req-un [:keys-spec/foo
:keys-spec/bar
:keys-spec/baz]))
(s/def :keys-spec/map-spec-2 (s/keys :req-un [:keys-spec/foo
:keys-spec/bar
:keys-spec/qux]))
(s/def :keys-spec/map-spec-3 (s/keys :req-un [:keys-spec/foo
:keys-spec/child-2]))
(deftest grouping-and-key-specs
(is (= (pf
"-- Spec failed --------------------
{:foo 1.2, :bar ..., :baz ...}
^^^
should satisfy
string?
-- Spec failed --------------------
{:foo ..., :bar 123, :baz ...}
^^^
should satisfy
string?
-- Spec failed --------------------
{:foo ..., :bar ..., :baz true}
^^^^
should satisfy
string?
-------------------------
Detected 3 errors\n")
(expound/expound-str :keys-spec/map-spec-1 {:foo 1.2
:bar 123
:baz true}
{:print-specs? false})))
(is (= (pf
"-- Spec failed --------------------
{:foo 1.2, :bar ..., :qux ...}
^^^
should satisfy
string?
-- Spec failed --------------------
{:foo ..., :bar 123, :qux ...}
^^^
should satisfy
string?
-- Spec failed --------------------
{:foo ..., :bar ..., :qux false}
^^^^^
should satisfy
string?
or
int?
-------------------------
Detected 3 errors\n")
(expound/expound-str :keys-spec/map-spec-2 {:foo 1.2
:bar 123
:qux false}
{:print-specs? false})))
(is (=
"-- Spec failed --------------------
{:foo 1.2, :child-2 ...}
^^^
should satisfy
string?
-- Spec failed --------------------
{:foo ..., :child-2 {:bar 123, :child-1 ...}}
^^^
should satisfy
string?
-- Spec failed --------------------
{:foo ...,
:child-2
{:bar ..., :child-1 {:baz true, :qux ...}}}
^^^^
should satisfy
string?
-- Spec failed --------------------
{:foo ...,
:child-2
{:bar ..., :child-1 {:baz ..., :qux false}}}
^^^^^
should satisfy
string?
or
int?
-------------------------
Detected 4 errors\n"
(expound/expound-str :keys-spec/map-spec-3 {:foo 1.2
:child-2 {:bar 123
:child-1 {:baz true
:qux false}}}
{:print-specs? false}))))
(s/def :multi-spec/value string?)
(s/def :multi-spec/children vector?)
(defmulti el-type :multi-spec/el-type)
(defmethod el-type :text [_x]
(s/keys :req [:multi-spec/value]))
(defmethod el-type :group [_x]
(s/keys :req [:multi-spec/children]))
(s/def :multi-spec/el (s/multi-spec el-type :multi-spec/el-type))
(defmulti multi-spec-bar-spec :type)
(defmethod multi-spec-bar-spec ::b [_] (s/keys :req [::b]))
(deftest multi-spec
(testing "missing dispatch key"
(is (=
(pf "-- Missing spec -------------------
Cannot find spec for
{}
with
Spec multimethod: `expound.alpha-test/el-type`
Dispatch value: `nil`
-- Relevant specs -------
:multi-spec/el:
(pf.spec.alpha/multi-spec
expound.alpha-test/el-type
:multi-spec/el-type)
-------------------------
Detected 1 error\n")
(expound/expound-str :multi-spec/el {}))))
(testing "invalid dispatch value"
(is (=
(pf "-- Missing spec -------------------
Cannot find spec for
{:multi-spec/el-type :image}
with
Spec multimethod: `expound.alpha-test/el-type`
Dispatch value: `:image`
-- Relevant specs -------
:multi-spec/el:
(pf.spec.alpha/multi-spec
expound.alpha-test/el-type
:multi-spec/el-type)
-------------------------
Detected 1 error\n")
(expound/expound-str :multi-spec/el {:multi-spec/el-type :image}))))
(testing "valid dispatch value, but other error"
(is (=
(pf "-- Spec failed --------------------
{:multi-spec/el-type :text}
should contain key: :multi-spec/value
| key | spec |
|===================+=========|
| :multi-spec/value | string? |
-- Relevant specs -------
:multi-spec/el:
(pf.spec.alpha/multi-spec
expound.alpha-test/el-type
:multi-spec/el-type)
-------------------------
Detected 1 error\n")
(expound/expound-str :multi-spec/el {:multi-spec/el-type :text}))))
;; https://github.com/bhb/expound/issues/122
(testing "when re-tag is a function"
(s/def :multi-spec/b string?)
(s/def :multi-spec/bar (s/multi-spec multi-spec-bar-spec (fn [val tag] (assoc val :type tag))))
(is (= "-- Missing spec -------------------
Cannot find spec for
{}
with
Spec multimethod: `expound.alpha-test/multi-spec-bar-spec`
Dispatch value: `nil`
-------------------------
Detected 1 error
"
(expound/expound-str :multi-spec/bar {} {:print-specs? false})))))
(s/def :recursive-spec/tag #{:text :group})
(s/def :recursive-spec/on-tap (s/coll-of map? :kind vector?))
(s/def :recursive-spec/props (s/keys :opt-un [:recursive-spec/on-tap]))
(s/def :recursive-spec/el (s/keys :req-un [:recursive-spec/tag]
:opt-un [:recursive-spec/props :recursive-spec/children]))
(s/def :recursive-spec/children (s/coll-of (s/nilable :recursive-spec/el) :kind vector?))
(s/def :recursive-spec/tag-2 (s/or :text (fn [n] (= n :text))
:group (fn [n] (= n :group))))
(s/def :recursive-spec/on-tap-2 (s/coll-of map? :kind vector?))
(s/def :recursive-spec/props-2 (s/keys :opt-un [:recursive-spec/on-tap-2]))
(s/def :recursive-spec/el-2 (s/keys :req-un [:recursive-spec/tag-2]
:opt-un [:recursive-spec/props-2
:recursive-spec/children-2]))
(s/def :recursive-spec/children-2 (s/coll-of (s/nilable :recursive-spec/el-2) :kind vector?))
(deftest recursive-spec
(testing "only shows problem with data at 'leaves' (not problems with all parents in tree)"
(is (= (pf
"-- Spec failed --------------------
{:tag ..., :children [{:tag :group, :children [{:tag :group, :props {:on-tap {}}}]}]}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
should satisfy
nil?
or value
{:tag ...,
:children [{:tag ..., :children [{:tag :group, :props {:on-tap {}}}]}]}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
should satisfy
nil?
or value
{:tag ...,
:children
[{:tag ...,
:children
[{:tag ..., :props {:on-tap {}}}]}]}
^^
should satisfy
vector?
-------------------------
Detected 1 error\n")
(expound/expound-str
:recursive-spec/el
{:tag :group
:children [{:tag :group
:children [{:tag :group
:props {:on-tap {}}}]}]}
{:print-specs? false}))))
(testing "test that our new recursive spec grouping function works with
alternative paths"
(is (= (pf
"-- Spec failed --------------------
{:tag-2 ..., :children-2 [{:tag-2 :group, :children-2 [{:tag-2 :group, :props-2 {:on-tap-2 {}}}]}]}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
should satisfy
nil?
or value
{:tag-2 ...,
:children-2 [{:tag-2 ..., :children-2 [{:tag-2 :group, :props-2 {:on-tap-2 {}}}]}]}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
should satisfy
nil?
or value
{:tag-2 ...,
:children-2
[{:tag-2 ...,
:children-2
[{:tag-2 ..., :props-2 {:on-tap-2 {}}}]}]}
^^
should satisfy
vector?
-------------------------
Detected 1 error\n")
(expound/expound-str
:recursive-spec/el-2
{:tag-2 :group
:children-2 [{:tag-2 :group
:children-2 [{:tag-2 :group
:props-2 {:on-tap-2 {}}}]}]}
{:print-specs? false})))))
(s/def :cat-wrapped-in-or-spec/kv (s/and
sequential?
(s/cat :k keyword? :v any?)))
(s/def :cat-wrapped-in-or-spec/type #{:text})
(s/def :cat-wrapped-in-or-spec/kv-or-string (s/or
:map (s/keys :req [:cat-wrapped-in-or-spec/type])
:kv :cat-wrapped-in-or-spec/kv))
(deftest cat-wrapped-in-or-spec
(is (= (pf "-- Spec failed --------------------
{\"foo\" \"hi\"}
should contain key: :cat-wrapped-in-or-spec/type
| key | spec |
|==============================+==========|
| :cat-wrapped-in-or-spec/type | #{:text} |
or
should satisfy
sequential?
-- Relevant specs -------
:cat-wrapped-in-or-spec/kv:
(pf.spec.alpha/and
pf.core/sequential?
(pf.spec.alpha/cat :k pf.core/keyword? :v pf.core/any?))
:cat-wrapped-in-or-spec/kv-or-string:
(pf.spec.alpha/or
:map
(pf.spec.alpha/keys :req [:cat-wrapped-in-or-spec/type])
:kv
:cat-wrapped-in-or-spec/kv)
-------------------------
Detected 1 error\n")
(expound/expound-str :cat-wrapped-in-or-spec/kv-or-string {"foo" "hi"}))))
(s/def :map-of-spec/name string?)
(s/def :map-of-spec/age pos-int?)
(s/def :map-of-spec/name->age (s/map-of :map-of-spec/name :map-of-spec/age))
(deftest map-of-spec
(is (= (pf "-- Spec failed --------------------
{\"Sally\" \"30\"}
^^^^
should satisfy
pos-int?
-- Relevant specs -------
:map-of-spec/age:
pf.core/pos-int?
:map-of-spec/name->age:
(pf.spec.alpha/map-of :map-of-spec/name :map-of-spec/age)
-------------------------
Detected 1 error\n")
(expound/expound-str :map-of-spec/name->age {"Sally" "30"})))
(is (= (pf "-- Spec failed --------------------
{:sally ...}
^^^^^^
should satisfy
string?
-- Relevant specs -------
:map-of-spec/name:
pf.core/string?
:map-of-spec/name->age:
(pf.spec.alpha/map-of :map-of-spec/name :map-of-spec/age)
-------------------------
Detected 1 error\n")
(expound/expound-str :map-of-spec/name->age {:sally 30}))))
(deftest generated-simple-spec
(checking
"simple spec"
(chuck/times num-tests)
[simple-spec sg/simple-spec-gen
form gen/any-printable]
(is (string? (expound/expound-str simple-spec form)))))
(deftest generated-coll-of-specs
(checking
"'coll-of' spec"
(chuck/times num-tests)
[simple-spec sg/simple-spec-gen
every-args (s/gen :specs/every-args)
:let [spec (sg/apply-coll-of simple-spec every-args)]
form gen/any-printable]
(is (string? (expound/expound-str spec form)))))
(deftest generated-and-specs
(checking
"'and' spec"
(chuck/times num-tests)
[simple-spec1 sg/simple-spec-gen
simple-spec2 sg/simple-spec-gen
:let [spec (s/and simple-spec1 simple-spec2)]
form gen/any-printable]
(is (string? (expound/expound-str spec form)))))
(deftest generated-or-specs
(checking
"'or' spec generates string"
(chuck/times num-tests)
[simple-spec1 sg/simple-spec-gen
simple-spec2 sg/simple-spec-gen
:let [spec (s/or :or1 simple-spec1 :or2 simple-spec2)]
form gen/any-printable]
(is (string? (expound/expound-str spec form))))
(checking
"nested 'or' spec reports on all problems"
(chuck/times num-tests)
[simple-specs (gen/vector-distinct
(gen/elements [:specs/string
:specs/vector
:specs/int
:specs/boolean
:specs/keyword
:specs/map
:specs/symbol
:specs/pos-int
:specs/neg-int
:specs/zero])
{:num-elements 4})
:let [[simple-spec1
simple-spec2
simple-spec3
simple-spec4] simple-specs
spec (s/or :or1
(s/or :or1.1
simple-spec1
:or1.2
simple-spec2)
:or2
(s/or :or2.1
simple-spec3
:or2.2
simple-spec4))
sp-form (s/form spec)]
form gen/any-printable]
(let [ed (s/explain-data spec form)]
(when-not (zero? (count (::s/problems ed)))
(is (= (dec (count (::s/problems ed)))
(count (re-seq #"\nor\n" (expound/expound-str spec form))))
(str "Failed to print out all problems\nspec: " sp-form "\nproblems: " (printer/pprint-str (::s/problems ed)) "\nmessage: " (expound/expound-str spec form)))))))
(deftest generated-map-of-specs
(checking
"'map-of' spec"
(chuck/times num-tests)
[simple-spec1 sg/simple-spec-gen
simple-spec2 sg/simple-spec-gen
simple-spec3 sg/simple-spec-gen
every-args1 (s/gen :specs/every-args)
every-args2 (s/gen :specs/every-args)
:let [spec (sg/apply-map-of simple-spec1 (sg/apply-map-of simple-spec2 simple-spec3 every-args1) every-args2)]
form test-utils/any-printable-wo-nan]
(is (string? (expound/expound-str spec form)))))
(s/def :expound.ds/spec-key (s/or :kw keyword?
:req (s/tuple
#{:expound.ds/req-key}
(s/map-of
#{:k}
keyword?
:count 1))
:opt (s/tuple
#{:expound.ds/opt-key}
(s/map-of
#{:k}
keyword?
:count 1))))
(defn real-spec [form]
(walk/prewalk
(fn [x]
(if (vector? x)
(case (first x)
:expound.ds/opt-key
(ds/map->OptionalKey (second x))
:expound.ds/req-key
(ds/map->RequiredKey (second x))
:expound.ds/maybe-spec
(ds/maybe (second x))
x)
x))
form))
(s/def :expound.ds/maybe-spec
(s/tuple
#{:expound.ds/maybe-spec}
:expound.ds/spec))
(s/def :expound.ds/simple-specs
#{string?
vector?
int?
boolean?
keyword?
map?
symbol?
pos-int?
neg-int?
nat-int?})
(s/def :expound.ds/vector-spec (s/coll-of
:expound.ds/spec
:count 1
:kind vector?))
(s/def :expound.ds/set-spec (s/coll-of
:expound.ds/spec
:count 1
:kind set?))
(s/def :expound.ds/map-spec
(s/map-of :expound.ds/spec-key
:expound.ds/spec))
(s/def :expound.ds/spec
(s/or
:map :expound.ds/map-spec
:vector :expound.ds/vector-spec
:set :expound.ds/set-spec
:simple :expound.ds/simple-specs
:maybe :expound.ds/maybe-spec))
(deftest generated-data-specs
(checking
"generated data specs"
(chuck/times num-tests)
[data-spec (s/gen :expound.ds/spec)
form test-utils/any-printable-wo-nan
prefix (s/gen qualified-keyword?)
:let [gen-spec (ds/spec prefix (real-spec data-spec))]]
(is (string? (expound/expound-str gen-spec form)))))
;; FIXME - keys
;; FIXME - cat + alt, + ? *
;; FIXME - nilable
;; FIXME - test coll-of that is a set . can i should a bad element of a set?
(s/def :test-assert/name string?)
(deftest test-assert
(testing "assertion passes"
(is (= "hello"
(s/assert :test-assert/name "hello"))))
(testing "assertion fails"
#?(:cljs
(try
(binding [s/*explain-out* expound/printer]
(s/assert :test-assert/name :hello))
(catch :default e
(is (= "Spec assertion failed\n-- Spec failed --------------------
:hello
should satisfy
string?
-- Relevant specs -------
:test-assert/name:
cljs.core/string?
-------------------------
Detected 1 error\n"
(.-message e)))))
:clj
(try
(binding [s/*explain-out* expound/printer]
(s/assert :test-assert/name :hello))
(catch Exception e
(is (= "Spec assertion failed
-- Spec failed --------------------
:hello
should satisfy
string?
-- Relevant specs -------
:test-assert/name:
clojure.core/string?
-------------------------
Detected 1 error\n"
;; FIXME - move assertion out of catch, similar to instrument tests
(:cause (Throwable->map e)))))))))
(s/def :test-explain-str/name string?)
(deftest test-explain-str
(is (= (pf "-- Spec failed --------------------
:hello
should satisfy
string?
-- Relevant specs -------
:test-explain-str/name:
pf.core/string?
-------------------------
Detected 1 error\n")
(binding [s/*explain-out* expound/printer]
(s/explain-str :test-explain-str/name :hello)))))
(s/fdef test-instrument-adder
:args (s/cat :x int? :y int?)
:fn #(> (:ret %) (-> % :args :x))
:ret pos-int?)
(defn test-instrument-adder [& args]
(let [[x y] args]
(+ x y)))
(defn no-linum [s]
(string/replace s #"(.cljc?):\d+" "$1:LINUM"))
(deftest test-instrument
(st/instrument `test-instrument-adder)
#?(:cljs (is (=
"Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
<filename missing>:<line number missing>
-- Spec failed --------------------
Function arguments
(\"\" ...)
^^
should satisfy
int?
-------------------------
Detected 1 error\n"
(formatted-exception {:print-specs? false} #(test-instrument-adder "" :x))))
:clj
(is (= "Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
alpha_test.cljc:LINUM
-- Spec failed --------------------
Function arguments
(\"\" ...)
^^
should satisfy
int?
-------------------------
Detected 1 error\n"
(no-linum
(formatted-exception {:print-specs? false :show-valid-values? false} #(test-instrument-adder "" :x))))))
(st/unstrument `test-instrument-adder))
(deftest test-instrument-with-orchestra-args-spec-failure
(orch.st/instrument `test-instrument-adder)
#?(:cljs (is (=
"Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
<filename missing>:<line number missing>
-- Spec failed --------------------
Function arguments
(\"\" ...)
^^
should satisfy
int?
-------------------------
Detected 1 error\n"
(no-linum (formatted-exception {:print-specs? false} #(test-instrument-adder "" :x)))))
:clj (is (= "Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
alpha_test.cljc:LINUM
-- Spec failed --------------------
Function arguments
(\"\" ...)
^^
should satisfy
int?
-------------------------
Detected 1 error\n"
(no-linum
(formatted-exception
{}
#(test-instrument-adder "" :x))))))
(orch.st/unstrument `test-instrument-adder))
;; Note - you may need to comment out this test out when
;; using figwheel.main for testing, since the compilation
;; warning seems to impact the building of other tests
(deftest test-instrument-with-orchestra-args-syntax-failure
(orch.st/instrument `test-instrument-adder)
#?(:cljs (is (=
"Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
<filename missing>:<line number missing>
-- Syntax error -------------------
Function arguments
(1)
should have additional elements. The next element \":y\" should satisfy
int?
-------------------------
Detected 1 error\n"
(no-linum (formatted-exception {:print-specs? false} #(test-instrument-adder 1)))))
:clj
(is (= "Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
alpha_test.cljc:LINUM
-- Syntax error -------------------
Function arguments
(1)
should have additional elements. The next element \":y\" should satisfy
int?
-------------------------
Detected 1 error\n"
(no-linum
(formatted-exception
{:print-specs? false}
#(test-instrument-adder 1))))))
(orch.st/unstrument `test-instrument-adder))
(deftest test-instrument-with-orchestra-ret-failure
(orch.st/instrument `test-instrument-adder)
#?(:cljs (is (=
"Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
<filename missing>:<line number missing>
-- Spec failed --------------------
Return value
-3
should satisfy
pos-int?
-------------------------
Detected 1 error\n"
(formatted-exception {}
#(test-instrument-adder -1 -2))
#_(.-message (try
(binding [s/*explain-out* expound/printer]
(test-instrument-adder -1 -2))
(catch :default e e)))))
:clj
(is (= "Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
alpha_test.cljc:LINUM
-- Spec failed --------------------
Return value
-3
should satisfy
pos-int?
-------------------------
Detected 1 error\n"
(no-linum
(formatted-exception
{:print-specs? false}
#(test-instrument-adder -1 -2))))))
(orch.st/unstrument `test-instrument-adder))
(deftest test-instrument-with-orchestra-fn-failure
(orch.st/instrument `test-instrument-adder)
#?(:cljs (is (=
"Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
<filename missing>:<line number missing>
-- Spec failed --------------------
Function arguments and return value
{:ret 1, :args {:x 1, :y 0}}
should satisfy
(fn [%] (> (:ret %) (-> % :args :x)))
-------------------------
Detected 1 error\n"
(formatted-exception {} #(test-instrument-adder 1 0))))
:clj
(is (= "Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
alpha_test.cljc:LINUM
-- Spec failed --------------------
Function arguments and return value
{:ret 1, :args {:x 1, :y 0}}
should satisfy
(fn
[%]
(> (:ret %) (-> % :args :x)))
-------------------------
Detected 1 error\n"
(no-linum
(formatted-exception {:print-specs? false} #(test-instrument-adder 1 0))))))
(orch.st/unstrument `test-instrument-adder))
(deftest test-instrument-with-custom-value-printer
(st/instrument `test-instrument-adder)
#?(:cljs
(is (=
"Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
<filename missing>:<line number missing>
-- Spec failed --------------------
Function arguments
(\"\" :x)
^^
should satisfy
int?
-------------------------
Detected 1 error\n"
(no-linum
(formatted-exception {:print-specs? false :show-valid-values? true} #(test-instrument-adder "" :x)))))
:clj
(is (=
"Call to #'expound.alpha-test/test-instrument-adder did not conform to spec.
alpha_test.cljc:LINUM
-- Spec failed --------------------
Function arguments
(\"\" :x)
^^
should satisfy
int?
-------------------------
Detected 1 error\n"
(no-linum
(formatted-exception {:print-specs? false :show-valid-values? true} #(test-instrument-adder "" :x))))))
(st/unstrument `test-instrument-adder))
(s/def :custom-printer/strings (s/coll-of string?))
(deftest custom-printer
(testing "custom value printer"
(is (= (pf "-- Spec failed --------------------
<HIDDEN>
should satisfy
string?
-- Relevant specs -------
:custom-printer/strings:
(pf.spec.alpha/coll-of pf.core/string?)
-------------------------
Detected 1 error
")
(binding [s/*explain-out* (expound/custom-printer {:value-str-fn (fn [_spec-name _form _path _val] " <HIDDEN>")})]
(s/explain-str :custom-printer/strings ["a" "b" :c]))))))
(s/def :alt-spec/int-alt-str (s/alt :int int? :string string?))
(s/def :alt-spec/num-types (s/alt :int int? :float float?))
(s/def :alt-spec/str-types (s/alt :int (fn [n] (= n "int"))
:float (fn [n] (= n "float"))))
(s/def :alt-spec/num-or-str (s/alt :num :alt-spec/num-types
:str :alt-spec/str-types))
(s/def :alt-spec/i int?)
(s/def :alt-spec/s string?)
(s/def :alt-spec/alt-or-map (s/or :i :alt-spec/i
:s :alt-spec/s
:k (s/keys :req-un [:alt-spec/i :alt-spec/s])))
(defmulti alt-spec-mspec :tag)
(s/def :alt-spec/mspec (s/multi-spec alt-spec-mspec :tag))
(defmethod alt-spec-mspec :x [_] (s/keys :req-un [:alt-spec/one-many-int]))
(deftest alt-spec
(testing "alternatives at different paths in spec"
(is (=
"-- Spec failed --------------------
[\"foo\"]
should satisfy
int?
or value
[\"foo\"]
^^^^^
should satisfy
int?
-------------------------
Detected 1 error\n"
(expound/expound-str
(s/or
:i int?
:seq (s/cat :x1 int? :x2 int?))
["foo"]
{:print-specs? false})))
(s/def :alt-spec/one-many-int (s/cat :bs (s/alt :one int?
:many (s/spec (s/+ int?)))))
(is (= (pf "-- Spec failed --------------------
[[\"1\"]]
^^^^^
should satisfy
int?
or value
[[\"1\"]]
^^^
should satisfy
int?
-- Relevant specs -------
:alt-spec/one-many-int:
(pf.spec.alpha/cat
:bs
(pf.spec.alpha/alt
:one
pf.core/int?
:many
(pf.spec.alpha/spec (pf.spec.alpha/+ pf.core/int?))))
-------------------------
Detected 1 error\n")
(binding [s/*explain-out* (expound/custom-printer {})]
(s/explain-str
:alt-spec/one-many-int
[["1"]]))))
(s/def :alt-spec/one-many-int-or-str (s/cat :bs (s/alt :one :alt-spec/int-alt-str
:many (s/spec (s/+ :alt-spec/int-alt-str)))))
(is (= "-- Spec failed --------------------
[[:one]]
^^^^^^
should satisfy
int?
or
string?
or value
[[:one]]
^^^^
should satisfy
int?
or
string?
-------------------------
Detected 1 error\n"
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str
:alt-spec/one-many-int-or-str
[[:one]]))))
(s/def :alt-spec/int-or-str (s/or :i int?
:s string?))
(s/def :alt-spec/one-many-int-or-str (s/cat :bs (s/alt :one :alt-spec/int-or-str
:many (s/spec (s/+ :alt-spec/int-or-str)))))
(is (= "-- Spec failed --------------------
[[:one]]
^^^^^^
should satisfy
int?
or
string?
or value
[[:one]]
^^^^
should satisfy
int?
or
string?
-------------------------
Detected 1 error\n"
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str
:alt-spec/one-many-int-or-str
[[:one]])))))
(is (= (pf "-- Spec failed --------------------
[:hi]
^^^
should satisfy
int?
or
string?
-- Relevant specs -------
:alt-spec/int-alt-str:
%s
-------------------------
Detected 1 error\n"
#?(:clj "(clojure.spec.alpha/alt
:int
clojure.core/int?
:string
clojure.core/string?)"
:cljs "(cljs.spec.alpha/alt :int cljs.core/int? :string cljs.core/string?)"))
(expound/expound-str :alt-spec/int-alt-str [:hi])))
(is (= "-- Spec failed --------------------
{:i \"\", :s 1}
should satisfy
int?
or
string?
-- Spec failed --------------------
{:i \"\", :s ...}
^^
should satisfy
int?
-- Spec failed --------------------
{:i ..., :s 1}
^
should satisfy
string?
-------------------------
Detected 3 errors
"
(expound/expound-str
:alt-spec/alt-or-map
{:i "" :s 1}
{:print-specs? false})))
(is (= "-- Spec failed --------------------
[true]
^^^^
should satisfy
int?
or
float?
or
(fn [n] (= n \"int\"))
or
(fn [n] (= n \"float\"))
-------------------------
Detected 1 error\n" (expound/expound-str :alt-spec/num-or-str [true] {:print-specs? false})))
;; If two s/alt specs have the same tags, we shouldn't confuse them.
(is (= "-- Spec failed --------------------
{:num-types [true], :str-types ...}
^^^^
should satisfy
int?
or
float?
-- Spec failed --------------------
{:num-types ..., :str-types [false]}
^^^^^
should satisfy
(fn [n] (= n \"int\"))
or
(fn [n] (= n \"float\"))
-------------------------
Detected 2 errors\n"
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str (s/keys :req-un [:alt-spec/num-types :alt-spec/str-types])
{:num-types [true] :str-types [false]}))))
(is (=
"-- Spec failed --------------------
[\"\"]
should satisfy
nil?
or value
[\"\"]
^^
should satisfy
int?
or
float?
-------------------------
Detected 1 error
"
(expound/expound-str
(s/nilable (s/cat :n (s/alt :int int? :float float?)))
[""]
{:print-specs? false})))
(is (=
;; This output is not what we want: ideally, the two alternates
;; should be grouped into a single problem.
;; I'm adding it as a spec to avoid regressions and to keep it as
;; an example of something I could improve.
;; The reason we can't do better is that we can't reliably look
;; at the form of a multi-spec. It would be nice if spec inserted
;; the actual spec form that was returned by the multi-spec, but
;; as it stands today, we'd have to figure out how to call the multi-
;; method with the actual value. That would be complicated and
;; potentially have unknown side effects from running arbitrary code.
"-- Spec failed --------------------
{:mspec {:tag ..., :one-many-int [[\"1\"]]}}
^^^^^
should satisfy
int?
-- Spec failed --------------------
{:mspec {:tag ..., :one-many-int [[\"1\"]]}}
^^^
should satisfy
int?
-------------------------
Detected 2 errors\n"
(expound/expound-str
(s/keys
:req-un [:alt-spec/mspec])
{:mspec
{:tag :x
:one-many-int [["1"]]}}
{:print-specs? false}))))
(defn mutate-coll [x]
(cond
(map? x)
(into [] x)
(vector? x)
(into #{} x)
(set? x)
(reverse (into '() x))
(list? x)
(into {} (map vec (partition 2 x)))
:else
x))
(defn mutate-type [x]
(cond
(number? x)
(str x)
(string? x)
(keyword x)
(keyword? x)
(str x)
(boolean? x)
(str x)
(symbol? x)
(str x)
(char? x)
#?(:cljs (.charCodeAt x)
:clj (int x))
(uuid? x)
(str x)
:else
x))
(defn mutate [form path]
(let [[head & rst] path]
(cond
(empty? path)
(if (coll? form)
(mutate-coll form)
(mutate-type form))
(map? form)
(if (empty? form)
(mutate-coll form)
(let [k (nth (keys form) (mod head (count (keys form))))]
(assoc form k
(mutate (get form k) rst))))
(vector? form)
(if (empty? form)
(mutate-coll form)
(let [idx (mod head (count form))]
(assoc form idx
(mutate (nth form idx) rst))))
(not (coll? form))
(mutate-type form)
:else
(mutate-coll form))))
(deftest test-assert2
(is (thrown-with-msg?
#?(:cljs :default :clj Exception)
#"\"Key must be integer\"\n\nshould be one of: \"Extra input\", \"Insufficient input\", \"no method"
(binding [s/*explain-out* expound/printer]
(try
(s/check-asserts true)
(s/assert (s/nilable #{"Insufficient input" "Extra input" "no method"}) "Key must be integer")
(finally (s/check-asserts false)))))))
(defn inline-specs [keyword]
(walk/postwalk
(fn [x]
(if (contains? (s/registry) x)
(s/form x)
x))
(s/form keyword)))
#?(:clj
(deftest real-spec-tests
(checking
"for any real-world spec and any data, explain-str returns a string"
;; At 50, it might find a bug in failures for the
;; :ring/handler spec, but keep it plugged in, since it
;; takes a long time to shrink
(chuck/times num-tests)
[spec sg/spec-gen
form gen/any-printable]
;; Can't reliably test fspecs until
;; https://dev.clojure.org/jira/browse/CLJ-2258 is fixed
;; because the algorithm to fix up the 'in' paths depends
;; on the non-conforming value existing somewhere within
;; the top-level form
(when-not
;; a conformer generally won't work against any arbitrary value
;; e.g. we can't conform 0 with the conformer 'seq'
(or (contains? #{:conformers-test/string-AB} spec)
(some
#{"clojure.spec.alpha/fspec"}
(->> spec
inline-specs
(tree-seq coll? identity)
(map str))))
(is (string? (expound/expound-str spec form)))))))
#?(:clj
(deftest assert-on-real-spec-tests
(checking
"for any real-world spec and any data, assert returns an error that matches explain-str"
(chuck/times num-tests)
[spec sg/spec-gen
form gen/any-printable]
;; Can't reliably test fspecs until
;; https://dev.clojure.org/jira/browse/CLJ-2258 is fixed
;; because the algorithm to fix up the 'in' paths depends
;; on the non-conforming value existing somewhere within
;; the top-level form
(when-not (some
#{"clojure.spec.alpha/fspec"}
(->> spec
inline-specs
(tree-seq coll? identity)
(map str)))
(when-not (s/valid? spec form)
(let [expected-err-msg (str "Spec assertion failed\n"
(binding [s/*explain-out* (expound/custom-printer {:print-specs? true})]
(s/explain-str spec form)))]
(is (thrown-with-msg?
#?(:cljs :default :clj Exception)
(re-pattern (java.util.regex.Pattern/quote expected-err-msg))
(binding [s/*explain-out* expound/printer]
(try
(s/check-asserts true)
(s/assert spec form)
(finally
(s/check-asserts false)))))
(str "Expected: " expected-err-msg))))))))
(deftest test-mutate
(checking
"mutation alters data structure"
(chuck/times num-tests)
[form gen/any-printable
mutate-path (gen/vector gen/nat 1 10)]
(is (not= form
(mutate form mutate-path)))))
#?(:clj
1
#_(deftest real-spec-tests-mutated-valid-value
;; FIXME - we need to use generate mutated value, instead
;; of adding randomness to test
#_(checking
"for any real-world spec and any mutated valid data, explain-str returns a string"
(chuck/times num-tests)
[spec sg/spec-gen
mutate-path (gen/vector gen/pos-int)]
(when-not (some
#{"clojure.spec.alpha/fspec"}
(->> spec
inline-specs
(tree-seq coll? identity)
(map str)))
(when (contains? (s/registry) spec)
(try
(let [valid-form (first (s/exercise spec 1))
invalid-form (mutate valid-form mutate-path)]
(is (string? (expound/expound-str spec invalid-form))))
(catch clojure.lang.ExceptionInfo e
(when (not= :no-gen (::s/failure (ex-data e)))
(when (not= "Couldn't satisfy such-that predicate after 100 tries." (.getMessage e))
(throw e))))))))))
;; Using conformers for transformation should not crash by default, or at least give useful error message.
(defn numberify [s]
(cond
(number? s) s
(re-matches #"^\d+$" s) #?(:cljs (js/parseInt s 10)
:clj (Integer. s))
:else ::s/invalid))
(s/def :conformers-test/number (s/conformer numberify))
(defn conform-by
[tl-key payload-key]
(s/conformer (fn [m]
(let [id (get m tl-key)]
(if (and id (map? (get m payload-key)))
(assoc-in m [payload-key tl-key] id)
::s/invalid)))))
(s/def :conformers-test.query/id qualified-keyword?)
(defmulti query-params :conformers-test.query/id)
(s/def :conformers-test.query/params (s/multi-spec query-params :conformers-test.query/id))
(s/def :user/id string?)
(defmethod query-params :conformers-test/lookup-user [_]
(s/keys :req [:user/id]))
(s/def :conformers-test/query
(s/and
(conform-by :conformers-test.query/id :conformers-test.query/params)
(s/keys :req [:conformers-test.query/id
:conformers-test.query/params])))
(s/def :conformers-test/string-AB-seq (s/cat :a #{\A} :b #{\B}))
(s/def :conformers-test/string-AB
(s/and
;; conform as sequence (seq function)
(s/conformer #(if (seqable? %) (seq %) %))
;; re-use previous sequence spec
:conformers-test/string-AB-seq))
(defn parse-csv [s]
(map string/upper-case (string/split s #",")))
(deftest conformers-test
;; Example from http://cjohansen.no/a-unified-specification/
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})
*print-namespace-maps* false]
(testing "conform string to int"
(is (string?
(s/explain-str :conformers-test/number "123a"))))
;; Example from https://github.com/bhb/expound/issues/15#issuecomment-326838879
(testing "conform maps"
(is (string? (s/explain-str :conformers-test/query {})))
(is (= "-- Spec failed --------------------
Part of the value
{:conformers-test.query/id :conformers-test/lookup-user, :conformers-test.query/params {}}
when conformed as
{:conformers-test.query/id :conformers-test/lookup-user}
should contain key: :user/id
| key | spec |
|==========+=========|
| :user/id | string? |
-------------------------
Detected 1 error\n"
(s/explain-str :conformers-test/query {:conformers-test.query/id :conformers-test/lookup-user
:conformers-test.query/params {}}))))
;; Minified example based on https://github.com/bhb/expound/issues/15
;; This doesn't look ideal, but really, it's not a good idea to use spec
;; for string parsing, so I'm OK with it
(testing "conform string to seq"
(is (=
;; clojurescript doesn't have a character type
#?(:cljs "-- Spec failed --------------------\n\n \"A\"C\"\"\n ^^^\n\nshould be: \"B\"\n\n-------------------------\nDetected 1 error\n"
:clj "-- Spec failed --------------------
\"A\\C\"
^^
should be: \\B
-------------------------
Detected 1 error
")
(s/explain-str :conformers-test/string-AB "AC"))))
(testing "s/cat"
(s/def :conformers-test/sorted-pair (s/and (s/cat :x int? :y int?) #(< (-> % :x) (-> % :y))))
(is (= (pf "-- Spec failed --------------------
[1 0]
when conformed as
{:x 1, :y 0}
should satisfy
%s
-------------------------
Detected 1 error
"
#?(:cljs "(fn [%] (< (-> % :x) (-> % :y)))"
:clj "(fn
[%]
(< (-> % :x) (-> % :y)))"))
(expound/expound-str :conformers-test/sorted-pair [1 0] {:print-specs? false})))
(is (= (pf "-- Spec failed --------------------
[... [1 0]]
^^^^^
when conformed as
{:x 1, :y 0}
should satisfy
%s
-------------------------
Detected 1 error\n"
#?(:cljs "(fn [%] (< (-> % :x) (-> % :y)))"
:clj "(fn
[%]
(< (-> % :x) (-> % :y)))"))
(expound/expound-str (s/coll-of :conformers-test/sorted-pair) [[0 1] [1 0]] {:print-specs? false})))
(is (= (pf "-- Spec failed --------------------
{:a [1 0]}
^^^^^
when conformed as
{:x 1, :y 0}
should satisfy
%s
-------------------------
Detected 1 error\n"
#?(:cljs "(fn [%] (< (-> % :x) (-> % :y)))"
:clj "(fn
[%]
(< (-> % :x) (-> % :y)))"))
(expound/expound-str (s/map-of keyword? :conformers-test/sorted-pair) {:a [1 0]} {:print-specs? false})))
(is (= (pf "-- Spec failed --------------------
[... \"a\"]
^^^
should satisfy
int?
-------------------------
Detected 1 error\n")
(expound/expound-str :conformers-test/sorted-pair [1 "a"] {:print-specs? false}))))
(testing "conformers that modify path of values"
(s/def :conformers-test/vals (s/coll-of (s/and string?
#(re-matches #"[A-G]+" %))))
(s/def :conformers-test/csv (s/and string?
(s/conformer parse-csv)
:conformers-test/vals))
(is (= "-- Spec failed --------------------
Part of the value
\"abc,def,ghi\"
when conformed as
\"GHI\"
should satisfy
(fn [%] (re-matches #\"[A-G]+\" %))
-------------------------
Detected 1 error\n"
(expound/expound-str :conformers-test/csv "abc,def,ghi" {:print-specs? false}))))
;; this is NOT recommended!
;; so I'm not inclined to make this much nicer than
;; the default spec output
(s/def :conformers-test/coerced-kw (s/and (s/conformer #(if (string? %)
(keyword %)
::s/invalid))
keyword?))
(testing "coercion"
(is (= (pf "-- Spec failed --------------------
nil
should satisfy
(pf.spec.alpha/conformer
(fn
[%%]
(if
(string? %%)
(keyword %%)
:pf.spec.alpha/invalid)))
-------------------------
Detected 1 error
")
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str :conformers-test/coerced-kw nil))))
(is (= (pf "-- Spec failed --------------------
[... ... ... 0]
^
should satisfy
(pf.spec.alpha/conformer
(fn
[%%]
(if
(string? %%)
(keyword %%)
:pf.spec.alpha/invalid)))
-------------------------
Detected 1 error
")
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str (s/coll-of :conformers-test/coerced-kw) ["a" "b" "c" 0])))))
;; Also not recommended
(s/def :conformers-test/str-kw? (s/and (s/conformer #(if (string? %)
(keyword %)
::s/invalid)
name) keyword?))
(testing "coercion with unformer"
(is (= (pf "-- Spec failed --------------------
nil
should satisfy
(pf.spec.alpha/conformer
(fn
[%%]
(if
(string? %%)
(keyword %%)
:pf.spec.alpha/invalid)))
-------------------------
Detected 1 error
")
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str :conformers-test/coerced-kw nil))))
(is (= (pf "-- Spec failed --------------------
[... ... ... 0]
^
should satisfy
(pf.spec.alpha/conformer
(fn
[%%]
(if
(string? %%)
(keyword %%)
:pf.spec.alpha/invalid)))
-------------------------
Detected 1 error
")
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str (s/coll-of :conformers-test/coerced-kw) ["a" "b" "c" 0])))))
(s/def :conformers-test/name string?)
(s/def :conformers-test/age pos-int?)
(s/def :conformers-test/person (s/keys* :req-un [:conformers-test/name
:conformers-test/age]))
;; FIXME: Implementation could be simpler once
;; https://dev.clojure.org/jira/browse/CLJ-2406 is fixed
(testing "spec defined with keys*"
(is (= "-- Spec failed --------------------
[... ... ... :Stan]
^^^^^
should satisfy
string?
-------------------------
Detected 1 error
"
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str :conformers-test/person [:age 30 :name :Stan])))))
(testing "spec defined with keys* and copies of bad value elsewhere in the data"
(is (= "-- Spec failed --------------------
Part of the value
[:Stan [:age 30 :name :Stan]]
when conformed as
:Stan
should satisfy
string?
-------------------------
Detected 1 error\n"
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str (s/tuple
keyword?
:conformers-test/person) [:Stan [:age 30 :name :Stan]])))))
(testing "ambiguous value"
(is (= (pf "-- Spec failed --------------------
{[0 1] ..., [1 0] ...}
^^^^^
when conformed as
{:x 1, :y 0}
should satisfy
%s
-------------------------
Detected 1 error
"
#?(:cljs "(fn [%] (< (-> % :x) (-> % :y)))"
:clj "(fn
[%]
(< (-> % :x) (-> % :y)))"))
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(s/explain-str (s/map-of :conformers-test/sorted-pair any?) {[0 1] [1 0]
[1 0] [1 0]})))))))
(s/def :duplicate-preds/str-or-str (s/or
;; Use anonymous functions to assure
;; non-equality
:str1 #(string? %)
:str2 #(string? %)))
(deftest duplicate-preds
(testing "duplicate preds only appear once"
(is (= (pf "-- Spec failed --------------------
1
should satisfy
(fn [%%] (string? %%))
-- Relevant specs -------
:duplicate-preds/str-or-str:
(pf.spec.alpha/or
:str1
(pf.core/fn [%%] (pf.core/string? %%))
:str2
(pf.core/fn [%%] (pf.core/string? %%)))
-------------------------
Detected 1 error
")
(expound/expound-str :duplicate-preds/str-or-str 1)))))
(s/def :fspec-test/div (s/fspec
:args (s/cat :x int? :y pos-int?)))
(defn my-div [x y]
(assert (not (zero? (/ x y)))))
(defn until-unsuccessful [f]
(let [nil-or-failure #(if (= "Success!
" %)
nil
%)]
(or (nil-or-failure (f))
(nil-or-failure (f))
(nil-or-failure (f))
(nil-or-failure (f))
(nil-or-failure (f)))))
(deftest fspec-exception-test
(testing "args that throw exception"
(is (= (pf "-- Exception ----------------------
expound.alpha-test/my-div
threw exception
\"Assert failed: (not (zero? (/ x y)))\"
with args:
0, 1
-- Relevant specs -------
:fspec-test/div:
(pf.spec.alpha/fspec
:args
(pf.spec.alpha/cat :x pf.core/int? :y pf.core/pos-int?)
:ret
pf.core/any?
:fn
nil)
-------------------------
Detected 1 error\n")
;;
(until-unsuccessful #(expound/expound-str :fspec-test/div my-div))))
(is (= (pf "-- Exception ----------------------
[expound.alpha-test/my-div]
^^^^^^^^^^^^^^^^^^^^^^^^^
threw exception
\"Assert failed: (not (zero? (/ x y)))\"
with args:
0, 1
-- Relevant specs -------
:fspec-test/div:
(pf.spec.alpha/fspec
:args
(pf.spec.alpha/cat :x pf.core/int? :y pf.core/pos-int?)
:ret
pf.core/any?
:fn
nil)
-------------------------
Detected 1 error\n")
(until-unsuccessful #(expound/expound-str (s/coll-of :fspec-test/div) [my-div]))))))
(s/def :fspec-ret-test/my-int pos-int?)
(s/def :fspec-ret-test/plus (s/fspec
:args (s/cat :x int? :y pos-int?)
:ret :fspec-ret-test/my-int))
(defn my-plus [x y]
(+ x y))
(deftest fspec-ret-test
(testing "invalid ret"
(is (= (pf "-- Function spec failed -----------
expound.alpha-test/my-plus
returned an invalid value
0
should satisfy
pos-int?
-------------------------
Detected 1 error\n")
(until-unsuccessful #(expound/expound-str :fspec-ret-test/plus my-plus {:print-specs? false}))))
(is (= (pf "-- Function spec failed -----------
[expound.alpha-test/my-plus]
^^^^^^^^^^^^^^^^^^^^^^^^^^
returned an invalid value
0
should satisfy
pos-int?
-------------------------
Detected 1 error\n")
(until-unsuccessful #(expound/expound-str (s/coll-of :fspec-ret-test/plus) [my-plus] {:print-specs? false}))))
(s/def :fspec-ret-test/return-map (s/fspec
:args (s/cat)
:ret (s/keys :req-un [:fspec-ret-test/my-int])))
(is (= (pf "-- Function spec failed -----------
<anonymous function>
returned an invalid value
{}
should contain key: :my-int
| key | spec |
|=========+==========|
| :my-int | pos-int? |
-------------------------
Detected 1 error
")
(until-unsuccessful #(expound/expound-str :fspec-ret-test/return-map
(fn [] {})
{:print-specs? false}))))))
(s/def :fspec-fn-test/minus (s/fspec
:args (s/cat :x int? :y int?)
:fn (s/and
#(< (:ret %) (-> % :args :x))
#(< (:ret %) (-> % :args :y)))))
(defn my-minus [x y]
(- x y))
(deftest fspec-fn-test
(testing "invalid ret"
(is (= (pf "-- Function spec failed -----------
expound.alpha-test/my-minus
failed spec. Function arguments and return value
{:args {:x 0, :y 0}, :ret 0}
should satisfy
%s
-------------------------
Detected 1 error\n"
#?(:clj
"(fn
[%]
(< (:ret %) (-> % :args :x)))"
:cljs "(fn [%] (< (:ret %) (-> % :args :x)))"))
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(until-unsuccessful #(s/explain-str :fspec-fn-test/minus my-minus)))))
(is (= (pf "-- Function spec failed -----------
[expound.alpha-test/my-minus]
^^^^^^^^^^^^^^^^^^^^^^^^^^^
failed spec. Function arguments and return value
{:args {:x 0, :y 0}, :ret 0}
should satisfy
%s
-------------------------
Detected 1 error\n"
#?(:clj
"(fn
[%]
(< (:ret %) (-> % :args :x)))"
:cljs "(fn [%] (< (:ret %) (-> % :args :x)))"))
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(until-unsuccessful #(s/explain-str (s/coll-of :fspec-fn-test/minus) [my-minus])))))))
(deftest ifn-fspec-test
(testing "keyword ifn / ret failure"
(is (= "-- Function spec failed -----------
[:foo]
^^^^
returned an invalid value
nil
should satisfy
int?
-------------------------
Detected 1 error\n"
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(until-unsuccessful #(s/explain-str (s/coll-of (s/fspec :args (s/cat :x int?) :ret int?))
[:foo])))))
(testing "set ifn / ret failure"
(is (= "-- Function spec failed -----------
[#{}]
^^^
returned an invalid value
nil
should satisfy
int?
-------------------------
Detected 1 error\n"
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(until-unsuccessful #(s/explain-str (s/coll-of (s/fspec :args (s/cat :x int?) :ret int?))
[#{}])))))))
#?(:clj
(testing "vector ifn / exception failure"
(is (= "-- Exception ----------------------
[[]]
^^
threw exception
nil
with args:
0
-------------------------
Detected 1 error\n"
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(until-unsuccessful #(s/explain-str (s/coll-of (s/fspec :args (s/cat :x int?) :ret int?))
[[]]))))))))
#?(:clj
(deftest form-containing-incomparables
(checking
"for any value including NaN, or Infinity, expound returns a string"
(chuck/times num-tests)
[form (gen/frequency
[[1 (gen/elements
[Double/NaN
Double/POSITIVE_INFINITY
Double/NEGATIVE_INFINITY
'(Double/NaN Double/POSITIVE_INFINITY Double/NEGATIVE_INFINITY)
[Double/NaN Double/POSITIVE_INFINITY Double/NEGATIVE_INFINITY]
{Double/NaN Double/NaN
Double/POSITIVE_INFINITY Double/POSITIVE_INFINITY
Double/NEGATIVE_INFINITY Double/NEGATIVE_INFINITY}])]
[5 gen/any-printable]])]
(is (string? (expound/expound-str (constantly false) form))))))
#?(:cljs
(deftest form-containing-incomparables
(checking
"for any value including NaN, or Infinity, expound returns a string"
(chuck/times num-tests)
[form (gen/frequency
[[1 (gen/elements
[js/NaN
js/Infinity
js/-Infinity
'(js/NaN js/Infinity js/-Infinity)
[js/NaN js/Infinity js/-Infinity]
{js/NaN js/NaN
js/Infinity js/Infinity
js/-Infinity js/-Infinity}])]
[5 gen/any-printable]])]
(is (string? (expound/expound-str (constantly false) form))))))
(defmulti pet :pet/type)
(defmethod pet :dog [_]
(s/keys))
(defmethod pet :cat [_]
(s/keys))
(defmulti animal :animal/type)
(defmethod animal :dog [_]
(s/keys))
(defmethod animal :cat [_]
(s/keys))
(s/def :multispec-in-compound-spec/pet1 (s/and
map?
(s/multi-spec pet :pet/type)))
(s/def :multispec-in-compound-spec/pet2 (s/or
:map1 (s/multi-spec pet :pet/type)
:map2 (s/multi-spec animal :animal/type)))
(deftest multispec-in-compound-spec
(testing "multispec combined with s/and"
(is (= (pf "-- Missing spec -------------------
Cannot find spec for
{:pet/type :fish}
with
Spec multimethod: `expound.alpha-test/pet`
Dispatch value: `:fish`
-- Relevant specs -------
:multispec-in-compound-spec/pet1:
(pf.spec.alpha/and
pf.core/map?
(pf.spec.alpha/multi-spec expound.alpha-test/pet :pet/type))
-------------------------
Detected 1 error\n")
(expound/expound-str :multispec-in-compound-spec/pet1 {:pet/type :fish}))))
;; FIXME - improve this, maybe something like:
;;;;;;;;;;;;;;;;;;;
;; {:pet/type :fish}
;; should be described by a spec multimethod, but
;; expound.alpha-test/pet
;; is missing a method for value
;; (:pet/type {:pet/type :fish}) ; => :fish
;; or
;; should be described by a spec multimethod, but
;; expound.alpha-test/pet
;; is missing a method for value
;; (:animal/type {:pet/type :fish}) ; => nil
(testing "multispec combined with s/or"
(is (= (pf "-- Missing spec -------------------
Cannot find spec for
{:pet/type :fish}
with
Spec multimethod: `expound.alpha-test/pet`
Dispatch value: `:fish`
or with
Spec multimethod: `expound.alpha-test/animal`
Dispatch value: `nil`
-- Relevant specs -------
:multispec-in-compound-spec/pet2:
(pf.spec.alpha/or
:map1
(pf.spec.alpha/multi-spec expound.alpha-test/pet :pet/type)
:map2
(pf.spec.alpha/multi-spec expound.alpha-test/animal :animal/type))
-------------------------
Detected 1 error\n")
(expound/expound-str :multispec-in-compound-spec/pet2 {:pet/type :fish})))))
(expound/def :predicate-messages/string string? "should be a string")
(expound/def :predicate-messages/vector vector? "should be a vector")
(deftest predicate-messages
(binding [s/*explain-out* (expound/custom-printer {:print-specs? false})]
(testing "predicate with error message"
(is (= "-- Spec failed --------------------
:hello
should be a string
-------------------------
Detected 1 error
"
(s/explain-str :predicate-messages/string :hello))))
(testing "predicate within a collection"
(is (= "-- Spec failed --------------------
[... :foo]
^^^^
should be a string
-------------------------
Detected 1 error
"
(s/explain-str (s/coll-of :predicate-messages/string) ["" :foo]))))
(testing "two predicates with error messages"
(is (= "-- Spec failed --------------------
1
should be a string
or
should be a vector
-------------------------
Detected 1 error
"
(s/explain-str (s/or :s :predicate-messages/string
:v :predicate-messages/vector) 1))))
(testing "one predicate with error message, one without"
(is (= "-- Spec failed --------------------
foo
should satisfy
pos-int?
or
vector?
or
should be a string
-------------------------
Detected 1 error
"
(s/explain-str (s/or :p pos-int?
:s :predicate-messages/string
:v vector?) 'foo))))
(testing "compound predicates"
(let [email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$"]
(expound/def :predicate-messages/email (s/and string? #(re-matches email-regex %)) "should be a valid email address")
(is (= "-- Spec failed --------------------
\"sally@\"
should be a valid email address
-------------------------
Detected 1 error
"
(s/explain-str
:predicate-messages/email
"sally@"))))
(expound/def :predicate-messages/score (s/int-in 0 100) "should be between 0 and 100")
(is (= "-- Spec failed --------------------
101
should be between 0 and 100
-------------------------
Detected 1 error
"
(s/explain-str
:predicate-messages/score
101))))))
(s/fdef results-str-fn1
:args (s/cat :x nat-int? :y nat-int?)
:ret pos?)
(defn results-str-fn1 [x y]
#?(:clj (+' x y)
:cljs (+ x y)))
(s/fdef results-str-fn2
:args (s/cat :x nat-int? :y nat-int?)
:fn #(let [x (-> % :args :x)
ret (-> % :ret)]
(< x ret)))
(defn results-str-fn2 [x y]
(+ x y))
(s/fdef results-str-fn3
:args (s/cat :x #{0} :y #{0})
:ret nat-int?)
(defn results-str-fn3 [x y]
(+ x y))
(s/fdef results-str-fn4
:args (s/cat :x int?)
:ret (s/coll-of int?))
(defn results-str-fn4 [x]
[x :not-int])
(s/fdef results-str-fn5
:args (s/cat :x #{1} :y #{1})
:ret string?)
(defn results-str-fn5
[_x _y]
#?(:clj (throw (Exception. "Ooop!"))
:cljs (throw (js/Error. "Oops!"))))
(s/fdef results-str-fn6
:args (s/cat :f fn?)
:ret any?)
(defn results-str-fn6
[f]
(f 1))
(s/def :results-str-fn7/k string?)
(s/fdef results-str-fn7
:args (s/cat :m (s/keys))
:ret (s/keys :req-un [:results-str-fn7/k]))
(defn results-str-fn7
[m]
m)
(s/fdef results-str-missing-fn
:args (s/cat :x int?))
(s/fdef results-str-missing-args-spec
:ret int?)
(defn results-str-missing-args-spec [] 1)
(deftest explain-results
(testing "explaining results with non-expound printer"
(is (thrown-with-msg?
#?(:cljs :default :clj Exception)
#"Cannot print check results"
(binding [s/*explain-out* s/explain-printer]
(expound/explain-results-str (st/check `results-str-fn1))))))
(testing "single bad result (failing return spec)"
(is (= (pf
"== Checked expound.alpha-test/results-str-fn1
-- Function spec failed -----------
(expound.alpha-test/results-str-fn1 0 0)
returned an invalid value.
0
should satisfy
pos?
-------------------------
Detected 1 error
")
(binding [s/*explain-out* expound/printer]
(expound/explain-results-str (orch-unstrument-test-fns #(st/check `results-str-fn1))))))
(is (= (pf
"== Checked expound.alpha-test/results-str-fn7
-- Function spec failed -----------
(expound.alpha-test/results-str-fn7 {})
returned an invalid value.
{}
should contain key: :k
| key | spec |
|=====+=========|
| :k | string? |
-------------------------
Detected 1 error
")
(binding [s/*explain-out* expound/printer]
(expound/explain-results-str (orch-unstrument-test-fns #(st/check `results-str-fn7)))))))
(testing "single bad result (failing fn spec)"
(is (= (pf "== Checked expound.alpha-test/results-str-fn2
-- Function spec failed -----------
(expound.alpha-test/results-str-fn2 0 0)
failed spec. Function arguments and return value
{:args {:x 0, :y 0}, :ret 0}
should satisfy
(fn
[%%]
(let
[x (-> %% :args :x) ret (-> %% :ret)]
(< x ret)))
-------------------------
Detected 1 error
")
(binding [s/*explain-out* expound/printer]
(expound/explain-results-str (orch-unstrument-test-fns #(st/check `results-str-fn2)))))))
(testing "single valid result"
(is (= "== Checked expound.alpha-test/results-str-fn3
Success!
"
(binding [s/*explain-out* expound/printer]
(expound/explain-results-str (st/check `results-str-fn3))))))
#?(:clj
(testing "multiple results"
(is (= "== Checked expound.alpha-test/results-str-fn2
-- Function spec failed -----------
(expound.alpha-test/results-str-fn2 0 0)
failed spec. Function arguments and return value
{:args {:x 0, :y 0}, :ret 0}
should satisfy
(fn
[%]
(let
[x (-> % :args :x) ret (-> % :ret)]
(< x ret)))
-------------------------
Detected 1 error
== Checked expound.alpha-test/results-str-fn3
Success!
"
(binding [s/*explain-out* expound/printer]
(expound/explain-results-str (orch-unstrument-test-fns #(st/check [`results-str-fn2 `results-str-fn3]))))))))
(testing "check-fn"
(is (= "== Checked <unknown> ========================
-- Function spec failed -----------
(<unknown> 0 0)
failed spec. Function arguments and return value
{:args {:x 0, :y 0}, :ret 0}
should satisfy
(fn
[%]
(let
[x (-> % :args :x) ret (-> % :ret)]
(< x ret)))
-------------------------
Detected 1 error
"
(binding [s/*explain-out* expound/printer]
(expound/explain-result-str (st/check-fn `results-str-fn1 (s/spec `results-str-fn2)))))))
#?(:clj (testing "custom printer"
(is (= "== Checked expound.alpha-test/results-str-fn4
-- Function spec failed -----------
(expound.alpha-test/results-str-fn4 0)
returned an invalid value.
[0 :not-int]
^^^^^^^^
should satisfy
int?
-------------------------
Detected 1 error
"
(binding [s/*explain-out* (expound/custom-printer {:show-valid-values? true})]
(expound/explain-results-str (orch-unstrument-test-fns #(st/check `results-str-fn4))))))))
(testing "exceptions raised during check"
(is (= "== Checked expound.alpha-test/results-str-fn5
(expound.alpha-test/results-str-fn5 1 1)
threw error"
(binding [s/*explain-out* expound/printer]
(take-lines 5 (expound/explain-results-str (st/check `results-str-fn5)))))))
(testing "colorized output"
(is (= (pf "<CYAN>== Checked expound.alpha-test/results-str-fn5 <NONE>
<RED> (expound.alpha-test/results-str-fn5 1 1)<NONE>
threw error")
(binding [s/*explain-out* (expound/custom-printer {:theme :figwheel-theme})]
(readable-ansi (take-lines 5 (expound/explain-results-str (st/check `results-str-fn5))))))))
(testing "failure to generate"
(is (=
#?(:clj "== Checked expound.alpha-test/results-str-fn6
Unable to construct generator for [:f] in
(clojure.spec.alpha/cat :f clojure.core/fn?)
"
;; CLJS doesn't contain correct data for check failure
:cljs "== Checked expound.alpha-test/results-str-fn6
Unable to construct gen at: [:f] for: fn? in
(cljs.spec.alpha/cat :f cljs.core/fn?)
")
(binding [s/*explain-out* expound/printer]
(expound/explain-results-str (st/check `results-str-fn6))))))
(testing "no-fn failure"
(is (= #?(:clj "== Checked expound.alpha-test/results-str-missing-fn
Failed to check function.
expound.alpha-test/results-str-missing-fn
is not defined
"
:cljs "== Checked <unknown> ========================
Failed to check function.
<unknown>
is not defined
")
(binding [s/*explain-out* expound/printer]
(expound/explain-results-str (st/check `results-str-missing-fn))))))
(testing "no args spec"
(is (= (pf "== Checked expound.alpha-test/results-str-missing-args-spec
Failed to check function.
(pf.spec.alpha/fspec :ret pf.core/int?)
should contain an :args spec
")
(binding [s/*explain-out* expound/printer]
(expound/explain-results-str (st/with-instrument-disabled (st/check `results-str-missing-args-spec))))))))
#?(:clj (deftest explain-results-gen
(checking
"all functions can be checked and printed"
(chuck/times num-tests)
[sym-to-check (gen/elements (remove
;; these functions print to stdout, but return
;; nothing
#{`expound/explain-results
`expound/explain-result
`expound/expound
`expound/printer}
(st/checkable-syms)))]
;; Just confirm an error is not thrown
(is (string?
(binding [s/*explain-out* expound/printer]
(expound/explain-results-str
(st/with-instrument-disabled
(st/check sym-to-check
{:clojure.spec.test.check/opts {:num-tests 10}})))))
(str "Failed to check " sym-to-check)))))
(s/def :colorized-output/strings (s/coll-of string?))
(deftest colorized-output
(is (= (pf "-- Spec failed --------------------
[... :a ...]
^^
should satisfy
string?
-- Relevant specs -------
:colorized-output/strings:
(pf.spec.alpha/coll-of pf.core/string?)
-------------------------
Detected 1 error
")
(expound/expound-str :colorized-output/strings ["" :a ""] {:theme :none})))
(is (= (pf "<NONE><NONE><CYAN>-- Spec failed --------------------<NONE>
[... <RED>:a<NONE> ...]
<MAGENTA> ^^<NONE>
should satisfy
<GREEN>string?<NONE>
<CYAN>-- Relevant specs -------<NONE>
:colorized-output/strings:
(pf.spec.alpha/coll-of pf.core/string?)
<CYAN>-------------------------<NONE>
<CYAN>Detected<NONE> <CYAN>1<NONE> <CYAN>error<NONE>
")
(readable-ansi (expound/expound-str :colorized-output/strings ["" :a ""] {:theme :figwheel-theme})))))
(s/def ::spec-name (s/with-gen
qualified-keyword?
#(gen/let [kw gen/keyword]
(keyword (str "expound-generated-spec/" (name kw))))))
(s/def ::fn-spec (s/with-gen
(s/or
:sym symbol?
:anon (s/cat :fn #{`fn `fn*}
:args-list (s/coll-of any? :kind vector?)
:body (s/* any?))
:form (s/cat :comp #{`comp `partial}
:args (s/+ any?)))
#(gen/return `any?)))
(s/def ::pred-spec
(s/with-gen
::fn-spec
#(gen/elements
[`any?
`boolean?
`bytes?
`double?
`ident?
`indexed?
`int?
`keyword?
`map?
`nat-int?
`neg-int?
`pos-int?
`qualified-ident?
`qualified-keyword?
`qualified-symbol?
`seqable?
`simple-ident?
`simple-keyword?
`simple-symbol?
`string?
`symbol?
`uri?
`uuid?
`vector?])))
(s/def ::and-spec (s/cat
:and #{`s/and}
:branches (s/+
::spec)))
(s/def ::or-spec (s/cat
:or #{`s/or}
:branches (s/+
(s/cat
:kw keyword?
:spec ::spec))))
(s/def ::set-spec (s/with-gen
(s/coll-of
any?
:kind set?
:min-count 1)
#(s/gen (s/coll-of
(s/or
:s string?
:i int?
:b boolean?
:k keyword?)
:kind set?))))
(s/def ::spec (s/or
:amp ::amp-spec
:alt ::alt-spec
:and ::and-spec
:cat ::cat-spec
:coll ::coll-spec
:defined-spec ::spec-name
:every ::every-spec
:fspec ::fspec-spec
:keys ::keys-spec
:map ::map-of-spec
:merge ::merge-spec
:multi ::multispec-spec
:nilable ::nilable-spec
:or ::or-spec
:regex-unary ::regex-unary-spec
:set ::set-spec
:simple ::pred-spec
:spec-wrapper (s/cat :wrapper #{`s/spec} :spec ::spec)
:conformer (s/cat
:conformer #{`s/conformer}
:f ::fn-spec
:unf ::fn-spec)
:with-gen (s/cat
:with-gen #{`s/with-gen}
:spec ::spec
:f ::fn-spec)
:tuple-spec ::tuple-spec))
(s/def ::every-opts (s/*
(s/alt
:kind (s/cat
:k #{:kind}
:v #{nil
vector? set? map? list?
`vector? `set? `map? `list?})
:count (s/cat
:k #{:count}
:v (s/nilable nat-int?))
:min-count (s/cat
:k #{:min-count}
:v (s/nilable nat-int?))
:max-count (s/cat
:k #{:max-count}
:v (s/nilable nat-int?))
:distinct (s/cat
:k #{:distinct}
:v (s/nilable boolean?))
:into (s/cat
:k #{:into}
:v (s/or :coll #{[] {} #{}}
:list #{'()}))
:gen-max (s/cat
:k #{:gen-max}
:v nat-int?))))
(s/def ::every-spec (s/cat
:every #{`s/every}
:spec ::spec
:opts ::every-opts))
(s/def ::coll-spec (s/cat
:coll-of #{`s/coll-of}
:spec (s/spec ::spec)
:opts ::every-opts))
(s/def ::map-of-spec (s/cat
:map-of #{`s/map-of}
:k ::spec
:w ::spec
:opts ::every-opts))
(s/def ::nilable-spec (s/cat
:nilable #{`s/nilable}
:spec ::spec))
(s/def ::name-combo
(s/or
:one ::spec-name
:combo (s/cat
:operator #{'and 'or}
:operands
(s/+
::name-combo))))
(s/def ::keys-spec (s/cat
:keys #{`s/keys `s/keys*}
:reqs (s/*
(s/cat
:op #{:req :req-un}
:names (s/coll-of
::name-combo
:kind vector?)))
:opts (s/*
(s/cat
:op #{:opt :opt-un}
:names (s/coll-of
::spec-name
:kind vector?)))))
(s/def ::amp-spec
(s/cat :op #{`s/&}
:spec ::spec
:preds (s/*
(s/with-gen
(s/or :pred ::pred-spec
:defined ::spec-name)
#(gen/return `any?)))))
(s/def ::alt-spec
(s/cat :op #{`s/alt}
:key-pred-forms (s/+
(s/cat
:key keyword?
:pred (s/spec ::spec)))))
(s/def ::regex-unary-spec
(s/cat :op #{`s/+ `s/* `s/?} :pred (s/spec ::spec)))
(s/def ::cat-pred-spec
(s/or
:spec (s/spec ::spec)
:regex-unary ::regex-unary-spec
:amp ::amp-spec
:alt ::alt-spec))
(defmulti fake-multimethod :fake-tag)
(s/def ::multispec-spec
(s/cat
:mult-spec #{`s/multi-spec}
:mm (s/with-gen
symbol?
#(gen/return `fake-multimethod))
:tag (s/with-gen
(s/or :sym symbol?
:k keyword?)
#(gen/return :fake-tag))))
(s/def ::cat-spec (s/cat
:cat #{`s/cat}
:key-pred-forms
(s/*
(s/cat
:key keyword?
:pred ::cat-pred-spec))))
(s/def ::fspec-spec (s/cat
:cat #{`s/fspec}
:args (s/cat
:args #{:args}
:spec ::spec)
:ret (s/?
(s/cat
:ret #{:ret}
:spec ::spec))
:fn (s/?
(s/cat
:fn #{:fn}
:spec (s/nilable ::spec)))))
(s/def ::tuple-spec (s/cat
:tuple #{`s/tuple}
:preds (s/+
::spec)))
(s/def ::merge-spec (s/cat
:merge #{`s/merge}
:pred-forms (s/* ::spec)))
(s/def ::spec-def (s/cat
:def #{`s/def}
:name ::spec-name
:spec (s/spec ::spec)))
#?(:clj (s/def ::spec-defs (s/coll-of ::spec-def
:min-count 1
:gen-max 3)))
(defn exercise-count [spec]
(case spec
(::spec-def ::fspec-spec ::regex-unary-spec ::spec-defs ::alt-spec) 1
(::cat-spec ::merge-spec ::and-spec ::every-spec ::spec ::coll-spec ::map-of-spec ::or-spec ::tuple-spec ::keys-spec) 2
4))
(deftest spec-specs-can-generate
(doseq [spec-spec (filter keyword? (sg/topo-sort (filter #(= "expound.alpha-test" (namespace %))
(keys (s/registry)))))]
(is
(doall (s/exercise spec-spec (exercise-count spec-spec)))
(str "Failed to generate examples for spec " spec-spec))))
#_(defn sample-seq
"Return a sequence of realized values from `generator`."
[generator seed]
(s/assert some? generator)
(let [max-size 1
r (if seed
(random/make-random seed)
(random/make-random))
size-seq (gen/make-size-range-seq max-size)]
(map #(rose/root (gen/call-gen generator %1 %2))
(gen/lazy-random-states r)
size-seq)))
#_(defn missing-specs [spec-defs]
(let [defined (set (map second spec-defs))
used (set
(filter
#(and (qualified-keyword? %)
(= "expound-generated-spec" (namespace %)))
(tree-seq coll? seq spec-defs)))]
(set/difference used defined)))
#?(:clj 1 #_(deftest eval-gen-test
;; FIXME - this is a useful test but not 100% reliable yet
;; so I'm disabling to get this PR in
(binding [s/*recursion-limit* 2]
(checking
"expound returns string"
5 ;; Hard-code at 5, since generating specs explodes in size quite quickly
[spec-defs (s/gen ::spec-defs)
pred-specs (gen/vector (s/gen ::pred-spec) 5)
seed (s/gen pos-int?)
mutate-path (gen/vector gen/pos-int)]
(try
(doseq [[spec-name spec] (map vector (missing-specs spec-defs) (cycle pred-specs))]
(eval `(s/def ~spec-name ~spec)))
(doseq [spec-def spec-defs]
(eval spec-def))
(let [spec (second (last spec-defs))
form (last (last spec-defs))
disallowed #{"clojure.spec.alpha/fspec"
"clojure.spec.alpha/multi-spec"
"clojure.spec.alpha/with-gen"}]
(when-not (or (some
disallowed
(map str (tree-seq coll? identity form)))
(some
disallowed
(->> spec
inline-specs
(tree-seq coll? identity)
(map str))))
(let [valid-form (first (sample-seq (s/gen spec) seed))
invalid-form (mutate valid-form mutate-path)]
(try
(is (string?
(expound/expound-str spec invalid-form)))
(is (not
(string/includes?
(expound/expound-str (second (last spec-defs)) invalid-form)
"should contain keys")))
(catch Exception e
(is (or
(string/includes?
(:cause (Throwable->map e))
"Method code too large!")
(string/includes?
(:cause (Throwable->map e))
"Cannot convert path."))))))))
(finally
;; Get access to private atom in clojure.spec
(def spec-reg (deref #'s/registry-ref))
(doseq [k (filter
(fn [k] (= "expound-generated-spec" (namespace k)))
(keys (s/registry)))]
(swap! spec-reg dissoc k))))))))
(deftest clean-registry
(testing "only base spec remains"
(is (<= (count (filter
(fn [k] (= "expound-generated-spec" (namespace k)))
(keys (s/registry))))
1)
(str "Found leftover specs: " (vec (filter
(fn [k] (= "expound-generated-spec" (namespace k)))
(keys (s/registry))))))))
(deftest valid-spec-spec
(checking
"spec for specs validates against real specs"
(chuck/times num-tests)
[sp (gen/elements
(sg/topo-sort
(remove
(fn [k]
(string/includes? (pr-str (s/form (s/get-spec k))) "clojure.core.specs.alpha/quotable"))
(filter
(fn [k] (or
(string/starts-with? (namespace k) "clojure")
(string/starts-with? (namespace k) "expound")
(string/starts-with? (namespace k) "onyx")
(string/starts-with? (namespace k) "ring")))
(keys (s/registry))))))]
(is (s/valid? ::spec (s/form (s/get-spec sp)))
(str
"Spec name: " sp "\n"
"Error: "
(binding [s/*explain-out* (expound/custom-printer {:show-valid-values? true
:print-specs? false
:theme :figwheel-theme})]
(s/explain-str ::spec (s/form (s/get-spec sp))))))))
(defmethod expound/problem-group-str ::test-problem1 [_type _spec-name _val _path _problems _opts]
"fake-problem-group-str")
(defmethod expound/problem-group-str ::test-problem2 [type spec-name val path problems opts]
(str "fake-problem-group-str\n"
(expound/expected-str type spec-name val path problems opts)))
(defmethod expound/expected-str ::test-problem2 [_type _spec-name _val _path _problems _opts]
"fake-expected-str")
(deftest extensibility-test
(testing "can overwrite entire message"
(let [printer-str #'expound/printer-str
ed (assoc-in (s/explain-data int? "")
[::s/problems 0 :expound.spec.problem/type]
::test-problem1)]
(is (= "fake-problem-group-str\n\n-------------------------\nDetected 1 error\n"
(printer-str {:print-specs? false} ed)))))
(testing "can overwrite 'expected' str"
(let [printer-str #'expound/printer-str
ed (assoc-in (s/explain-data int? "")
[::s/problems 0 :expound.spec.problem/type]
::test-problem2)]
(is (= "fake-problem-group-str\nfake-expected-str\n\n-------------------------\nDetected 1 error\n"
(printer-str {:print-specs? false} ed)))))
(testing "if type has no mm implemented, throw an error"
(let [printer-str #'expound/printer-str
ed (assoc-in (s/explain-data int? "")
[::s/problems 0 :expound.spec.problem/type]
::test-problem3)]
(is (thrown-with-msg?
#?(:cljs :default :clj Exception)
#"No method in multimethod"
(printer-str {:print-specs? false} ed))))))
#?(:clj (deftest macroexpansion-errors
(let [actual (formatted-exception {:print-specs? false} #(macroexpand '(clojure.core/let [a] 2)))]
(is (or
(= "Call to #'clojure.core/let did not conform to spec.
-- Spec failed --------------------
([a] ...)
^^^
should satisfy
even-number-of-forms?
-------------------------
Detected 1 error\n"
actual)
(= "Call to clojure.core/let did not conform to spec.
-- Spec failed --------------------
([a] ...)
^^^
should satisfy
even-number-of-forms?
-------------------------
Detected 1 error\n"
actual))))
(let [ed (try
(macroexpand '(clojure.core/let [a] 2))
(catch Exception e
(-> (Throwable->map e) :via last :data)))]
(is (= "-- Spec failed --------------------
([a] ...)
^^^
should satisfy
even-number-of-forms?
-------------------------
Detected 1 error\n"
(with-out-str ((expound/custom-printer {:print-specs? false})
ed)))))))
(deftest sorted-map-values
(is (= "-- Spec failed --------------------
{\"bar\" 1}
should satisfy
number?
-------------------------
Detected 1 error\n"
(expound/expound-str
number?
(sorted-map "bar" 1))))
(is (= "-- Spec failed --------------------
{:foo {\"bar\" 1}}
should satisfy
number?
-------------------------
Detected 1 error\n"
(expound/expound-str
number?
{:foo (sorted-map "bar"
1)}))))
(defn select-expound-info [spec value]
(->> (s/explain-data spec value)
(problems/annotate)
(:expound/problems)
(map #(select-keys % [:expound.spec.problem/type :expound/in]))
(set)))
#?(:clj
(deftest or-includes-problems-for-each-branch
(let [p1 (select-expound-info :ring.sync/handler (fn handler [_req] {}))
p2 (select-expound-info :ring.async/handler (fn handler [_req] {}))
p3 (select-expound-info :ring.sync+async/handler (fn handler [_req] {}))
all-problems (select-expound-info :ring/handler (fn handler [_req] {}))]
(is (set/subset? p1 all-problems) {:extra (set/difference p1 all-problems)})
(is (set/subset? p2 all-problems) {:extra (set/difference p2 all-problems)})
(is (set/subset? p3 all-problems) {:extra (set/difference p3 all-problems)})))
:cljs
(set/index #{} [:x]) ; noop to keep clj-kondo happy
)
(deftest defmsg-test
(s/def :defmsg-test/id1 string?)
(expound/defmsg :defmsg-test/id1 "should be a string ID")
(testing "messages for predicate specs"
(is (= "-- Spec failed --------------------
123
should be a string ID
-------------------------
Detected 1 error\n"
(expound/expound-str
:defmsg-test/id1
123
{:print-specs? false}))))
(s/def :defmsg-test/id2 (s/and string?
#(<= 4 (count %))))
(expound/defmsg :defmsg-test/id2 "should be a string ID of length 4 or more")
(testing "messages for 'and' specs"
(is (= "-- Spec failed --------------------
\"123\"
should be a string ID of length 4 or more
-------------------------
Detected 1 error\n"
(expound/expound-str
:defmsg-test/id2
"123"
{:print-specs? false}))))
(s/def :defmsg-test/statuses #{:ok :failed})
(expound/defmsg :defmsg-test/statuses "should be either :ok or :failed")
(testing "messages for set specs"
(is (= "-- Spec failed --------------------
:oak
should be either :ok or :failed
-------------------------
Detected 1 error
"
(expound/expound-str
:defmsg-test/statuses
:oak
{:print-specs? false}))))
(testing "messages for alt specs"
(s/def ::x int?)
(s/def ::y int?)
(expound/defmsg ::x "must be an integer")
(is (=
"-- Spec failed --------------------
[\"\" ...]
^^
must be an integer
-------------------------
Detected 1 error\n"
(expound/expound-str (s/alt :one
(s/cat :x ::x)
:two
(s/cat :x ::x
:y ::y))
["" ""]
{:print-specs? false}))))
(testing "messages for alt specs (if user duplicates existing message)"
(s/def ::x int?)
(s/def ::y int?)
(expound/defmsg ::x "should satisfy\n\n int?")
(is (=
"-- Spec failed --------------------
[\"\"]
^^
should satisfy
int?
-------------------------
Detected 1 error\n"
(expound/expound-str (s/alt :one
::x
:two
::y)
[""]
{:print-specs? false}))))
(testing "messages for alternatives and set specs"
(is (= "-- Spec failed --------------------
:oak
should be either :ok or :failed
or
should satisfy
string?
-------------------------
Detected 1 error\n"
(expound/expound-str
(s/or
:num
:defmsg-test/statuses
:s string?)
:oak
{:print-specs? false})))))
(deftest printer
(st/instrument ['expound/printer])
(binding [s/*explain-out* expound/printer]
(is (string? (s/explain-str int? "a")))
(is (= "Success!\n" (s/explain-str int? 1)))
(is (= "Success!\n" (with-out-str (expound/printer (s/explain-data int? 1))))))
(st/unstrument ['expound/printer]))
(deftest undefined-key
(is (= "-- Spec failed --------------------
{}
should contain key: :undefined-key/does-not-exist
| key | spec |
|===============================+===============================|
| :undefined-key/does-not-exist | :undefined-key/does-not-exist |
-------------------------
Detected 1 error
"
(expound/expound-str (s/keys :req [:undefined-key/does-not-exist])
{}
{:print-specs? false}))))
#?(:clj
(deftype FakeDB [m]
clojure.lang.Seqable
(seq [_]
(seq m))
clojure.lang.IPersistentCollection
(count [_]
(count m))
(cons [_ _o]
(throw (Exception. "FakeDB doesn't implement 'cons'")))
(empty [_]
(FakeDB. {}))
(equiv [_ o]
(=
m
(:m o)))
clojure.lang.Associative
(containsKey [_ k] (contains? m k))
(entryAt [_ k] (get m k))
clojure.lang.IPersistentMap
(assoc [_this _k _v] (throw (Exception. "FakeDB doesn't implement 'assoc'")))
(assocEx [_this _k _v] (throw (Exception. "FakeDB doesn't implement 'assocEx'")))
(without [_this _k] (throw (Exception. "FakeDB doesn't implement 'without'")))
clojure.lang.ILookup
(valAt [_ k]
(get m k))
(valAt [_ k not-found]
(get m k not-found))))
(s/def ::db-val (s/or :i int? :s string?))
;; https://github.com/bhb/expound/issues/205
#?(:clj (deftest unwalkable-values
;; run bin/test-datomic for real test of datomic DB,
;; but this at least simulates the failure. We should not
;; try to walk arbitrary values
(let [db (FakeDB. {:a 1})]
(is (= true (map? db)))
(is (= "Success!\n"
(expound/expound-str some? db)))
(is (= "-- Spec failed --------------------
[{:a 1}]
^^^^^^
should contain key: :expound.alpha-test/db-val
| key | spec |
|============================+=========================|
| :expound.alpha-test/db-val | (or :i int? :s string?) |
-------------------------
Detected 1 error
"
(expound/expound-str (s/cat :db (s/keys
:req [::db-val])) [db]))))))
;; https://github.com/bhb/expound/issues/217
(deftest small-values-for-print-length
(binding [*print-length* 5]
(is (= "-- Spec failed --------------------
9
in
(0 1 2 3 4 ...)
should satisfy
(fn [x] (< x 9))
-------------------------
Detected 1 error
"
(expound/expound-str
(clojure.spec.alpha/coll-of (fn [x] (< x 9)))
(range 10))))))
;; https://github.com/bhb/expound/issues/215
(s/def :keys-within-operators.user/name string?)
(s/def :keys-within-operators.user/age pos-int?)
(deftest keys-within-operators
(is (= "-- Spec failed --------------------
{}
should contain keys: :age, :keys-within-operators.user/name
| key | spec |
|==================================+==========|
| :age | pos-int? |
|----------------------------------+----------|
| :keys-within-operators.user/name | string? |
-------------------------
Detected 1 error\n"
(expound/expound-str (s/and (s/keys :req [:keys-within-operators.user/name]
:req-un [:keys-within-operators.user/age])
#(contains? % :foo)) {} {:print-specs? false})))
(is (= "-- Spec failed --------------------
{}
should contain keys: :age, :foo, :keys-within-operators.user/name
| key | spec |
|==================================+===================================================|
| :age | pos-int? |
|----------------------------------+---------------------------------------------------|
| :foo | <can't find spec for unqualified spec identifier> |
|----------------------------------+---------------------------------------------------|
| :keys-within-operators.user/name | string? |
-------------------------
Detected 1 error\n"
(expound/expound-str (s/or :k1 (s/keys :req [:keys-within-operators.user/name]
:req-un [:keys-within-operators.user/age])
:k2 #(contains? % :foo)) {} {:print-specs? false}))))