-;;; -*- mode: lisp; syntax: common-lisp; coding: utf-8-unix; package: cl-mock; -*-
+;; -*- mode: lisp; syntax: common-lisp; coding: utf-8-unix; package: cl-mock; -*-
(in-package #:cl-mock)
-
+\f
;;; mocking of regular functions
(defstruct mock-bindings
+ "Contains a set of mocked functions and their behaviour."
mocks)
(defvar *previous*)
(defvar *arguments*)
(defun call-previous (&rest args)
+ "Invokes the previous binding either with the current arguments or with
+the given ones. Use *PREVIOUS*/*ARGUMENTS* directly in edge cases."
(apply *previous* (or args *arguments*)))
(defun find-and-invoke-mock (*previous* cases *arguments*)
+ "Looks for a compatible mock (i.e. calls the TEST until one returns true)
+and executes it. If no mock was found, no values are returned instead."
(dolist (case cases (values))
(when (ignore-errors (apply (car case) *arguments*))
(return (apply (cdr case) *arguments*)))))
(defun call-with-mocks (mock-bindings function &key (recordp T))
"Calls FUNCTION with the given MOCK-BINDINGS established and returns
-its first return value, if any. If RECORDP is set, all invocations will
-be recorded and returned as the second return value, else NIL."
+its return values as a LIST. If RECORDP is set, all invocations will be
+recorded and returned as the second return value, else NIL."
(let* ((mocks (mock-bindings-mocks mock-bindings))
(functions (mapcar #'car mocks))
- (previous (mapcar #'maybe-fdefinition functions))
- invocations)
- (call-with-function-bindings
- functions
- (mapcar (lambda (binding previous &aux (name (car binding)) (cases (cdr binding)))
- (lambda (&rest args)
- (when recordp
- (push (cons name args) invocations))
- (find-and-invoke-mock previous cases args)))
- mocks previous)
- (lambda ()
- (values
- (funcall function)
- (nreverse invocations)))
- previous)))
+ (previous (mapcar #'maybe-fdefinition functions)))
+ (with-collector (invocations)
+ (call-with-function-bindings
+ functions
+ (mapcar (lambda (binding previous &aux (name (car binding)) (cases (cdr binding)))
+ (lambda (&rest args)
+ (when recordp
+ (invocations (cons name args)))
+ (find-and-invoke-mock previous cases args)))
+ mocks previous)
+ (lambda ()
+ (values
+ (multiple-value-list
+ (funcall function))
+ (invocations)))
+ previous))))
+
+(defmacro with-mocks ((mock-bindings &key (recordp T)) form &body body)
+ `(multiple-value-bind (,values ,calls)
+ (call-with-mocks
+ ,mock-bindings
+ (lambda () ,form)
+ :recordp ,recordp)
+ ,@body))
(defun register-mock (mock-bindings name)
+ "Registers a mocked function under NAME. The mocked function will
+return no values. See IF-CALLED to add some behaviour to it."
(let ((found (member name (mock-bindings-mocks mock-bindings) :key #'car :test #'eq)))
(or (car found)
(let ((binding (list name)))
binding))))
(defun if-called (mock-bindings name test function &key at-start)
+ "Registers a new binding to be called when the TEST function returns
+true. If AT-START is set, the binding is put at the start of the bindings
+list instead. Calls REGISTER-MOCK automatically."
(let ((binding (register-mock mock-bindings name))
(case (cons test function)))
(if at-start