;;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*-
;;;; *************************************************************************
;;;; FILE IDENTIFICATION
;;;;
;;;; ID:      $Id$
;;;; Purpose: Test fixtures for XLUnit
;;;;
;;;; *************************************************************************

(in-package #:xlunit)


(defclass test-fixture ()
  ((test-fn
    :initarg :test-fn :reader test-fn :initform nil
    :documentation
    "A function designator which will be applied to this instance
to perform that test-case.")
   (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."))

(defgeneric setup (test)
  (:documentation
   "Method called before performing a test, should set up the
environment the test-case needs to operate in."))

(defmethod setup ((test test-fixture))
  t)

(defgeneric teardown (test)
  (:documentation
   "Method called after performing a test.  Should reverse everything
that the setup method did for this instance."))

(defmethod teardown ((test 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)))


(defmethod run-test ((test test-fixture)
                     &key (result (make-instance 'test-result))
                     (handle-errors t))
  "Perform the test represented by the given test-case or test-suite.
Returns a test-result object."
  (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-fn test) test))))
           (when (typep res 'test-failure-condition)
             (push (make-test-failure test res) failures)))
         (test-failure-condition (failure)
           (push (make-test-failure test failure) failures))
         (error (err)
           (push (make-test-failure test err) errors)))

        (if handle-errors
            (handler-case
                (teardown test)
              (error (err)
                (push (make-test-failure test err) errors)))
            (teardown test))))
  result)


(defun make-test (fixture name &key test-fn test-suite description)
  "Create a test-case which is an instance of FIXTURE.  TEST-FN 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 (etypecase name
                                (symbol
                                 (string-downcase (symbol-name name)))
                                (string
                                 name))
                   :test-fn
                   (if(and (symbolp name) (null test-fn))
                       name
                     test-fn)
                   :description description)))
    (when test-suite (add-test newtest test-suite))
    newtest))
