Skip to content

Commit 574ea97

Browse files
stuarthallowayrichhickey
authored andcommitted
instrument and test enhancements 5
Signed-off-by: Rich Hickey <richhickey@gmail.com>
1 parent 22289b2 commit 574ea97

File tree

2 files changed

+357
-206
lines changed

2 files changed

+357
-206
lines changed

src/clj/clojure/spec.clj

+165-84
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,9 @@
225225
spec (specize spec)]
226226
(if-let [g (c/or (get overrides path) (gen* spec overrides path rmap))]
227227
(gen/such-that #(valid? spec %) g 100)
228-
(throw (IllegalStateException. (str "Unable to construct gen at: " path " for: " (abbrev form)))))))
228+
(let [abbr (abbrev form)]
229+
(throw (ex-info (str "Unable to construct gen at: " path " for: " abbr)
230+
{::path path ::no-gen-for form}))))))
229231

230232
(defn gen
231233
"Given a spec, returns the generator for it, or throws if none can
@@ -277,10 +279,9 @@
277279
(defn ns-qualify
278280
"Qualify symbol s by resolving it or using the current *ns*."
279281
[s]
280-
(if (namespace s)
281-
(let [v (resolve s)]
282-
(assert v (str "Unable to resolve: " s))
283-
(->sym v))
282+
(if-let [ns-sym (some-> s namespace symbol)]
283+
(c/or (some-> (get (ns-aliases *ns*) ns-sym) str (symbol (name s)))
284+
s)
284285
(symbol (str (.name *ns*)) (str s))))
285286

286287
(defmacro def
@@ -585,8 +586,9 @@
585586
~@body))
586587

587588
(defn- spec-checking-fn
588-
[v f]
589-
(let [conform! (fn [v role spec data args]
589+
[v f fn-spec]
590+
(let [fn-spec (maybe-spec fn-spec)
591+
conform! (fn [v role spec data args]
590592
(let [conformed (conform spec data)]
591593
(if (= ::invalid conformed)
592594
(let [ed (assoc (explain-data* spec [role] [] [] data)
@@ -599,16 +601,15 @@
599601
[& args]
600602
(if *instrument-enabled*
601603
(with-instrument-disabled
602-
(let [specs (get-spec v)]
603-
(when (:args specs) (conform! v :args (:args specs) args args))
604-
(binding [*instrument-enabled* true]
605-
(.applyTo ^clojure.lang.IFn f args))))
604+
(when (:args fn-spec) (conform! v :args (:args fn-spec) args args))
605+
(binding [*instrument-enabled* true]
606+
(.applyTo ^clojure.lang.IFn f args)))
606607
(.applyTo ^clojure.lang.IFn f args)))))
607608

608609
(defn- macroexpand-check
609610
[v args]
610-
(let [specs (get-spec v)]
611-
(when-let [arg-spec (:args specs)]
611+
(let [fn-spec (get-spec v)]
612+
(when-let [arg-spec (:args fn-spec)]
612613
(when (= ::invalid (conform arg-spec args))
613614
(let [ed (assoc (explain-data* arg-spec [:args]
614615
(if-let [name (spec-name arg-spec)] [name] []) [] args)
@@ -654,13 +655,13 @@
654655
`(clojure.spec/def ~fn-sym (clojure.spec/fspec ~@specs)))
655656

656657
(defn- no-fn-spec
657-
[v specs]
658+
[v spec]
658659
(ex-info (str "Fn at " v " is not spec'ed.")
659-
{:var v :specs specs}))
660+
{:var v :spec spec}))
660661

661662
(def ^:private instrumented-vars
662663
"Map for instrumented vars to :raw/:wrapped fns"
663-
(atom {}))
664+
(atom {}))
664665

665666
(defn- ->var
666667
[s-or-v]
@@ -671,87 +672,167 @@
671672
v
672673
(throw (IllegalArgumentException. (str (pr-str s-or-v) " does not name a var")))))))
673674

675+
(defn- instrument-choose-fn
676+
"Helper for instrument."
677+
[f spec sym {:keys [stub replace]}]
678+
(if (some #{sym} stub)
679+
(-> spec gen gen/generate)
680+
(get replace sym f)))
681+
682+
(defn- instrument-choose-spec
683+
"Helper for instrument"
684+
[spec sym {overrides :spec}]
685+
(get overrides sym spec))
686+
687+
(defn- as-seqable
688+
[x]
689+
(if (seqable? x) x (list x)))
690+
691+
(defn- instrument-1
692+
[s opts]
693+
(when-let [v (resolve s)]
694+
(let [spec (get-spec v)
695+
{:keys [raw wrapped]} (get @instrumented-vars v)
696+
current @v
697+
to-wrap (if (= wrapped current) raw current)
698+
ospec (c/or (instrument-choose-spec spec s opts)
699+
(throw (no-fn-spec v spec)))
700+
ofn (instrument-choose-fn to-wrap ospec s opts)
701+
checked (spec-checking-fn v ofn ospec)]
702+
(alter-var-root v (constantly checked))
703+
(swap! instrumented-vars assoc v {:raw to-wrap :wrapped checked}))
704+
(->sym v)))
705+
674706
(defn instrument
675-
"Instruments the var at v, a var or symbol, to check specs
676-
registered with fdef. Wraps the fn at v to check the :args
677-
spec, if it exists, throwing an ex-info with explain-data if a
678-
check fails. Idempotent."
679-
[v]
680-
(let [v (->var v)
681-
spec (get-spec v)]
682-
(if (fn-spec? spec)
683-
(locking instrumented-vars
684-
(let [{:keys [raw wrapped]} (get @instrumented-vars v)
685-
current @v]
686-
(when-not (= wrapped current)
687-
(let [checked (spec-checking-fn v current)]
688-
(alter-var-root v (constantly checked))
689-
(swap! instrumented-vars assoc v {:raw current :wrapped checked}))))
690-
v)
691-
(throw (no-fn-spec v spec)))))
707+
"Instruments the vars named by sym-or-syms, a symbol or a
708+
collection of symbols. Idempotent.
709+
710+
If a var has an :args fn-spec, sets the var's root binding to a
711+
fn that checks arg conformance (throwing an exception on failure)
712+
before delegating to the original fn.
713+
714+
The opts map can be used to override registered specs, and/or to
715+
replace fn implementations entirely:
716+
717+
:spec a map from fn symbols to spec overrides
718+
:stub a collection of fn symbols to stub
719+
:replace a map from fn symbols to fn overrides
720+
721+
:spec overrides registered fn-specs with specs your provide. Use
722+
:spec overrides to provide specs for libraries that do not have
723+
them, or to constrain your own use of a fn to a subset of its
724+
spec'ed contract.
725+
726+
:stub replaces a fn with a stub that checks :args, then uses the
727+
:ret spec to generate a return value.
728+
729+
:replace replaces a fn with a fn that check :args, then invokes
730+
a fn you provide, enabling arbitrary stubbing and mocking.
731+
732+
:spec can be used in combination with :stub or :replace.
733+
734+
Opts for symbols not named by sym-or-syms are ignored. This
735+
facilitates sharing a common options map across many different
736+
calls to instrument.
737+
738+
Returns a collection of syms naming the vars instrumented."
739+
([sym-or-syms] (instrument sym-or-syms nil))
740+
([sym-or-syms opts]
741+
(locking instrumented-vars
742+
(into
743+
[]
744+
(comp (map #(instrument-1 % opts))
745+
(remove nil?))
746+
(as-seqable sym-or-syms)))))
747+
748+
(defn- unstrument-1
749+
[s]
750+
(when-let [v (resolve s)]
751+
(when-let [{:keys [raw wrapped]} (get @instrumented-vars v)]
752+
(let [current @v]
753+
(when (= wrapped current)
754+
(alter-var-root v (constantly raw))))
755+
(swap! instrumented-vars dissoc v))
756+
(->sym v)))
692757

693758
(defn unstrument
694-
"Undoes instrument on the var at v, a var or symbol. Idempotent."
695-
[v]
696-
(let [v (->var v)]
697-
(locking instrumented-vars
698-
(when-let [{:keys [raw wrapped]} (get @instrumented-vars v)]
699-
(let [current @v]
700-
(when (= wrapped current)
701-
(alter-var-root v (constantly raw))))
702-
(swap! instrumented-vars dissoc v))
703-
v)))
704-
705-
(defn speced-vars
706-
"Returns the set of vars whose namespace is in ns-syms AND
707-
whose vars have been speced with fdef. If no ns-syms are
708-
specified, return speced vars from all namespaces."
709-
[& ns-syms]
710-
(let [ns-match? (if (seq ns-syms)
711-
(set (map str ns-syms))
712-
(constantly true))]
713-
(reduce-kv
714-
(fn [s k _]
715-
(if (c/and (symbol? k)
716-
(ns-match? (namespace k)))
717-
(if-let [v (resolve k)]
718-
(conj s v)
719-
s)
720-
s))
721-
#{}
722-
(registry))))
759+
"Undoes instrument on the vars named by sym-or-syms. Idempotent.
760+
Returns a collection of syms naming the vars unstrumented."
761+
[sym-or-syms]
762+
(locking instrumented-vars
763+
(into
764+
[]
765+
(comp (map #(unstrument-1 %))
766+
(remove nil?))
767+
(as-seqable sym-or-syms))))
768+
769+
(defn- opt-syms
770+
"Returns set of symbols referenced by 'instrument' opts map"
771+
[opts]
772+
(reduce into #{} [(:stub opts) (c/keys (:replace opts)) (c/keys (:spec opts))]))
773+
774+
(defn- ns-matcher
775+
[ns-syms]
776+
(let [ns-names (into #{} (map str) ns-syms)]
777+
(fn [s]
778+
(contains? ns-names (namespace s)))))
723779

724780
(defn instrument-ns
725-
"Call instrument for all speced-vars in namespaces named
726-
by ns-syms. Idempotent."
727-
[& ns-syms]
728-
(when (seq ns-syms)
729-
(locking instrumented-vars
730-
(doseq [v (apply speced-vars ns-syms)]
731-
(instrument v)))))
781+
"Like instrument, but works on all symbols whose namespace is
782+
in ns-or-nses, specified as a symbol or a seq of symbols."
783+
([] (instrument-ns (.name ^clojure.lang.Namespace *ns*)))
784+
([ns-or-nses] (instrument-ns ns-or-nses nil))
785+
([ns-or-nses opts]
786+
(let [ns-match? (ns-matcher (as-seqable ns-or-nses))]
787+
(locking instrumented-vars
788+
(into
789+
[]
790+
(comp c/cat
791+
(filter symbol?)
792+
(filter ns-match?)
793+
(distinct)
794+
(map #(instrument-1 % opts))
795+
(remove nil?))
796+
[(c/keys (registry)) (opt-syms opts)])))))
732797

733798
(defn unstrument-ns
734-
"Call unstrument for all speced-vars in namespaces named
735-
by ns-syms. Idempotent."
736-
[& ns-syms]
737-
(when (seq ns-syms)
799+
"Like unstrument, but works on all symbols whose namespace is
800+
in ns-or-nses, specified as a symbol or a seq of symbols."
801+
[ns-or-nses]
802+
(let [ns-match? (ns-matcher (as-seqable ns-or-nses))]
738803
(locking instrumented-vars
739-
(doseq [v (apply speced-vars ns-syms)]
740-
(unstrument v)))))
804+
(into
805+
[]
806+
(comp (map ->sym)
807+
(filter ns-match?)
808+
(map unstrument-1)
809+
(remove nil?))
810+
(c/keys @instrumented-vars)))))
741811

742812
(defn instrument-all
743-
"Call instrument for all speced-vars. Idempotent."
744-
[]
745-
(locking instrumented-vars
746-
(doseq [v (speced-vars)]
747-
(instrument v))))
813+
"Like instrument, but works on all vars."
814+
([] (instrument-all nil))
815+
([opts]
816+
(locking instrumented-vars
817+
(into
818+
[]
819+
(comp c/cat
820+
(filter symbol?)
821+
(distinct)
822+
(map #(instrument-1 % opts))
823+
(remove nil?))
824+
[(c/keys (registry)) (opt-syms opts)]))))
748825

749826
(defn unstrument-all
750-
"Call unstrument for all speced-vars. Idempotent"
827+
"Like unstrument, but works on all vars."
751828
[]
752829
(locking instrumented-vars
753-
(doseq [v (speced-vars)]
754-
(unstrument v))))
830+
(into
831+
[]
832+
(comp (map ->sym)
833+
(map unstrument-1)
834+
(remove nil?))
835+
(c/keys @instrumented-vars))))
755836

756837
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; impl ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
757838
(defn- recur-limit? [rmap id path k]
@@ -1674,4 +1755,4 @@ by ns-syms. Idempotent."
16741755
~@(when-not NaN? '[#(not (Double/isNaN %))])
16751756
~@(when max `[#(<= % ~max)])
16761757
~@(when min `[#(<= ~min %)]))
1677-
:gen #(gen/double* ~m)))
1758+
:gen #(gen/double* ~m)))

0 commit comments

Comments
 (0)