gfredericks / test.chuck

A utility library for test.check
Eclipse Public License 1.0
215 stars 26 forks source link

Add gen-datetime function. #12

Closed firesofmay closed 9 years ago

firesofmay commented 9 years ago

I had a need of generating various date time using generators. Thought this might be a good addition to this library. I am open to suggestions on how this function should behave and what arguments it should take.

Thanks.

gfredericks commented 9 years ago

This sounds cool. I'm a little hesitant about taking on a dependency like clj-time, but I'm not sure if it's worth doing something fancy like runtime-detection of availability. I can't think of a serious concrete downside.

Will be looking at this more closely soon.

firesofmay commented 9 years ago

Ack. Waiting for your response. :)

gfredericks commented 9 years ago

There are some interesting choices here; part of the reason I never added any generator like this is because there seemed to be so many options and I couldn't figure out what was the best approach, so it's good to see somebody less afraid of the problem.

Certainly the biggest problem is handling the infinite domain, and I like that you've made it so configurable. The use of choose is definitely the easiest approach, but has two quasi-downsides:

I think test.chuck's bounded-int is an alternative to choose that addresses the first issue. Addressing the second one might be subtler, but could be informed by figuring out how test.check's int generator shrinks to zero from both sides.

I'm certainly open to thoughts/objections.

gfredericks commented 9 years ago

should also check if bounded-int already shrinks to 0 the way you'd want. I wrote it but I forget how it works :)

firesofmay commented 9 years ago

bounded-int definetly looks better as you suggested. But I am not clear how do you understand how a generator shrinks? This is still a mystery to me.

Can you point out to some material/code/post/video which helps me understand this?

Btw now I understood the use case of bounded-int over choose. I didn't get it earlier but now that I think about it bounded-int is certainly more useful. :)

gfredericks commented 9 years ago

shrinking is done by walking this giant lazy tree that a generator returns. You can write code to return a sample shrink pretty easily:

(defn sample-shrink
  [gen]
  (let [tree (gen/call-gen gen (clojure.test.check.random/make-random) 30)]
    ((fn self [tree] (if (empty? (rose/children tree)) [(rose/root tree)] (cons (rose/root tree) (self (rand-nth (rose/children tree)))))) tree)))

or something

firesofmay commented 9 years ago

Wow this was really interesting!

I rewrote your functions like this for better understanding:

(defn shrink-tree
  "Given a Rose tree it'll shrink the tree
   and return all its node values."
  [tree]
  (if (empty? (rose/children tree))
    [(rose/root tree)]
    (cons (rose/root tree)
          (shrink-tree (rand-nth (rose/children tree))))))

(defn gen-rose-trees
  "Given a generator and max-size it'll generate
   an infinite seq of rose-trees."
  [generator max-size]
  (let [r (random/make-random)
        size-seq (gen/make-size-range-seq max-size)]
    (map #(gen/call-gen generator %1 %2)
         (gen/lazy-random-states r)
         size-seq)))

And then ran this on the repl:

(let [v (take 10 (gen-rose-trees string-ascii 5))]
  (println "Actual values")
  (pprint (mapv rose/root v))
  (println "\nEach tree")
  (pprint (mapv shrink-tree v)))

;;=>
Actual values
["" "," "" "" "[E" "" "E" "" ":1M" "x"]

Each tree
[[""]
 ("," "!" " " "")
 [""]
 [""]
 ("[E" "[=" "[6" "[5" ".5" ".(" ".'" "-'" "'" "#" "\"" " " "")
 [""]
 ("E" "A" "?" "8" "1" "%" "")
 [""]
 (":1M" ":M" "9M" "9L" "9&" "6&" "5&" "5%" "4%" "%" "#" "!" " " "")
 ("x" "q" "p" "b" "\\" "Q" "P" "N" "L" "9" "")]

Correct me if I am wrong, so basically it creates an infinite lazy random number generator & a cycle of values upto max-size, and then maps these over the given generator to generate a list of RoseTrees.

To generate the values you basically map over root of the trees and return a sample sequence. In a test, when it tries to check for the property and one of the values fails, it'll start going down that particular trees children via its root. And that's how it finds the base case for say string generator to give "" string. It stops whenever tests start passing again and returns the last failing nodes root value. Am I right in understanding this?

What I don't understand is, how is this tree being generated? I am unable to follow the chain. Can you guide me in this?

Sorry for being so verbose and kinda off topic, but this just blew my mind :smiley: Thanks a lot :smile:

PS: Do you think it makes sense to add such functions (maybe in a separate ns) which allows you to inspect a generator? We can decide on what kind of functions would be useful. My idea is generators are really opaque beasts. It would be nice to have helper functions to understand whats happening under the hood. And maybe tweak them nicely to play well with shrinking. It's very easy to write generators which won't shrink well or whose behaviour is not exactly what you needed. And such helper functions would be helpful in this regard. Thoughts?

gfredericks commented 9 years ago

Your understanding of how the shrink-tree gets used sounds correct. How it gets generated is combinatorial -- the base generator is gen/choose, and it creates a tree for shrinking a single number by first trying 0, then n/2, then 3n/4, until it gets up to n-1 or so; the children's children are generated recursively like that.

All of the other generators do some sort of combinatorial thing that's natural for them. E.g., the vector generator will make some of the tree's children by shrinking the elements of the vector and others by removing elements from the vector. I.e., in the same way that those generator combinators take other generator[s] and wrap the things they generate, they also take the returned rose tree[s] and wrap them in some similar way. If you want to dig into that more, you could e.g. start with fmap or such-that and try to follow the code all the way through.

I can definitely imagine some helper functions for developing/debugging generators. Not sure if they belong in test.check.generators, test.check.something-else, or test.chuck.

gfredericks commented 9 years ago

Looks like bounded-int does shrink to zero, and glancing over your code I think if you just drop that in in place of choose you'll get the better behavior.

firesofmay commented 9 years ago

Done. Pushed the change. Didn't get the time to investigate how trees are made. Will do in few days. I think bounded-int should be good for now. Thanks a lot!

gfredericks commented 9 years ago

Should be good to merge now, I'll take one last look soon. Thanks again!

firesofmay commented 9 years ago

Hey, Any updates on this? :)

gfredericks commented 9 years ago

It looks good -- thanks again, I'm pretty happy with it and will probably end up using it myself shortly.