;;;; side-effect-free tests of the condition system

;;;; This software is part of the SBCL system. See the README file for
;;;; more information.
;;;;
;;;; While most of SBCL is derived from the CMU CL system, the test
;;;; files (like this one) were written from scratch after the fork
;;;; from CMU CL.
;;;;
;;;; This software is in the public domain and is provided with
;;;; absolutely no warranty. See the COPYING and CREDITS files for
;;;; more information.

(cl:in-package :cl-user)

(load "test-util.lisp")

;;; Until 0.7.7.21, (MAKE-CONDITION 'FILE-ERROR :PATHNAME "FOO")
;;; wasn't printable, because the REPORT function for FILE-ERROR
;;; referred to unbound slots. This was reported and fixed by Antonio
;;; Martinez (sbcl-devel 2002-09-10).
(format t
        "~&printable now: ~A~%"
        (make-condition 'file-error :pathname "foo"))

(assert (eq
         (block nil
           (macrolet ((opaque-error (arg) `(error ,arg)))
             (handler-bind
                 ((error (lambda (c)
                           (let ((restarts (remove 'res (compute-restarts c)
                                                   :key #'restart-name
                                                   :test-not #'eql)))
                             (assert (= (length restarts) 2))
                             (invoke-restart (second restarts))))))
               (let ((foo1 (make-condition 'error))
                     (foo2 (make-condition 'error)))
                 (restart-case
                     (with-condition-restarts foo1 (list (find-restart 'res))
                       (restart-case
                           (opaque-error foo2)
                         (res () 'int1)
                         (res () 'int2)))
                   (res () 'ext))))))
         'int2))

(assert (eq
         (block nil
           (macrolet ((opaque-error (arg) `(error ,arg)))
             (let ((foo1 (make-condition 'error))
                   (foo2 (make-condition 'error)))
               (handler-bind
                   ((error (lambda (c)
                             (declare (ignore c))
                             (let ((restarts (remove 'res (compute-restarts foo1)
                                                     :key #'restart-name
                                                     :test-not #'eql)))
                               (assert (= (length restarts) 1))
                               (invoke-restart (first restarts))))))
                 (restart-case
                     (with-condition-restarts foo1 (list (find-restart 'res))
                       (restart-case
                           (opaque-error foo2)
                         (res () 'int1)
                         (res () 'int2)))
                   (res () 'ext))))))
         'ext))

(assert (eq
         'ext
         (block nil
           (let ((visible nil)
                 (c1 (make-condition 'error))
                 (c2 (make-condition 'error)))
             (handler-bind
                 ((error
                   (lambda (c)
                     (declare (ignore c))
                     (flet ((check-restarts (length)
                              (assert (= length
                                         (length (remove 'foo (compute-restarts c1)
                                                         :key #'restart-name
                                                         :test-not #'eql))))))
                       (check-restarts 1)
                       (setq visible t)
                       (check-restarts 1)
                       (invoke-restart (find-restart 'foo c1))))))
               (restart-case
                   (restart-case
                       (error c2)
                     (foo () 'in1)
                     (foo () :test (lambda (c) (declare (ignore c)) visible)
                          'in2))
                 (foo () 'ext)))))))

;;; First argument of CERROR is a format control
(assert
 (eq (block nil
       (handler-bind
           ((type-error (lambda (c)
                          (declare (ignore c))
                          (return :failed)))
            (simple-error (lambda (c)
                            (declare (ignore c))
                            (return (if (find-restart 'continue)
                                        :passed
                                        :failed)))))
         (cerror (formatter "Continue from ~A") "bug ~A" :bug)))
     :passed))

(with-test (:name (handler-bind :smoke))
  (let ((called?))
    (flet ((handler (condition)
             (declare (ignore condition))
             (setf called? t)))
      (macrolet ((test (handler)
                   `(progn
                      (setf called? nil)
                      (handler-bind ((condition ,handler))
                        (signal 'condition))
                      (assert called?))))
        ;; Test optimized special cases.
        (test (lambda (condition) (handler condition)))
        (test #'(lambda (condition) (handler condition)))
        ;; Test default behavior.
        ;; (test 'handler) would require function definition => not pure
        (test #'handler)))))

(with-test (:name (handler-bind :malformed-bindings))
  (flet ((test (binding)
           (assert (eq :ok
                       (handler-case
                           (macroexpand `(handler-bind (,binding)))
                         (simple-error (e)
                           (assert (equal (list binding)
                                          (simple-condition-format-arguments e)))
                           :ok))))))

    (test 1)                     ; not even a list
    (test '())                   ; missing condition type and handler
    (test '(error))              ; missing handler
    (test '(error #'print :foo)) ; too many elements
    ))

;;; clauses in HANDLER-CASE are allowed to have declarations (and
;;; indeed, only declarations)
(assert
 (null (handler-case (error "foo") (error () (declare (optimize speed))))))

(handler-case
    (handler-bind ((warning #'muffle-warning))
      (signal 'warning))
  ;; if it's a control error, it had better be printable
  (control-error (c) (format nil "~A" c))
  ;; there had better be an error
  (:no-error (&rest args) (error "No error: ~S" args)))

(handler-case
    (funcall (lambda (x) (check-type x fixnum) x) t)
  (type-error (c)
    (assert (and (subtypep (type-error-expected-type c) 'fixnum)
                 (subtypep 'fixnum (type-error-expected-type c))))
    (assert (eq (type-error-datum c) t)))
  (:no-error (&rest rest) (error "no error: ~S" rest)))

;;; ANSI specifies TYPE-ERROR if datum and arguments of ERROR are not
;;; designators for a condition. Reported by Bruno Haible on cmucl-imp
;;; 2004-10-12.
(flet ((test (&rest args)
         (multiple-value-bind (res err)
             (ignore-errors (apply #'error args))
           (assert (not res))
           (assert (typep err 'type-error))
           (assert (not (nth-value 1 (ignore-errors
                                       (type-error-datum err)))))
           (assert (not (nth-value 1 (ignore-errors
                                       (type-error-expected-type err))))))))
  (test '#:no-such-condition)
  (test nil)
  (test t)
  (test 42)
  (test (make-instance 'standard-object)))

;;; If CERROR is given a condition, any remaining arguments are only
;;; used for the continue format control.
(with-test (:name (cerror :condition-object-and-format-arguments))
  (let ((x 0))
    (handler-bind
        ((simple-error (lambda (c) (incf x) (continue c))))
      (cerror "Continue from ~A at ~A"
              (make-condition 'simple-error :format-control "foo"
                                            :format-arguments nil)
              'cerror (get-universal-time))
      (assert (= x 1)))))

;; Test some of the variations permitted by the RESTART-CASE syntax.
(with-test (:name (restart-case :smoke))
  (macrolet
      ((test (clause &optional (expected ''(:ok)) (args '(:ok)))
         `(assert (equal ,expected
                         (multiple-value-list
                          (restart-case
                              (handler-bind
                                  ((error (lambda (c)
                                            (invoke-restart ',(first clause) ,@args))))
                                (error "foo"))
                            ,clause))))))

    (test (foo (quux) quux))
    (test (foo (&optional quux) quux))
    ;; Multiple values should work.
    (test (foo (a b) (values a b)) '(1 2) (1 2))
    ;; Although somewhat unlikely, these should be legal and return
    ;; the respective keyword when the restart is invoked.
    (test (foo () :report) '(:report) ())
    (test (foo () :interactive) '(:interactive) ())
    (test (foo () :test) '(:test) ())
    ;; Declarations should work normally as part of the restart body.
    (test (foo (quux) :declare ()) '(nil))
    (test (foo () :declare () :report "quux") '("quux") ())))

(with-test (:name (restart-case :malformed-clauses))
  (macrolet
      ((test (clause &optional (expected clause))
         `(assert (eq :ok
                      (handler-case
                          (macroexpand
                           `(restart-case (error "foo") ,',clause))
                        (simple-error (e)
                          (assert (equal '(restart-case ,expected)
                                         (simple-condition-format-arguments e)))
                          :ok))))))

    (test :report)                     ; not even a list
    (test ())                          ; empty
    (test (foo))                       ; no lambda-list
    (test (foo :report))               ; no lambda-list
    (test (foo :report "quux"))        ; no lambda-list
    (test (foo :report "quux" (quux))) ; confused report and lambda list
    ))

(with-test (:name :simple-condition-without-args)
  (let ((sc (make-condition 'simple-condition)))
    (assert (not (simple-condition-format-control sc)))
    (assert (not (simple-condition-format-arguments sc)))
    (assert (stringp (prin1-to-string sc)))
    (assert
     (eq :ok
         (handler-case
             (princ-to-string sc)
           (simple-error (c)
             (when (and (equal "No format-control for ~S"
                               (simple-condition-format-control c))
                        (eq sc (car
                                (simple-condition-format-arguments c))))
               :ok)))))))

(with-test (:name :malformed-simple-condition-printing-type-error)
  (assert (eq :type-error
              (handler-case
                  (princ-to-string
                   (make-condition 'simple-error :format-control "" :format-arguments 8))
                (type-error (e)
                  (when (and (eq 'list (type-error-expected-type e))
                             (eql 8 (type-error-datum e)))
                    :type-error))))))

(with-test (:name (:printing-unintitialized-condition :bug-1184586))
  (prin1-to-string (make-condition 'simple-type-error)))

(with-test (:name (:print-undefined-function-condition))
  (handler-case (funcall '#:foo)
    (undefined-function (c) (princ-to-string c))))

;; Printing a READER-ERROR while the underlying stream is still open
;; should print the stream position information.
(with-test (:name (reader-error :stream-error-position-info :open-stream :bug-1264902))
  (assert
   (search
    "Line: 1, Column: 22, File-Position: 22"
    (with-input-from-string (stream "no-such-package::symbol")
      (handler-case
          (read stream)
        (reader-error (condition) (princ-to-string condition)))))))

;; Printing a READER-ERROR when the underlying stream has been closed
;; should still work, but the stream information will not be printed.
(with-test (:name (reader-error :stream-error-position-info :closed-stream :bug-1264902))
  (assert
   (search
    "Package NO-SUCH-PACKAGE does not exist"
    (handler-case
        (with-input-from-string (stream "no-such-package::symbol")
          (read stream))
      (reader-error (condition) (princ-to-string condition))))))

(with-test (:name (make-condition :non-condition-class))
  (handler-case
      (make-condition 'standard-class)
    (type-error (condition)
      (assert (search "not a condition class"
                      (princ-to-string condition))))))

;; When called with a symbol not designating a condition class,
;; MAKE-CONDITION used to signal an error which printed as "NIL does
;; not designate a condition class.".
(with-test (:name (make-condition :correct-error-for-undefined-condition
                   :bug-1199223))
  (handler-case
      (make-condition 'no-such-condition)
    (type-error (condition)
      (assert (search (string 'no-such-condition)
                      (princ-to-string condition))))))

;; Using an undefined condition type in a HANDLER-BIND clause should
;; signal an ERROR at runtime. Bug 1378939 was about landing in LDB
;; because of infinite recursion in SIGNAL instead.
;;
;; We suppress the compile-time WARNING to avoid noise when running
;; tests.
(locally (declare (muffle-conditions warning))
  (with-test (:name (handler-bind :undefined-condition-type
                     :bug-1378939))
    (assert-error
     (handler-bind ((no-such-condition-class #'print))
       (error "does not matter")))))

;; Using an undefined condition type in a HANDLER-BIND clause should
;; signal a [STYLE-]WARNING at compile time.
(with-test (:name (handler-bind :undefined-condition-type
                   :compile-time-warning))
  (handler-bind ((warning #'muffle-warning))
    (assert-signal
     (compile nil '(lambda () (handler-bind
                                  ((no-such-condition-class #'print)))))
     warning)))

;; Empty bindings in HANDLER-BIND pushed an empty cluster onto
;; *HANDLER-CLUSTERS* which was not expected by SIGNAL (and wasteful).
(with-test (:name (handler-bind :empty-bindings :bug-1388707))
  (multiple-value-bind (value condition)
      (ignore-errors (handler-bind () (error "Foo")))
    (assert (null value))
    (assert (typep condition 'simple-error))))
