;-*- Mode:     Lisp -*-
;;;; Author:   Paul Dietz
;;;; Created:  Sat May  9 11:21:25 1998
;;;; Contains: Common code for creating structure tests

(in-package :cl-test)
(declaim (optimize (safety 3)))

(defun make-struct-test-name (structure-name n)
  ;; (declare (type (or string symbol character) structure-name)
  ;;  (type fixnum n))
  (assert (typep structure-name '(or string symbol character)))
  ;; (assert (typep n 'fixnum))
  (setf structure-name (string structure-name))
  (intern (concatenate 'string
	    structure-name
	    "/"
	    (princ-to-string n))))

(defun make-struct-p-fn (structure-name)
  (assert (typep structure-name '(or string symbol character)))
  (setf structure-name (string structure-name))
  (intern (concatenate 'string
	    structure-name
	    (string '#:-p))))

(defun make-struct-copy-fn (structure-name)
  (assert (typep structure-name '(or string symbol character)))
  (setf structure-name (string structure-name))
  (intern (concatenate 'string
		       (string '#:copy-)
		       structure-name)))

(defun make-struct-field-fn (conc-name field-name)
  "Make field accessor for a field in a structure"
  (cond
   ((null conc-name) field-name)
   (t
    (assert (typep conc-name '(or string symbol character)))
    (assert (typep field-name '(or string symbol character)))
    (setf conc-name (string conc-name))
    (setf field-name (string field-name))
    (intern (concatenate 'string conc-name field-name)))))

(defun make-struct-make-fn (structure-name)
  "Make the make- function for a structure"
  (assert (typep structure-name '(or string symbol character)))
  (setf structure-name (string structure-name))
  (intern (concatenate 'string
	    (string '#:make-) structure-name)))

(defun create-instance-of-type (type)
  "Return an instance of a type.  Signal an error if
  it can't figure out a value for the type."
  (cond
   ((eqt type t)  ;; anything
    'a)
   ((eqt type 'symbol)
    'b)
   ((eqt type 'null) nil)
   ((eqt type 'boolean) t)
   ((eqt type 'keyword) :foo)
   ((eqt type nil) (error "Cannot obtain element of type ~S~%" type))
   ((eqt type 'cons) (cons 'a 'b))
   ((eqt type 'list) (list 1 2 3))
   ((eqt type 'fixnum) 17)
   ((eqt type 'bignum)
    (let ((x 1))
      (loop until (typep x 'bignum)
	  do (setq x (* 2 x)))
      x))
   ((and (symbolp type)
	 (typep type 'structure-class))
    (let ((make-fn
	   (intern (concatenate 'string (string '#:make-) (symbol-name type))
		   (symbol-package type))))
      (eval (list make-fn))))
   ((eqt type 'character) #\w)
   ((eqt type 'base-char) #\z)
   ((member type '(integer unsigned-byte signed-byte)) 35)
   ((eqt type 'bit) 1)
   ((and (consp type)
	 (consp (cdr type))
	 (consp (cddr type))
	 (null (cdddr type))
	 (eqt (car type) 'integer)
	 (integerp (second type)))
    (second type))
   ((member type '(float single-float long-float double-float short-float))
    0.0)
   ((and (consp type)
	 (eqt (car type) 'member)
	 (consp (cdr type)))
    (second type))
   ((and (consp type)
	 (eqt (car type) 'or)
	 (consp (second type)))
    (create-instance-of-type (second type)))
   (t (error "Cannot generate element for type ~S~%" type))))

(defun find-option (option-list option &optional default)
  (loop for opt in option-list
	when (or (eq opt option)
		 (and (consp opt)
		      (eq (car opt) option)))
	return opt
	finally (return default)))

(defvar *defstruct-with-tests-names* nil
  "Names of structure types defined with DEFSRUCT-WITH-TESTS.")

#|
(defvar *subtypep-works-with-classes* t
  "Becomes NIL if SUBTYPEP doesn't work with classes.  We test this first to avoid
   repeated test failures that cause GCL to bomb.")

(deftest subtypep-works-with-classes
  (let ((c1 (find-class 'vector)))
    ;; (setq *subtypep-works-with-classes* nil)
    (subtypep c1 'vector)
    (subtypep 'vector c1)
    ;; (setq *subtypep-works-with-classes* t))
  t)

(defvar *typep-works-with-classes* t
  "Becomes NIL if TYPEP doesn't work with classes.  We test this first to avoid
   repeated test failures that cause GCL to bomb.")

(deftest typep-works-with-classes
  (let ((c1 (find-class 'vector)))
    ;; (setq *typep-works-with-classes* nil)
    (typep #(0 0) c1)
    ;; (setq *typep-works-with-classes* t))
  t)
|#

;;
;; There are a number of standardized tests for
;; structures.  The following macro generates the
;; structure definition and the tests.
;;

(defmacro defstruct-with-tests
    (name-and-options &body slot-descriptions-and-documentation)
"Construct standardized tests for a defstruct, and also
do the defstruct."
  (defstruct-with-tests-fun name-and-options
    slot-descriptions-and-documentation))

(defun defstruct-with-tests-fun (name-and-options
				 slot-descriptions-and-documentation)
  ;; Function called from macro defstruct-with-tests
  (let* (
	 ;; Either NIL or the documentation string for the structure
	 (doc-string
	  (when (and (consp slot-descriptions-and-documentation)
		     (stringp (car slot-descriptions-and-documentation)))
	    (car slot-descriptions-and-documentation)))

	 ;; The list of slot descriptions that follows either the
	 ;; name and options or the doc string
	 (slot-descriptions
	  (if doc-string (cdr slot-descriptions-and-documentation)
	    slot-descriptions-and-documentation))

	 ;; The name of the structure (should be a symbol)
	 (name (if (consp name-and-options)
		   (car name-and-options)
		 name-and-options))

	 ;; The options list, or NIL if there were no options
	 (options (if (consp name-and-options)
		      (cdr name-and-options)
		    nil))

	 ;; List of symbols that are the names of the slots
	 (slot-names
	  (loop
	   for x in slot-descriptions collect
	   (if (consp x) (car x) x)))

	 ;; List of slot types, if any
	 (slot-types
	  (loop
	   for x in slot-descriptions collect
	   (if (consp x)
	       (getf (cddr x) :type :none)
	     :none)))

	 ;; read-only flags for slots
	 (slot-read-only
	  (loop
	   for x in slot-descriptions collect
	   (and (consp x)
		(getf (cddr x) :read-only))))

	 ;; Symbol obtained by prepending MAKE- to the name symbol
	 (make-fn (make-struct-make-fn name))

	 ;; The type option, if specified
	 (type-option (find-option options :type))
	 (struct-type (second type-option))

	 (named-option (find-option options :named))
	 (include-option (find-option options :include))

	 ;; The :predicate option entry from OPTIONS, or NIL if none
	 (predicate-option (find-option options :predicate))

	 ;; The name of the -P function, either the default or the
	 ;; one specified in the :predicate option
	 (p-fn-default (make-struct-p-fn name))
	 (p-fn (cond
		((and type-option (not named-option)) nil)
		((or (eq predicate-option :predicate)
		     (null (cdr predicate-option)))
		 p-fn-default)
		((cadr predicate-option) (cadr predicate-option))
		(t nil)))

	 ;; The :copier option, or NIL if no such option specified
	 (copier-option (find-option options :copier))
	 ;; The name of the copier function, either the default or
	 ;; one speciefied in the :copier option
	 (copy-fn-default (make-struct-copy-fn name))
	 (copy-fn (cond
		   ((or (eq copier-option :copier)
			(null (cdr copier-option)))
		    copy-fn-default)
		   ((cadr copier-option) (cadr copier-option))
		   (t nil)))

	 ;; The :conc-name option, or NIL if none specified
	 (conc-option (find-option options :conc-name))
	 ;; String to be prepended to slot names to get the
	 ;; slot accessor function
	 (conc-prefix-default (concatenate 'string (string name) "-"))
	 (conc-prefix (cond
		       ((null conc-option)
			conc-prefix-default)
		       ((or (eq conc-option :conc-name)
			    (null (cadr conc-option)))
			nil)
		       (t (string (cadr conc-option)))))

	 (initial-offset-option (find-option options :initial-offset))
	 (initial-offset (second initial-offset-option))
	 
	 ;; Accessor names
	 (field-fns
	  (loop for slot-name in slot-names
		collect (make-struct-field-fn conc-prefix slot-name)))

	 ;; a list of initial values
	 (initial-value-alist
	  (loop
	   for slot-desc in slot-descriptions
	   for slot-name in slot-names
	   for type      in slot-types
	   for i from 1
	   collect (if (not (eq type :none))
		       (cons slot-name (create-instance-of-type type))
		     (cons slot-name (defstruct-maketemp name "SLOTTEMP" i)))))
	 )
    ;; Build the tests in an eval-when form
    `(eval-when (compile load eval)

       (ignore-errors
	 (eval '(defstruct ,name-and-options
		  ,@slot-descriptions-and-documentation))
	 ,(unless (or type-option include-option)
	    `(pushnew ',name *defstruct-with-tests-names*))
	 nil)

       ;; Test that structure is of the correct type
       (deftest ,(make-struct-test-name name 1)
	 (and (fboundp (quote ,make-fn))
	      (functionp (function ,make-fn))
	      (symbol-function (quote ,make-fn))
	      (typep (,make-fn) (quote ,(if type-option struct-type
					  name)))
	      t)
	 t)

       ;; Test that the predicate exists
       ,@(when p-fn
	   `((deftest ,(make-struct-test-name name 2)
	       (let ((s (,make-fn)))
		 (and (fboundp (quote ,p-fn))
		      (functionp (function ,p-fn))
		      (symbol-function (quote ,p-fn))
		      (notnot (funcall #',p-fn s))
		      (notnot-mv (,p-fn s))
		      ))
	       t)
	     (deftest ,(make-struct-test-name name "ERROR.1")
	       (classify-error (,p-fn))
	       program-error)
	     (deftest ,(make-struct-test-name name "ERROR.2")
	       (classify-error (,p-fn (,make-fn) nil))
	       program-error)
	     ))

       ;; Test that the elements of *universe* are not
       ;; of this type
       ,@(when p-fn
	   `((deftest ,(make-struct-test-name name 3)
	       (count-if (function ,p-fn) *universe*)
	       0)))
       ,@(unless type-option
	   `((deftest ,(make-struct-test-name name 4)
	       (count-if (function (lambda (x) (typep x (quote ,name))))
			 *universe*)
	       0)))

       ;; Check that the fields can be read after being initialized
       (deftest ,(make-struct-test-name name 5)
	 ,(let ((inits nil)
		(tests nil)
		(var (defstruct-maketemp name "TEMP-5")))
	    (loop
	     for (slot-name . initval) in initial-value-alist
	     for field-fn in field-fns
	     do
	     (setf inits
		   (list* (intern (string slot-name) "KEYWORD")
			  (list 'quote initval)
			  inits))
	     (push `(and 
		     (eqlt (quote ,initval)
			   (,field-fn ,var))
		     (eqlt (quote ,initval)
			   (funcall #',field-fn ,var)))
		   tests))
	    `(let ((,var (,make-fn . ,inits)))
	       (and ,@tests t)))
	 t)

       (deftest ,(make-struct-test-name name "ERROR.3")
	 (remove nil
		 (list
		  ,@(loop
		     for (slot-name . initval) in initial-value-alist
		     for field-fn in field-fns
		     collect
		     `(let ((x (classify-error (,field-fn))))
			(unless (eqt x 'program-error)
			  (list ',slot-name ',field-fn x))))))
	 nil)

       (deftest ,(make-struct-test-name name "ERROR.4")
	 (remove nil
		 (list
		  ,@(loop
		     for (slot-name . initval) in initial-value-alist
		     for field-fn in field-fns
		     collect
		     `(let ((x (classify-error (,field-fn (,make-fn) nil))))
			(unless (eqt x 'program-error)
			  (list ',slot-name ',field-fn x))))))
	 nil)

       ;; Check that two invocations return different structures
       (deftest ,(make-struct-test-name name 6)
	 (eqt (,make-fn) (,make-fn))
	 nil)

       ;; Check that we can setf the fields
       (deftest ,(make-struct-test-name name 7)
	 ,(let* ((var (defstruct-maketemp name "TEMP-7-1"))
		 (var2 (defstruct-maketemp name "TEMP-7-2"))
		 (tests
		  (loop
		   for (slot-name . initval) in initial-value-alist
		   for read-only-p in slot-read-only
		   for slot-desc in slot-descriptions
		   for field-fn in field-fns
		   unless read-only-p
		   collect
		   `(let ((,var2 (quote ,initval)))
		      (setf (,field-fn ,var) ,var2)
		      (eqlt (,field-fn ,var) ,var2)))))
	    `(let ((,var (,make-fn)))
	       (and ,@tests t)))
	 t)

       ;; Check that the copy function exists
       ,@(when copy-fn
	   `((deftest ,(make-struct-test-name name 8)
	       (and (fboundp (quote ,copy-fn))
		    (functionp (function ,copy-fn))
		    (symbol-function (quote ,copy-fn))
		    t)
	       t)
	     (deftest ,(make-struct-test-name name "ERROR.5")
	       (classify-error (,copy-fn))
	       program-error)
	     (deftest ,(make-struct-test-name name "ERROR.6")
	       (classify-error (,copy-fn (,make-fn) nil))
	       program-error)
	     ))	     

       ;; Check that the copy function properly copies fields
       ,@(when copy-fn
	   `((deftest ,(make-struct-test-name name 9)
	       ,(let* ((var 'XTEMP-9)
		       (var2 'YTEMP-9)
		       (var3 'ZTEMP-9))	       
		  `(let ((,var (,make-fn
				,@(loop
				   for (slot-name . initval)
				   in initial-value-alist
				   nconc (list (intern (string slot-name)
						       "KEYWORD")
					       `(quote ,initval))))))
		     (let ((,var2 (,copy-fn ,var))
			   (,var3 (funcall #',copy-fn ,var)))
		       (and
			(not (eqlt ,var ,var2))
			(not (eqlt ,var ,var3))
			(not (eqlt ,var2 ,var3))
			,@(loop
			   for (slot-name . nil) in initial-value-alist
			   for fn in field-fns
			   collect
			   `(and (eqlt (,fn ,var) (,fn ,var2))
				 (eqlt (,fn ,var) (,fn ,var3))))
			t))))
	       t)))

       ;; When the predicate is not the default, check
       ;; that the default is not defined.  Tests should
       ;; be designed so that this function name doesn't
       ;; collide with anything else.
       ,@(unless (eq p-fn p-fn-default)
	   `((deftest ,(make-struct-test-name name 10)
	       (fboundp (quote ,p-fn-default))
	       nil)))

       ;; When the copy function name is not the default, check
       ;; that the default function is not defined.  Tests should
       ;; be designed so that this name is not accidently defined
       ;; for something else.
       ,@(unless (eq copy-fn copy-fn-default)
	   `((deftest ,(make-struct-test-name name 11)
	       (fboundp (quote ,copy-fn-default))
	       nil)))

       ;; When there are read-only slots, test that the SETF
       ;; form for them is not FBOUNDP
       ,@(when (loop for x in slot-read-only thereis x)
	   `((deftest ,(make-struct-test-name name 12)
	       (and
		,@(loop for slot-name in slot-names
			for read-only in slot-read-only
			for field-fn in field-fns
			when read-only
			collect `(not-mv (fboundp '(setf ,field-fn))))
		t)
	       t)))

       ;; When the structure is a true structure type, check that
       ;; the various class relationships hold
       ,@(unless type-option
	   `(
	     (deftest ,(make-struct-test-name name 13)
	       (notnot-mv (typep (,make-fn) (find-class (quote ,name))))
	       t)
	     (deftest ,(make-struct-test-name name 14)
	       (let ((class (find-class (quote ,name))))
		 (notnot-mv (typep class 'structure-class)))
	       t)
	     (deftest ,(make-struct-test-name name 15)
	       (notnot-mv (typep (,make-fn) 'structure-object))
	       t)
	     (deftest ,(make-struct-test-name name 16)
	       (loop for type in *disjoint-types-list*
		     unless (and
			     (equalt (multiple-value-list
				      (subtypep* type (quote ,name)))
				     '(nil t))
			     (equalt (multiple-value-list
				      (subtypep* (quote ,name) type))
				     '(nil t)))				    
		     collect type)
	       nil)
	     (deftest ,(make-struct-test-name name 17)
	       (let ((class (find-class (quote ,name))))
		 (loop for type in *disjoint-types-list*
		       unless (and
			       (equalt (multiple-value-list
					(subtypep* type class))
				       '(nil t))
			       (equalt (multiple-value-list
					(subtypep* class type))
				       '(nil t)))
		       collect type))
	       nil)
	     (deftest ,(make-struct-test-name name "15A")
	       (let ((class (find-class (quote ,name))))
		 (notnot-mv (subtypep class 'structure-object)))
	       t t)
	     (deftest ,(make-struct-test-name name "15B")
	       (notnot-mv (subtypep (quote ,name) 'structure-object))
	       t t)

	     ))

       ;;; Documentation tests

       ,(when doc-string
	  `(deftest ,(make-struct-test-name name 18)
	     (let ((doc (documentation ',name 'structure)))
	       (or (null doc) (equalt doc ',doc-string)))
	     t))

       ,(when (and doc-string (not type-option))
	  `(deftest ,(make-struct-test-name name 19)
	     (let ((doc (documentation ',name 'type)))
	       (or (null doc) (equalt doc ',doc-string)))
	     t))

       ;; Test that COPY-STRUCTURE works, if this is a structure
       ;; type
       ,@(unless type-option
	   `((deftest ,(make-struct-test-name name 20)
	       ,(let* ((var 'XTEMP-20)
		       (var2 'YTEMP-20))
		  `(let ((,var (,make-fn
				,@(loop
				   for (slot-name . initval)
				   in initial-value-alist
				   nconc (list (intern (string slot-name)
						       "KEYWORD")
					       `(quote ,initval))))))
		     (let ((,var2 (copy-structure ,var)))
		       (and
			(not (eqlt ,var ,var2))
			,@(loop
			   for (slot-name . nil) in initial-value-alist
			   for fn in field-fns
			   collect
			   `(eqlt (,fn ,var) (,fn ,var2)))
			t))))
	       t)))
       nil
       )))

(defun defstruct-maketemp (stem suffix1 &optional suffix2)
  "Make a temporary variable for DEFSTRUCT-WITH-TESTS."
  (intern (if suffix2 (format nil "~A-~A-~A" stem suffix1 suffix2)
	    (format nil "~A-~A" stem suffix1))))
