Skip to content

Instantly share code, notes, and snippets.

@olivergeorge
Last active March 31, 2016 01:38
Show Gist options
  • Save olivergeorge/386ff4a559197dd794c1 to your computer and use it in GitHub Desktop.
Save olivergeorge/386ff4a559197dd794c1 to your computer and use it in GitHub Desktop.
Assert macro patch to show details about failed assertions.

This is an attempt to make assert more friendly by including more details in the assertion error.

  • show the result of the assertion expression
  • show local variable values used in assertion form

In other words, this should make assertion errors easier to reason about:

AssertionError Assert failed: Should be equal
(= x y) => false
 where x is 222
 where y is 1

Two approches are included below which handle fn* pre/post conditions as well as inline asserts.

An easy way to include in your clojure code is:

(defproject my-proj "0.1.0-SNAPSHOT"
  ...
  :injections [(load-file "scripts/patch_assert.clj")])

NOTE: I'd love to know how to do the same for clojurecript but I can't see how. I'd appreciate any advice.

Examples of use:

(defn boom [s]
  {:pre [(string? s)]}
  (str s "!!!"))
=> #'user/boom
(boom "ROAR")
=> "ROAR!!!"
(boom 123)
AssertionError Assert failed: (string? s) => false
 where s is 123
  user/boom (form-init1236645087160261984.clj:1)

(let [x 222 y 1 z (range 10)]
  (assert (= x y ) "Should be equal"))
=> #'clojure.core/assert
AssertionError Assert failed: Should be equal
(= x y) => false
 where x is 222
 where y is 1
  user/eval9905 (form-init1236645087160261984.clj:3)
(let [x 222 y 1 z (range 10)]
  (assert (= 3 (count z)) "Should be 3 long"))
=> #'clojure.core/assert
AssertionError Assert failed: Should be 3 long
(= 3 (count z)) => false
 where z is (0 1 2 3 4 5 6 7 8 9)
  user/eval9806 (form-init1236645087160261984.clj:3)

Test in a repl with something like this:

(load-file "scripts/patch_assert.clj")
(let [x 222 y 1 z (range 10)]
  (assert (= x y ) "Should be equal"))

How it works...

  • defmacro has access to &env which is a map of variables. Using this and the assert form we can lookup relevant variables.
  • lein :injections allows code to be run before your project code. With this we can patch the assert macro before loading project code.
(in-ns 'clojure.core)
(defn show-env
"Macro helper. Walk the form looking for env keys.
NOTE: Could have false positives: any form matching an env key gets printed."
[form env]
(let [xs (tree-seq seq? identity form)
ks (set (keys env))]
(for [k (filter ks xs)]
`(str " where " '~k " is " ~k "\n"))))
(defmacro assert
"Evaluates expr and throws an exception if it does not evaluate to
logical true."
{:added "1.0"}
([x]
`(when-not ~x
(throw (new AssertionError (str "Assert failed: "
(pr-str '~x) " => " (pr-str ~x) "\n"
~@(show-env x &env))))))
([x message]
`(when-not ~x
(throw (new AssertionError (str "Assert failed: " ~message "\n"
(pr-str '~x) " => " (pr-str ~x) "\n"
~@(show-env x &env)))))))
(in-ns 'clojure.core)
;; This version uses ^:var meta to identify things to display in assert statement
;;
;; (let [x ["Connor" "Kurgan"]]
;; (assert (= 1 (count ^:var x)) "there can be only one highlander"))
;;
;; user=> (defn get-highlander [hs]
;; {:pre [(= 1 (count ^:var hs))]}
;; (first hs))
;; #'user/get-highlander
;; user=> (get-highlander ["Connor" "Kurgan"])
;; AssertionError Assert failed: (= 1 (count hs)) => false
;; where hs is ["Connor" "Kurgan"] user/get-highlander (NO_SOURCE_FILE:7)
(defn pr-vars [form env]
(let [var? (fn [x] (-> x meta :var))]
(for [var (filter var? (tree-seq seq? identity form))]
`(str "\n where " '~var " is " ~var ))))
(defmacro assert
"Evaluates expr and throws an exception if it does not evaluate to
logical true."
{:added "1.0"}
([x]
(when *assert*
`(when-not ~x
(throw (new AssertionError (str "Assert failed: "
(pr-str '~x) " => " (pr-str ~x)
~@(pr-vars x &env)))))))
([x message]
(when *assert*
`(when-not ~x
(throw (new AssertionError (str "Assert failed: " ~message "\n"
(pr-str '~x) " => " (pr-str ~x)
~@(pr-vars x &env))))))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment