Share your thoughts in the 2024 State of Clojure Survey!

Welcome! Please see the About page for a little more info on how this works.

0 votes
in Errors by
Here is an implementation you can paste into a repl. Feedback wanted:


(defn ^{:private true} local-bindings
  "Produces a map of the names of local bindings to their values."
  [env]
  (let [symbols (map key env)]
    (zipmap (map (fn [sym] `(quote ~sym)) symbols) symbols)))

(defmacro assert
  "Evaluates expr and throws an exception if it does not evaluate to
 logical true."
  {:added "1.0"}
  [x]
  (when *assert*
    (let [bindings (local-bindings &env)]
      `(when-not ~x
         (let [sep# (System/getProperty "line.separator")]
           (throw (AssertionError. (apply str "Assert failed: " (pr-str '~x) sep#
                                          (map (fn [[k# v#]] (str "\t" k# " : " v# sep#)) ~bindings)))))))))

15 Answers

0 votes
by

Comment made by: importer

fogus said: Hmmm, but that fails entirely for: (let (link: x 1 y 2 z 3 a 1 b 2 c 3) (assert (= (link: x y) (link: a c)))). So maybe it's better just to print all of the locals unless you really want to get complicated.
:f

0 votes
by

Comment made by: importer

Converted from http://www.assembla.com/spaces/clojure/tickets/415

0 votes
by

Comment made by: importer

alexdmiller said: A simple example I tried for illustration:

`user=> (let [a 1 b 2] (assert (= a b)))

<CompilerException java.lang.AssertionError: Assert failed: (= a b)

a : 1
b : 2
`

0 votes
by

Comment made by: importer

jawolfe said: See also some comments in:

http://groups.google.com/group/clojure-dev/browse_frm/thread/68d49cd7eb4a4899/9afc6be4d3f8ae27?lnk=gst&q=assert#9afc6be4d3f8ae27

Plus one more suggestion to add to the mix: in addition to / instead of printing the locals, how about saving them somewhere. For example, the var assert-bindings could be bound to the map of locals. This way you don't run afoul of infinite/very large sequences, and allow the user to do more detailed interrogation of the bad values (especially useful when some of the locals print opaquely).

0 votes
by

Comment made by: importer

stuart.sierra said: Another approach, which I wil willingly donate:
http://github.com/stuartsierra/lazytest/blob/master/src/main/clojure/lazytest/expect.clj

0 votes
by
_Comment made by: importer_

fogus said: Of course it's weird if you do something like:
(let [x 1 y 2 z 3 a 1 b 2 c 3] (assert (= x y)))
java.lang.AssertionError: Assert failed: (= x y)
 x : 1
 y : 2
 z : 3
 a : 1
 b : 2
 c : 3
 (NO_SOURCE_FILE:0)
</code></pre>

So maybe it could be slightly changed to:
<pre><code>(defmacro assert
  "Evaluates expr and throws an exception if it does not evaluate to logical true."
  {:added "1.0"}
  [x]
  (when *assert*
    (let [bindings (local-bindings &env)]
      `(when-not ~x
         (let [sep#  (System/getProperty "line.separator")
               form# '~x]
           (throw (AssertionError. (apply str "Assert failed: " (pr-str form#) sep#
                                          (map (fn [[k# v#]]
                                                 (when (some #{k#} form#)
                                                   (str "\t" k# " : " v# sep#)))
                                               ~bindings)))))))))
</code></pre>

So that. now it's just:
<pre><code>(let [x 1 y 2 z 3 a 1 b 2 c 3] (assert (= x y)))
java.lang.AssertionError: Assert failed: (= x y)
 x : 1
 y : 2
 (NO_SOURCE_FILE:0)

:f
0 votes
by
_Comment made by: jweiss_

There's one more tweak to fogus's last comment, which I'm actually using.  You need to flatten the quoted form before you can use 'some' to check whether the local was used in the form:

(defmacro assert
  "Evaluates expr and throws an exception if it does not evaluate to logical true."
  {:added "1.0"}
  [x]
  (when *assert*
    (let [bindings (local-bindings &env)]
      `(when-not ~x
         (let [sep#  (System/getProperty "line.separator")
               form# '~x]
           (throw (AssertionError. (apply str "Assert failed: " (pr-str form#) sep#
                                          (map (fn [[k# v#]]
                                                 (when (some #{k#} (flatten form#))
                                                   (str "\t" k# " : " v# sep#)))
                                               ~bindings)))))))))
0 votes
by

Comment made by: stu

I am holding off on this until we have more solidity around http://dev.clojure.org/display/design/Error Handling. (Considering, for instance, having all exceptions thrown from Clojure provide access to locals.)

When my pipe dream fades I will come back and screen this before the next release.

0 votes
by
_Comment made by: stu_

Why try to guess what someone wants to do with the locals (or any other context, for that matter) when you can specify a callback (see below). This would have been useful last week when I had an assertion that failed only on the CI box, where no debugger is available.

Rich, at the risk of beating a dead horse, I still think this is a good idea. Debuggers are not always available, and this is an example of where a Lisp is intrinsically capable of providing better information than can be had in other environments. If you want a patch for the code below please mark waiting on me, otherwise please decline this ticket so I stop looking at it. :-)



(def ^:dynamic *assert-handler* nil)

(defn ^{:private true} local-bindings
  "Produces a map of the names of local bindings to their values."
  [env]
  (let [symbols (map key env)]
    (zipmap (map (fn [sym] `(quote ~sym)) symbols) symbols)))

(defmacro assert
  [x]
  (when *assert*
    (let [bindings (local-bindings &env)]
      `(when-not ~x
         (let [sep#  (System/getProperty "line.separator")
               form# '~x]
           (if *assert-handler*
             (*assert-handler* form# ~bindings)
             (throw (AssertionError. (apply str "Assert failed: " (pr-str form#) sep#
                                            (map (fn [[k# v#]]
                                                   (when (some #{k#} (flatten form#))
                                                     (str "\t" k# " : " v# sep#)))
                                                 ~bindings))))))))))
0 votes
by

Comment made by: jweiss

A slight improvement I made in my own version of this code: flatten does not affect set literals. So if you do (assert (some #{x} (link: a b c d))) the value of x will not be printed. Here's a modified flatten that does the job:

(defn symbols [sexp] "Returns just the symbols from the expression, including those inside literals (sets, maps, lists, vectors)." (distinct (filter symbol? (tree-seq coll? seq sexp))))

0 votes
by

Comment made by: jafingerhut

Attaching git format patch clj-415-assert-prints-locals-v1.txt of Stuart Halloway's version of this idea. I'm not advocating it over the other variations, just getting a file attached to the JIRA ticket.

0 votes
by

Comment made by: michaelblume

Previous patch was incompatible with CLJ-1005, which moves zipmap later in clojure.core. Rewrote to use into.

0 votes
by

Comment made by: michaelblume

Both patches are somehow incompatible with CLJ-1224. When building my compojure-api project I get

`
Exception in thread "main" java.lang.UnsupportedOperationException: Can't type hint a primitive local, compiling:(schema/core.clj:680:27)

at clojure.lang.Compiler.analyze(Compiler.java:6569)
       ...
at clojure.main.main(main.java:37)

Caused by: java.lang.UnsupportedOperationException: Can't type hint a primitive local

at clojure.lang.Compiler$LocalBindingExpr.<init>(Compiler.java:5792)
at clojure.lang.Compiler.analyzeSymbol(Compiler.java:6929)
at clojure.lang.Compiler.analyze(Compiler.java:6532)
... 299 more

Failed.
`

0 votes
by

Comment made by: jafingerhut

Michael, are you finding these incompatibilities between patches because you want to run a modified version of Clojure with all of these patches? Understood, if so.

If you are looking for pairs of patches that are incompatible with each other, I'd recommend a different hobby :-) They get applied at the rate of about 9 per month, on average, so there should be plenty of time to resolve inconsistencies between them later.

0 votes
by
Reference: https://clojure.atlassian.net/browse/CLJ-415 (reported by alex+import)
Welcome to Clojure Q&A, where you can ask questions and receive answers from members of the Clojure community.
...