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 ::RSpec::Mocks.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 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 = MessageExpectation.new(error_generator, expectation_ordering, expected_from, self, 1, opts, &implementation) 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? NilClass === @object 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