Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I get Clojure :pre & :post to report their failing value?

(defn string-to-string [s1]    {:pre  [(string? s1)]    :post [(string? %)]}   s1) 

I like :pre and :post conditions, they allow me to figure out when I have put "square pegs in round holes" more quickly. Perhaps it is wrong, but I like using them as a sort of poor mans type checker. This isn't philosophy though, this is a simple question.

It seems in the above code that I should easily be able to determine that s1 is a function argument in the :pre condition. Similarily, % in the :post condition is always the function return value.

What I would like is to print the value of s1 or % when either of these respective conditions fail within the AssertionError. So I get something like

(string-to-string 23)  AssertionError Assert failed: (string? s1)  (pr-str s1) => 23  

With the AssertionError containing a single line for every variable that was identified as being from the function argument list and that was referenced in the failing test. I would also like something similar when the return value of the function fails the :post condition.

This would make it trivial to quickly spot how I misused a function when trying to diagnose from the AssertionError. It would at least let me know if the value is nil or an actual value (which is the most common error I make).

I have some ideas that this could be done with a macro, but I was wondering if there was any safe and global way to basically just redefine what (defn and (fn and friends do so that :pre and :post would also print the value(s) that lead to the test failing.

like image 451
Stephen Cagle Avatar asked Jul 18 '14 21:07

Stephen Cagle


2 Answers

You could wrap your predicate with the is macro from clojure.test

(defn string-to-string [s1]    {:pre  [(is (string? s1))]    :post [(is (string? %))]}  s1) 

Then you get:

(string-to-string 10) ;FAIL in clojure.lang.PersistentList$EmptyList@1 (scratch.clj:5) ;expected: (string? s1) ;actual: (not (string? 10)) 
like image 112
optevo Avatar answered Oct 18 '22 04:10

optevo


@octopusgrabbus kind of hinted at this by proposing (try ... (catch ...)), and you mentioned that that might be too noisy, and is still wrapped in an assert. A simpler and less noisy variant of this would be a simple (or (condition-here) (throw-exception-with-custom-message)) syntax, like this:

(defn string-to-string [s1]    {:pre  [(or (string? s1)               (throw (Exception. (format "Pre-condition failed; %s is not a string." s1))))]    :post [(or (string? %)               (throw (Exception. (format "Post-condition failed; %s is not a string." %))))]}   s1) 

This essentially lets you use pre- and post-conditions with custom error messages -- the pre- and post-conditions are still checked like they normally would be, but your custom exception is evaluated (and thus thrown) before the AssertionError can happen.

like image 20
Dave Yarwood Avatar answered Oct 18 '22 05:10

Dave Yarwood