2024年Clojure状态调查!中分享您的想法。

欢迎!有关如何操作的更多信息,请参阅关于页面。

0
Spec
已关闭

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))
作为重复项已关闭:在仪表期间禁用fspec验证

1 答案

0

这是一个已知问题,将在规范2中重新审查。

...