Fork me on GitHub
#clojure-spec
<
2016-09-30
>
ag01:09:51

hey guys… anyone here using spec and prop. based testing in clojuresript? I have encountered strange problem. (s/gen) within a test that works perfectly on my machine, on CI machine (same java, lein, node and phantomjs versions) says ""Couldn't satisfy such-that predicate after 100 tries.”,

ag01:09:23

is it possible that a certain spec would work on one machine and fail on other?

ag01:09:01

it’s not like random. I run it many times. it passes on my machine and it ALWAYS fails on CI machine

ag01:09:19

other tests work fine though

ag01:09:24

¯\(ツ)

danielcompton01:09:29

what kind of spec is it?

ag02:09:23

oh… oh… it’s a bit nasty… I need to generate iso-date strings and I did this:

(s/def :date/iso (s/with-gen (s/and string?
                               #(re-matches iso-date-regex %))
                   #(gen'/for [d (gen'/datetime)]
                      #?(:clj  (time-f/unparse local-date-formatter d)
                         :cljs (.format (js/moment (js/Date. d)))))))

ag02:09:30

using test.chuck

bfabry02:09:47

@ag my first guess would be a different timezone setting between CI and your pc

danielstockton06:09:33

I know this has probably been asked 1000 times but what is the prevailing preference for where to put specs? In their own parallel namespace or inline?

stathissideris06:09:19

my impression is inline for function specs, different namespace for “domain” specs

seancorfield06:09:07

But yeah, I think @stathissideris comment reflects what is becoming the most likely best practice at this point. It's what we're doing at World Singles so far...

danielstockton06:09:41

and "domain" spec means specs which are likely to impact more than one namespace and be reused a lot?

seancorfield06:09:04

Data structure specs, as opposed to function specs.

danielstockton06:09:20

Know of any open source projects that are already using it, that I could take a look at?

seancorfield06:09:53

clojure.java.jdbc? 🙂

danielstockton06:09:49

Thanks. Looks like they follow the some.namespace and some.namespace.spec convention instead

danielstockton06:09:40

I was thinking an advantage of in-lining is that it provides some documentation, but am I right that spec adds something to the docstring anyway?

danielstockton06:09:03

s/they/yourself/ i see

bfabry06:09:10

fdef specs are added to the output of doc ya

danielstockton06:09:26

Ok, I might be leaning towards a separate namespace in that case

seancorfield06:09:03

Inlining fdef makes sense for documentation, but not for portability across versions. def for data structures in a separate namespace makes sense regardless.

seancorfield06:09:00

We've generally found if we start to def a data structure spec in a namespace with functions, it's gets messy ... keeping the data structure specs separate is more flexible...

danielstockton06:09:27

I'm worried things will be messy with function specs too, it seems the documentation advantage is somewhat negated by the docstring

seancorfield06:09:35

Not sure what you mean...? Specs just add to the docstring...

danielstockton07:09:34

I mean that if it's added to the docstring then it isn't much of an advantage having it inline as well.

seancorfield07:09:31

Right, yes, but the docstring is only updated if you've loaded the ns with the spec so you might as well have the fdef inline (IMO)

bfabry07:09:37

yeah.. different documentation requirements. documentation for people reading/maintaining the code, documentation for people using the function

yenda07:09:36

Hi, I am trying to use spec to conform the arguments of a function: there is at most 4 arguments with 4 distinct types. I want to conform them into a map but the solution I came up with is not fully compliant because it allows 2 arguments of the same type. Is there a way to make sure there isn't 2 arguments of the same type ?

seancorfield07:09:59

Not sure I follow... can you share some code?

yenda07:09:18

@seancorfield yes sorry you are too quick 😄

seancorfield07:09:29

Your coll-of is a map so each :message item overwrites any prior item before :distinct can be checked.

seancorfield07:09:09

And if you used :into [] you would still accept those two :message arguments because they are different values so they are distinct.

seancorfield07:09:47

The :distinct flag says that all the conformed values must be distinct -- which they are.

seancorfield07:09:22

You'd need your ::argument spec to conform to just the type, not the type and value.

seancorfield07:09:38

Hmm, that doesn't quite work either:

(s/def ::argument (s/and (s/or :message string? :app-data map? :type keyword? :variables vector?) (s/conformer first)))
(s/def ::arguments (s/coll-of ::argument :distinct true :into [] :max-count 4))

seancorfield07:09:57

(s/conform ::arguments ["helo {a} hello"  "hello" ::random-log {:c '(inc c)}])
[:message :message :type :app-data]
Not what I expected.

yenda07:09:12

@seancorfield if I have 2 identical arguments the predicate distinct? is triggered so the distinct check happens before the into {}

yenda07:09:10

however this not really what I want I could remove it, what I would like is to have distinct types for the arguments not distinct arguments

seancorfield07:09:33

Yeah, I think you're right about the :distinct check... it would check distinct values before the conforming.

bfabry07:09:48

there's something from a very long time ago whispering in my ear that this is an irregular grammar, and so you're going to need a custom predicate to validate it. but I could be wrong

bfabry07:09:14

s/irregular/not context free/

seancorfield07:09:18

So you need s/and on ::arguments to force it to be distinct types.

seancorfield07:09:46

(s/def ::arguments (s/and (s/coll-of ::argument :into [] :max-count 4) (s/coll-of keyword? :distinct true)))

seancorfield07:09:57

(s/conform ::arguments ["helo {a} hello"  "hello" ::random-log {:c '(inc c)}])
:clojure.spec/invalid

seancorfield07:09:15

(s/conform ::arguments ["helo {a} hello" ['b] ::random-log {:c '(inc c)}])
[:message :variables :type :app-data]

seancorfield07:09:42

And that's with

(s/def ::argument (s/and (s/or :message string? :app-data map? :type keyword? :variables vector?) (s/conformer first)))

seancorfield07:09:43

So each argument needs to conform to its type (throwing away the value) and then the arguments collection needs to flow the conformed arguments into a distinct collection

yenda07:09:13

wow thank you it works but it also makes me realize I still have a lot of work to do on spec 🙂

seancorfield07:09:38

Yeah, it's radically changing how we approach several types of problems.

yenda07:09:24

oh ok I understand now because I didn't realize that I don't get the values anymore while conforming

yenda07:09:45

the function I don't get is conformer

seancorfield07:09:27

Took me a while too. conformer uses its function argument to transform the data rather than just being a true/false predicate.

seancorfield07:09:59

But Alex says it's an anti-pattern so be cautious.

yenda08:09:35

well I'll play a bit to see if I can keep my conformed map and still have the validation on distinct keywords

yenda08:09:34

btw do I misunderstand unform ? I thought this call (s/unform ::x (s/conform ::x data-to-conform)) would return data-to-conform but it just returns the conformed data

yenda08:09:22

@seancorfield is the explanation on why it is an anti-pattern available somewhere ?

yenda08:09:00

below is the final spec I made thanks to your input that matches my needs, I could eventually just do the map transform outside of the spec iff conformer is an anti-pattern

jmglov08:09:03

@ag Here's how I generate date strings:

(require '[clj-time.core :as t]
         '[clj-time.format :as f])
(import '(org.joda.time.format DateTimeFormatter))

(defn parseable-timestamp
  "Returns a spec for a timestamp string can be parsed with the specified
   datetime formatter."
  [formatter]
  (s/and string?
         (fn [timestamp-str]
           (try
             (f/parse formatter timestamp-str)
             true
             (catch Exception _
               false)))))

(defn make-timestamp-gen
  "Returns a generator for a timestamp string between the minimum year and
   maximum year (exclusive) that can be parsed with the specified datetime
   formatter."
  [min-year max-year formatter]
  (fn []
    (let [year-gen (s/gen (s/int-in min-year max-year))
          month-gen (s/gen (s/int-in 1 12))
          day-gen (s/gen (s/int-in 1 28))
          hour-gen (s/gen (s/int-in 0 23))
          m-s-gen (s/gen (s/int-in 0 59))]
      (->> [year-gen month-gen day-gen hour-gen m-s-gen m-s-gen m-s-gen]
           (map #(gen/fmap vector %))
           (apply gen/cat)
           (gen/fmap #(->> (apply t/date-time %)
                           (f/unparse formatter)))))))

(def timestamp-formatter (f/formatters :date-time-no-ms))

(s/def ::timestamp
  (s/with-gen
    (parseable-timestamp timestamp-formatter)
    (make-timestamp-gen 2000 2050 timestamp-formatter)))

mpenet13:09:03

asking again: I'd like to generate documentation from specs, is there a way to resolve/expand specs from (s/form ...) (or other) ?

mpenet13:09:47

I guess I can hack this walking the (s/registry) + (s/form ...), but that seems a bit "hairy"

mpenet13:09:35

(it's for front-end team, so I cant just spit keywords, i need to resolve the leafs of specs to stuff they understand)

mpenet13:09:43

might be a good fit for some specter juggling

Alex Miller (Clojure team)14:09:57

are you talking about a “deep” version of s/form?

Alex Miller (Clojure team)14:09:52

yeah, we’ve talked about providing that but it doesn’t exist right now

mpenet14:09:19

that'd be nice, it's should be easy enough to write, but that's something one would expect out of the box imho

Alex Miller (Clojure team)14:09:21

I have specs for spec forms (kind of, modulo a number of bugs in s/form) which could help a lot

mpenet14:09:39

sounds good

Alex Miller (Clojure team)14:09:49

because you could conform the spec and then pick the parts per form that you need

Alex Miller (Clojure team)14:09:01

I will probably get around to this soon-ish as I need it for other things

mpenet14:09:11

I didn't actually think about approaching it that way, neat

Alex Miller (Clojure team)14:09:35

having specs for specs opens up all sorts of things

Alex Miller (Clojure team)14:09:19

for example, automated testing of spec by generating specs from spec specs, then checking conformance on data generated for the generated spec

mpenet14:09:47

that's spec inception

mpenet14:09:56

oh since you're here, I have another question about the conformer + :clojure.spec/invalid issue I mentioned yesterday

Alex Miller (Clojure team)14:09:02

yes, it feels like that :)

Alex Miller (Clojure team)14:09:20

can you repeat? I can’t keep up the slacks

mpenet14:09:34

it used to compile (I think with alpha10)

Alex Miller (Clojure team)14:09:40

yeah, that’s been logged

mpenet14:09:45

probably a result of the recent optimisations

Alex Miller (Clojure team)14:09:50

would have changed as of alpha 11 or 12

mpenet14:09:57

oki, good to hear

Alex Miller (Clojure team)14:09:10

I mean changed as in “started failing”

mpenet14:09:11

I couldn't find a ticket about it in jira

Alex Miller (Clojure team)14:09:24

I’m not sure there is a good solution to it

mpenet14:09:58

oh, isn't this considered a bug?

mpenet14:09:57

it makes writing conformers a bit tricky, I basically have to (def invalid :clojure.spec/invalid) and use this in the body of the functions I am using

Alex Miller (Clojure team)14:09:02

yeah, but wait for a spec on def to break that too :)

Alex Miller (Clojure team)14:09:22

it’s a broader problem

Alex Miller (Clojure team)14:09:58

one possible solution would be to do something like is done in the reader where a reader is handed a token that it can return to indicate a special case

Alex Miller (Clojure team)14:09:45

so rather than being expected to return ::s/invalid, you are given a token that you can return (and that can be a per-instance (Object.))

Alex Miller (Clojure team)14:09:06

that still doesn’t solve the problems around core specs though

Alex Miller (Clojure team)14:09:23

it needs a longer conversation with Rich and he hasn’t had the time to have it

mpenet14:09:54

any hint of what might come in the next alpha?

Alex Miller (Clojure team)14:09:58

whatever we fix or add next :)

Alex Miller (Clojure team)14:09:41

CLJ-2024, CLJ-2027, and CLJ-2026 all have patches that are ready for Stu and Rich to look at so I expect those to be in the next alpha

Alex Miller (Clojure team)14:09:51

(presuming they like them)

mpenet14:09:52

Actually walking the specs isn't really possible in my case: if you do (s/def ::foo ::bar) (s/form ::foo) will have expanded ::bar to a predicate so I cannot check my stop point (since I don't want to expand down to the last bits, I have a set of specs that are the surface I want to expose to the front-end people)

mpenet14:09:24

(s/def ::foo any?)
(s/def ::bar ::foo)
(s/form ::bar)
=> clojure.core/any?

mpenet14:09:13

((s/registry) ::bar) works in that case actually

jrheard19:09:27

i’ve got a somewhat complex map spec that i’m trying to s/exercise, and i’m getting the dreaded "Couldn't satisfy such-that predicate after 100 tries.” error. what are the primary things i should be looking for when tracking down the root cause?

jrheard19:09:44

i’m familiar with s/and and how you should eg do int? first, followed by even?, etc

jrheard19:09:00

is naive s/and usage the primary trigger for this error, or are there other categories of spec misuse that often cause it?

jrheard19:09:27

(if it’s helpful, the spec i’m trying to exercise is https://github.com/jrheard/voke/blob/spec/src/voke/specs.cljs#L74 )

jrheard19:09:59

i’ve only got one s/and in here, and it’s a (s/and number? pos?), so i feel like there must be some other category of thing i’m doing totally incorrectly

jrheard19:09:39

hm, seems to be specific to :component/input

jrheard19:09:52

the documentation on :kind says: "Note that if :kind is specified and :into is not, this pred must generate in order for every to generate.”; and adding :into #{} fixes the issue. guess i need to sit down and take some time to understand the semantics of :kind and :into. thanks all ❤️

jrheard19:09:44

(if you click on those links after coming back from lunch, pretend that the (s/coll-of) calls with :kind specified do not have a corresponding :into; that’s the state the file was in when i linked it, and that’s what the problem was 🙂 )

seancorfield19:09:59

@jrheard Late to your spec party but, yeah, naïve s/and has been my primary trigger for that error — I’ve taken to using s/with-gen quite a lot to "help" clojure.spec exercise stuff.

danielstockton20:09:42

I s/fdefed a function but the doc string doesn't contain my spec. Did I miss something?

kenny21:09:22

Is this the expected behavior?

(require '[clojure.core.specs :as clj-specs])
(def c (s/conform ::clj-specs/defn-args '(t
                                           [x y]
                                           x)))
=> #'boot.user/c
(s/unform ::clj-specs/defn-args c)
=> (t (x y) x)
Shouldn’t the args be unconformed to a vector, not a list?

kenny21:09:32

(let [args '(t
              [x y]
              x)
      args-s ::clj-specs/defn-args
      c (s/conform args-s args)]
  (s/conform args-s (s/unform args-s c)))
=> :clojure.spec/invalid

bfabry21:09:13

::arg-list seems to use s/and not s/coll-of

bfabry21:09:41

still, wouldn't flowing back through vector? cause it to be turned into a vector? shrug