Fork me on GitHub
#clojure-spec
<
2016-09-14
>
hiredman00:09:08

it seems like that could end up mixing testing code in with, production code (for lack of a better description)

hiredman00:09:35

is the idea that people will do similar lazying loading kind of stuff like clojure.spec does for test.check?

seancorfield00:09:19

"mixing testing code in with production code" … how? It’s not the case with how WS is using spec at the moment.

seancorfield00:09:46

Our spec namespaces have just specs in them — which our (production) code uses for explicit validation and conforming of data at each "system" boundary (input; domain; persistence).

seancorfield00:09:35

In addition, our test code can pull in the specs and instrument and/or check code. Any test-specific code would live in the test (expectations) namespace, not the main (production) namespace.

seancorfield00:09:24

(it’s certainly possible I’ve missed some subtlety here but…)

hiredman00:09:50

do you have specs with custom generators?

hiredman00:09:24

or, I guess I should say, do you define your own predicates with generators? it seems like that would depend at least on test.check, so you would either need to do lazy loading or have a runtime dependency on test.check

hiredman00:09:30

I haven't used spec in anger, so I dunno how it plays out, just something I wonder about

bfabry00:09:16

@hiredman you can specify the generators in the spec.test/check or instrument call, instead of with the spec itself

seancorfield00:09:25

We lazy-load via the functions that return generators (and do so in a separate ns):

;; copyright (c) 2016 world singles llc

(ns ws.spec.generators
  "Helpers for our specs that need custom generators.
  Dynamically loads the namespaces as needed so we can compiler
  this without test dependencies.")

(defn fn-string-from-regex
  "Return a function that produces a generator for the given
  regular expression string."
  [regex]
  (fn []
    (require '[com.gfredericks.test.chuck.generators :as xgen])
    (let [string-from-regex (resolve 'xgen/string-from-regex)]
      (string-from-regex regex))))

seancorfield00:09:51

Ooh, typo. compiler instead of compile...

seancorfield00:09:10

We have a few with-gens that leverage stuff in clojure.spec or code that’s already available in production, otherwise we defer to our ws.spec.generators "proxy" namespace.

seancorfield00:09:06

But, yeah, ensuring those sort of test artifacts only get loaded when we’re testing was something we had to navigate at first...

seancorfield00:09:39

In our build.boot we have

(deftask testing-context
  "Provide main testing context."
  []
  (merge-env! :dependencies '[[com.gfredericks/test.chuck "0.2.7" :scope "test"
                               :exclusions [*/*]]
                              [javax.servlet/servlet-api "2.5" :scope "test"]
                              [org.clojure/test.check "0.9.0" :scope "test"]])
  identity)

seancorfield00:09:16

and that’s only used by our test tasks

bnoguchi00:09:52

I have been having trouble spec’ing an function arg to another function where the former can be either a function with arity 0 or a function with arity 1 (both not variadic functions). Any ideas?

(s/explain (s/fspec :args (s/alt :arity-0 (s/cat) :arity-1 (s/cat :arg1 any?)) :ret any?) (fn []))
; val: (nil) fails predicate: (apply fn),  Wrong number of args (1) passed to: user/eval16844/fn--16845

bfabry00:09:43

@bnoguchi your spec seems to be working. you said the function should accept 1 or 0, your function only accepts 1

hiredman00:09:04

I think you need to pull the alt out

bfabry00:09:17

@bnoguchi fwiw there's a briefer way to describe that spec: (s/cat :arg1 (s/? any?))

bnoguchi00:09:06

@bfabry It’s written to alternatively accept 1 or 0.

bfabry00:09:27

@bnoguchi your spec is, but the anonymous function (fn [] ) only accepts 0 arguments

hiredman00:09:52

(s/explain (s/alt :a0 (s/fspec :args (s/cat) :ret any?) :a1 (s/cat :a1 any?)) (fn []))

bfabry00:09:59

boot.user=> (s/explain (s/fspec :args (s/cat :arg1 (s/? any?)) :ret any?) (fn []))
val: (nil) fails predicate: (apply fn),  Wrong number of args (1) passed to: user/eval2135/fn--2136
nil
boot.user=> (s/explain (s/fspec :args (s/cat :arg1 (s/? any?)) :ret any?) (fn [a]))
val: () fails predicate: (apply fn),  Wrong number of args (0) passed to: user/eval2260/fn--2261
nil
boot.user=> (s/explain (s/fspec :args (s/cat :arg1 (s/? any?)) :ret any?) (fn [& a]))
Success!
nil

bnoguchi00:09:40

@bfabry That is not what I’m after, however

bnoguchi00:09:58

i.e., I don’t want to spec a variadic function. Perhaps this form will help

hiredman00:09:59

(s/explain (s/alt :a0  (s/fspec :args (s/cat) :ret any?) :a1 (s/fspec :args (s/cat :a1 any?) :ret any?)) (fn []))

hiredman00:09:22

if you have two fspecs, with the alt outside, then it seems like you get he behavior you want

hiredman00:09:18

I think that makes sense, but it is hard to write the distinction in prose

hiredman00:09:54

a function that takes 0 or 1 arg vs a function that takes 0 args, or a function that takes 1 arg

bfabry00:09:57

it's not that it's variadic, it's that a variadic function satisfies the spec. so does a function that is arity 1 or 0

boot.user=> (defn foo
       #_=>  ([] nil)
       #_=>  ([a] nil))
#'boot.user/foo
boot.user=> (s/explain (s/fspec :args (s/cat :arg1 (s/? any?)) :ret any?) foo)
Success!
nil

hiredman00:09:52

oh, am I am just wrong, and not paying close enough attention

hiredman00:09:16

user=> (s/explain (s/or :a0  (s/fspec :args (s/cat) :ret any?) :a1 (s/fspec :args (s/cat :a1 any?) :ret any?)) (fn []))
Success!
nil
user=> 

hiredman00:09:35

there we go, of course it needs to be s/or not s/alt

hiredman00:09:47

(outside of matching the arglist

bfabry00:09:39

oh I'm sorry you wanted to spec either one or the other, not a function that expects both. in that case yes you need two fspec's (two different fiunction specs) not one fspec (a single function that does two things)

bnoguchi00:09:46

(defn arity [f] (-> f class .getDeclaredMethods first .getParameterTypes alength))

(defn fn-expecting-a-fn-arg-either-0-or-1-arity [func] (if (zero? (arity func)) (func) (func 1)))

(s/fdef fn-expecting-a-fn-arg-either-0-or-1-arity :args (s/cat :fn (s/fspec :args (s/or :arity-0 (s/cat) :arity-1 (s/cat :arg1 any?)) :ret any?)) :ret any?)

(require '[clojure.spec.test :as stest])

(stest/instrument `fn-expecting-a-fn-arg-either-0-or-1-arity)

(fn-expecting-a-fn-arg-either-0-or-1-arity (fn []))
throws
ExceptionInfo Call to #'user/fn-expecting-a-fn-arg-either-0-or-1-arity did not conform to spec:
val: (#object[user$eval16875$fn__16876 0x5fa44d5a "user$eval16875$fn__16876@5fa44d5a"]) fails at: [:args] predicate: ifn?
:clojure.spec/args  (#object[user$eval16875$fn__16876 0x5fa44d5a "user$eval16875$fn__16876@5fa44d5a"])
:clojure.spec/failure  :instrument
:clojure.spec.test/caller  {:file "form-init4211676892686280453.clj", :line 1, :var-scope user/eval16875}
  clojure.core/ex-info (core.clj:4725)

hiredman00:09:51

you can't put the choice (alt or or) in the args for the function, because that means the function needs to satisfy both

bfabry00:09:57

@hiredman's example is correct, you want to s/or two s/fspec's

hiredman00:09:30

you have to spec the possible arguments distinctly in different f/specs

bnoguchi01:09:59

@hiredman Yeah I think you’re right

bnoguchi01:09:14

This works

(s/fdef fn-expecting-a-fn-arg-either-0-or-1-arity :args (s/alt :arity-0 (s/fspec :args (s/cat) :ret any?) :arity-1 (s/fspec :args (s/cat :arg1 any?) :ret any?)) :ret any?)

jetzajac11:09:50

Hello here! I’m trying to use spec with a DataScript app. And I want to validate several different things: 1) tx-data in a map form 2) results of a pull 3) entities In these 3 flavours of data keywords may have different sorts of values. like it’s ok to have an int as a value of ref attr in tx-data, not in a pull. It’s ok to miss some attrs in pull result, not in a tx-data with negative id (creating a new entity, we have a notion of required attrs). And the question is if we can have different spaces of specs? like separate registries and somehow refer them in spec and make it narrow down to this space. Say, space of tx-data and pulls that have to be validated differently. Not sure if I described the demand clear, bit hope I could find somebody who does these kind of things or be clarified why it’s impossible or is a bad idea. Thanx.

jetzajac12:09:47

not sure if I like the idea to have one (private) atom for registry. why not a dynamic binding?

jmglov12:09:53

@bahulneel @otfrom This is what I'm using in tests so far:

(defn- check [function num-tests]
  (if num-tests
    (stest/check function {:clojure.spec.test.check/opts {:num-tests num-tests}})
    (stest/check function)))

(defn checking
  ([function]
   (checking function nil))
  ([function num-tests]
   (testing "Schema"
     (let [result (-> (check function num-tests)
                      first
                      :clojure.spec.test.check/ret
                      :result)]
       (if (true? result)
         (is result)
         (is (= {} (ex-data result))))))))

(defn fails-spec [f & args]
  (try
    (let [res (apply f args)]
      (is (instance? Exception res)))
    (catch Exception e
      (is (contains? (ex-data e) :clojure.spec/problems)))))

jmglov12:09:33

And then tests can look like this:

(deftest plus
  (checking 'foo/plus)
  (testing "Adding 0"
    (is (= 1 (foo/plus 1 0))))
  (testing "Invalid input"
    (fails-spec foo/plus 1 :forty-two)))

jmglov12:09:35

Is there an "any" spec?

bahulneel12:09:45

@jmglov I've found that i need to extend the (if (true? result) ... to

(cond
          (true? result) (t/is result)
          (nil? (ex-data result)) (t/is (= {} result))
          :else (t/is (= {} (ex-data result))))

jmglov12:09:56

I see that @esp1 pointed out that :ret is required for fdef now, and I'm trying to spec a function for which the return value is effectively Unit.

jmglov12:09:16

So I don't care what it returns, only that spec is happy with it. 😉

bahulneel12:09:02

@jmglov What about a predicate like (constantly true)

bahulneel12:09:46

(def any? (constantly true))

jmglov12:09:20

Probably shouldn't call it any?, though. 😉

jmglov12:09:51

user> any?
#function[clojure.core/any?]

bahulneel12:09:52

yeah, that was more for illustration, best to make it explicit and not hide behind a name

jmglov12:09:24

I actually like naming it. I think that makes the intention quite clear. I'll probably just call it unit.

jmglov12:09:29

Clear enough to me.

jmglov12:09:23

Thanks for that! It's a nice, simple solution.

bahulneel12:09:24

@jmglov any plans to throw that testing code into a little library to house test helpers?

jmglov12:09:13

@bahulneel I put it in its own namespace, but I'm not planning to lib it up, no.

jmglov12:09:33

Not sure the Clojure community needs yet another library of utility functions. 😉

jmglov12:09:30

I figure best practises will emerge as more people start mixing specs into their tests.

bahulneel12:09:57

fair enough. Maybe a GIST would do the trick, at least then it's updatable and discoverable

jmglov12:09:01

I'll stick it in a Gist.

jmglov12:09:13

Haha, great minds think alike.

jmglov12:09:19

I'll do that.

bahulneel12:09:21

also if you put a namespace declaration at the top then, in theory, you could include it as a git submodule in your source tree

bahulneel13:09:03

@jmglov thanks, I've added it as a submodule and then added the directory to my test-paths

jmglov13:09:31

Please ping me as you find improvements.

madstap13:09:07

There is already a spec utils library that @gfredericks made... https://github.com/gfredericks/schpec

madstap13:09:47

Just throwing that out there, maybe useful to collect things like these in one place

jmglov13:09:25

@madstap Thanks for the heads up!

bahulneel14:09:01

@jmglov I noticed that you need to require [clojure.test.check.clojure-test] so that it's loded as some useful multimethods are added

kkruit16:09:21

Is there any reason to not use instrument for validation if the behavior I want is for the function to throw an error if the data passed is invalid?

kkruit16:09:51

(in production)

Alex Miller (Clojure team)16:09:42

no (as long as it meets your perf concerns)

kkruit16:09:25

I'm thinking it wouldn't be less performant than doing a verify?, throw, and explain, would it?

kkruit16:09:04

I can run some tests...

Alex Miller (Clojure team)16:09:10

with alpha12, it’s pretty fast but test it and see

kkruit16:09:50

Will do. Thanks!

kkruit16:09:12

and by verify? i meant valid?...

kkruit21:09:47

So I'm looking at using spec in place of schema for a rest api and am running into a question about the best way to handle something. I have something like this:

kkruit21:09:33

I like using :ns/map-key in combination with :req-un like that but I don't like how long the namespace is/could get any thoughts? Am i shoehorning :ns/map-key somewhere where it shouldn't go?

arohner21:09:31

@kkruit you can alias namespaces using :require

arohner21:09:00

(:require [my.controller.spec.update :as c])  (s/def ::c/body …))

kkruit21:09:32

doh! I was trying to do:

kkruit21:09:43

(:require [my.controller.spec.update :as c]) (s/def :c/body …))

arohner21:09:41

that would look for an unaliased namespace named ‘c, rather than resolving the alias ‘c

stuartmitchell22:09:25

Is this a known issue

(def common-attributes [::id ::type ::from-date ::to-date ::style])

(s/def ::common
  ;; defines the common parts of an activity
  (s/keys :req-un common-attributes))
Gives this compiler error
----  Could not Analyze  src/day8/plan8/spec/activity.cljs   line:24  column:3  ----

  Don't know how to create ISeq from: clojure.lang.Symbol

  22  (s/def ::common
  23    ;; defines the common parts of an activity
  24    (s/keys :req-un common-attributes))
        ^--- Don't know how to create ISeq from: clojure.lang.Symbol
  25  

seancorfield22:09:26

s/keys is a macro so it doesn’t evaluate its arguments (so this is by design)

stuartmitchell22:09:04

strangely this works

(def common-attributes [::id ::type ::from-date ::to-date ::style])

(s/def ::common
  ;; defines the common parts of an activity
  (s/keys :req-un (concat common-attributes)))

stuartmitchell22:09:15

but will it fail later on?

seancorfield22:09:25

It does not do what you think it does.

seancorfield22:09:45

(I’m a bit surprised it conforms to spec’s specs — are you trying this on Alpha 12 or an earlier version?)

stuartmitchell22:09:58

1.8.0 I'll bump it up

stuartmitchell22:09:03

but how should we compose specs i.e. I want spec ::b to be spec ::a with some extra keys

stuartmitchell22:09:40

this is what I wanted

(def common-attributes [::id ::type ::from-date ::to-date ::style])

(s/def ::common
  ;; defines the common parts of an activity
  (s/keys :req-un common-attributes))

(s/def ::tv
  ;; defines the attributes for a tv activity
  (s/keys :req-un (concat common-attributes [::daymask ::tarp ::cpt ::rate
                                              ::market])))

seancorfield22:09:51

Oh, so you’re using the backport of clojure.spec rather than the official version?

seancorfield22:09:35

You want clojure.spec/merge for merging key specs.

seancorfield22:09:58

boot.user=> (doc s/merge)
-------------------------
clojure.spec/merge
([& pred-forms])
Macro
  Takes map-validating specs (e.g. 'keys' specs) and
  returns a spec that returns a conformed map satisfying all of the
  specs.  Unlike 'and', merge can generate maps satisfying the
  union of the predicates.

seancorfield22:09:11

(note: also a macro)

stuartmitchell23:09:58

Thx, I also tried the concat version on 1.9.0-alpha12 and it didn't give a compiler error however I am using clojurescript as well so someone should probably try it on vanilla java, if indeed it should fail spec's specs

Alex Miller (Clojure team)23:09:32

there are no included spec specs

Alex Miller (Clojure team)23:09:44

I’ve made some but they are a bit out of date and unclear whether we will release them. there are some recursion issues with checking specs in spec too. :)

seancorfield23:09:31

@alexmiller Am I right that (s/keys :req-un (concat common-attributes)) is going to create a spec where the two required keys are the symbols concat and common-attributes? And that will error out when used…?