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

欢迎!请参阅关于页面了解有关本网站如何运作的更多信息。

0 投票
Spec
已关闭

TL;DR
当启用仪表功能时,spec通过使用生成的参数调用实际函数来验证出现在另一个fspec的:args中的fspec,如果它执行副作用,可能会导致不可预期的和不正确的结果。

问题
如果我们有以下函数和规范

(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的规范有args,在这种情况下clojure.alpha.spec.test/instrument将执行以下操作

如果有一个变量有一个:args fn-spec,则设置变量的根绑定为一个函数
该函数在将控制权委托给原始函数之前检查参数是否遵从(在失败时抛出异常)

这意味着它必须检查作为参数传递给函数的函数是否遵从::my-generator规范,该规范本身就是一个fspec。为此,它调用fspec重新实例化的conform*,该实现运行一种快速检查风格的测试,根据:args规范生成随机参数并调用实际函数。这可以通过在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.0版规范中再次审查。

...