Table of Contents
Explain
explain is the single extension point for failure messages: a generic
function dispatched on the operator of a failed function-call form, returning
one line or NIL.
(defgeneric explain (operator values)
(:documentation "One line on why (OPERATOR . VALUES) was false, or NIL."))
- It is asked only when an assertion failed and the form was a function-call
form.
(not ...)forms and opaque forms get no explanation. operatoris the symbol at the head of the form;valuesis the list of every operand's value in call order, literals included.- A method returns a string without a trailing newline, or
NILwhen it has nothing useful to say. A method must not signal — a method that errors is a bug in the method.
The point of the protocol is the eq/eql/equal hints: the five minutes
everyone loses once a year to (eq "a" "a") become one line.
Shipped methods
Text in code is the exact output. ~s prints under the report's printer
settings (downcased, no package prefixes). Each method returns NIL when the
values are not of the kind it describes.
| Operator | When | Explanation |
|---|---|---|
= |
two reals | differ by ~a with (abs (- a b)) |
eq |
two numbers that are = |
= but not EQ; use = or EQL |
eq |
two values that are equal |
EQUAL but not EQ (distinct objects); did you mean EQUAL? |
eql |
two numbers = but not eql |
= but not EQL (different types); use = |
eql |
two values that are equal |
EQUAL but not EQL (distinct objects); did you mean EQUAL? |
equal, equalp |
two strings | as string= below |
equal, equalp |
two sequences, mismatch past the end of one | lengths differ: ~d vs ~d |
equal, equalp |
two sequences, mismatch inside both | first difference at position ~d: ~s vs ~s |
equal |
two numbers that are = |
numerically equal but of different types; use = |
equalp |
two hash tables | keys only in the first: ~s; keys only in the second: ~s, or values differ at key ~s: ~s vs ~s |
string=, string-equal |
two strings, mismatch past the end of one | same up to index ~d, then lengths differ: ~d vs ~d |
string=, string-equal |
two strings, mismatch inside both | differ at index ~d: ~s vs ~s, each an excerpt of up to 20 characters with … where cut |
typep |
always | actual type: ~s with (type-of value) |
every |
a predicate and a sequence | first failing element at position ~d: ~s |
some |
a predicate and a sequence | no element satisfies ~s among ~d |
subsetp |
two lists | missing from the second list: ~s |
search |
two sequences | ~s does not occur in ~s |
approx= |
always | differ by ~a, tolerance ~a |
called-p |
always (see Mocks) | ~(~s~) was called ~d time~:p, with ~{~s~^, ~} or ~(~s~) was never called |
Teaching assay about your predicates
Add a method with an (eql 'symbol) specializer:
(defmethod assay:explain ((op (eql 'vec3=)) values)
(destructuring-bind (a b) values
(format nil "components differ by ~a" (map 'list #'- a b))))
CL-USER> (is (vec3= #(1.0 2.0 3.0) #(1.0 2.5 3.0)))
✗ (is (vec3= #(1.0 2.0 3.0) #(1.0 2.5 3.0)))
#(1.0 2.0 3.0) ⇒ #(1.0 2.0 3.0)
#(1.0 2.5 3.0) ⇒ #(1.0 2.5 3.0)
components differ by (0.0 -0.5 0.0)
Return NIL to decline — the operands are still shown, just without an extra
line. Because values holds every operand (keyword arguments included), a
method for a predicate that takes options can pick them out:
(defmethod assay:explain ((op (eql 'close-enough)) values)
(destructuring-bind (a b &key (epsilon 1e-3)) values
(format nil "off by ~a (epsilon ~a)" (abs (- a b)) epsilon)))
Next: Structure.