emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[nongnu] elpa/buttercup 929a904 143/340: Implement closure-based expect


From: ELPA Syncer
Subject: [nongnu] elpa/buttercup 929a904 143/340: Implement closure-based expect macro
Date: Thu, 16 Dec 2021 14:59:21 -0500 (EST)

branch: elpa/buttercup
commit 929a904debd4cf5846bbcb7e3c9ee21f7636c5bb
Author: Ryan C. Thompson <rct@thompsonclan.org>
Commit: Jorgen Schäfer <Jorgen.Schaefer@gmail.com>

    Implement closure-based expect macro
    
    This redefines the "expect" macro to wrap all its arguments (except
    the matcher) in closures. Matchers are now expected to accept all
    arguments as closures and must "funcall" them to obtain their value.
    This allows matchers to also obtain the unevaluated expression for
    each of their arguments in order to generate more informative error
    messages. All built-in matchers are reimplemented to conform to this
    requirement. This fixes #54.
    
    In addition, this adds some convenience macros for defining matchers.
    The macro "buttercup-define-matcher-for-unary-function" automatically
    generates a matcher for any predicate function, while
    "buttercup-define-matcher-for-binary-function" does the same for a
    comparison function, and also automatically uses any explainer defined
    by ERT for that function for more precise messages in case of a
    mismatch. This fixes #28. More generally, the
    "buttercup--test-expectation" macro makes it easier to construct the
    non-intuitive return values required by buttercup matchers. This fixes
    
    the change to closure-based matchers is mostly backward-compatible,
    with the only user-visible diferences being better error messages.
    However, one major backward-incompatible change is the handling of the
    ":to-throw" matcher, which no longer requires its argument to be
    wrapped in a lambda form, since the "expect" macro now does so
    automatically for all matchers. This fixes #41. In order to aid
    migration, it would be helpful to add a warning when a function
    returns another function, which is a likely indicator that the desired
    expression was wrongly wrapped in a lambda form and wasn't evaluated.
    
    Another new feature is support for issuing warnings during tests. Any
    warning issued during a test is collected in a special buffer and
    displayed after the test finishes.
    
    Tests are added for all new functionality, and existing tests are
    adapted to the new code.
---
 buttercup.el            | 738 +++++++++++++++++++++++++++++++++++++-----------
 docs/writing-tests.md   |  36 ++-
 tests/test-buttercup.el | 224 ++++++++-------
 3 files changed, 708 insertions(+), 290 deletions(-)

diff --git a/buttercup.el b/buttercup.el
index e6c21c3..64048f8 100644
--- a/buttercup.el
+++ b/buttercup.el
@@ -41,6 +41,56 @@
 
 (require 'cl-lib)
 (require 'buttercup-compat)
+(require 'format-spec)
+(require 'ert nil t)
+(require 'warnings)
+
+;;;;;;;;;;;;;;;;;;;;;;;;
+;;; closure manipulation
+
+(defun buttercup--enclosed-expr (x)
+  "Given a zero-arg closure, return its unevaluated expression.
+
+The closure MUST have one of the following forms:
+
+\(closure (ENVLIST) () EXPR)
+\(closure (ENVLIST) () (quote EXPR) EXPR)
+
+and the return value will be EXPR, unevaluated. The latter form
+is useful if EXPR is a macro call, in which case the `quote'
+ensures access to the un-expanded form."
+  (pcase x
+    (`(closure ,(pred listp) nil ,expr) expr)
+    (`(closure ,(pred listp) nil (quote ,expr) . ,rest) expr)
+    (`(closure ,(pred listp) nil ,expr . ,(pred identity))
+     (error "Closure contains multiple expressions: %S" x))
+    (`(closure ,(pred listp) ,(pred identity) . ,(pred identity))
+     (error "Closure has nonempty arglist: %S" x))
+    (`(lambda nil ,expr) expr)
+    (`(lambda nil (quote ,expr) . ,rest) expr)
+    (`(lambda nil ,expr . ,(pred identity))
+     (error "Function contains multiple expressions: %S" x))
+        (`(lambda ,(pred identity) . ,(pred identity))
+     (error "Function has nonempty arglist: %S" x))
+    (_ (error "Not a zero-arg one-expression closure: %S" x))))
+
+(defun buttercup--closure-expr-and-value (x)
+  "Given a closure X, return its quoted expression and value.
+
+The closure must be a zero-argument one-expression closure, i.e.
+anything matched by `buttercup--closure-p'. The return value
+is `(cons EXPR VALUE)', where EXPR is the unevaluated expression
+in the closure, and VALUE is the result of calling the closure as
+a function."
+  (cons (buttercup--enclosed-expr x)
+        (funcall x)))
+
+(defun buttercup--closure-p (x)
+  "Returns non-nil if X is a zero-arg one-expression closure."
+  (condition-case nil
+      (prog1 t
+        (buttercup--enclosed-expr x))
+    (error nil)))
 
 ;;;;;;;;;;
 ;;; expect
@@ -65,35 +115,31 @@ This macro knows three forms:
 
 \(expect ARG)
   Fail the current test iff ARG is not true."
-  (cond
-   ((and (not matcher)
-         (consp arg))
-    `(buttercup-expect ,(cadr arg)
-                       #',(car arg)
-                       ,@(cddr arg)))
-   ((and (not matcher)
-         (not (consp arg)))
-    `(buttercup-expect ,arg))
-   (t
-    `(buttercup-expect ,arg ,matcher ,@args))))
+  (let ((args-closures
+         (mapcar (lambda (expr) `(lambda () (quote ,expr) ,expr)) args)))
+    `(buttercup-expect
+      (lambda () (quote ,arg) ,arg)
+      ,(or matcher :to-be-truthy)
+      ,@args-closures)))
 
 (defun buttercup-expect (arg &optional matcher &rest args)
-  "The function for the `expect' macro.
-
-See the macro documentation for details."
+  (cl-assert (cl-every #'buttercup--closure-p (cons arg args)) t)
   (if (not matcher)
-      (when (not arg)
-        (buttercup-fail "Expected %S to be non-nil" arg))
+      (progn
+        (cl-assert (not args) t)
+        (when (not (funcall arg))
+          (buttercup-fail "Expected %S to be non-nil"
+                          (buttercup--enclosed-expr arg))))
     (let ((result (buttercup--apply-matcher matcher (cons arg args))))
       (if (consp result)
           (when (not (car result))
             (buttercup-fail "%s" (cdr result)))
         (when (not result)
-          (buttercup-fail "Expected %S %S %S"
-                          arg
+          (buttercup-fail "Expected %S %S %s"
+                          (buttercup--enclosed-expr arg)
                           matcher
                           (mapconcat (lambda (obj)
-                                       (format "%S" obj))
+                                       (format "%S" (funcall obj)))
                                      args
                                      " ")))))))
 
@@ -121,119 +167,468 @@ MESSAGE is omitted or nil show the condition form 
instead."
 (defmacro buttercup-define-matcher (matcher args &rest body)
   "Define a matcher to be used in `expect'.
 
-The BODY should return either a simple boolean, or a cons cell of
-the form (RESULT . MESSAGE). If RESULT is nil, MESSAGE should
-describe why the matcher failed. If RESULT is non-nil, MESSAGE
-should describe why a negated matcher failed."
+The BODY will receive ARGS as closures that can be `funcall'ed to
+get their values. BODY should return either a simple boolean, or
+a cons cell of the form (RESULT . MESSAGE). If RESULT is nil,
+MESSAGE should describe why the matcher failed. If RESULT is
+non-nil, MESSAGE should describe why a negated matcher failed."
   (declare (indent defun))
   `(put ,matcher 'buttercup-matcher
         (lambda ,args
           ,@body)))
 
+(defun buttercup--function-as-matcher (fun)
+  (cl-assert (functionp fun) t)
+  (lambda (&rest args)
+    (apply fun (mapcar #'funcall args))))
+
+(defun buttercup--find-matcher-function (matcher)
+  (let ((matcher-prop
+         (when (symbolp matcher)
+           (get matcher 'buttercup-matcher))))
+    (cond
+     ;; Use `buttercup-matcher' property if it's a function
+     ((functionp matcher-prop)
+      matcher-prop)
+     (matcher-prop
+      (error "%S %S has a `buttercup-matcher' property that is not a function. 
Buttercup has been misconfigured."
+             (if (keywordp matcher) "Keyword" "Symbol") matcher))
+     ;; Otherwise just use `matcher' as a function, wrapping it in
+     ;; closure-unpacking code.
+     ((functionp matcher)
+      (buttercup--function-as-matcher matcher))
+     (matcher (error "Not a test: `%S'" matcher))
+     ;; If `matcher' is nil, then we just want a basic truth test
+     ((null matcher)
+      (buttercup--find-matcher-function :to-be-truthy))
+     (t (error "This line should never run")))))
+
 (defun buttercup--apply-matcher (matcher args)
   "Apply MATCHER to ARGS.
 
+ARGS is a list of closures that must be `funcall'ed to get their
+values.
+
 MATCHER is either a matcher defined with
 `buttercup-define-matcher', or a function."
-  (let ((function (or (get matcher 'buttercup-matcher)
-                      matcher)))
-    (when (not (functionp function))
-      (error "Not a test: %S" matcher))
+  (cl-assert (cl-every #'buttercup--closure-p args) t)
+  (let ((function
+         (buttercup--find-matcher-function matcher)))
     (apply function args)))
 
+(cl-defmacro buttercup--test-expectation
+    (expr &key expect-match-phrase expect-mismatch-phrase)
+  "Wrapper for the common matcher case of two possible messages.
+
+The logic for the return values of buttercup matchers can be
+unintuitive, since the return value is a cons cell whose first
+element is t for a mismatch and nil for a match. In the simple
+case where there are only two possible messages (one for a match
+and one for a mismatch), this macro allows you to simply specify
+those two phrases and the expression to test."
+  (declare (indent 1))
+  (cl-assert expect-match-phrase)
+  (cl-assert expect-mismatch-phrase)
+  `(let ((value ,expr))
+     (if value
+         (cons t ,expect-mismatch-phrase)
+       (cons nil ,expect-match-phrase))))
+
+(cl-defmacro buttercup-define-matcher-for-unary-function
+    (matcher function &key
+             expect-match-phrase expect-mismatch-phrase function-name)
+  "Shortcut to define a macther for a 1-argument function.
+
+When the matcher is used, keyword arguments EXPECT-MATCH-PHRASE
+and EXPECT-MISMATCH-PHRASE are used to construct the return
+message. It may contain `%f', `%A', and `%a', which will be
+replaced with the function name, the expression of the argument
+the matcher was called on, and the value of that argument,
+respectively. If not provided, the default EXPECT-MATCH-PHRASE
+is:
+
+    Expected `%A' to match `%f', but instead it was `%a'.
+
+Similarly, the default EXPECT-MISMATCH-PHRASE is:
+
+    Expected `%A' not to match `%f', but it was `%a'.
+
+To include a literal `%' in either message, use `%%'.
+
+If FUNCTION is passed as a lambda expression or other non-symbol, then
+you must provide a keyword argument FUNCTION-NAME to be used in
+the match/mismatch messages. Otherwise, FUNCTION-NAME will be
+used instead of FUNCTION if both are non-nil SYMBOLS.
+
+If FUNCTION (or FUNCTION-NAME) has an `ert-explainer' property,
+this will be used to generate the default EXPECT-MATCH-PHRASE.
+
+See also `buttercup-define-matcher'."
+  (declare (indent 2))
+  ;; Use the ERT explainer for FUNCTION if available to generate the
+  ;; default expect-match phrase.
+  (let ((explainer (or (when function-name
+                         (get function-name 'ert-explainer))
+                       (when (symbolp function)
+                         (get function 'ert-explainer)))))
+    (cl-assert (symbolp function-name) t)
+    (cl-assert (functionp function) t)
+    (unless expect-match-phrase
+      (setq expect-match-phrase
+            (if explainer
+                ;; %x is the undocumented substitution for the
+                ;; explainer's output
+                "Expected `%A' to match `%f', but instead it was `%a' which 
did not match because: %x."
+              "Expected `%A' to match `%f', but instead it was `%a'.")))
+    (unless expect-mismatch-phrase
+      (setq expect-mismatch-phrase
+            "Expected `%A' not to match `%f', but it was `%a'."))
+    (when (and (null function-name)
+               ;; Only need a function name if either phrase contains
+               ;; an unescaped `%f'.
+               (string-match-p
+                "%f"
+                (replace-regexp-in-string
+                 "%%" ""
+                 (concat expect-match-phrase " "
+                         expect-mismatch-phrase))))
+      (if (symbolp function)
+          (setq function-name function)
+        (error "The `:function-name' keyword is required if FUNCTION is not a 
symbol")))
+    `(buttercup-define-matcher ,matcher (arg)
+       (let* ((expr (buttercup--enclosed-expr arg))
+              (value (funcall arg))
+              (explanation (and ',explainer (funcall ',explainer arg)))
+              (spec (format-spec-make
+                     ?f ',function-name
+                     ?A (format "%S" expr)
+                     ?a (format "%S" value)
+                     ?x (format "%S" explanation))))
+         (buttercup--test-expectation (funcall ',function value)
+           :expect-match-phrase (format-spec ,expect-match-phrase spec)
+           :expect-mismatch-phrase (format-spec ,expect-mismatch-phrase 
spec))))))
+
+(cl-defmacro buttercup-define-matcher-for-binary-function
+    (matcher function &key
+             expect-match-phrase expect-mismatch-phrase function-name)
+  "Shortcut to define a macther for a 2-argument function.
+
+When the matcher is used, keyword arguments EXPECT-MATCH-PHRASE
+and EXPECT-MISMATCH-PHRASE are used to construct the return
+message. It may contain `%f', `%A', `%a', `%B', and `%b'. The
+token `%f' will be replaced with the function name. `%A' and `%B'
+will be replaced with the unevaluted expressions of the two
+arguments passed to the function, while `%a' and `%b' will be
+replaced with their values. not provided, the default
+EXPECT-MATCH-PHRASE is:
+
+    Expected `%A' to be `%f' to `%b', but instead it was `%a'.
+
+Similarly, the default EXPECT-MISMATCH-PHRASE is:
+
+    Expected `%A' not to be `%f' to `%b', but it was.
+
+To include a literal `%' in either message, use `%%'.
+
+If FUNCTION is passed as a lambda expression or other non-symbol, then
+you must provide a keyword argument FUNCTION-NAME to be used in
+the match/mismatch messages (unless neither one contains `%f').
+If both are non-nil symbols, FUNCTION-NAME will be used instead
+of FUNCTION in messages.
+
+If FUNCTION (or FUNCTION-NAME) has an `ert-explainer' property,
+this will be used to generate the default EXPECT-MATCH-PHRASE.
+
+See also `buttercup-define-matcher'."
+  (declare (indent 2))
+  ;; Use the ERT explainer for FUNCTION if available to generate the
+  ;; default expect-match phrase.
+  (let ((explainer (or (when function-name
+                         (get function-name 'ert-explainer))
+                       (when (symbolp function)
+                         (get function 'ert-explainer)))))
+    (cl-assert (symbolp function-name) t)
+    (cl-assert (functionp function) t)
+    (unless expect-match-phrase
+      (setq expect-match-phrase
+            (if explainer
+                ;; %x is the undocumented substitution for the
+                ;; explainer's output
+                "Expected `%A' to be `%f' to `%b', but instead it was `%a' 
which does not match because: %x."
+              "Expected `%A' to be `%f' to `%b', but instead it was `%a'.")))
+    (unless expect-mismatch-phrase
+      (setq expect-mismatch-phrase
+            "Expected `%A' not to be `%f' to `%b', but it was."))
+    (when (and (null function-name)
+               ;; Only need a function name if either phrase contains
+               ;; an unescaped `%f'.
+               (string-match-p
+                "%f"
+                (replace-regexp-in-string
+                 "%%" ""
+                 (concat expect-match-phrase " "
+                         expect-mismatch-phrase))))
+      (if (symbolp function)
+          (setq function-name function)
+        (error "The `:function-name' keyword is required if FUNCTION is not a 
symbol")))
+    `(buttercup-define-matcher ,matcher (a b)
+       (cl-destructuring-bind
+           ((a-expr . a) (b-expr . b))
+           (mapcar #'buttercup--closure-expr-and-value (list a b))
+         (let* ((explanation (and ',explainer (funcall ',explainer a b)))
+                (spec (format-spec-make
+                       ?f ',function-name
+                       ?A (format "%S" a-expr)
+                       ?a (format "%S" a)
+                       ?B (format "%S" b-expr)
+                       ?b (format "%S" b)
+                       ?x (format "%S" explanation))))
+           (buttercup--test-expectation (funcall #',function a b)
+             :expect-match-phrase (format-spec ,expect-match-phrase spec)
+             :expect-mismatch-phrase (format-spec ,expect-mismatch-phrase 
spec)))))))
+
 ;;;;;;;;;;;;;;;;;;;;;
 ;;; Built-in matchers
 
+(buttercup-define-matcher-for-unary-function :to-be-truthy identity
+  :expect-match-phrase "Expected `%A' to be non-nil, but instead it was nil."
+  :expect-mismatch-phrase "Expected `%A' to be nil, but instead it was `%a'.")
+
+(buttercup-define-matcher-for-binary-function :to-be eq)
+(buttercup-define-matcher-for-binary-function :to-equal equal)
+
 (buttercup-define-matcher :not (obj matcher &rest args)
-  (let ((result (buttercup--apply-matcher matcher (cons obj args))))
+  (let* ((matcher (funcall matcher))
+         (result (buttercup--apply-matcher matcher (cons obj args))))
     (if (consp result)
         (cons (not (car result))
               (cdr result))
       (not result))))
 
-(buttercup-define-matcher :to-be (a b)
-  (if (eq a b)
-      (cons t (format "Expected %S not to be `eq' to %S" a b))
-    (cons nil (format "Expected %S to be `eq' to %S" a b))))
-
-(buttercup-define-matcher :to-equal (a b)
-  (if (equal a b)
-      (cons t (format "Expected %S not to `equal' %S" a b))
-    (cons nil (format "Expected %S to `equal' %S" a b))))
-
 (buttercup-define-matcher :to-have-same-items-as (a b)
-  (if (and (cl-subsetp a b :test #'equal)
-           (cl-subsetp b a :test #'equal))
-      (cons t (format "Expected %S not to have same items as %S" a b))
-    (cons nil (format "Expected %S to have same items as %S" a b))))
+  (cl-destructuring-bind
+      ((a-expr . a) (b-expr . b))
+      (mapcar #'buttercup--closure-expr-and-value (list a b))
+    (let* ((a-uniques (cl-set-difference a b :test #'equal))
+           (b-uniques (cl-set-difference b a :test #'equal))
+           (spec (format-spec-make
+                  ?A (format "%S" a-expr)
+                  ?a (format "%S" a)
+                  ?B (format "%S" b-expr)
+                  ?b (format "%S" b)
+                  ?m (format "%S" b-uniques)
+                  ?p (format "%S" a-uniques))))
+      (cond
+       ((and a-uniques b-uniques)
+        (cons nil (format-spec
+                 "Expected `%A' to contain the same items as `%b', but `%m' 
are missing and `%p' are present unexpectedly."
+                 spec)))
+       (a-uniques
+        (cons nil (format-spec
+                 "Expected `%A' to contain the e items as `%b', but `%p' are 
present unexprctedly."
+                 spec)))
+       (b-uniques
+        (cons nil (format-spec
+                 "Expected `%A' to contain the same items as `%b', but `%m' 
are missing."
+                 spec)))
+       (t
+        (cons t (format-spec
+                 "Expected `%A' not to have same items as `%b'"
+                 spec)))))))
 
 (buttercup-define-matcher :to-match (text regexp)
-  (if (string-match regexp text)
-      (cons t (format "Expected %S not to match the regexp %S"
-                      text regexp))
-    (cons nil (format "Expected %S to match the regexp %S"
-                      text regexp))))
-
-(buttercup-define-matcher :to-be-truthy (arg)
-  (if arg
-      (cons t (format "Expected %S not to be true" arg))
-    (cons nil (format "Expected %S to be true" arg))))
-
-(buttercup-define-matcher :to-contain (seq elt)
-  (if (member elt seq)
-      (cons t (format "Expected %S not to contain %S" seq elt))
-    (cons nil (format "Expected %S to contain %S" seq elt))))
-
-(buttercup-define-matcher :to-be-less-than (a b)
-  (if (< a b)
-      (cons t (format "Expected %S not to be less than %S" a b))
-    (cons nil (format "Expected %S to be less than %S" a b))))
-
-(buttercup-define-matcher :to-be-greater-than (a b)
-  (if (> a b)
-      (cons t (format "Expected %S not to be greater than %S" a b))
-    (cons nil (format "Expected %S to be greater than %S" a b))))
+  (cl-destructuring-bind
+      ((text-expr . text) (regexp-expr . regexp))
+      (mapcar #'buttercup--closure-expr-and-value (list text regexp))
+    (let* (;; For string literals, juse use them normally, but for
+           ;; expressions, show both the expr and its string value
+           (text-is-literal (equal text-expr text))
+           (regexp-is-literal (equal regexp-expr regexp))
+           (text-desc
+            (if text-is-literal
+                text-expr
+              (format "`%S' with value %S"
+                      text-expr text)))
+           (regexp-desc
+            (if regexp-is-literal
+                regexp-expr
+              (format "`%S' with value %S"
+                      regexp-expr regexp)))
+           (match-p (string-match regexp text))
+           ;; Get some more details about the match
+           (start
+            (when match-p
+              (match-beginning 0)))
+           (end
+            (when match-p
+              (match-end 0)))
+           (matched-substring
+            (when match-p
+              (substring text start end)))
+           (spec (format-spec-make
+                  ?T text-desc
+                  ?t (format "%S" text)
+                  ?R regexp-desc
+                  ?r (format "%S" regexp)
+                  ?m (format "%S" matched-substring)
+                  ?a start
+                  ?z end)))
+      (buttercup--test-expectation match-p
+        :expect-match-phrase
+        (format-spec "Expected %T to match the regexp %r, but instead it was 
%t."
+                     spec)
+        :expect-mismatch-phrase
+        (format-spec "Expected %T not to match the regexp %r, but it matched 
the substring %m from position %a to %z."
+                     spec)))))
+
+(buttercup-define-matcher-for-binary-function
+    :to-be-in member
+  :expect-match-phrase "Expected `%A' to be an element of `%b', but it was 
`%a'."
+  :expect-mismatch-phrase "Expected `%A' not to be an element of `%b', but it 
was `%a'.")
+
+(buttercup-define-matcher-for-binary-function
+    ;; Reverse the args
+    :to-contain (lambda (a b) (member b a))
+  :expect-match-phrase "Expected `%A' to be a list containing `%b', but 
instead it was `%a'."
+  :expect-mismatch-phrase "Expected `%A' to be a list not containing `%b', but 
instead it was `%a'.")
+
+(buttercup-define-matcher-for-binary-function
+    :to-be-less-than <
+  :expect-match-phrase "Expected `%A' < %b, but `%A' was %a."
+  :expect-mismatch-phrase "Expected `%A' >= %b, but `%A' was %a.")
+(buttercup-define-matcher-for-binary-function
+    :to-be-greater-than >
+  :expect-match-phrase "Expected `%A' > %b, but `%A' was %a."
+  :expect-mismatch-phrase "Expected `%A' <= %b, but `%A' was %a.")
+(buttercup-define-matcher-for-binary-function
+    :to-be-weakly-less-than <=
+  :expect-match-phrase "Expected `%A' <= %b, but `%A' was %a."
+  :expect-mismatch-phrase "Expected `%A' > %b, but `%A' was %a.")
+(buttercup-define-matcher-for-binary-function
+    :to-be-weakly-greater-than >=
+  :expect-match-phrase "Expected `%A' >= %b, but `%A' was %a."
+  :expect-mismatch-phrase "Expected `%A' < %b, but `%A' was %a.")
 
 (buttercup-define-matcher :to-be-close-to (a b precision)
-  (if (< (abs (- a b))
-         (/ 1 (expt 10.0 precision)))
-      (cons t (format "Expected %S not to be close to %S to %s positions"
-                      a b precision))
-    (cons nil (format "Expected %S to be greater than %S to %s positions"
-                      a b precision))))
-
-(buttercup-define-matcher :to-throw (function &optional signal signal-args)
-  ;; This will trigger errors relating to FUNCTION not being a
-  ;; function outside the following `condition-case'.
-  (when (not (functionp function))
-    (funcall function))
-  (condition-case err
-      (progn
-        (funcall function)
-        (cons nil (format "Expected %S to throw an error" function)))
-    (error
-     (cond
-      ((and signal signal-args)
-       (cond
-        ((not (memq signal (get (car err) 'error-conditions)))
-         (cons nil (format "Expected %S to throw a child signal of %S, not %S"
-                           function signal (car err))))
-        ((not (equal signal-args (cdr err)))
-         (cons nil (format "Expected %S to throw %S with args %S, not %S with 
%S"
-                           function signal signal-args (car err) (cdr err))))
-        (t
-         (cons t (format (concat "Expected %S not to throw a child signal "
-                                 "of %S with args %S, but it did throw %S")
-                         function signal signal-args (car err))))))
-      (signal
-       (if (not (memq signal (get (car err) 'error-conditions)))
-           (cons nil (format "Expected %S to throw a child signal of %S, not 
%S"
-                             function signal (car err)))
-         (cons t (format (concat "Expected %S not to throw a child signal "
-                                 "of %S, but it threw %S")
-                         function signal (car err)))))
-      (t
-       (cons t (format "Expected %S not to throw an error" function)))))))
+  (cl-destructuring-bind
+      (precision (a-expr . a) (b-expr . b))
+      (cons (funcall precision)
+            (mapcar #'buttercup--closure-expr-and-value (list a b)))
+    (let ((tolerance (expt 10.0 (- precision))))
+      (buttercup--test-expectation
+          (< (abs (- a b)) tolerance)
+        :expect-match-phrase
+        (format "Expected `%S' to be within %s of %s, but instead it was %s, 
with a difference of %s"
+                a-expr tolerance b a (abs (- a b)))
+        :expect-mismatch-phrase
+        (format "Expected `%S' to differ from %s by more than %s, but instead 
it was %s, with a difference of %s"
+                a-expr b tolerance a (abs (- a b)))))))
+
+(buttercup-define-matcher :to-throw (expr &optional signal signal-args)
+  (let ((expected-signal-symbol (or (and signal (funcall signal)) 'error))
+        (expected-signal-args (and signal-args (funcall signal-args)))
+        (unevaluated-expr (buttercup--enclosed-expr expr))
+        expr-value
+        thrown-signal
+        thrown-signal-symbol
+        thrown-signal-args)
+    (when (and (functionp unevaluated-expr)
+               (member (car unevaluated-expr) '(lambda closure)))
+      (display-warning
+       'buttercup
+       (buttercup-colorize
+        (format "Probable incorrect use of `:to-throw' matcher: pass an 
expression instead of a function: `%S'"
+                unevaluated-expr)
+        'yellow)))
+    ;; If no signal specificaiton, use `error' as the signal symbol
+    (when (and (null expected-signal-symbol)
+               (null expected-signal-args))
+      (setq expected-signal-symbol 'error))
+    ;; Set the above 4 variables
+    (condition-case err
+        (setq expr-value
+              (funcall expr))
+      (error
+       (setq thrown-signal err
+             thrown-signal-symbol (car err)
+             thrown-signal-args (cdr err))
+       nil))
+    (let*
+        ((matched
+          (and thrown-signal
+               (or (null expected-signal-symbol)
+                   (memq expected-signal-symbol (get thrown-signal-symbol 
'error-conditions)))
+               (or (null expected-signal-args)
+                   (equal thrown-signal-args expected-signal-args))))
+         (spec (format-spec-make
+                ?E (format "%S" unevaluated-expr)
+                ?e (format "%S" expr-value)
+                ?t (format "%S" thrown-signal)
+                ?s (if expected-signal-symbol
+                       (format "a child signal of `%S'" expected-signal-symbol)
+                     "a signal")
+                ?a (if expected-signal-args
+                       (format " with args `%S'" expected-signal-args)
+                     "")))
+         (result-text
+          (if thrown-signal
+              (format-spec "it threw %t" spec)
+            (format-spec "it evaluated successfully, returning value `%e'" 
spec)))
+
+         (expect-match-text
+          (concat (format-spec "Expected `%E' to throw %s%a" spec)
+                  ", but instead "
+                  result-text))
+         (expect-mismatch-text
+          (concat (format-spec "Expected `%E' not to throw %s%a" spec)
+                  ", but "
+                  result-text)))
+      (buttercup--test-expectation matched
+                                   :expect-match-phrase expect-match-text
+                                   :expect-mismatch-phrase 
expect-mismatch-text))))
+
+(buttercup-define-matcher :to-have-been-called (spy)
+  (setq spy (funcall spy))
+  (cl-assert (symbolp spy))
+  (if (spy-calls-all (funcall spy))
+      t
+    nil))
+
+(buttercup-define-matcher :to-have-been-called-with (spy &rest args)
+  (setq spy (funcall spy))
+  (cl-assert (symbolp spy))
+  (setq args (mapcar #'funcall args))
+  (let* ((calls (mapcar 'spy-context-args (spy-calls-all spy))))
+    (cond
+     ((not calls)
+      (cons nil
+            (format "Expected `%s' to have been called with %s, but it was not 
called at all" spy args)))
+     ((not (member args calls))
+      (cons nil
+            (format "Expected `%s' to have been called with %s, but it was 
called with %s"
+                    spy
+                    args
+                    (mapconcat (lambda (args)
+                                 (format "%S" args))
+                               calls
+                               ", "))))
+     (t
+      t))))
+
+(buttercup-define-matcher :to-have-been-called-times (spy number)
+  (setq spy (funcall spy)
+        number (funcall number))
+  (cl-assert (symbolp spy))
+  (let* ((call-count (length (spy-calls-all spy))))
+    (cond
+     ((= number call-count)
+      t)
+     (t
+      (cons nil
+            (format "Expected `%s' to have been called %s %s, but it was 
called %s %s"
+                    spy
+                    number (if (= number 1) "time" "times")
+                    call-count (if (= call-count 1) "time" "times")))))))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;; Suite and spec data structures
@@ -406,8 +801,11 @@ form.")
       (let ((dups (buttercup--find-duplicate-spec-names
                    (list buttercup--current-suite))))
         (when dups
-          (message "Found duplicate spec names in suite: %S"
-                   (delete-dups dups))))
+          ;; TODO: Use `buttercup--warn'
+          (display-warning
+           'buttercup
+           (format "Found duplicate spec names in suite: %S"
+                   (delete-dups dups)))))
       (setq buttercup-suites (append buttercup-suites
                                      (list buttercup--current-suite))))))
 
@@ -579,8 +977,10 @@ responsibility to ensure ARG is a command."
              (let ((replacement-intform (interactive-form arg)))
                (when (and replacement-intform
                           (not (equal orig-intform replacement-intform)))
-                 (display-warning 'buttercup
-                                  "While spying on `%S': replacement does not 
have the same interactive form"))
+                 (display-warning
+                  'buttercup
+                  (format "While spying on `%S': replacement does not have the 
same interactive form"
+                          symbol)))
                `(lambda (&rest args)
                   ,(or replacement-intform orig-intform)
                   (apply (function ,arg) args))))
@@ -659,40 +1059,10 @@ responsibility to ensure ARG is a command."
            buttercup--spy-contexts))
 
 (buttercup-define-matcher :to-have-been-called (spy)
-  (if (spy-calls-all spy)
+  (if (spy-calls-all (funcall spy))
       t
     nil))
 
-(buttercup-define-matcher :to-have-been-called-with (spy &rest args)
-  (let* ((calls (mapcar 'spy-context-args (spy-calls-all spy))))
-    (cond
-     ((not calls)
-      (cons nil
-            (format "Expected `%s' to have been called with %s, but it was not 
called at all" spy args)))
-     ((not (member args calls))
-      (cons nil
-            (format "Expected `%s' to have been called with %s, but it was 
called with %s"
-                    spy
-                    args
-                    (mapconcat (lambda (args)
-                                 (format "%S" args))
-                               calls
-                               ", "))))
-     (t
-      t))))
-
-(buttercup-define-matcher :to-have-been-called-times (spy number)
-  (let* ((call-count (length (spy-calls-all spy))))
-    (cond
-     ((= number call-count)
-      t)
-     (t
-      (cons nil
-            (format "Expected `%s' to have been called %s %s, but it was 
called %s %s"
-                    spy
-                    number (if (= number 1) "time" "times")
-                    call-count (if (= call-count 1) "time" "times")))))))
-
 (defun spy-calls-any (spy)
   "Return t iff SPY has been called at all, nil otherwise."
   (if (spy-calls-all spy)
@@ -847,14 +1217,31 @@ Do not change the global value.")
     (funcall buttercup-reporter 'suite-done suite)))
 
 (defun buttercup--run-spec (spec)
-  (funcall buttercup-reporter 'spec-started spec)
-  (buttercup-with-cleanup
-   (dolist (f buttercup--before-each)
-     (buttercup--update-with-funcall spec f))
-   (buttercup--update-with-funcall spec (buttercup-spec-function spec))
-   (dolist (f buttercup--after-each)
-     (buttercup--update-with-funcall spec f)))
-  (funcall buttercup-reporter 'spec-done spec))
+  (unwind-protect
+      (progn
+        ;; Kill any previous warning buffer, just in case
+        (when (get-buffer buttercup-warning-buffer-name)
+          (kill-buffer buttercup-warning-buffer-name))
+        (get-buffer-create buttercup-warning-buffer-name)
+
+        (funcall buttercup-reporter 'spec-started spec)
+        (buttercup-with-cleanup
+         (dolist (f buttercup--before-each)
+           (buttercup--update-with-funcall spec f))
+         (buttercup--update-with-funcall spec (buttercup-spec-function spec))
+         (dolist (f buttercup--after-each)
+           (buttercup--update-with-funcall spec f)))
+        (funcall buttercup-reporter 'spec-done spec)
+        ;; Display warnings that were issued while running the the
+        ;; spec, if any
+        (with-current-buffer buttercup-warning-buffer-name
+          (when (string-match-p "[^[:space:]\n\r]" (buffer-string))
+            (buttercup--print
+             (buttercup-colorize
+              (buffer-string)
+              'yellow)))))
+    (when (get-buffer buttercup-warning-buffer-name)
+      (kill-buffer buttercup-warning-buffer-name))))
 
 (defun buttercup--update-with-funcall (suite-or-spec function &rest args)
   (let* ((result (apply 'buttercup--funcall function args))
@@ -968,7 +1355,7 @@ Calls either `buttercup-reporter-batch' or
                        (list arg))))
         ((eq (buttercup-spec-status arg) 'pending)
          (buttercup--print "  %s\n" (buttercup-spec-failure-description arg)))
-        (_
+        (t
          (error "Unknown spec status %s" (buttercup-spec-status arg)))))
 
       (`suite-done
@@ -1041,7 +1428,7 @@ Calls either `buttercup-reporter-batch' or
                              (make-string (* 2 level) ?\s)
                              (buttercup-spec-description arg)
                              (buttercup-spec-failure-description arg))))
-        (_
+        (t
          (error "Unknown spec status %s" (buttercup-spec-status arg))))))
 
     (`buttercup-done
@@ -1100,6 +1487,35 @@ Calls either `buttercup-reporter-batch' or
 (defun buttercup--print (fmt &rest args)
   (send-string-to-terminal (apply #'format fmt args)))
 
+
+(defconst buttercup-warning-buffer-name " *Buttercup-Warnings*"
+  "Buffer name used to collect warnings issued while running a spec.
+
+A buffer with this name should only exist while running a test
+spec, and should be killed after running the spec.")
+
+(defadvice display-warning (around buttercup-defer-warnings activate)
+  "Log all warnings to a special buffer while running buttercup tests.
+
+Emacs' normal display logic for warnings doesn't mix well with
+buttercup, for several reasons. So instead, while a buttercup
+test is running, BUFFER-NAME defaults to a special buffer that
+exists only during the test (see
+`buttercup-warning-buffer-name'). When logging to this buffer,
+`warning-minimum-level' is set to `:emergency' and the `message'
+function is disabled to suppress display of all warning messages.
+The contents of this buffer are then displayed after the test
+finishes."
+  (when (and (null buffer-name)
+             (get-buffer buttercup-warning-buffer-name))
+    (setq buffer-name buttercup-warning-buffer-name))
+  (if (equal buffer-name buttercup-warning-buffer-name)
+      (cl-letf
+          ((warning-minimum-level :emergency)
+           ((symbol-function 'message) 'ignore))
+        ad-do-it)
+    ad-do-it))
+
 (defconst buttercup-colors
   '((black   . 30)
     (red     . 31)
@@ -1210,17 +1626,23 @@ or a macro/special form.")
                             "...")))
        line))
     (`pretty
-     (thread-last (pp-to-string (cdr frame))
+     (let ((text (pp-to-string (cdr frame))))
        ;; Delete empty trailing line
-       (replace-regexp-in-string "\n[[:space:]]*\\'"
-                                 "")
+       (setq text
+             (replace-regexp-in-string
+              "\n[[:space:]]*\\'" ""
+              text))
        ;; Indent 2 spaces
-       (replace-regexp-in-string "^"
-                                 "  ")
+       (setq text
+             (replace-regexp-in-string
+              "^" "  "
+              text))
        ;; Prefix first line with lambda for function call and M for
        ;; macro/special form
-       (replace-regexp-in-string "\\` "
-                                 (if (car frame) "λ" "M"))))
+       (setq text
+             (replace-regexp-in-string
+              "\\` " (if (car frame) "λ" "M")
+              text))))
     (_ (error "Unknown stack trace style: %S" style))))
 
 (defmacro buttercup-with-converted-ert-signals (&rest body)
diff --git a/docs/writing-tests.md b/docs/writing-tests.md
index 23f8c59..da1041d 100644
--- a/docs/writing-tests.md
+++ b/docs/writing-tests.md
@@ -151,26 +151,15 @@ that are not included below.
       (expect pi :to-be-close-to e 0)))
 
   (describe "The :to-throw matcher"
-    (it "is for testing if a function throws an exception"
-      (let ((foo (lambda () (+ 1 2)))
-            (bar (lambda () (+ a 1))))
-        (expect foo :not :to-throw)
-        (expect bar :to-throw)))
+    (it "is for testing if an expression throws an exception"
+      (expect (+ 1 2) :not :to-throw)
+      (expect (+ a 1) :to-throw))
     (it "accepts a symbol to check for the signal thrown"
-      (let ((foo (lambda () (/ 1 0)))
-            (bar (lambda () (+ a 1))))
-        (expect foo :not :to-throw 'void-variable)
-        (expect bar :to-throw 'void-variable)))
+      (expect (/ 1 0) :not :to-throw 'void-variable)
+      (expect (+ a 1) :to-throw 'void-variable))
     (it "optionally matches arguments to signals"
-      (let ((foo (lambda () (+ a 1)))
-            (bar (lambda () (+ a 1))))
-        (expect foo :not :to-throw 'void-variable '(b))
-        (expect bar :to-throw 'void-variable '(a))))
-    (it "only works on functions"
-      (expect (lambda () (expect nil :to-throw 'error))
-              :to-throw 'void-function)
-      (expect (lambda () (expect "hello" :not :to-throw 'error))
-              :to-throw 'invalid-function))))
+        (expect (+ a 1) :not :to-throw 'void-variable '(b))
+        (expect (+ a 1) :to-throw 'void-variable '(a)))))
 ```
 
 ## Grouping Related Specs with `describe`
@@ -488,7 +477,7 @@ will `signal` the specified value as an error.
     (spy-on 'get-bar :and-throw-error 'error))
 
   (it "throws the error"
-    (expect (lambda () (get-bar))
+    (expect (get-bar)
             :to-throw 'error)))
 ```
 
@@ -604,3 +593,12 @@ Finally, `spy-calls-reset` clears all tracking for a spy.
             :to-be
             nil)))
 ```
+
+## Warnings in tests
+
+```Emacs-Lisp
+(describe "A test"
+  (it "can issue warnings while running"
+    (display-warning 'buttercup "This warning should be visible after the test 
report.")
+    (expect (+ 2 2) :to-equal 4)))
+```
diff --git a/tests/test-buttercup.el b/tests/test-buttercup.el
index 4455572..ef316b6 100644
--- a/tests/test-buttercup.el
+++ b/tests/test-buttercup.el
@@ -20,65 +20,60 @@
 (require 'buttercup)
 (require 'autoload)
 (require 'ert)
+(require 'cl-lib)
+
+(defun make-list-of-closures (items)
+  "For each element of ITEMS, return a closure that returns it."
+  (mapcar (lambda (item)
+            (lambda () item))
+          items))
 
 ;;;;;;;;;;
 ;;; expect
 
 (describe "The buttercup-failed signal"
   (it "can be raised"
-    (expect (lambda ()
-              (signal 'buttercup-failed t))
+    (expect (signal 'buttercup-failed t)
             :to-throw
             'buttercup-failed)))
 
 (describe "The buttercup-pending signal"
   (it "can be raised"
-    (expect (lambda ()
-              (signal 'buttercup-pending t))
+    (expect (signal 'buttercup-pending t)
             :to-throw
             'buttercup-pending)))
 
 (describe "The `expect' form"
-  (it "with a matcher should translate directly to the function call"
-    (expect (macroexpand '(expect (+ 1 1) :to-equal 2))
-            :to-equal
-            '(buttercup-expect (+ 1 1) :to-equal 2)))
-
-  (it "with a form argument should extract the matcher from the form"
-    (expect (macroexpand '(expect (equal (+ 1 1) 2)))
-            :to-equal
-            '(buttercup-expect (+ 1 1) #'equal 2)))
-
-  (it "with a single argument should pass it to the function"
-    (expect (macroexpand '(expect t))
-            :to-equal
-            '(buttercup-expect t))))
+  (it "with a matcher should translate to the function call with closures"
+    (let ((expansion (macroexpand '(expect (+ 1 1) :to-equal 2))))
+      (expect (length expansion) :to-equal 4)
+      (expect (nth 0 expansion) :to-be 'buttercup-expect)
+      (expect (functionp (nth 1 expansion)))
+      (expect (nth 2 expansion) :to-be :to-equal)
+      (expect (functionp (nth 3 expansion)))))
+
+  (it "with no matcher should use `:to-be-truthy' as the matcher"
+    (let ((expansion (macroexpand '(expect (equal (+ 1 1) 2)))))
+      (expect (length expansion) :to-equal 3)
+      (expect (nth 0 expansion) :to-be 'buttercup-expect)
+      (expect (functionp (nth 1 expansion)))
+      (expect (nth 2 expansion) :to-be :to-be-truthy))))
 
 (describe "The `buttercup-expect' function"
-  (describe "with a single argument"
-    (it "should not raise an error if the argument is true"
-      (expect (lambda ()
-                (buttercup-expect t))
-              :not :to-throw
-              'buttercup-failed))
-
-    (it "should raise an error if the argument is false"
-      (expect (lambda ()
-                (buttercup-expect nil))
-              :to-throw
-              'buttercup-failed
-              "Expected nil to be non-nil")))
-
   (describe "with a function as a matcher argument"
     (it "should not raise an error if the function returns true"
-      (expect (lambda ()
-                (buttercup-expect t #'eq t))
+      (expect (buttercup-expect
+               (lambda () t)
+               #'eq
+               (lambda () t))
               :not :to-throw
               'buttercup-failed))
 
     (it "should raise an error if the function returns false"
-      (expect (lambda ()
-                (buttercup-expect t #'eq nil))
+      (expect (buttercup-expect
+               (lambda () t)
+               #'eq
+               (lambda () nil))
               :to-throw
               'buttercup-failed)))
 
@@ -87,72 +82,69 @@
     (buttercup-define-matcher :always-false (a) nil)
 
     (it "should not raise an error if the matcher returns true"
-      (expect (lambda ()
-                (buttercup-expect 1 :always-true))
+      (expect (buttercup-expect (lambda () 1) :always-true)
               :not :to-throw
               'buttercup-failed))
 
     (it "should raise an error if the matcher returns false"
-      (expect (lambda ()
-                (buttercup-expect 1 :always-false))
+      (expect (buttercup-expect (lambda () 1) :always-false)
               :to-throw
               'buttercup-failed))))
 
 (describe "The `buttercup-fail' function"
   (it "should raise a signal with its arguments"
-    (expect (lambda ()
-              (buttercup-fail "Explanation" ))
+    (expect (buttercup-fail "Explanation" )
             :to-throw
             'buttercup-failed "Explanation")))
 
 (describe "The `assume' form"
   (it "should raise a signal if the condition is nil"
-    (expect (lambda ()
-              (assume nil "Explanation"))
+    (expect (assume nil "Explanation")
             :to-throw
             'buttercup-pending "!! CANCELLED !! Explanation"))
 
   (it "should show the format if no message is given"
-    (expect (lambda ()
-              (assume (< 1 0)))
+    (expect (assume (< 1 0))
             :to-throw
             'buttercup-pending "!! CANCELLED !! (< 1 0) => nil"))
 
   (it "should not raise a signal if the condition is non-nil"
-    (expect (lambda ()
-              (assume 'non-nil "Explanation"))
+    (expect (assume 'non-nil "Explanation")
             :not :to-throw)))
 
 (describe "The `buttercup-skip function"
   (it "should raise a signal with its arguments"
-    (expect (lambda ()
-              (buttercup-skip "Explanation" ))
+    (expect (buttercup-skip "Explanation" )
             :to-throw
             'buttercup-pending "Explanation")))
 
 (buttercup-define-matcher :test-matcher (a b)
-  (+ a b))
+  (+ (funcall a) (funcall b)))
 
 (describe "The `buttercup-define-matcher' macro"
   (it "should create a matcher usable by apply-matcher"
-    (expect (buttercup--apply-matcher :test-matcher '(1 2))
+    (expect (buttercup--apply-matcher
+             :test-matcher (make-list-of-closures '(1 2)))
             :to-equal
             3)))
 
-(describe "The `buttercup--apply-matcher'"
+(describe "The `buttercup--apply-matcher' function"
   (it "should work with functions"
-    (expect (buttercup--apply-matcher #'+ '(1 2))
+    (expect (buttercup--apply-matcher
+             #'+
+             (make-list-of-closures '(1 2)))
             :to-equal
             3))
 
   (it "should work with matchers"
-    (expect (buttercup--apply-matcher :test-matcher '(1 2))
+    (expect (buttercup--apply-matcher
+             :test-matcher (make-list-of-closures '(1 2)))
             :to-equal
             3))
 
   (it "should fail if the matcher is not defined"
-    (expect (lambda ()
-              (buttercup--apply-matcher :not-defined '(1 2)))
+    (expect (buttercup--apply-matcher
+             :not-defined (make-list-of-closures '(1 2)))
             :to-throw)))
 
 ;;;;;;;;;;;;;;;;;;;;;
@@ -356,9 +348,8 @@
 
 (describe "The `buttercup-it' function"
   (it "should fail if not called from within a describe form"
-    (expect (lambda ()
-              (let ((buttercup--current-suite nil))
-                (buttercup-it "" (lambda ()))))
+    (expect (let ((buttercup--current-suite nil))
+              (buttercup-it "" (lambda ())))
             :to-throw))
 
   (it "should add a spec to the current suite"
@@ -451,10 +442,9 @@
 
 (describe "The `buttercup-xdescribe' function"
   (it "should be a no-op"
-    (expect (lambda ()
-              (buttercup-xdescribe
-               "bla bla"
-               (lambda () (error "should not happen"))))
+    (expect (buttercup-xdescribe
+             "bla bla"
+             (lambda () (error "should not happen")))
             :not :to-throw))
 
   (it "should add a pending suite"
@@ -478,19 +468,20 @@
 
 (describe "The `buttercup-xit' function"
   (it "should be a no-op"
-    (expect (lambda ()
-              (let ((buttercup--current-suite (make-buttercup-suite)))
-                (buttercup-xit
-                 "bla bla"
-                 (lambda () (error "should not happen")))))
-            :not :to-throw))
+    (expect
+     (let ((buttercup--current-suite (make-buttercup-suite)))
+       (buttercup-xit
+           "bla bla"
+         (lambda () (error "should not happen")))))
+    :not :to-throw)
 
   (it "should add a function that raises a pending signal"
     (let ((buttercup--current-suite (make-buttercup-suite)))
       (buttercup-xit "bla bla" (lambda ()
                                  (error "should not happen")))
-      (expect (buttercup-spec-function
-               (car (buttercup-suite-children buttercup--current-suite)))
+      (expect (funcall
+               (buttercup-spec-function
+                (car (buttercup-suite-children buttercup--current-suite))))
               :to-throw 'buttercup-pending)))
 
   (it "should mark the suite as pending"
@@ -527,7 +518,7 @@
       (it "allows a spied-on command to be executed as a command"
         (spy-on 'test-command)
         (expect (commandp 'test-command))
-        (expect (lambda () (command-execute 'test-command))
+        (expect (command-execute 'test-command)
                 :not :to-throw)
         (expect 'test-command :to-have-been-called))
 
@@ -556,16 +547,16 @@
 
       (it "only accepts ARG for keywords that use it"
         (expect
-         (lambda () (spy-on 'test-function :and-call-through :arg-not-allowed))
+         (spy-on 'test-function :and-call-through :arg-not-allowed)
          :to-throw)
         (expect
-         (lambda () (spy-on 'test-function nil :arg-not-allowed))
+         (spy-on 'test-function nil :arg-not-allowed)
          :to-throw)
         (expect
-         (lambda () (spy-on 'test-function :and-throw-error))
+         (spy-on 'test-function :and-throw-error)
          :not :to-throw)
         (expect
-         (lambda () (test-function 1 2))
+         (test-function 1 2)
          :to-throw 'error)))
 
     (describe ":to-have-been-called matcher"
@@ -573,15 +564,17 @@
         (spy-on 'test-function))
 
       (it "returns false if the spy was not called"
-        (expect (buttercup--apply-matcher :to-have-been-called
-                                          '(test-function))
+        (expect (buttercup--apply-matcher
+                 :to-have-been-called
+                 (list (lambda () 'test-function)))
                 :to-be
                 nil))
 
       (it "returns true if the spy was called at all"
         (test-function 1 2 3)
-        (expect (buttercup--apply-matcher :to-have-been-called
-                                          '(test-function))
+        (expect (buttercup--apply-matcher
+                 :to-have-been-called
+                 (list (lambda () 'test-function)))
                 :to-be
                 t)))
 
@@ -591,7 +584,8 @@
 
       (it "returns false if the spy was not called at all"
         (expect (buttercup--apply-matcher
-                 :to-have-been-called-with '(test-function 1 2 3))
+                 :to-have-been-called-with
+                 (make-list-of-closures '(test-function 1 2 3)))
                 :to-equal
                 (cons nil
                       "Expected `test-function' to have been called with (1 2 
3), but it was not called at all")))
@@ -599,7 +593,8 @@
       (it "returns false if the spy was called with different arguments"
         (test-function 3 2 1)
         (expect (buttercup--apply-matcher
-                 :to-have-been-called-with '(test-function 1 2 3))
+                 :to-have-been-called-with
+                 (make-list-of-closures '(test-function 1 2 3)))
                 :to-equal
                 (cons nil
                       "Expected `test-function' to have been called with (1 2 
3), but it was called with (3 2 1)")))
@@ -607,7 +602,8 @@
       (it "returns true if the spy was called with those arguments"
         (test-function 1 2 3)
         (expect (buttercup--apply-matcher
-                 :to-have-been-called-with '(test-function 1 2 3))
+                 :to-have-been-called-with
+                 (make-list-of-closures '(test-function 1 2 3)))
                 :to-be
                 t)))
 
@@ -617,7 +613,8 @@
 
       (it "returns error if the spy was called less than expected"
         (expect (buttercup--apply-matcher
-                 :to-have-been-called-times '(test-function 1))
+                 :to-have-been-called-times
+                 (make-list-of-closures '(test-function 1)))
                 :to-equal
                 (cons nil
                       "Expected `test-function' to have been called 1 time, 
but it was called 0 times")))
@@ -626,7 +623,8 @@
         (test-function)
         (test-function)
         (expect (buttercup--apply-matcher
-                 :to-have-been-called-times '(test-function 1))
+                 :to-have-been-called-times
+                 (make-list-of-closures '(test-function 1)))
                 :to-equal
                 (cons nil
                       "Expected `test-function' to have been called 1 time, 
but it was called 2 times")))
@@ -635,21 +633,24 @@
         (test-function)
         (test-function)
         (expect (buttercup--apply-matcher
-                 :to-have-been-called-times '(test-function 2))
+                 :to-have-been-called-times
+                 (make-list-of-closures '(test-function 2)))
                 :to-equal t))
 
       (it "use plural words in error message"
         (test-function)
         (test-function)
         (expect (buttercup--apply-matcher
-                 :to-have-been-called-times '(test-function 3))
+                 :to-have-been-called-times
+                 (make-list-of-closures '(test-function 3)))
                 :to-equal
                 (cons nil
                       "Expected `test-function' to have been called 3 times, 
but it was called 2 times")))
 
       (it "use singular expected word in error message"
         (expect (buttercup--apply-matcher
-                 :to-have-been-called-times '(test-function 1))
+                 :to-have-been-called-times
+                 (make-list-of-closures '(test-function 1)))
                 :to-equal
                 (cons nil
                       "Expected `test-function' to have been called 1 time, 
but it was called 0 times")))
@@ -657,7 +658,8 @@
       (it "use singular actual word in error message"
         (test-function)
         (expect (buttercup--apply-matcher
-                 :to-have-been-called-times '(test-function 2))
+                 :to-have-been-called-times
+                 (make-list-of-closures '(test-function 2)))
                 :to-equal
                 (cons nil
                       "Expected `test-function' to have been called 2 times, 
but it was called 1 time"))))
@@ -749,7 +751,7 @@
         (spy-on 'test-function :and-throw-error 'error))
 
       (it "throws an error when called"
-        (expect (lambda () (test-function 1 2))
+        (expect (test-function 1 2)
                 :to-throw
                 'error "Stubbed error")))))
 
@@ -814,8 +816,7 @@
       (it "should throw an error for an unknown spec status"
         (setf (buttercup-spec-status spec) 'unknown)
 
-        (expect (lambda ()
-                  (buttercup-reporter-batch 'spec-done spec))
+        (expect (buttercup-reporter-batch 'spec-done spec)
                 :to-throw)))
 
     (describe "on the suite-done event"
@@ -838,14 +839,12 @@
       (it "should raise an error if at least one spec failed"
         (setf (buttercup-spec-status spec) 'failed)
 
-        (expect (lambda ()
-                  (buttercup-reporter-batch 'buttercup-done (list spec)))
+        (expect (buttercup-reporter-batch 'buttercup-done (list spec))
                 :to-throw)))
 
     (describe "on an unknown event"
       (it "should raise an error"
-        (expect (lambda ()
-                  (buttercup-reporter-batch 'unknown-event nil))
+        (expect (buttercup-reporter-batch 'unknown-event nil)
                 :to-throw)))))
 
 (describe "The `buttercup--print' function"
@@ -865,16 +864,14 @@
 (describe "Buttercup's ERT compatibility wrapper"
   (it "should convert `ert-test-failed' into `buttercup-failed"
     (expect
-     (lambda ()
-       (buttercup-with-converted-ert-signals
-         (should (equal 1 2))))
+     (buttercup-with-converted-ert-signals
+       (should (equal 1 2)))
      :to-throw 'buttercup-failed))
   (it "should convert `ert-test-skipped' into `buttercup-pending"
     (assume (functionp 'ert-skip) "Loaded ERT version does not provide 
`ert-skip'")
     (expect
-     (lambda ()
-       (buttercup-with-converted-ert-signals
-         (ert-skip "Skipped this test")))
+     (buttercup-with-converted-ert-signals
+       (ert-skip "Skipped this test"))
      :to-throw 'buttercup-pending)))
 
 ;;;;;;;;;;;;;
@@ -883,16 +880,17 @@
 ;; We can't test `buttercup--funcall' with buttercup, because the way
 ;; we get the backtrace from Emacs does not nest.
 
-(let ((res (buttercup--funcall (lambda () (+ 2 3)))))
-  (when (not (equal res (list 'passed 5 nil)))
-    (error "Expected passing buttercup--funcall not to return %S"
-           res)))
+(let ((res (buttercup--funcall (lambda () (+ 2 3))))
+      (expected '(passed 5 nil)))
+  (when (not (equal res expected))
+    (error "Expected passing buttercup--funcall to return `%S', not `%S'"
+           expected res)))
 
 (let ((res (buttercup--funcall (lambda () (/ 1 0)))))
-  (when (not (equal res (list 'failed
-                              '(error (arith-error))
-                              (list '(t / 1 0)))))
-    (error "Expected erroring buttercup--funcall not to return %S"
+  (when (not (and
+              (equal (car res) 'failed)
+              (equal (cadr res) '(error (arith-error)))))
+    (error "Expected erroring buttercup--funcall not to return `%S'"
            res)))
 
 ;;;;;;;;;;;;;
@@ -934,7 +932,7 @@
             (specs (assoc "Spec" imenu--index-alist)))
         (expect suites :to-be-truthy)
         (expect (length (cdr suites)) :to-equal 1)
-        (expect (caadr suites) :to-equal "A test suite")
+        (expect (cl-caadr suites) :to-equal "A test suite")
         (expect specs :to-be-truthy)
         (expect (length (cdr specs)) :to-equal 1)
-        (expect (caadr specs) :to-equal "should fontify special keywords")))))
+        (expect (cl-caadr specs) :to-equal "should fontify special 
keywords")))))



reply via email to

[Prev in Thread] Current Thread [Next in Thread]