lib/rspec/mocks/method_double.rb



module RSpec
  module Mocks
    # @private
    class MethodDouble < Hash
      # @private
      attr_reader :method_name, :object

      # @private
      def initialize(object, method_name, proxy)
        @method_name = method_name
        @object = object
        @proxy = proxy

        @method_stasher = InstanceMethodStasher.new(object_singleton_class, @method_name)
        @method_is_proxied = false
        store(:expectations, [])
        store(:stubs, [])
      end

      # @private
      def expectations
        self[:expectations]
      end

      # @private
      def stubs
        self[:stubs]
      end

      # @private
      def visibility
        if TestDouble === @object
          'public'
        elsif object_singleton_class.private_method_defined?(@method_name)
          'private'
        elsif object_singleton_class.protected_method_defined?(@method_name)
          'protected'
        else
          'public'
        end
      end

      # @private
      def original_method
        if @method_stasher.method_is_stashed?
          # Example: a singleton method defined on @object
          method_handle_for(@object, @method_stasher.stashed_method_name)
        elsif meth = original_unrecorded_any_instance_method
          # Example: a method that has been mocked through
          #   klass.any_instance.should_receive(:msg).and_call_original
          # any_instance.should_receive(:msg) causes the method to be
          # replaced with a proxy method, and then `and_call_original`
          # is recorded and played back on the object instance. We need
          # special handling here to get a handle on the original method
          # object rather than the proxy method.
          meth
        else
          begin
            # Example: an instance method defined on one of @object's ancestors.
            original_method_from_ancestor(object_singleton_class.ancestors)
          rescue NameError
            raise unless @object.respond_to?(:superclass)

            # Example: a singleton method defined on @object's superclass.
            #
            # Note: we have to give precedence to instance methods
            # defined on @object's class, because in a case like:
            #
            # `klass.should_receive(:new).and_call_original`
            #
            # ...we want `Class#new` bound to `klass` (which will return
            # an instance of `klass`), not `klass.superclass.new` (which
            # would return an instance of `klass.superclass`).
            original_method_from_superclass
          end
        end
      rescue NameError
        # We have no way of knowing if the object's method_missing
        # will handle this message or not...but we can at least try.
        # If it's not handled, a `NoMethodError` will be raised, just
        # like normally.
        Proc.new do |*args, &block|
          @object.__send__(:method_missing, @method_name, *args, &block)
        end
      end

      def original_unrecorded_any_instance_method
        return nil unless any_instance_class_recorder_observing_method?(@object.class)
        alias_name = @object.class.__recorder.build_alias_method_name(@method_name)
        @object.method(alias_name)
      end

      def any_instance_class_recorder_observing_method?(klass)
        return true if klass.__recorder.already_observing?(@method_name)
        superklass = klass.superclass
        return false if superklass.nil?
        any_instance_class_recorder_observing_method?(superklass)
      end

      def original_method_from_ancestor(ancestors)
        klass, *rest = ancestors
        klass.instance_method(@method_name).bind(@object)
      rescue NameError
        raise if rest.empty?
        original_method_from_ancestor(rest)
      end

      if RUBY_VERSION.to_f > 1.8
        # @private
        def original_method_from_superclass
          @object.superclass.
                  singleton_class.
                  instance_method(@method_name).
                  bind(@object)
        end
      else
        # Our implementation for 1.9 (above) causes an error on 1.8:
        # TypeError: singleton method bound for a different object
        #
        # This doesn't work quite right in all circumstances but it's the
        # best we can do.
        # @private
        def original_method_from_superclass
          ::Kernel.warn <<-WARNING.gsub(/^ +\|/, '')
            |
            |WARNING: On ruby 1.8, rspec-mocks is unable to bind the original
            |`#{@method_name}` method to your partial mock object (#{@object})
            |for `and_call_original`. The superclass's `#{@method_name}` is being
            |used instead; however, it may not work correctly when executed due
            |to the fact that `self` will be #{@object.superclass}, not #{@object}.
            |
            |Called from: #{caller[2]}
          WARNING

          @object.superclass.method(@method_name)
        end
      end

      # @private
      OBJECT_METHOD_METHOD = ::Object.instance_method(:method)

      # @private
      def method_handle_for(object, method_name)
        OBJECT_METHOD_METHOD.bind(object).call(method_name)
      end

      # @private
      def object_singleton_class
        class << @object; self; end
      end

      # @private
      def configure_method
        RSpec::Mocks::space.add(@object) if RSpec::Mocks::space
        warn_if_nil_class
        @original_visibility = visibility_for_method
        @method_stasher.stash unless @method_is_proxied
        define_proxy_method
      end

      # @private
      def define_proxy_method
        return if @method_is_proxied

        object_singleton_class.class_eval <<-EOF, __FILE__, __LINE__ + 1
          def #{@method_name}(*args, &block)
            __mock_proxy.message_received :#{@method_name}, *args, &block
          end
          #{visibility_for_method}
        EOF
        @method_is_proxied = true
      end

      # @private
      def visibility_for_method
        "#{visibility} :#{method_name}"
      end

      # @private
      def restore_original_method
        return unless @method_is_proxied

        object_singleton_class.__send__(:remove_method, @method_name)
        @method_stasher.restore
        restore_original_visibility

        @method_is_proxied = false
      end

      # @private
      def restore_original_visibility
        return unless object_singleton_class.method_defined?(@method_name) || object_singleton_class.private_method_defined?(@method_name)
        object_singleton_class.class_eval(@original_visibility, __FILE__, __LINE__)
      end

      # @private
      def verify
        expectations.each {|e| e.verify_messages_received}
      end

      # @private
      def reset
        reset_nil_expectations_warning
        restore_original_method
        clear
      end

      # @private
      def clear
        expectations.clear
        stubs.clear
      end

      # @private
      def add_expectation(error_generator, expectation_ordering, expected_from, opts, &implementation)
        configure_method
        expectation = if existing_stub = stubs.first
                        existing_stub.build_child(expected_from, 1, opts, &implementation)
                      else
                        MessageExpectation.new(error_generator, expectation_ordering,
                                               expected_from, self, 1, opts, &implementation)
                      end
        expectations << expectation
        expectation
      end

      # @private
      def add_negative_expectation(error_generator, expectation_ordering, expected_from, &implementation)
        configure_method
        expectation = NegativeMessageExpectation.new(error_generator, expectation_ordering,
                                                     expected_from, self, &implementation)
        expectations.unshift expectation
        expectation
      end

      # @private
      def add_stub(error_generator, expectation_ordering, expected_from, opts={}, &implementation)
        configure_method
        stub = MessageExpectation.new(error_generator, expectation_ordering, expected_from,
                                      self, :any, opts, &implementation)
        stubs.unshift stub
        stub
      end

      # @private
      def add_default_stub(*args, &implementation)
        return if stubs.any?
        add_stub(*args, &implementation)
      end

      # @private
      def remove_stub
        raise_method_not_stubbed_error if stubs.empty?
        expectations.empty? ? reset : stubs.clear
      end

      # @private
      def proxy_for_nil_class?
        @object.nil?
      end

      # @private
      def warn_if_nil_class
        if proxy_for_nil_class? & RSpec::Mocks::Proxy.warn_about_expectations_on_nil
          Kernel.warn("An expectation of :#{@method_name} was set on nil. Called from #{caller[4]}. Use allow_message_expectations_on_nil to disable warnings.")
        end
      end

      # @private
      def raise_method_not_stubbed_error
        raise MockExpectationError, "The method `#{method_name}` was not stubbed or was already unstubbed" 
      end

      # @private
      def reset_nil_expectations_warning
        RSpec::Mocks::Proxy.warn_about_expectations_on_nil = true if proxy_for_nil_class?
      end
    end
  end
end