(defpackage #:mock (:use :cl) (:export #:mock-labels #:call-mocked-function #:mock-call-args #:mock-call-return-values #:mocked-function-called-p #:mocked-function-calls)) (in-package #:mock) (defstruct mock-state (calls (make-hash-table :test 'eq))) (defstruct mock-call args return-values) (defvar *mock-state-chain* nil) (defun binding-name (binding) (etypecase binding (list (car binding)) (symbol binding))) (defun make-mock-lambda (binding orig-definition state) (let* ((args (gensym "ARGS")) (results (gensym "RESULTS")) (name (binding-name binding)) (body (etypecase binding (list `(apply (lambda ,(second binding) ,@(cddr binding)) ,args)) (symbol '(call-mocked-function))))) `(lambda (&rest ,args) (macrolet ((call-mocked-function (&rest changed-args) (if (null changed-args) `(apply ,',orig-definition ,',args) `(funcall ,',orig-definition ,@changed-args)))) (let ((,results (multiple-value-list ,body))) (push (make-mock-call :args ,args :return-values ,results) (gethash ',name (mock-state-calls ,state))) (values-list ,results)))))) (defmacro mock-labels (bindings &body body) (let ((temps (loop for b in bindings collect (gensym))) (state (gensym "MOCK-STATE"))) `(let* ((,state (make-mock-state)) (*mock-state-chain* (cons ,state *mock-state-chain*)) ,@temps) (unwind-protect (progn ,@(loop for binding in bindings for temp in temps for name = (binding-name binding) collect `(setf ,temp (fdefinition ',name)) collect `(setf (fdefinition ',name) ,(make-mock-lambda binding temp state))) ,@body) ,@(loop for binding in bindings for temp in temps for name = (binding-name binding) collect `(setf (fdefinition ',name) ,temp)))))) (defmacro call-mocked-function (&rest args) (declare (ignore args)) (error "~A used outside of ~A definition" 'call-mocked-function 'mock-labels)) (defun mocked-function-calls (name) (loop for s in *mock-state-chain* thereis (gethash name (mock-state-calls s)))) (defun mocked-function-called-p (name) (not (null (mocked-function-calls name)))) ;; (defun foobar (x y) ;; (+ x y)) ;; (defun mock-test-1 () ;; (mock-labels ((foobar (x y) (call-mocked-function x (1+ y)))) ;; (values (foobar 2 3) ;; (foobar 1 1) ;; (mocked-function-calls 'foobar)))) ;; (defun mock-test-2 () ;; (mock-labels (foobar) ;; (values (foobar 2 3) ;; (foobar 1 1) ;; (mocked-function-calls 'foobar))))