;;; -*- mode: Lisp; Syntax: Common-Lisp; -*- ;;; ;;; Copyright (c) 2006 by the authors. ;;; ;;; See LICENCE for details. (in-package :hu.dwim.stefil) #+nil(hu.dwim.defclass-star:defclass* test (testable) ((package nil) (lambda-list nil) (compile-before-run t :type boolean) (declarations nil) (documentation nil) (body nil))) (defclass test (testable) ((package :initform nil :accessor package-of :initarg :package) (lambda-list :initform nil :accessor lambda-list-of :initarg :lambda-list) (compile-before-run :initform t :accessor compile-before-run-p :initarg :compile-before-run :type boolean) (declarations :initform nil :accessor declarations-of :initarg :declarations) (documentation :initform nil :accessor documentation-of :initarg :documentation) (body :initform nil :accessor body-of :initarg :body))) (defun ensure-test (name &rest args &key &allow-other-keys) (let ((test (find-test name :otherwise nil))) (if test (apply #'reinitialize-instance test args) (apply #'make-instance 'test :name name args)))) (defun ensure-test-lambda (test context) (check-type test test) (check-type context global-context) (multiple-value-bind (test-lambda found-p) (gethash test (test-lambdas-of context)) (unless found-p (setf test-lambda (let* ((*package* (package-of test)) (*readtable* (copy-readtable))) (compile nil `(lambda ,(lambda-list-of test) ,@(body-of test))))) (setf (gethash test (test-lambdas-of context)) test-lambda)) test-lambda)) (defun call-with-test-handlers (function) ;; NOTE: the order of the bindings in this handler-bind is important (handler-bind ((assertion-failed (lambda (c) (declare (ignore c)) (unless (debug-on-assertion-failure-p *global-context*) (continue)))) (serious-condition (lambda (c) (record/unexpected-error c) (return-from call-with-test-handlers)))) (funcall function))) (defun run-test-body-in-handlers (test function) (declare (type test test) (type function function)) (register-test-being-run test) (labels ((prune-failure-descriptions () ;; drop failures recorded by the previous run of this test (loop repeat (number-of-added-failure-descriptions-of *context*) do (vector-pop (failure-descriptions-of *global-context*))) (setf (number-of-added-failure-descriptions-of *context*) 0)) (run-test-body () (call-with-test-handlers (lambda () (restart-case (let* ((*package* (package-of test)) (*readtable* (copy-readtable)) (start-time (get-internal-run-time))) (multiple-value-prog1 (funcall function) (setf (internal-realtime-spent-with-test-of *context*) (- (get-internal-run-time) start-time)))) (continue () :report (lambda (stream) (format stream "~@" (name-of test))) (values)) (retest () :report (lambda (stream) (format stream "~@" (name-of test))) ;; TODO: this will only prune the failures that were recorded in the current context. ;; in case of nesting it will leave alone the failures recorded in deeper levels. (prune-failure-descriptions) (return-from run-test-body (run-test-body)))))))) (run-test-body))) (defun run-test-body (test function arguments toplevel-p timeout) (declare (type test test)) (when timeout (error "TODO: timeouts are not implemented yet in hu.dwim.stefil")) (let* ((result-values '())) (flet ((body () (with-new-context (:test test :test-arguments arguments) (when toplevel-p (setf (toplevel-context-of *global-context*) (current-context))) (setf result-values (multiple-value-list (run-test-body-in-handlers test function)))))) (if toplevel-p (with-toplevel-restarts (body)) (body)) (if toplevel-p (progn (when (print-test-run-progress-p *global-context*) (terpri *debug-io*)) (if result-values (values-list (append result-values (list *global-context*))) *global-context*)) (values-list result-values))))) (defmacro deftest (&whole whole name args &body body) (multiple-value-bind (remaining-forms declarations documentation) (parse-body body :documentation t :whole whole) (destructuring-bind (name &rest test-args &key (compile-before-run *compile-tests-before-run*) (in nil in-provided?) timeout &allow-other-keys) (ensure-list name) (remove-from-plistf test-args :in) (unless (or (not (symbol-package name)) (eq (symbol-package name) *package*)) (warn 'test-style-warning :test name :format-control "Defining test on symbol ~S whose home package is not *package* which is ~A" :format-arguments (list name *package*))) (with-unique-names (test test-lambda global-context toplevel-p body) `(progn (eval-when (:load-toplevel :execute) (ensure-test ',name :package ,*package* :lambda-list ',args :declarations ',declarations :documentation ',documentation :body ',remaining-forms ,@(when in-provided? `(:in (find-test ',in))) ,@test-args)) (defun ,name ,args ,@(when documentation (list documentation)) ,@declarations ,@(when *compile-tests-with-debug-level* `((declare (optimize (debug ,*compile-tests-with-debug-level*))))) (let* ((,test (find-test ',name)) (,toplevel-p (not (has-global-context))) (,global-context (unless ,toplevel-p (current-global-context)))) ;; for convenience we define a function in a LABELS with the test name, so the debugger shows it in the backtrace (labels (,@(unless compile-before-run `((,name () ,@remaining-forms))) (,body () ,(if compile-before-run `(let* ((,test-lambda (ensure-test-lambda ,test ,global-context))) (run-test-body ,test (lambda () ;; TODO install a labels entry with the test name? to avoid compile at each recursion... ,(lambda-list-to-funcall-expression test-lambda args)) ,(lambda-list-to-value-list-expression args) ,toplevel-p ,timeout)) `(run-test-body ,test #',name ,(lambda-list-to-value-list-expression args) ,toplevel-p ,timeout)))) (declare (dynamic-extent ,@(unless compile-before-run `(#',name)) #',body)) (if ,toplevel-p (with-new-global-context* () (setf ,global-context (current-global-context)) (push ,global-context *test-result-history*) (setf *last-test-result* ,global-context) (,body)) (,body))))))))))