Skip to content

Commit

Permalink
Merge 78f2250 into 31efa49
Browse files Browse the repository at this point in the history
  • Loading branch information
myronmarston committed Jun 25, 2013
2 parents 31efa49 + 78f2250 commit 9187a20
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 107 deletions.
123 changes: 16 additions & 107 deletions lib/rspec/mocks/method_double.rb
Expand Up @@ -3,7 +3,7 @@ module Mocks
# @private
class MethodDouble < Hash
# @private
attr_reader :method_name, :object
attr_reader :method_name, :object, :original_method

# @private
def initialize(object, method_name, proxy)
Expand All @@ -13,10 +13,25 @@ def initialize(object, method_name, proxy)

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

def find_original_method
method_handle_name = if any_instance_class_recorder_observing_method?(@object.class)
::RSpec::Mocks.any_instance_recorder_for(@object.class).build_alias_method_name(@method_name)
else
@method_name
end

::RSpec::Mocks.method_handle_for(@object, method_handle_name)
rescue NameError
Proc.new do |*args, &block|
@object.__send__(:method_missing, @method_name, *args, &block)
end
end

# @private
def expectations
self[:expectations]
Expand All @@ -40,119 +55,13 @@ def visibility
end
end

class ProcWithBlock < Struct.new(:object, :method_name)

def call(*args, &block)
self.object.__send__(:method_missing, self.method_name, *args, &block)
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
# Example: an instance method defined on one of @object's ancestors.
original_method_from_ancestry
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.
ProcWithBlock.new(@object,@method_name)
end

def original_unrecorded_any_instance_method
return nil unless any_instance_class_recorder_observing_method?(@object.class)
alias_name = ::RSpec::Mocks.any_instance_recorder_for(@object.class).build_alias_method_name(@method_name)
@object.method(alias_name)
end

def any_instance_class_recorder_observing_method?(klass)
return true if ::RSpec::Mocks.any_instance_recorder_for(klass).already_observing?(@method_name)
superklass = klass.superclass
return false if superklass.nil?
any_instance_class_recorder_observing_method?(superklass)
end

our_singleton_class = class << self; self; end
if our_singleton_class.ancestors.include? our_singleton_class
# In Ruby 2.1, ancestors include the correct ancestors, including the singleton classes
def original_method_from_ancestry
# Lookup in the ancestry, skipping over the singleton class itself
original_method_from_ancestor(object_singleton_class.ancestors.drop(1))
end
else
# @private
def original_method_from_ancestry
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

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
Expand Down
15 changes: 15 additions & 0 deletions spec/rspec/mocks/and_call_original_spec.rb
Expand Up @@ -26,6 +26,12 @@ def self.new_instance
expect(instance.meth_1).to eq(:original)
end

it 'ignores prior declared stubs' do
instance.stub(:meth_1).and_return(:stubbed_value)
instance.should_receive(:meth_1).and_call_original
expect(instance.meth_1).to eq(:original)
end

it 'passes args and blocks through to the original method' do
instance.should_receive(:meth_2).and_call_original
value = instance.meth_2(:submitted_arg) { |a, b| [a, b] }
Expand Down Expand Up @@ -77,6 +83,15 @@ def instance.foo; :bar; end
expect(sub_klass.foo).to eq(:sub_klass_bar)
end

it "finds the method on the most direct singleton class ancestors even if the method " +
"is available on more distant ancestors" do
klass.extend Module.new { def foo; :klass_bar; end }
sub_klass = Class.new(klass) { def self.foo; :sub_klass_bar; end }
sub_sub_klass = Class.new(sub_klass)
sub_sub_klass.should_receive(:foo).and_call_original
expect(sub_sub_klass.foo).to eq(:sub_klass_bar)
end

context 'when using any_instance' do
it 'works for instance methods defined on the class' do
klass.any_instance.should_receive(:meth_1).and_call_original
Expand Down

0 comments on commit 9187a20

Please sign in to comment.