diff --git a/deps.edn b/deps.edn index ac77c3b5..7e71621c 100644 --- a/deps.edn +++ b/deps.edn @@ -53,8 +53,8 @@ :lib-tests {:extra-paths ["process/src" "process/test" "test-resources/lib_tests"] :extra-deps {org.clj-commons/clj-http-lite {:mvn/version "0.4.392"} - borkdude/spartan.spec {:git/url "https://github.com/borkdude/spartan.spec" - :sha "7689f85d4c79de6c7b5ee3ab62e3e72eeccd3318"} + org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha" + :sha "9597a68339ab6be83323345fc0cea4ce848266b9"} lambdaisland/regal {:git/url "https://github.com/lambdaisland/regal" :sha "f902d2c43121f9e1c48603d6eb99f5900eb6a9f6"} weavejester/medley {:git/url "https://github.com/weavejester/medley" diff --git a/test-resources/lib_tests/babashka/run_all_libtests.clj b/test-resources/lib_tests/babashka/run_all_libtests.clj index 1b54e373..064e6901 100644 --- a/test-resources/lib_tests/babashka/run_all_libtests.clj +++ b/test-resources/lib_tests/babashka/run_all_libtests.clj @@ -1,10 +1,9 @@ (ns babashka.run-all-libtests (:require [clojure.java.io :as io] [clojure.string :as str] - [clojure.test :as t] - [spartan.spec])) + [clojure.test :as t])) -(require 'clojure.spec.alpha) +#_(require 'clojure.spec.alpha) (def ns-args (set (map symbol *command-line-args*))) @@ -34,9 +33,10 @@ (test-namespaces 'clj-http.lite.client-test) -;;;; spartan.spec +;; ;;;; clojure.spec -(test-namespaces 'spartan.spec-test) +(test-namespaces 'clojure.test-clojure.spec + 'clojure.test-clojure.instr) ;;;; regal diff --git a/test-resources/lib_tests/clojure/test_clojure/instr.clj b/test-resources/lib_tests/clojure/test_clojure/instr.clj new file mode 100644 index 00000000..e6b1e238 --- /dev/null +++ b/test-resources/lib_tests/clojure/test_clojure/instr.clj @@ -0,0 +1,178 @@ +(ns clojure.test-clojure.instr + (:require [clojure.spec.alpha :as s] + [clojure.spec.gen.alpha :as gen] + [clojure.spec.test.alpha :as stest] + [clojure.test :refer :all])) + +(set! *warn-on-reflection* true) + +;; utils + +(defmacro with-feature [feature & body] + `(try ~feature + ~@body + (catch Exception ex#))) + +;; instrument tests + +(defn kwargs-fn + ([opts] opts) + ([a b] [a b]) + ([a b & {:as m}] [a b m])) + +(defn no-kwargs-fn + ([opts] opts) + ([a b] [a b]) + ([args inner & opts] [args inner opts])) + +(defn no-kwargs-destruct-fn + ([opts] opts) + ([{:as a} b] [a b]) + ([{:as args} inner & opts] [args inner opts])) + +(defn just-varargs [& args] + (apply + args)) + +(defn add10 [n] + (+ 10 n)) + +(alter-meta! #'add10 dissoc :arglists) + +;;; Specs + +(s/def ::a any?) +(s/def ::b number?) +(s/def ::c any?) +(s/def ::m map?) + +(s/fdef kwargs-fn + :args (s/alt :unary (s/cat :a ::a) + :binary (s/cat :a ::a :b ::b) + :variadic (s/cat :a ::a + :b ::b + :kwargs (s/keys* :opt-un [::a ::b ::c])))) + +(s/fdef no-kwargs-fn + :args (s/alt :unary (s/cat :a ::a) + :binary (s/cat :a ::a :b ::b) + :variadic (s/cat :a ::a + :b ::b + :varargs (s/cat :numbers (s/* number?))))) + +(s/fdef no-kwargs-destruct-fn + :args (s/alt :unary (s/cat :a ::a) + :binary (s/cat :a ::a :m ::m) + :variadic (s/cat :a ::a + :b ::b + :varargs (s/cat :numbers (s/* number?))))) + +(s/fdef just-varargs + :args (s/cat :numbers (s/* number?)) + :ret number?) + +(s/fdef add10 + :args (s/cat :arg ::b) + :ret number?) + +(defn- fail-no-kwargs [& args] (apply no-kwargs-fn args)) +(defn- fail-kwargs [& args] (apply kwargs-fn args)) + +(with-feature (kwargs-fn 1 2 {:a 1 :b 2}) + (deftest test-instrument + (testing "that a function taking fixed args and varargs is spec'd and checked at runtime" + (letfn [(test-varargs-raw [] + (are [x y] (= x y) + 1 (no-kwargs-fn 1) + [1 2] (no-kwargs-fn 1 2) + [1 2 [3 4 5]] (no-kwargs-fn 1 2 3 4 5)))] + (testing "that the raw kwargs function operates as expected" + (test-varargs-raw)) + + (testing "that the instrumented kwargs function operates as expected" + (stest/instrument `no-kwargs-fn {}) + + (test-varargs-raw) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform to spec" (no-kwargs-fn 1 :not-num))) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform to spec" (no-kwargs-fn 1 2 :not-num 3))) + + #_(testing "that the ex-info data looks correct" + (try (fail-no-kwargs 1 :not-num) + (catch Exception ei + (is (= 'clojure.test-clojure.instr/fail-no-kwargs (-> ei ex-data :clojure.spec.test.alpha/caller :var-scope))))) + + (try (fail-no-kwargs 1 2 :not-num 3) + (catch Exception ei + (is (= 'clojure.test-clojure.instr/fail-no-kwargs (-> ei ex-data :clojure.spec.test.alpha/caller :var-scope))))))) + + (testing "that the uninstrumented kwargs function operates as the raw function" + (stest/unstrument `no-kwargs-fn) + (test-varargs-raw)))) + + (testing "that a function taking only varargs is spec'd and checked at runtime" + (letfn [(test-varargs-raw [] + (are [x y] (= x y) + 1 (just-varargs 1) + 3 (just-varargs 1 2) + 15 (just-varargs 1 2 3 4 5)))] + (testing "that the raw varargs function operates as expected" + (test-varargs-raw)) + + (testing "that the instrumented varargs function operates as expected" + (stest/instrument `just-varargs {}) + + (test-varargs-raw) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform to spec" (just-varargs 1 :not-num))) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform to spec" (just-varargs 1 2 :not-num 3)))) + + (testing "that the uninstrumented kwargs function operates as the raw function" + (stest/unstrument `just-varargs) + (test-varargs-raw)))) + + (testing "that a function taking keyword args is spec'd and checked at runtime" + (letfn [(test-kwargs-baseline [] + (are [x y] (= x y) + 1 (kwargs-fn 1) + [1 2] (kwargs-fn 1 2) + [1 2 {:a 1}] (kwargs-fn 1 2 :a 1) + [1 2 {:a 1}] (kwargs-fn 1 2 {:a 1}) + [1 2 {:a 1 :b 2}] (kwargs-fn 1 2 :a 1 {:b 2}))) + (test-kwargs-extended [] + (are [x y] (= x y) + [1 :not-num] (kwargs-fn 1 :not-num) + [1 2 {:a 1 :b :not-num}] (kwargs-fn 1 2 :a 1 {:b :not-num})))] + (testing "that the raw kwargs function operates as expected" + (test-kwargs-baseline) + (test-kwargs-extended)) + + (testing "that the instrumented kwargs function operates as expected" + (stest/instrument `kwargs-fn {}) + + (test-kwargs-baseline) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform to spec" (kwargs-fn 1 :not-num))) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform to spec" (kwargs-fn 1 2 :a 1 {:b :not-num}))) + + #_(testing "that the ex-info data looks correct" + (try (fail-kwargs 1 :not-num) + (catch Exception ei + (is (= 'clojure.test-clojure.instr/fail-kwargs (-> ei ex-data :clojure.spec.test.alpha/caller :var-scope))))) + + (try (fail-kwargs 1 2 :a 1 {:b :not-num}) + (catch Exception ei + (is (= 'clojure.test-clojure.instr/fail-kwargs (-> ei ex-data :clojure.spec.test.alpha/caller :var-scope))))))) + + (testing "that the uninstrumented kwargs function operates as the raw function" + (stest/unstrument `kwargs-fn) + (test-kwargs-baseline) + (test-kwargs-extended)))) + + (testing "that a var with no arglists meta is spec'd and checked at runtime" + (stest/instrument `add10 {}) + (is (= 11 (add10 1))) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform to spec" (add10 :not-num))) + (is (= 11 (add10 1)))) + + (testing "that a function with positional destructuring in its parameter list is spec'd and checked at runtime" + (stest/instrument `no-kwargs-destruct-fn {}) + + (is (= [{:a 1} {}] (no-kwargs-destruct-fn {:a 1} {}))) + (is (= [{:a 1} 2 [3 4 5]] (no-kwargs-destruct-fn {:a 1} 2 3 4 5)))))) diff --git a/test-resources/lib_tests/clojure/test_clojure/spec.clj b/test-resources/lib_tests/clojure/test_clojure/spec.clj new file mode 100644 index 00000000..40b6ed66 --- /dev/null +++ b/test-resources/lib_tests/clojure/test_clojure/spec.clj @@ -0,0 +1,305 @@ +(ns clojure.test-clojure.spec + (:require [clojure.spec.alpha :as s] + [clojure.spec.gen.alpha :as gen] + [clojure.spec.test.alpha :as stest] + [clojure.test :refer :all])) + +(set! *warn-on-reflection* true) + +(defmacro result-or-ex [x] + `(try + ~x + (catch Throwable t# + (.getName (class t#))))) + +(def even-count? #(even? (count %))) + +(defn submap? + "Is m1 a subset of m2?" + [m1 m2] + (if (and (map? m1) (map? m2)) + (every? (fn [[k v]] (and (contains? m2 k) + (submap? v (get m2 k)))) + m1) + (= m1 m2))) + +(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) + s (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 (result-or-ex (s/conform spec x)) + e (result-or-ex (::s/problems (s/explain-data spec x)))] + (when (not= conformed co) (println "conform fail\n\texpect=" conformed "\n\tactual=" co)) + (when (not (every? true? (map submap? ed e))) + (println "explain failures\n\texpect=" ed "\n\tactual failures=" e "\n\tsubmap?=" (map submap? ed e))) + (and (= conformed co) (every? true? (map submap? ed e)))) + + lrange 7 7 nil + lrange 8 8 nil + lrange 42 ::s/invalid [{:pred '(clojure.core/fn [%] (clojure.spec.alpha/int-in-range? 7 42 %)), :val 42}] + + irange #inst "1938" ::s/invalid [{:pred '(clojure.core/fn [%] (clojure.spec.alpha/inst-in-range? #inst "1939-01-01T00:00:00.000-00:00" #inst "1946-01-01T00:00:00.000-00:00" %)), :val #inst "1938"}] + irange #inst "1942" #inst "1942" nil + irange #inst "1946" ::s/invalid [{:pred '(clojure.core/fn [%] (clojure.spec.alpha/inst-in-range? #inst "1939-01-01T00:00:00.000-00:00" #inst "1946-01-01T00:00:00.000-00:00" %)), :val #inst "1946"}] + + drange 3.0 ::s/invalid [{:pred '(clojure.core/fn [%] (clojure.core/<= 3.1 %)), :val 3.0}] + drange 3.1 3.1 nil + drange 3.2 3.2 nil + ;; TODO: drange Double/POSITIVE_INFINITY ::s/invalid [{:pred '(clojure.core/fn [%] (clojure.core/not (Double/isInfinite %))), :val Double/POSITIVE_INFINITY}] + ;; See https://github.com/babashka/sci/issues/647 + ;; can't use equality-based test for Double/NaN + ;; drange Double/NaN ::s/invalid {[] {:pred '(clojure.core/fn [%] (clojure.core/not (Double/isNaN %))), :val Double/NaN}} + + keyword? :k :k nil + keyword? nil ::s/invalid [{:pred `keyword? :val nil}] + keyword? "abc" ::s/invalid [{:pred `keyword? :val "abc"}] + + a 6 6 nil + a 3 ::s/invalid '[{:pred (clojure.core/fn [%] (clojure.core/> % 5)), :val 3}] + a 20 ::s/invalid '[{:pred (clojure.core/fn [%] (clojure.core/< % 10)), :val 20}] + a nil "java.lang.NullPointerException" "java.lang.NullPointerException" + a :k "java.lang.ClassCastException" "java.lang.ClassCastException" + + o "a" [:s "a"] nil + o :a [:k :a] nil + o 'a ::s/invalid '[{:pred clojure.core/string?, :val a, :path [:s]} {:pred clojure.core/keyword?, :val a :path [:k]}] + + c nil ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/string?, :val (), :path [:a]}] + c [] ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/string?, :val (), :path [:a]}] + c [:a] ::s/invalid '[{:pred clojure.core/string?, :val :a, :path [:a], :in [0]}] + c ["a"] ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/keyword?, :val (), :path [:b]}] + c ["s" :k] '{:a "s" :b :k} nil + c ["s" :k 5] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec.alpha/cat :a clojure.core/string? :b clojure.core/keyword?), :val (5)}] + (s/cat) nil {} nil + (s/cat) [5] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec.alpha/cat), :val (5), :in [0]}] + + either nil ::s/invalid '[{:reason "Insufficient input", :pred (clojure.spec.alpha/alt :a clojure.core/string? :b clojure.core/keyword?), :val () :via []}] + either [] ::s/invalid '[{:reason "Insufficient input", :pred (clojure.spec.alpha/alt :a clojure.core/string? :b clojure.core/keyword?), :val () :via []}] + either [:k] [:b :k] nil + either ["s"] [:a "s"] nil + either [:b "s"] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec.alpha/alt :a clojure.core/string? :b clojure.core/keyword?), :val ("s") :via []}] + + star nil [] nil + star [] [] nil + star [:k] [:k] nil + star [:k1 :k2] [:k1 :k2] nil + star [:k1 :k2 "x"] ::s/invalid '[{:pred clojure.core/keyword?, :val "x" :via []}] + star ["a"] ::s/invalid '[{:pred clojure.core/keyword?, :val "a" :via []}] + + plus nil ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/keyword?, :val () :via []}] + plus [] ::s/invalid '[{:reason "Insufficient input", :pred clojure.core/keyword?, :val () :via []}] + plus [:k] [:k] nil + plus [:k1 :k2] [:k1 :k2] nil + plus [:k1 :k2 "x"] ::s/invalid '[{:pred clojure.core/keyword?, :val "x", :in [2]}] + plus ["a"] ::s/invalid '[{:pred clojure.core/keyword?, :val "a" :via []}] + + opt nil nil nil + opt [] nil nil + opt :k ::s/invalid '[{:pred (clojure.core/fn [%] (clojure.core/or (clojure.core/nil? %) (clojure.core/sequential? %))), :val :k}] + opt [:k] :k nil + opt [:k1 :k2] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec.alpha/? clojure.core/keyword?), :val (:k2)}] + opt [:k1 :k2 "x"] ::s/invalid '[{:reason "Extra input", :pred (clojure.spec.alpha/? clojure.core/keyword?), :val (:k2 "x")}] + opt ["a"] ::s/invalid '[{:pred clojure.core/keyword?, :val "a"}] + + andre nil nil nil + andre [] nil nil + andre :k :clojure.spec.alpha/invalid '[{:pred (clojure.core/fn [%] (clojure.core/or (clojure.core/nil? %) (clojure.core/sequential? %))), :val :k}] + andre [:k] ::s/invalid '[{:pred clojure.test-clojure.spec/even-count?, :val [:k]}] + andre [:j :k] [:j :k] nil + + andre2 nil :clojure.spec.alpha/invalid [{:pred #{[:a]}, :val []}] + andre2 [] :clojure.spec.alpha/invalid [{:pred #{[:a]}, :val []}] + andre2 [:a] [:a] nil + + m nil ::s/invalid '[{:pred clojure.core/map?, :val nil}] + m {} {} nil + m {:a "b"} {:a "b"} nil + + mkeys nil ::s/invalid '[{:pred clojure.core/map?, :val nil}] + mkeys {} {} nil + mkeys {:a 1 :b 2} {:a 1 :b 2} nil + + mkeys2 nil ::s/invalid '[{:pred clojure.core/map?, :val nil}] + mkeys2 {} {} nil + mkeys2 {:a 1 :b 2} {"a" 1 "b" 2} nil + + s '([:a 1] [:b "2"]) '({:tag :a :val 1} {:tag :b :val "2"}) nil + + v [:a :b] [:a :b] nil + v '(:a :b) ::s/invalid '[{:pred clojure.core/vector? :val (:a :b)}] + + coll nil ::s/invalid '[{:path [], :pred clojure.core/coll?, :val nil, :via [], :in []}] + coll [] [] nil + coll [:a] [:a] nil + coll [:a :b] [:a :b] nil + coll (map identity [:a :b]) '(:a :b) nil + ;;coll [:a "b"] ::s/invalid '[{:pred (coll-checker keyword?), :val [:a b]}] + ))) + +(deftest describing-evaled-specs + (let [sp #{1 2}] + (is (= (s/describe sp) (s/form sp) sp))) + + (is (= (s/describe odd?) 'odd?)) + (is (= (s/form odd?) 'clojure.core/odd?)) + + #_(is (= (s/describe #(odd? %)) ::s/unknown)) + #_(is (= (s/form #(odd? %)) ::s/unknown))) + +(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)))) + +(deftest nilable-conform-unform + (check-conform-unform + (s/nilable int?) + [5 nil] + [5 nil]) + (check-conform-unform + (s/nilable (s/or :i int? :s string?)) + [5 "x" nil] + [[:i 5] [:s "x"] nil])) + +(deftest nonconforming-conform-unform + (check-conform-unform + (s/nonconforming (s/or :i int? :s string?)) + [5 "x"] + [5 "x"])) + +(deftest coll-form + (are [spec form] + (= (s/form spec) form) + (s/map-of int? any?) + '(clojure.spec.alpha/map-of clojure.core/int? clojure.core/any?) + + (s/coll-of int?) + '(clojure.spec.alpha/coll-of clojure.core/int?) + + (s/every-kv int? int?) + '(clojure.spec.alpha/every-kv clojure.core/int? clojure.core/int?) + + (s/every int?) + '(clojure.spec.alpha/every clojure.core/int?) + + (s/coll-of (s/tuple (s/tuple int?))) + '(clojure.spec.alpha/coll-of (clojure.spec.alpha/tuple (clojure.spec.alpha/tuple clojure.core/int?))) + + (s/coll-of int? :kind vector?) + '(clojure.spec.alpha/coll-of clojure.core/int? :kind clojure.core/vector?) + + (s/coll-of int? :gen #(gen/return [1 2])) + '(clojure.spec.alpha/coll-of clojure.core/int? :gen (fn* [] (gen/return [1 2]))))) + +(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"}])) + +(deftest &-explain-pred + (are [val expected] + (= expected (-> (s/explain-data (s/& int? even?) val) ::s/problems first :pred)) + [] 'clojure.core/int? + [0 2] '(clojure.spec.alpha/& clojure.core/int? clojure.core/even?))) + +(deftest keys-explain-pred + (is (= 'clojure.core/map? (-> (s/explain-data (s/keys :req [::x]) :a) ::s/problems first :pred)))) + +(deftest remove-def + (is (= ::ABC (s/def ::ABC string?))) + (is (= ::ABC (s/def ::ABC nil))) + (is (nil? (s/get-spec ::ABC)))) + +;; TODO replace this with a generative test once we have specs for s/keys +(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?) + + (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 (= #{[:a] + [:a :b] + [:a :b :c] + [:a :c]} + (->> (s/exercise (s/keys :req-un [::a] :opt-un [::b ::c]) 100) + (map (comp sort keys first)) + (into #{})))) + + (is (= #{[::a ::b] + [::a ::b ::c ::d] + [::a ::b ::c ::d ::e] + [::a ::b ::c ::e] + [::a ::c ::d] + [::a ::c ::d ::e] + [::a ::c ::e]} + (->> (s/exercise (s/keys :req [::a (or ::b (and ::c (or ::d ::e)))]) 200) + (map (comp vec sort keys first)) + (into #{})))) + + (is (= #{[:a :b] + [:a :b :c :d] + [:a :b :c :d :e] + [:a :b :c :e] + [:a :c :d] + [:a :c :d :e] + [:a :c :e]} + (->> (s/exercise (s/keys :req-un [::a (or ::b (and ::c (or ::d ::e)))]) 200) + (map (comp vec sort keys first)) + (into #{}))))) + +(deftest tuple-explain-pred + (are [val expected] + (= expected (-> (s/explain-data (s/tuple int?) val) ::s/problems first :pred)) + :a 'clojure.core/vector? + [] '(clojure.core/= (clojure.core/count %) 1))) + +(comment + (require '[clojure.test :refer (run-tests)]) + (in-ns 'clojure.test-clojure.spec) + (run-tests) + + )