;;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*- ;;;; ************************************************************************* ;;;; FILE IDENTIFICATION ;;;; ;;;; Name: src.lisp ;;;; Purpose: eXtreme Lisp Test Suite ;;;; Authors: Kevin Rosenberg and Craig Brozefsky ;;;; ;;;; $Id: src.lisp,v 1.3 2003/08/04 09:50:33 kevin Exp $ ;;;; ************************************************************************* (in-package #:xlunit) (defclass test-fixture () ((test-thunk :initarg :test-thunk :reader test-thunk :initform 'perform-test :documentation "A thunk or symbol which will be applied to this instance, a test-case, to perform that test-case. Defaults to 'perform-test") (test-name :initarg :test-name :reader test-name :documentation "The name of this test-case, used in reports.") (test-description :initarg :description :reader description :documentation "Short description of this test-case, uses in reports")) (:documentation "Base class for test-fixtures. Test-cases are instances of test-fixtures.")) (defmethod setup ((test test-fixture)) "Method called before performing a test, should set up the environment the test-case needs to operate in." t) (defmethod teardown ((test test-fixture)) "Method called after performing a test. Should reverse everything that the setup method did for this instance." t) (define-condition test-failure-condition (simple-condition) () (:documentation "Base class for all test failures.")) (defclass test-failure () ((failed-test :initarg :failed-test :reader failed-test) (thrown-condition :initarg :thrown-condition :reader thrown-condition))) (defmethod print-object ((obj test-failure) stream) (print-unreadable-object (obj stream :type t :identity nil) (format stream "~A: " (failed-test obj)) (apply #'format stream (simple-condition-format-control (thrown-condition obj)) (simple-condition-format-arguments (thrown-condition obj))))) (defmethod is-failure ((failure test-failure)) (typep (thrown-condition failure) 'test-failure-condition)) (defun failure (format-str &rest args) "Signal a test failure and exit the test." (signal 'test-failure-condition :format-control format-str :format-arguments args)) (defmacro test-assert (test &optional msg) `(unless ,test (failure "Test assertion: ~s" ',test))) (defun assert-equal (v1 v2 &optional msg) (unless (equal v1 v2) (failure "Test equal: ~s ~s" v1 v2))) (defun assert-true (v &optional msg) (unless v (failure "Test true: ~s [~A]" v (if msg msg "")))) (defun assert-false (v &optional msg) (when v (failure "Test false ~A" (if msg msg "")))) (defmethod perform-test ((test test-fixture)) "Default method for performing tests upon a test-fixture." t) (defmacro handler-case-if (test form &body cases) `(if ,test (handler-case ,form ,@cases) ,form)) (defmacro unwind-protect-if (test protected cleanup) `(if ,test (unwind-protect ,protected ,cleanup) (progn ,protected ,cleanup))) (defclass test-result () ((test :initarg :test :reader result-test) (count :initform 0 :accessor test-count) (failures :initarg :failures :reader test-failures :initform nil) (errors :initarg :errors :reader test-errors :initform nil)) (:documentation "The result of applying a test")) (defclass test-suite () ((name :initarg :name :reader test-suite-name) (tests :initarg :tests :accessor tests-hash :initform (make-hash-table :test 'equal)) (description :initarg :description :reader description :initform "No description."))) (defmethod setup-testsuite-named (name) (declare (ignore name)) t) (defmethod teardown-testsuite-named (name) (declare (ignore name)) t) (defmethod run-test ((suite test-suite) (result test-result) &key (handle-errors t)) (setup-testsuite-named (slot-value suite 'name)) (dolist (test (tests suite)) (run-test test result :handle-errors handle-errors)) (teardown-testsuite-named (slot-value suite 'name)) (values)) (defmethod run-test ((test test-fixture) result &key (handle-errors t)) "Perform the test represented by the given test-case or test-suite. Returns one or more test-result objects, one for each test-case performed." (incf (test-count result)) (with-slots (failures errors) result (unwind-protect-if handle-errors (handler-case-if handle-errors (let ((res (progn (setup test) (funcall (test-thunk test) test)))) (if (typep res 'test-failure-condition) (push (make-instance 'test-failure :failed-test test :thrown-condition res) failures))) (test-failure-condition (failure) (push (make-instance 'test-failure :failed-test test :thrown-condition failure) failures)) (error (err) (push (make-instance 'test-failure :failed-test test :thrown-condition err) errors))) (if handle-errors (handler-case (teardown test) (error (err) (push (make-instance 'test-failure :failed-test test :thrown-condition err) errors))) (teardown test)))) (values)) (defun make-test (fixture name &key test-thunk test-suite description) "Create a test-case which is an instance of FIXTURE. TEST-THUNK is the method that will be invoked when perfoming this test, and can be a symbol or a lambda taking a single argument, the test-fixture instance. DESCRIPTION is obviously what it says it is." (let ((newtest (make-instance fixture :test-name (string name) :test-thunk (if(and (symbolp name) (null test-thunk)) name test-thunk) :description description))) (if test-suite (add-test newtest test-suite)) newtest)) (defmethod tests ((suite test-suite)) (let ((tlist nil)) (maphash #'(lambda (k v) (declare (ignore k)) (setf tlist (cons v tlist))) (tests-hash suite)) (reverse tlist))) (defun make-test-suite (name-or-fixture &optional description testspecs) "Returns a new test-suite based on a name and TESTSPECS or a fixture instance" (etypecase name-or-fixture (symbol (make-test-suite-for-fixture (make-instance name-or-fixture))) (string (let ((suite (make-instance 'test-suite :name name-or-fixture :description description))) (dolist (testspec testspecs) (add-test (apply #'make-test testspec) suite)) suite)))) (defmethod add-test ((test test-fixture) (suite test-suite)) (setf (gethash (test-name test) (tests-hash suite)) test)) (defmethod add-test ((test test-suite) (suite test-suite)) (setf (gethash (test-suite-name test) (tests-hash suite)) test)) (defmethod remove-test ((test test-fixture) (suite test-suite)) (remhash (test-name test) (tests-hash suite))) (defmethod remove-test ((test test-suite) (suite test-suite)) (remhash (test-suite-name test) (tests-hash suite))) (defmethod test-named ((name string) (suite test-suite)) (gethash name (tests-hash suite))) (defmethod was-successful ((result test-result)) (and (null (test-failures result)) (null (test-errors result)))) (defmethod text-testrunner ((suite test-suite) &key (stream t) (handle-errors t)) (let ((result (make-instance 'test-result)) (start-time (get-internal-real-time))) (run-test suite result :handle-errors handle-errors) (let ((seconds (/ (- (get-internal-real-time) start-time) internal-time-units-per-second))) (result-printer result seconds stream)))) (defun result-printer (result seconds stream) (format stream "~&Time: ~D~%~%" (coerce seconds 'float)) (print-defects (test-errors result) "error" stream) (print-defects (test-failures result) "failure" stream) (if (was-successful result) (format stream "OK (~D tests)~%" (test-count result)) (progn (format stream "~%FAILURES!!!~%") (format stream "Tests run: ~D, Failures: ~D, Errors: ~D~%" (test-count result) (length (test-failures result)) (length (test-errors result)))))) (defun print-defects (defects type stream) (when defects (let ((count (length defects))) (if (= count 1) (format stream "~&There was ~D ~A:~%" count type) (format stream "~&There were ~D ~As:~%" count type)) (dotimes (i count) (let ((defect (nth i defects))) (format stream "~&~D) ~A " i (class-name (class-of (failed-test defect)))) (apply #'format stream (simple-condition-format-control (thrown-condition defect)) (simple-condition-format-arguments (thrown-condition defect))) (fresh-line stream)))))) (defmethod summary ((result test-result)) (format nil "~D run, ~D errored, ~D failed" (test-count result) (length (test-errors result)) (length (test-failures result)))) ;;; Dynamic test suite addition by Kevin Rosenberg 8/2003 (defun make-test-suite-for-fixture (fixture &key (name (format nil "Automatic for ~A" (if (slot-boundp fixture 'test-name) (test-name fixture) (type-of fixture)))) description) (let ((suite (make-instance 'test-suite :name name :description description)) (fns (find-test-generic-functions fixture))) (dolist (fn fns) (make-test (class-name (class-of fixture)) fn :test-suite suite)) suite)) (defun find-test-generic-functions (instance) "Return a list of symbols for generic functions specialized on the class of an instance and whose name begins with the string 'test-'. This is used to dynamically generate a list of tests for a fixture." (let ((res) (package (symbol-package (class-name (class-of instance))))) (do-symbols (s package) (when (and (> (length (symbol-name s)) 5) (string-equal "test-" (subseq (symbol-name s) 0 5)) (fboundp s) (typep (symbol-function s) 'generic-function) (plusp (length (compute-applicable-methods (ensure-generic-function s) (list instance))))) (push s res))) (nreverse res)))