;;; eieio-tests.el --- eieio test routines -*- lexical-binding: t -*-

;; Copyright (C) 1999-2003, 2005-2010, 2012-2022 Free Software
;; Foundation, Inc.

;; Author: Eric M. Ludlam <zappo@gnu.org>

;; This file is part of GNU Emacs.

;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Test the various features of EIEIO.

(require 'ert)
(require 'eieio)
(require 'eieio-base)
;; FIXME: See Bug#52971.
(with-no-warnings
  (require 'eieio-compat))
(require 'eieio-opt)

(eval-when-compile (require 'cl-lib))

;; Silence byte-compiler.
(eval-when-compile
  (dolist (slot '(:a :b ooga-booga :derived-value missing-slot))
    (cl-pushnew slot eieio--known-slot-names)))

;;; Code:
;; Set up some test classes
(defclass class-a ()
  ((water :initarg :water
          :initform 'h20
	  :type symbol
	  :documentation "Detail about water.")
   (classslot :initform 'penguin
	      :type symbol
	      :documentation "A class allocated slot."
	      :allocation :class)
   (test-tag :initform nil
	     :documentation "Used to make sure methods are called.")
   (self :initform nil
	 :type (or null class-a)
	 :documentation "Test self referencing types.")
   )
  "Class A.")

;; Silence compiler warning about `water' not being a class-allocated slot.
(defclass eieio-tests--dummy () ((water :allocation :class)))

(defclass class-b ()
  ((land :initform "Sc"
	 :type string
	 :documentation "Detail about land."))
  "Class B.")

(defclass class-ab (class-a class-b)
  ((amphibian :initform "frog"
	      :documentation "Detail about amphibian on land and water."))
  "Class A and B combined.")

(with-no-warnings ; FIXME: Make more specific.
  (defclass class-c ()
    ((slot-1 :initarg :moose
             :initform 'moose
             :type symbol
             :allocation :instance
             :documentation "First slot testing slot arguments."
             :custom symbol
             :label "Wild Animal"
             :group borg
             :protection :public)
     (slot-2 :initarg :penguin
             :initform "penguin"
             :type string
             :allocation :instance
             :documentation "Second slot testing slot arguments."
             :custom string
             :label "Wild bird"
             :group vorlon
             :accessor get-slot-2
             :protection :private)
     (slot-3 :initarg :emu
             :initform 'emu
             :type symbol
             :allocation :class
             :documentation "Third slot test class allocated accessor"
             :custom symbol
             :label "Fuzz"
             :group tokra
             :accessor get-slot-3
             :protection :private)
     )
    (:custom-groups (foo))
    "A class for testing slot arguments."
    ))

(defclass class-subc (class-c)
  ((slot-1 ;; :initform moose  - don't override this
    )
   (slot-2 :initform "linux" ;; Do override this one
	   :protection :private
	   ))
  "A class for testing slot arguments.")

;;; Defining a class with a slot tag error
;;
;; Temporarily disable this test because of macro expansion changes in
;; current Emacs trunk. It can be re-enabled when we have moved
;; `eieio-defclass' into the `defclass' macro and the
;; `eval-and-compile' there is removed.

;; (let ((eieio-error-unsupported-class-tags t))
;;   (condition-case nil
;;       (progn
;; 	(defclass class-error ()
;; 	  ((error-slot :initarg :error-slot
;; 		       :badslottag 1))
;; 	  "A class with a bad slot tag.")
;; 	(error "No error was thrown for badslottag"))
;;     (invalid-slot-type nil)))

;; (let ((eieio-error-unsupported-class-tags nil))
;;   (condition-case nil
;;       (progn
;; 	(defclass class-error ()
;; 	  ((error-slot :initarg :error-slot
;; 		       :badslottag 1))
;; 	  "A class with a bad slot tag."))
;;     (invalid-slot-type
;;      (error "invalid-slot-type thrown when eieio-error-unsupported-class-tags is nil")
;;      )))

;; Silence byte-compiler.
(declare-function eitest-subordinate--eieio-childp nil)
(declare-function class-alloc-initarg--eieio-childp nil)
(ert-deftest eieio-test-01-mix-alloc-initarg ()
  ;; Only run this test if the message framework thingy works.
  (skip-unless (and (message "foo") (string= "foo" (current-message))))

  ;; Defining this class should generate a warning(!) message that
  ;; you should not mix :initarg with class allocated slots.
  (with-no-warnings ; FIXME: Make more specific.
    (defclass class-alloc-initarg ()
      ((throwwarning :initarg :throwwarning
                     :allocation :class))
      "Throw a warning mixing allocation class and an initarg."))

  ;; Check that message is there
  (should (current-message))
  (should (string-match "Class allocated slots do not need :initarg"
                        (current-message))))

(defclass abstract-class ()
  ((some-slot :initarg :some-slot
	      :initform nil
	      :documentation "A slot."))
  :documentation "An abstract class."
  :abstract t)

(ert-deftest eieio-test-02-abstract-class ()
  ;; Abstract classes cannot be instantiated, so this should throw an
  ;; error
  (should-error (abstract-class)))

(with-suppressed-warnings ((obsolete defgeneric))
  (defgeneric generic1 () "First generic function."))

(ert-deftest eieio-test-03-generics ()
  (with-suppressed-warnings ((obsolete defmethod)
                             (obsolete defgeneric))
    (defun anormalfunction () "A plain function for error testing." nil)
    (should-error
     (progn
       (defgeneric anormalfunction ()
         "Attempt to turn it into a generic.")))

    ;; Check that generic-p works
    (should (generic-p 'generic1))

    (defmethod generic1 ((_c class-a))
      "Method on generic1."
      'monkey)

    (defmethod generic1 (not-an-object)
      "Method generic1 that can take a non-object."
      not-an-object)

    (let ((ans-obj (generic1 (class-a)))
          (ans-num (generic1 666)))
      (should (eq ans-obj 'monkey))
      (should (eq ans-num 666)))))

(defclass static-method-class ()
  ((some-slot :initform nil
	      :allocation :class
	      :documentation "A slot."))
  :documentation "A class used for testing static methods.")

(with-suppressed-warnings ((obsolete defmethod)
                           (obsolete defgeneric))
  (defmethod static-method-class-method :STATIC ((c static-method-class) value)
    "Test static methods.
Argument C is the class bound to this static method."
    (if (eieio-object-p c) (setq c (eieio-object-class c)))
    (oset-default c some-slot value)))

;; Silence byte-compiler.
(declare-function static-method-class-2 nil)
(declare-function static-method-class-2--eieio-childp nil)
(ert-deftest eieio-test-04-static-method ()
  ;; Call static method on a class and see if it worked
  (static-method-class-method 'static-method-class 'class)
  (should (eq (oref-default 'static-method-class some-slot) 'class))
  (static-method-class-method (static-method-class) 'object)
  (should (eq (oref-default 'static-method-class some-slot) 'object)))

(ert-deftest eieio-test-05-static-method-2 ()
  (defclass static-method-class-2 (static-method-class)
    ()
    "A second class after the previous for static methods.")

  (with-suppressed-warnings ((obsolete defmethod)
                             (obsolete defgeneric))
    (defmethod static-method-class-method :STATIC ((c static-method-class-2) value)
      "Test static methods.
Argument C is the class bound to this static method."
      (if (eieio-object-p c) (setq c (eieio-object-class c)))
      (oset-default c some-slot (intern (concat "moose-" (symbol-name value))))))

  (static-method-class-method 'static-method-class-2 'class)
  (should (eq (oref-default 'static-method-class-2 some-slot) 'moose-class))
  (static-method-class-method (static-method-class-2) 'object)
  (should (eq (oref-default 'static-method-class-2 some-slot) 'moose-object)))


;;; Perform method testing
;;

;;; Multiple Inheritance, and method signal testing
;;
(defvar eitest-ab nil)
(defvar eitest-a nil)
(defvar eitest-b nil)
(ert-deftest eieio-test-06-allocate-objects ()
   ;; allocate an object to use
   (should (setq eitest-ab (class-ab)))
   (should (setq eitest-a (class-a)))
   (should (setq eitest-b (class-b))))

(ert-deftest eieio-test-07-make-instance ()
  (should (make-instance 'class-ab))
  (should (make-instance 'class-a :water 'cho))
  (should (make-instance 'class-b)))

(with-suppressed-warnings ((obsolete defmethod)
                           (obsolete defgeneric))
  (defmethod class-cn ((_a class-a))
    "Try calling `call-next-method' when there isn't one.
Argument A is object of type symbol `class-a'."
    (with-suppressed-warnings ((obsolete call-next-method))
      (call-next-method)))

  (defmethod no-next-method ((_a class-a) &rest _args)
    "Override signal throwing for variable `class-a'.
Argument A is the object of class variable `class-a'."
    'moose))

(ert-deftest eieio-test-08-call-next-method ()
  ;; Play with call-next-method
  (should (eq (class-cn eitest-ab) 'moose)))

(with-suppressed-warnings ((obsolete defmethod)
                           (obsolete defgeneric))
  (defmethod no-applicable-method ((_b class-b) _method &rest _args)
    "No need.
Argument B is for booger.
METHOD is the method that was attempting to be called."
    'moose))

(ert-deftest eieio-test-09-no-applicable-method ()
  ;; Non-existing methods.
  (should (eq (class-cn eitest-b) 'moose)))

(with-suppressed-warnings ((obsolete defmethod)
                           (obsolete defgeneric))
  (defmethod class-fun ((_a class-a))
    "Fun with class A."
    'moose)

  (defmethod class-fun ((_b class-b))
    "Fun with class B."
    (error "Class B fun should not be called"))

  (defmethod class-fun-foo ((_b class-b))
    "Foo Fun with class B."
    'moose)

  (defmethod class-fun2 ((_a class-a))
    "More fun with class A."
    'moose)

  (defmethod class-fun2 ((_b class-b))
    "More fun with class B."
    (error "Class B fun2 should not be called"))

  (defmethod class-fun2 ((_ab class-ab))
    "More fun with class AB."
    (with-suppressed-warnings ((obsolete call-next-method))
      (call-next-method)))

  ;; How about if B is the only slot?
  (defmethod class-fun3 ((_b class-b))
    "Even More fun with class B."
    'moose)

  (defmethod class-fun3 ((_ab class-ab))
    "Even More fun with class AB."
    (with-suppressed-warnings ((obsolete call-next-method))
      (call-next-method))))

(ert-deftest eieio-test-10-multiple-inheritance ()
  ;; play with methods and mi
  (should (eq (class-fun eitest-ab) 'moose))
  (should (eq (class-fun-foo eitest-ab) 'moose))
  ;; Play with next-method and mi
  (should (eq (class-fun2 eitest-ab) 'moose))
  (should (eq (class-fun3 eitest-ab) 'moose)))

(ert-deftest eieio-test-11-self ()
  ;; Try the self referencing test
  (should (oset eitest-a self eitest-a))
  (should (oset eitest-ab self eitest-ab)))


(defvar class-fun-value-seq '())
(with-suppressed-warnings ((obsolete defmethod)
                           (obsolete defgeneric))
  (defmethod class-fun-value :BEFORE ((_a class-a))
    "Return `before', and push `before' in `class-fun-value-seq'."
    (push 'before class-fun-value-seq)
    'before)

  (defmethod class-fun-value :PRIMARY ((_a class-a))
    "Return `primary', and push `primary' in `class-fun-value-seq'."
    (push 'primary class-fun-value-seq)
    'primary)

  (defmethod class-fun-value :AFTER ((_a class-a))
    "Return `after', and push `after' in `class-fun-value-seq'."
    (push 'after class-fun-value-seq)
    'after))

(ert-deftest eieio-test-12-generic-function-call ()
  ;; Test value of a generic function call
  ;;
  (let* ((class-fun-value-seq nil)
	 (value (class-fun-value eitest-a)))
    ;; Test if generic function call returns the primary method's value
    (should (eq value 'primary))
    ;; Make sure :before and :after methods were run
    (should (equal class-fun-value-seq '(after primary before)))))

;;; Test initialization methods
;;

(ert-deftest eieio-test-13-init-methods ()
  (with-suppressed-warnings ((obsolete defmethod)
                             (obsolete defgeneric)
                             (obsolete call-next-method))
    (defmethod initialize-instance ((a class-a) &rest _slots)
      "Initialize the slots of class-a."
      (call-next-method)
      (if (/= (oref a test-tag) 1)
          (error "shared-initialize test failed."))
      (oset a test-tag 2))

    (defmethod shared-initialize ((a class-a) &rest _slots)
      "Shared initialize method for class-a."
      (call-next-method)
      (oset a test-tag 1))

    (let ((ca (class-a)))
      (should (= (oref ca test-tag) 2)))))


;;; Perform slot testing
;;
(ert-deftest eieio-test-14-slots ()
  ;; Check slot existence
  (should (oref eitest-ab water))
  (should (oref eitest-ab land))
  (should (oref eitest-ab amphibian)))

(ert-deftest eieio-test-15-slot-missing ()
  (with-suppressed-warnings ((obsolete defmethod)
                             (obsolete defgeneric))
    (defmethod slot-missing ((_ab class-ab) &rest _foo)
      "If a slot in AB is unbound, return something cool.  FOO."
      'moose))

  (should (eq (oref eitest-ab ooga-booga) 'moose))
  (should-error (oref eitest-a ooga-booga) :type 'invalid-slot-name))

(ert-deftest eieio-test-16-slot-makeunbound ()
  (slot-makeunbound eitest-a 'water)
  ;; Should now be unbound
  (should-not (slot-boundp eitest-a 'water))
  ;; But should still exist
  (should (slot-exists-p eitest-a 'water))
  (should-not (slot-exists-p eitest-a 'moose))
  ;; oref of unbound slot must fail
  (should-error (oref eitest-a water) :type 'unbound-slot))

(defvar eitest-vsca nil)
(defvar eitest-vscb nil)
(defclass virtual-slot-class ()
  ((base-value :initarg :base-value))
  "Class has real slot :base-value and simulated slot :derived-value.")
(with-suppressed-warnings ((obsolete defmethod)
                           (obsolete defgeneric))
  (defmethod slot-missing ((vsc virtual-slot-class)
                           slot-name operation &optional new-value)
    "Simulate virtual slot derived-value."
    (cond
     ((or (eq slot-name :derived-value)
          (eq slot-name 'derived-value))
      (with-slots (base-value) vsc
        (if (eq operation 'oref)
            (+ base-value 1)
          (setq base-value (- new-value 1)))))
     (t (with-suppressed-warnings ((obsolete call-next-method))
          (call-next-method))))))

(ert-deftest eieio-test-17-virtual-slot ()
  (setq eitest-vsca (virtual-slot-class :base-value 1))
  ;; Check slot values
  (should (= (oref eitest-vsca base-value) 1))
  (should (= (oref eitest-vsca :derived-value) 2))

  (oset eitest-vsca derived-value 3)
  (should (= (oref eitest-vsca base-value) 2))
  (should (= (oref eitest-vsca :derived-value) 3))

  (oset eitest-vsca base-value 3)
  (should (= (oref eitest-vsca base-value) 3))
  (should (= (oref eitest-vsca :derived-value) 4))

  ;; should also be possible to initialize instance using virtual slot

  (setq eitest-vscb (virtual-slot-class :derived-value 5))
  (should (= (oref eitest-vscb base-value) 4))
  (should (= (oref eitest-vscb :derived-value) 5)))

(ert-deftest eieio-test-18-slot-unbound ()
  (with-suppressed-warnings ((obsolete defmethod)
                             (obsolete defgeneric))
    (defmethod slot-unbound ((_a class-a) &rest _foo)
      "If a slot in A is unbound, ignore FOO."
      'moose)

    (should (eq (oref eitest-a water) 'moose))

    ;; Check if oset of unbound works
    (oset eitest-a water 'moose)
    (should (eq (oref eitest-a water) 'moose))

    ;; oref/oref-default comparison
    (should-not (eq (oref eitest-a water) (oref-default eitest-a water)))

    ;; oset-default -> oref/oref-default comparison
    (oset-default (eieio-object-class eitest-a) water 'moose)
    (should (eq (oref eitest-a water) (oref-default eitest-a water)))

    ;; After setting 'water to 'moose, make sure a new object has
    ;; the right stuff.
    (oset-default (eieio-object-class eitest-a) water 'penguin)
    (should (eq (oref (class-a) water) 'penguin))

    ;; Revert the above
    (defmethod slot-unbound ((_a class-a) &rest _foo)
      "If a slot in A is unbound, ignore FOO."
      ;; Disable the old slot-unbound so we can run this test
      ;; more than once
      (with-suppressed-warnings ((obsolete call-next-method))
        (call-next-method)))))

(ert-deftest eieio-test-19-slot-type-checking ()
  ;; Slot type checking
  ;; We should not be able to set a string here
  (should-error (oset eitest-ab water "a string, not a symbol") :type 'invalid-slot-type)
  (should-error (oset eitest-ab classslot "a string, not a symbol") :type 'invalid-slot-type)
  (should-error (class-a :water "a string not a symbol") :type 'invalid-slot-type))

(ert-deftest eieio-test-20-class-allocated-slots ()
  ;; Test out class allocated slots
  (defvar eitest-aa nil)
  (setq eitest-aa (class-a))

  ;; Make sure class slots do not track between objects
  (let ((newval 'moose))
    (oset eitest-aa classslot newval)
    (should (eq (oref eitest-a classslot) newval))
    (should (eq (oref eitest-aa classslot) newval)))

  ;; Slot should be bound
  (should (slot-boundp eitest-a 'classslot))
  (should (slot-boundp 'class-a 'classslot))

  (slot-makeunbound eitest-a 'classslot)

  (should-not (slot-boundp eitest-a 'classslot))
  (should-not (slot-boundp 'class-a 'classslot)))


(defvar eieio-test-permuting-value nil)
(defvar eitest-pvinit nil)
(eval-and-compile
  (setq eieio-test-permuting-value 1))

(defclass inittest nil
  ((staticval :initform 1)
   (symval :initform 'eieio-test-permuting-value)
   (evalval :initform (symbol-value 'eieio-test-permuting-value))
   (evalnow :initform (symbol-value 'eieio-test-permuting-value)
	    :allocation :class)
   )
  "Test initforms that eval.")

(ert-deftest eieio-test-21-eval-at-construction-time ()
  ;; initforms that need to be evalled at construction time.
  (setq eieio-test-permuting-value 2)
  (setq eitest-pvinit (inittest))

  (should (eq (oref eitest-pvinit staticval) 1))
  (should (eq (oref eitest-pvinit symval) 'eieio-test-permuting-value))
  (should (eq (oref eitest-pvinit evalval) 2))
  (should (eq (oref eitest-pvinit evalnow) 1)))

;; Silence byte-compiler.
(defvar eitest-tests nil)
(declare-function eitest-superior nil)
(declare-function eitest-superior--eieio-childp nil)
(ert-deftest eieio-test-22-init-forms-dont-match-runnable ()
  ;; Init forms with types that don't match the runnable.
  (defclass eitest-subordinate nil
    ((text :initform "" :type string))
    "Test class that will be a calculated value.")

  (defclass eitest-superior nil
    ((sub :initform (funcall #'eitest-subordinate)
	  :type eitest-subordinate))
    "A class with an initform that creates a class.")

  (should (setq eitest-tests (eitest-superior)))

  (should-error
   (eval
    '(defclass broken-init nil
       ((broken :initform 1
		:type string))
       "This class should break."))
   :type 'invalid-slot-type))

(ert-deftest eieio-test-23-inheritance-check ()
  (should (child-of-class-p 'class-ab 'class-a))
  (should (child-of-class-p 'class-ab 'class-b))
  (should (object-of-class-p eitest-a 'class-a))
  (should (object-of-class-p eitest-ab 'class-a))
  (should (object-of-class-p eitest-ab 'class-b))
  (should (object-of-class-p eitest-ab 'class-ab))
  (should (eq (eieio-class-parents 'class-a) nil))
  (should (equal (eieio-class-parents 'class-ab)
                 (mapcar #'find-class '(class-a class-b))))
  (should (same-class-p eitest-a 'class-a))
  (should (class-a-p eitest-a))
  (should (not (class-a-p eitest-ab)))
  (should (cl-typep eitest-a 'class-a))
  (should (cl-typep eitest-ab 'class-a))
  (should (not (class-a-p "foo")))
  (should (not (cl-typep "foo" 'class-a))))

(ert-deftest eieio-test-24-object-predicates ()
  (let ((listooa (list (class-ab) (class-a)))
	(listoob (list (class-ab) (class-b))))
    (should (cl-typep listooa '(list-of class-a)))
    (should (cl-typep listoob '(list-of class-b)))
    (should-not (cl-typep listooa '(list-of class-b)))
    (should-not (cl-typep listoob '(list-of class-a)))))

;; Silence byte-compiler.
(defvar eitest-t1 nil)
(declare-function eieio-tests-initform-not-evaluated-when-initarg-is-present nil)
(declare-function eieio-tests-initform-not-evaluated-when-initarg-is-present--eieio-childp nil)
(ert-deftest eieio-test-25-slot-tests ()
  (setq eitest-t1 (class-c))
  ;; Slot initialization
  (should (eq (oref eitest-t1 slot-1) 'moose))
  ;; Accessing via the initarg name is deprecated!
  ;; (should (eq (oref eitest-t1 :moose) 'moose))
  ;; Don't pass reference of private slot
  ;;PRIVATE (should-error (oref eitest-t1 slot-2) :type 'invalid-slot-name)
  ;; Check private slot accessor
  (should (string= (get-slot-2 eitest-t1) "penguin"))
  ;; Pass string instead of symbol
  (should-error (class-c :moose "not a symbol") :type 'invalid-slot-type)
  (should (eq (get-slot-3 eitest-t1) 'emu))
  (should (eq (get-slot-3 'class-c) 'emu))
  ;; Check setf
  (setf (get-slot-3 eitest-t1) 'setf-emu)
  (should (eq (get-slot-3 eitest-t1) 'setf-emu))
  ;; Roll back
  (setf (get-slot-3 eitest-t1) 'emu)
  (defvar eieio-tests-initform-was-evaluated)
  (defclass eieio-tests-initform-not-evaluated-when-initarg-is-present ()
    ((slot-with-initarg-and-initform
      :initarg :slot-with-initarg-and-initform
      :initform (setf eieio-tests-initform-was-evaluated t))))
  (setq eieio-tests-initform-was-evaluated nil)
  (make-instance
   'eieio-tests-initform-not-evaluated-when-initarg-is-present)
  (should eieio-tests-initform-was-evaluated)
  (setq eieio-tests-initform-was-evaluated nil)
  (make-instance
   'eieio-tests-initform-not-evaluated-when-initarg-is-present
   :slot-with-initarg-and-initform t)
  (should-not eieio-tests-initform-was-evaluated))

(defvar eitest-t2 nil)
(ert-deftest eieio-test-26-default-inheritance ()
  ;; See previous test, nor for subclass
  (setq eitest-t2 (class-subc))
  (should (eq (oref eitest-t2 slot-1) 'moose))
  ;; Accessing via the initarg name is deprecated!
  ;;(should (eq (oref eitest-t2 :moose) 'moose))
  (should (string= (get-slot-2 eitest-t2) "linux"))
  ;;PRIVATE (should-error (oref eitest-t2 slot-2) :type 'invalid-slot-name)
  (should (string= (get-slot-2 eitest-t2) "linux"))
  (should-error (class-subc :moose "not a symbol") :type 'invalid-slot-type))

;;(ert-deftest eieio-test-27-inherited-new-value ()
  ;;; HACK ALERT: The new value of a class slot is inherited by the
  ;; subclass!  This is probably a bug.  We should either share the slot
  ;; so sets on the baseclass change the subclass, or we should inherit
  ;; the original value.
;;  (should (eq (get-slot-3 eitest-t2) 'emu))
;;  (should (eq (get-slot-3 class-subc) 'emu))
;;  (setf (get-slot-3 eitest-t2) 'setf-emu)
;;  (should (eq (get-slot-3 eitest-t2) 'setf-emu)))

;; Slot protection
(defclass prot-0 ()
  ()
  "Protection testing baseclass.")

(with-suppressed-warnings ((obsolete defmethod)
                           (obsolete defgeneric))
  (defmethod prot0-slot-2 ((s2 prot-0))
    "Try to access slot-2 from this class which doesn't have it.
The object S2 passed in will be of class prot-1, which does have
the slot.  This could be allowed, and currently is in EIEIO.
Needed by the eieio persistent base class."
    (oref s2 slot-2)))

(defclass prot-1 (prot-0)
  ((slot-1 :initarg :slot-1
	   :initform nil
	   :protection :public)
   (slot-2 :initarg :slot-2
	   :initform nil
	   :protection :protected)
   (slot-3 :initarg :slot-3
	   :initform nil
	   :protection :private))
  "A class for testing the :protection option.")

(defclass prot-2 (prot-1)
  nil
  "A class for testing the :protection option.")

(with-suppressed-warnings ((obsolete defmethod)
                           (obsolete defgeneric))
  (defmethod prot1-slot-2 ((s2 prot-1))
    "Try to access slot-2 in S2."
    (oref s2 slot-2))

  (defmethod prot1-slot-2 ((s2 prot-2))
    "Try to access slot-2 in S2."
    (oref s2 slot-2))

  (defmethod prot1-slot-3-only ((s2 prot-1))
    "Try to access slot-3 in S2.
Do not override for `prot-2'."
    (oref s2 slot-3))

  (defmethod prot1-slot-3 ((s2 prot-1))
    "Try to access slot-3 in S2."
    (oref s2 slot-3))

  (defmethod prot1-slot-3 ((s2 prot-2))
    "Try to access slot-3 in S2."
    (oref s2 slot-3)))

(defvar eitest-p1 nil)
(defvar eitest-p2 nil)
(ert-deftest eieio-test-28-slot-protection ()
  (setq eitest-p1 (prot-1))
  (setq eitest-p2 (prot-2))
  ;; Access public slots
  (oref eitest-p1 slot-1)
  (oref eitest-p2 slot-1)
  ;; Accessing protected slot out of context used to fail, but we dropped this
  ;; feature, since it was underused and no one noticed that the check was
  ;; incorrect (much too loose).
  ;;PROTECTED (should-error (oref eitest-p1 slot-2) :type 'invalid-slot-name)
  ;; Access protected slot in method
  (prot1-slot-2 eitest-p1)
  ;; Protected slot in subclass method
  (prot1-slot-2 eitest-p2)
  ;; Protected slot from parent class method
  (prot0-slot-2 eitest-p1)
  ;; Accessing private slot out of context used to fail, but we dropped this
  ;; feature, since it was not used.
  ;;PRIVATE (should-error (oref eitest-p1 slot-3) :type 'invalid-slot-name)
  ;; Access private slot in method
  (prot1-slot-3 eitest-p1)
  ;; Access private slot in subclass method must fail
  ;;PRIVATE (should-error (prot1-slot-3 eitest-p2) :type 'invalid-slot-name)
  ;; Access private slot by same class
  (prot1-slot-3-only eitest-p1)
  ;; Access private slot by subclass in sameclass method
  (prot1-slot-3-only eitest-p2))

;;; eieio-instance-inheritor
;; Test to make sure this works.
(defclass II (eieio-instance-inheritor)
  ((slot1 :initform 1)
   (slot2)
   (slot3))
  "Instance Inheritor test class.")

(defvar eitest-II1 nil)
(defvar eitest-II2 nil)
(defvar eitest-II3 nil)
(ert-deftest eieio-test-29-instance-inheritor ()
  (setq eitest-II1 (II))
  (oset eitest-II1 slot2 'cat)
  (setq eitest-II2 (clone eitest-II1 "eitest-II2 Test."))
  (oset eitest-II2 slot1 'moose)
  (setq eitest-II3 (clone eitest-II2 "eitest-II3 Test."))
  (oset eitest-II3 slot3 'penguin)

  ;; Test that slots are non-initialized slots are unbounded
  (oref eitest-II2 slot1)
  (should (slot-boundp eitest-II2 'slot1))
  (should-not (slot-boundp eitest-II2 'slot2))
  (should-not (slot-boundp eitest-II2 'slot3))
  (should-not (slot-boundp eitest-II3 'slot2))
  (should-not (slot-boundp eitest-II3 'slot1))
  (should-not (slot-boundp eitest-II3 'slot2))
  (should (eieio-instance-inheritor-slot-boundp eitest-II3 'slot2))
  (should (slot-boundp eitest-II3 'slot3))

  ;; Test level 1 inheritance
  (should (eq (oref eitest-II3 slot1) 'moose))
  ;; Test level 2 inheritance
  (should (eq (oref eitest-II3 slot2) 'cat))
  ;; Test level 0 inheritance
  (should (eq (oref eitest-II3 slot3) 'penguin)))

(defclass slotattr-base ()
  ((initform :initform 'init)
   (type :type list)
   (initarg :initarg :initarg)
   (protection :protection :private)
   (custom :custom (repeat string)
	   :label "Custom Strings"
	   :group moose)
   (docstring :documentation
	      "Replace the doc-string for this property.")
   (printer :printer printer1)
   )
  "Baseclass we will attempt to subclass.
Subclasses to override slot attributes.")

(defclass slotattr-ok (slotattr-base)
  ((initform :initform 'no-init)
   (initarg :initarg :initblarg)
   (custom :custom string
	   :label "One String"
	   :group cow)
   (docstring :documentation
	      "A better doc string for this class.")
   (printer :printer printer2)
   )
  "This class should allow overriding of various slot attributes.")


(ert-deftest eieio-test-30-slot-attribute-override ()
  ;; Subclass should not override :protection slot attribute
  ;;PROTECTION is gone.
  ;;(should-error
  ;;       (eval
  ;;        '(defclass slotattr-fail (slotattr-base)
  ;;           ((protection :protection :public)
  ;;            )
  ;;           "This class should throw an error.")))

  ;; Subclass should not override :type slot attribute
  (should-error
	(eval
	 '(defclass slotattr-fail (slotattr-base)
	  ((type :type string)
	   )
	  "This class should throw an error.")))

  ;; Initform should override instance allocation
  (let ((obj (slotattr-ok)))
    (should (eq (oref obj initform) 'no-init))))

(with-no-warnings ; FIXME: Make more specific.
  (defclass slotattr-class-base ()
    ((initform :allocation :class
               :initform 'init)
     (type :allocation :class
           :type list)
     (initarg :allocation :class
              :initarg :initarg)
     (protection :allocation :class
                 :protection :private)
     (custom :allocation :class
             :custom (repeat string)
             :label "Custom Strings"
             :group moose)
     (docstring :allocation :class
                :documentation
                "Replace the doc-string for this property.")
     )
    "Baseclass we will attempt to subclass.
Subclasses to override slot attributes."))

(defclass slotattr-class-ok (slotattr-class-base)
  ((initform :initform 'no-init)
   (initarg :initarg :initblarg)
   (custom :custom string
	   :label "One String"
	   :group cow)
   (docstring :documentation
	      "A better doc string for this class.")
   )
  "This class should allow overriding of various slot attributes.")


(ert-deftest eieio-test-31-slot-attribute-override-class-allocation ()
  ;; Same as test-30, but with class allocation
  ;;PROTECTION is gone.
  ;;(should-error
  ;;     (eval
  ;;      '(defclass slotattr-fail (slotattr-class-base)
  ;;         ((protection :protection :public)
  ;;          )
  ;;         "This class should throw an error.")))
  (should-error
      (eval
       '(defclass slotattr-fail (slotattr-class-base)
	  ((type :type string)
	   )
	  "This class should throw an error.")))
  (should (eq (oref-default 'slotattr-class-ok initform) 'no-init)))

(ert-deftest eieio-test-32-slot-attribute-override-2 ()
  (let* ((cv (cl--find-class 'slotattr-ok))
         (slots  (eieio--class-slots cv))
	 (args   (eieio--class-initarg-tuples cv)))
    ;; :initarg should override for subclass
    (should (assoc :initblarg args))

    (dotimes (i (length slots))
      (let* ((slot (aref slots i))
             (props (cl--slot-descriptor-props slot)))
        (cond
         ((eq (cl--slot-descriptor-name slot) 'custom)
          ;; Custom slot attributes must override
          (should (eq (alist-get :custom props) 'string))
          ;; Custom label slot attribute must override
          (should (string= (alist-get :label props) "One String"))
          (let ((grp (alist-get :group props)))
            ;; Custom group slot attribute must combine
            (should (and (memq 'moose grp) (memq 'cow grp)))))
         (t nil))))))

(defvar eitest-CLONETEST1 nil)
(defvar eitest-CLONETEST2 nil)

(ert-deftest eieio-test-32-test-clone-boring-objects ()
  ;; A simple make instance with EIEIO extension
  (should (setq eitest-CLONETEST1 (make-instance 'class-a)))
  (should (setq eitest-CLONETEST2 (clone eitest-CLONETEST1)))

  ;; CLOS form of make-instance
  (should (setq eitest-CLONETEST1 (make-instance 'class-a)))
  (should (setq eitest-CLONETEST2 (clone eitest-CLONETEST1))))

(defclass IT (eieio-instance-tracker)
  ((tracking-symbol :initform 'IT-list)
   (slot1 :initform 'die))
  "Instance Tracker test object.")

(ert-deftest eieio-test-33-instance-tracker ()
  (defvar IT-list)
  (let (IT-list IT1)
    (should (setq IT1 (IT)))
    ;; The instance tracker must find this
    (should (eieio-instance-tracker-find 'die 'slot1 'IT-list))
    ;; Test deletion
    (delete-instance IT1)
    (should-not (eieio-instance-tracker-find 'die 'slot1 'IT-list))))

(defclass SINGLE (eieio-singleton)
  ((a-slot :initarg :a-slot :initform t))
  "A Singleton test object.")

(ert-deftest eieio-test-34-singletons ()
  (let ((obj1 (SINGLE))
	(obj2 (SINGLE)))
    (should (eieio-object-p obj1))
    (should (eieio-object-p obj2))
    (should (eq obj1 obj2))
    (should (oref obj1 a-slot))))

(defclass NAMED (eieio-named)
  ((some-slot :initform nil))
  "A class inheriting from eieio-named.")

(ert-deftest eieio-test-35-named-object ()
  (let (N)
    (should (setq N (NAMED :object-name "Foo")))
    (should (string= "Foo" (oref N object-name)))
    (should-error (oref N missing-slot) :type 'invalid-slot-name)
    (oset N object-name "NewName")
    (should (string= "NewName" (oref N object-name)))))

(defclass opt-test1 ()
  ()
  "Abstract base class."
  :abstract t)

(defclass opt-test2 (opt-test1)
  ()
  "Instantiable child.")

(ert-deftest eieio-test-36-build-class-alist ()
  (should (= (length (eieio-build-class-alist 'opt-test1 nil)) 2))
  (should (= (length (eieio-build-class-alist 'opt-test1 t)) 1)))

(defclass eieio--testing () ())

(with-suppressed-warnings ((obsolete defmethod)
                           (obsolete defgeneric))
  (defmethod constructor :static ((_x eieio--testing) newname &rest _args)
    (list newname 2)))

(ert-deftest eieio-test-37-obsolete-name-in-constructor ()
  ;; FIXME repeated intermittent failures on hydra and elsewhere (bug#24503).
  :tags '(:unstable)
  ;; Disable byte-compiler "Warning: Obsolete name arg "toto" to
  ;; constructor eieio--testing".  This could be made more specific
  ;; with changes to `with-suppressed-warnings', but it's not worth
  ;; the hassle for just this one test.
  (with-no-warnings
    (should (equal (eieio--testing "toto") '("toto" 2)))))

(ert-deftest eieio-autoload ()
  "Tests to see whether reftex-auc has been autoloaded"
  (should
   (fboundp 'eieio--defalias)))

(ert-deftest eieio-test-38-clone-named-object ()
  (let* ((A (NAMED :object-name "aa"))
         (B (clone A :object-name "bb"))
         (C (clone A "cc"))
         (D (clone A))
         (E (clone D)))
    (should (string= "aa" (oref A object-name)))
    (should (string= "bb" (oref B object-name)))
    (should (string= "cc" (oref C object-name)))
    (should (string= "aa-1" (oref D object-name)))
    (should (string= "aa-2" (oref E object-name)))))

(defclass TII (eieio-instance-inheritor)
  ((a :initform 1 :initarg :a)
   (b :initarg :b)
   (c :initarg :c))
  "Instance Inheritor test class.")

(ert-deftest eieio-test-39-clone-instance-inheritor-with-args ()
  (let* ((A (TII))
         (B (clone A :b "bb"))
         (C (clone B :a "aa")))

    (should (string= "aa" (oref C :a)))
    (should (string= "bb" (oref C :b)))

    (should (slot-boundp A :a))
    (should-not (slot-boundp A :b))
    (should-not (slot-boundp A :c))

    (should-not (slot-boundp B :a))
    (should (slot-boundp B :b))
    (should-not (slot-boundp A :c))

    (should (slot-boundp C :a))
    (should-not (slot-boundp C :b))
    (should-not (slot-boundp C :c))

    (should (eieio-instance-inheritor-slot-boundp C :a))
    (should (eieio-instance-inheritor-slot-boundp C :b))
    (should-not (eieio-instance-inheritor-slot-boundp C :c))))

;;;; Interaction with defstruct

(cl-defstruct eieio-test--struct a b (c nil :read-only t))

(ert-deftest eieio-test-defstruct-slot-value ()
  (let ((x (make-eieio-test--struct :a 'A :b 'B :c 'C)))
    (should (eq (eieio-test--struct-a x)
                (slot-value x 'a)))
    (should (eq (eieio-test--struct-b x)
                (slot-value x 'b)))
    (should (eq (eieio-test--struct-c x)
                (slot-value x 'c)))
    (setf (slot-value x 'a) 1)
    (should (eq (eieio-test--struct-a x) 1))
    (should-error (setf (slot-value x 'c) 3) :type 'eieio-read-only)))

(provide 'eieio-tests)

;;; eieio-tests.el ends here