TL;DR
当启用仪表时,spec通过使用生成的参数调用实际函数来验证出现在另一个fspec的:args中的fspec,这如果在执行副作用时可能导致意外和不正确的结果。
问题
如果我们有以下函数和spec
(defn create-generator []
(let [counter (atom [])]
(fn [& _]
(swap! counter inc))))
(s/def ::my-generator (s/fspec :args (s/cat :identifier-arguments (s/* string?))
:ret integer?))
(defn my-fn [generator]
(generator))
(s/fdef my-fn
:args (s/cat :my-context ::my-generator)
:ret string?)
调用(my-fn (create-generator))将返回不同结果,具体取决于仪表是否启用。
启用仪表时,我们的函数被调用21次,这比预期的单次调用多(提示:这个数字来自clojure.alpha.spec/fspec-iterations)。
这是因为my-fn的spec有args,在这种情况下clojure.alpha.spec.test/instrument会执行以下操作
如果变量有一个:args fn-spec,将变量的根绑定设置为一个函数
该函数检查arg的一致性(在失败时抛出异常)然后在
委托到原始函数。
这意味着它必须检查接收到的函数是否符合::my-generator spec,这是一个自身也是fspec的spec。为此,它调用fspec的实现conform*,该实现运行一个快速检查风格测试,根据:args spec生成随机参数并调用实际函数。可以通过在create-generator返回的函数中创建异常来观察这一点,堆栈跟踪如下所示
'[[clojure.test_clojure.fspec_bug_test$create_generator$fn__5468 doInvoke "fspec_bug_test.clj" 4]
[clojure.lang.RestFn applyTo "RestFn.java" 137]
[clojure.core$apply invokeStatic "core.clj" 665]
[clojure.core$apply invoke "core.clj" 660]
[clojure.alpha.spec.impl$call_valid_QMARK_ invokeStatic "impl.clj" 1616]
[clojure.alpha.spec.impl$call_valid_QMARK_ invoke "impl.clj" 1612]
[clojure.alpha.spec.impl$validate_fn$fn__4117 invoke "impl.clj" 1627]
[clojure.lang.AFn applyToHelper "AFn.java" 154]
[clojure.lang.AFn applyTo "AFn.java" 144]
[clojure.core$apply invokeStatic "core.clj" 665]
[clojure.core$apply invoke "core.clj" 660]
[clojure.test.check.properties$apply_gen$fn__5403$fn__5404 invoke "properties.cljc" 31]
[clojure.test.check.properties$apply_gen$fn__5403 invoke "properties.cljc" 30]
[clojure.test.check.rose_tree$fmap invokeStatic "rose_tree.cljc" 77]
[clojure.test.check.rose_tree$fmap invoke "rose_tree.cljc" 73]
[clojure.test.check.generators$fmap$fn__4871 invoke "generators.cljc" 104]
[clojure.test.check.generators$gen_fmap$fn__4845 invoke "generators.cljc" 59]
[clojure.test.check.generators$call_gen invokeStatic "generators.cljc" 43]
[clojure.test.check.generators$call_gen invoke "generators.cljc" 39]
[clojure.test.check$quick_check invokeStatic "check.cljc" 211]
[clojure.test.check$quick_check doInvoke "check.cljc" 59]
[clojure.lang.RestFn invoke "RestFn.java" 425]
[clojure.lang.AFn applyToHelper "AFn.java" 156]
[clojure.lang.RestFn applyTo "RestFn.java" 132]
[clojure.core$apply invokeStatic "core.clj" 665]
[clojure.core$apply invoke "core.clj" 660]
[clojure.alpha.spec.gen$quick_check invokeStatic "gen.clj" 32]
[clojure.alpha.spec.gen$quick_check doInvoke "gen.clj" 30]
[clojure.lang.RestFn invoke "RestFn.java" 421]
[clojure.alpha.spec.impl$validate_fn invokeStatic "impl.clj" 1628]
[clojure.alpha.spec.impl$validate_fn invoke "impl.clj" 1623]
[clojure.alpha.spec.impl$fspec_impl$reify__4124 conform_STAR_ "impl.clj" 1646]
[clojure.alpha.spec$conform invokeStatic "spec.clj" 245]
[clojure.alpha.spec$conform invoke "spec.clj" 237]
[clojure.alpha.spec$conform invokeStatic "spec.clj" 241]
[clojure.alpha.spec$conform invoke "spec.clj" 237]
[clojure.alpha.spec.impl$dt invokeStatic "impl.clj" 219]
[clojure.alpha.spec.impl$dt invoke "impl.clj" 214]
[clojure.alpha.spec.impl$dt invokeStatic "impl.clj" 215]
[clojure.alpha.spec.impl$dt invoke "impl.clj" 214]
[clojure.alpha.spec.impl$deriv invokeStatic "impl.clj" 1426]
[clojure.alpha.spec.impl$deriv invoke "impl.clj" 1420]
[clojure.alpha.spec.impl$deriv invokeStatic "impl.clj" 1434]
[clojure.alpha.spec.impl$deriv invoke "impl.clj" 1420]
[clojure.alpha.spec.impl$re_conform invokeStatic "impl.clj" 1564]
[clojure.alpha.spec.impl$re_conform invoke "impl.clj" 1555]
[clojure.alpha.spec.impl$as_regex_spec$fn__3849 invoke "impl.clj" 1238]
[clojure.alpha.spec.protocols$eval1958$fn__2056$G__1939__2067 invoke "protocols.clj" 11]
[clojure.alpha.spec$conform invokeStatic "spec.clj" 245]
[clojure.alpha.spec$conform invoke "spec.clj" 237]
[clojure.alpha.spec$conform invokeStatic "spec.clj" 241]
[clojure.alpha.spec$conform invoke "spec.clj" 237]
[clojure.alpha.spec.test$spec_checking_fn$conform_BANG___4398 invoke "test.clj" 131]
[clojure.alpha.spec.test$spec_checking_fn$fn__4400 doInvoke "test.clj" 150]
[clojure.lang.RestFn invoke "RestFn.java" 408]
[clojure.test_clojure.fspec_bug_test$eval5473 invokeStatic "fspec_bug_test.clj" 2]
[clojure.test_clojure.fspec_bug_test$eval5473 invoke "fspec_bug_test.clj" 44]
...]
当然,可以将问题简化为conform*的fspec实现,因为仅调用s/valid?就会显示被调用的函数的行为,但这很罕见,而函数的注释和在测试期间启用仪表是常见做法,这就是为什么这个PR中的例子展示了fspec当前实现的特定问题场景。
; This already shows the actual function being called, but is an unrealistic example
(s/valid? ::my-generator (fn [& args]
(println args)
1))