From 6287f7ad0f657e7d22eaf71dd97926952624e7d3 Mon Sep 17 00:00:00 2001 From: Sam Phippen Date: Sun, 7 Jul 2013 13:10:00 +0100 Subject: [PATCH 1/5] Pass the instance to any instance stubs. Signed-off-by: Sam Phippen --- lib/rspec/mocks/any_instance/recorder.rb | 7 ++- lib/rspec/mocks/configuration.rb | 8 ++++ spec/rspec/mocks/any_instance_spec.rb | 58 ++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/lib/rspec/mocks/any_instance/recorder.rb b/lib/rspec/mocks/any_instance/recorder.rb index 2f3bc095b..c390cf856 100644 --- a/lib/rspec/mocks/any_instance/recorder.rb +++ b/lib/rspec/mocks/any_instance/recorder.rb @@ -55,7 +55,7 @@ def stub_chain(*method_names_and_optional_return_values, &block) # @see Methods#should_receive def should_receive(method_name, &block) @expectation_set = true - observe!(method_name) + observe!(method_name, true) message_chains.add(method_name, PositiveExpectationChain.new(method_name, &block)) end @@ -166,13 +166,16 @@ def stop_observing!(method_name) @observed_methods.delete(method_name) end - def observe!(method_name) + def observe!(method_name, ignore_instance=false) stop_observing!(method_name) if already_observing?(method_name) @observed_methods << method_name backup_method!(method_name) @klass.__send__(:define_method, method_name) do |*args, &blk| klass = ::RSpec::Mocks.method_handle_for(self, method_name).owner ::RSpec::Mocks.any_instance_recorder_for(klass).playback!(self, method_name) + if !ignore_instance && ::RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs + args.unshift(self) + end self.__send__(method_name, *args, &blk) end end diff --git a/lib/rspec/mocks/configuration.rb b/lib/rspec/mocks/configuration.rb index 4e5cd8cc4..23a26b3b1 100644 --- a/lib/rspec/mocks/configuration.rb +++ b/lib/rspec/mocks/configuration.rb @@ -23,6 +23,14 @@ def add_stub_and_should_receive_to(*modules) end end + def pass_instance_to_any_instance_stubs + @pass_instance_to_any_instance_stubs ||= false + end + + def pass_instance_to_any_instance_stubs=(arg) + @pass_instance_to_any_instance_stubs = arg + end + def syntax=(values) if Array(values).include?(:expect) Syntax.enable_expect diff --git a/spec/rspec/mocks/any_instance_spec.rb b/spec/rspec/mocks/any_instance_spec.rb index 555357b63..fdde6c13e 100644 --- a/spec/rspec/mocks/any_instance_spec.rb +++ b/spec/rspec/mocks/any_instance_spec.rb @@ -830,6 +830,64 @@ def foo; end end end + context "passing self" do + context "when configured to pass the instance" do + before(:each) do + @orig_pass = RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs + RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs = true + end + + after(:each) do + RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs = @orig_pass + end + + describe "an any instance stub" do + it "receives the instance" do + klass = Struct.new(:science) + instance = klass.new + klass.any_instance.stub(:bees) { |*args| expect(args.first).to eq(instance) } + instance.bees + end + end + + describe "an any instance expectation" do + it "doesn't effect with" do + klass = Struct.new(:science) + instance = klass.new + klass.any_instance.should_receive(:bees).with(:sup) + instance.bees(:sup) + end + + it "does not pass the instance" do + klass = Struct.new(:science) + instance = klass.new + klass.any_instance.should_receive(:bees).with(:sup) { |*args| expect(args.first).to eq(:sup) } + instance.bees(:sup) + end + end + end + + context "when configured not to pass the instance" do + before(:each) do + @orig_pass = RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs + RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs = false + end + + after(:each) do + RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs = @orig_pass + end + + describe "an any instance stub" do + it "does not receive the instance" do + klass = Struct.new(:science) + instance = klass.new + klass.any_instance.stub(:bees) { |*args| expect(args).to be_empty } + instance.bees + end + end + end + end + context 'when used in conjunction with a `dup`' do it "doesn't cause an infinite loop" do pending "This intermittently fails on JRuby" if RUBY_PLATFORM == 'java' From a37e021716b766432b5d854584fdd43349c2b388 Mon Sep 17 00:00:00 2001 From: Sam Phippen Date: Sun, 7 Jul 2013 13:34:09 +0100 Subject: [PATCH 2/5] Pass the instance to should_receive matchers on any instance too. Signed-off-by: Sam Phippen --- lib/rspec/mocks/any_instance/expectation_chain.rb | 5 +++++ lib/rspec/mocks/any_instance/recorder.rb | 2 +- lib/rspec/mocks/message_expectation.rb | 5 +++++ spec/rspec/mocks/any_instance_spec.rb | 11 +++++++++-- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/rspec/mocks/any_instance/expectation_chain.rb b/lib/rspec/mocks/any_instance/expectation_chain.rb index 699a2153b..d6dc8d54e 100644 --- a/lib/rspec/mocks/any_instance/expectation_chain.rb +++ b/lib/rspec/mocks/any_instance/expectation_chain.rb @@ -25,6 +25,11 @@ class PositiveExpectationChain < ExpectationChain def create_message_expectation_on(instance) proxy = ::RSpec::Mocks.proxy_for(instance) expected_from = IGNORED_BACKTRACE_LINE + if @expectation_args.last.is_a? Hash + @expectation_args.last[:is_any_instance_expectation] = true + else + @expectation_args << {:is_any_instance_expectation => true} + end proxy.add_message_expectation(expected_from, *@expectation_args, &@expectation_block) end diff --git a/lib/rspec/mocks/any_instance/recorder.rb b/lib/rspec/mocks/any_instance/recorder.rb index c390cf856..7a1bf3799 100644 --- a/lib/rspec/mocks/any_instance/recorder.rb +++ b/lib/rspec/mocks/any_instance/recorder.rb @@ -173,7 +173,7 @@ def observe!(method_name, ignore_instance=false) @klass.__send__(:define_method, method_name) do |*args, &blk| klass = ::RSpec::Mocks.method_handle_for(self, method_name).owner ::RSpec::Mocks.any_instance_recorder_for(klass).playback!(self, method_name) - if !ignore_instance && ::RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs + if ::RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs args.unshift(self) end self.__send__(method_name, *args, &blk) diff --git a/lib/rspec/mocks/message_expectation.rb b/lib/rspec/mocks/message_expectation.rb index b6b9a939e..1f0f0f967 100644 --- a/lib/rspec/mocks/message_expectation.rb +++ b/lib/rspec/mocks/message_expectation.rb @@ -24,6 +24,7 @@ def initialize(error_generator, expectation_ordering, expected_from, method_doub @args_to_yield = [] @failed_fast = nil @eval_context = nil + @is_any_instance_expectation = opts[:is_any_instance_expectation] @implementation = Implementation.new self.inner_implementation_action = implementation_block @@ -168,6 +169,10 @@ def and_yield(*args, &block) # @private def matches?(message, *args) + args = args.dup + if @is_any_instance_expectation && ::RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs + args.shift + end @message == message && @argument_list_matcher.args_match?(*args) end diff --git a/spec/rspec/mocks/any_instance_spec.rb b/spec/rspec/mocks/any_instance_spec.rb index fdde6c13e..ec9ed3371 100644 --- a/spec/rspec/mocks/any_instance_spec.rb +++ b/spec/rspec/mocks/any_instance_spec.rb @@ -858,10 +858,10 @@ def foo; end instance.bees(:sup) end - it "does not pass the instance" do + it "passes the instance" do klass = Struct.new(:science) instance = klass.new - klass.any_instance.should_receive(:bees).with(:sup) { |*args| expect(args.first).to eq(:sup) } + klass.any_instance.should_receive(:bees).with(:sup) { |*args| expect(args.first).to eq(instance) } instance.bees(:sup) end end @@ -884,6 +884,13 @@ def foo; end klass.any_instance.stub(:bees) { |*args| expect(args).to be_empty } instance.bees end + + it "gets data from with correctly" do + klass = Struct.new(:science) + instance = klass.new + klass.any_instance.should_receive(:bees).with(:faces) + instance.bees(:faces) + end end end end From 4cb7c1a220ebeda50de0a8fa829b4bdcc2cf76b2 Mon Sep 17 00:00:00 2001 From: Sam Phippen Date: Sun, 7 Jul 2013 13:40:49 +0100 Subject: [PATCH 3/5] Remove the unused ignore_instance parameter. Signed-off-by: Sam Phippen --- lib/rspec/mocks/any_instance/recorder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rspec/mocks/any_instance/recorder.rb b/lib/rspec/mocks/any_instance/recorder.rb index 7a1bf3799..4af7e648d 100644 --- a/lib/rspec/mocks/any_instance/recorder.rb +++ b/lib/rspec/mocks/any_instance/recorder.rb @@ -55,7 +55,7 @@ def stub_chain(*method_names_and_optional_return_values, &block) # @see Methods#should_receive def should_receive(method_name, &block) @expectation_set = true - observe!(method_name, true) + observe!(method_name) message_chains.add(method_name, PositiveExpectationChain.new(method_name, &block)) end @@ -166,7 +166,7 @@ def stop_observing!(method_name) @observed_methods.delete(method_name) end - def observe!(method_name, ignore_instance=false) + def observe!(method_name) stop_observing!(method_name) if already_observing?(method_name) @observed_methods << method_name backup_method!(method_name) From 95476b124b1a15916b84ed52156a6a75d94f2a45 Mon Sep 17 00:00:00 2001 From: Sam Phippen Date: Sun, 14 Jul 2013 19:49:31 +0100 Subject: [PATCH 4/5] Simpler implementation of yielding args for any instance calls. Signed-off-by: Sam Phippen --- lib/rspec/mocks/any_instance/expectation_chain.rb | 10 +++++----- lib/rspec/mocks/any_instance/recorder.rb | 3 --- lib/rspec/mocks/any_instance/stub_chain.rb | 8 +++++++- lib/rspec/mocks/message_expectation.rb | 13 +++++++++---- lib/rspec/mocks/proxy.rb | 6 ++++++ 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/rspec/mocks/any_instance/expectation_chain.rb b/lib/rspec/mocks/any_instance/expectation_chain.rb index d6dc8d54e..234d3a7a4 100644 --- a/lib/rspec/mocks/any_instance/expectation_chain.rb +++ b/lib/rspec/mocks/any_instance/expectation_chain.rb @@ -25,12 +25,12 @@ class PositiveExpectationChain < ExpectationChain def create_message_expectation_on(instance) proxy = ::RSpec::Mocks.proxy_for(instance) expected_from = IGNORED_BACKTRACE_LINE - if @expectation_args.last.is_a? Hash - @expectation_args.last[:is_any_instance_expectation] = true - else - @expectation_args << {:is_any_instance_expectation => true} + me = proxy.add_message_expectation(expected_from, *@expectation_args, &@expectation_block) + if RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs + me.and_yield_receiver_to_implementation end - proxy.add_message_expectation(expected_from, *@expectation_args, &@expectation_block) + + me end def invocation_order diff --git a/lib/rspec/mocks/any_instance/recorder.rb b/lib/rspec/mocks/any_instance/recorder.rb index 4af7e648d..2f3bc095b 100644 --- a/lib/rspec/mocks/any_instance/recorder.rb +++ b/lib/rspec/mocks/any_instance/recorder.rb @@ -173,9 +173,6 @@ def observe!(method_name) @klass.__send__(:define_method, method_name) do |*args, &blk| klass = ::RSpec::Mocks.method_handle_for(self, method_name).owner ::RSpec::Mocks.any_instance_recorder_for(klass).playback!(self, method_name) - if ::RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs - args.unshift(self) - end self.__send__(method_name, *args, &blk) end end diff --git a/lib/rspec/mocks/any_instance/stub_chain.rb b/lib/rspec/mocks/any_instance/stub_chain.rb index 9b1712a70..75be639d9 100644 --- a/lib/rspec/mocks/any_instance/stub_chain.rb +++ b/lib/rspec/mocks/any_instance/stub_chain.rb @@ -14,7 +14,13 @@ def expectation_fulfilled? def create_message_expectation_on(instance) proxy = ::RSpec::Mocks.proxy_for(instance) expected_from = IGNORED_BACKTRACE_LINE - proxy.add_stub(expected_from, *@expectation_args, &@expectation_block) + stub = proxy.add_stub(expected_from, *@expectation_args, &@expectation_block) + + if RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs + stub.and_yield_receiver_to_implementation + end + + stub end def invocation_order diff --git a/lib/rspec/mocks/message_expectation.rb b/lib/rspec/mocks/message_expectation.rb index 1f0f0f967..c3b80ac97 100644 --- a/lib/rspec/mocks/message_expectation.rb +++ b/lib/rspec/mocks/message_expectation.rb @@ -5,6 +5,8 @@ class MessageExpectation # @private attr_accessor :error_generator, :implementation attr_reader :message + attr_reader :orig_object + attr_reader :yield_receiver_to_implementation attr_writer :expected_received_count, :expected_from, :argument_list_matcher protected :expected_received_count=, :expected_from=, :error_generator, :error_generator=, :implementation= @@ -15,6 +17,7 @@ def initialize(error_generator, expectation_ordering, expected_from, method_doub @error_generator.opts = opts @expected_from = expected_from @method_double = method_double + @orig_object = @method_double.object @message = @method_double.method_name @actual_received_count = 0 @expected_received_count = expected_received_count @@ -24,6 +27,7 @@ def initialize(error_generator, expectation_ordering, expected_from, method_doub @args_to_yield = [] @failed_fast = nil @eval_context = nil + @yield_receiver_to_implementation = false @is_any_instance_expectation = opts[:is_any_instance_expectation] @implementation = Implementation.new @@ -90,6 +94,11 @@ def and_return(*values, &implementation) end end + def and_yield_receiver_to_implementation + @yield_receiver_to_implementation = true + self + end + # Tells the object to delegate to the original unmodified method # when it receives the message. # @@ -169,10 +178,6 @@ def and_yield(*args, &block) # @private def matches?(message, *args) - args = args.dup - if @is_any_instance_expectation && ::RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs - args.shift - end @message == message && @argument_list_matcher.args_match?(*args) end diff --git a/lib/rspec/mocks/proxy.rb b/lib/rspec/mocks/proxy.rb index 5dc63b40f..da5660635 100644 --- a/lib/rspec/mocks/proxy.rb +++ b/lib/rspec/mocks/proxy.rb @@ -139,8 +139,14 @@ def message_received(message, *args, &block) if expectation = find_almost_matching_expectation(message, *args) expectation.advise(*args) unless expectation.expected_messages_received? end + if stub.yield_receiver_to_implementation + args.unshift(stub.orig_object) + end stub.invoke(nil, *args, &block) elsif expectation + if expectation.yield_receiver_to_implementation + args.unshift(expectation.orig_object) + end expectation.invoke(stub, *args, &block) elsif expectation = find_almost_matching_expectation(message, *args) expectation.advise(*args) if null_object? unless expectation.expected_messages_received? From cf5ef393bd0f208e5a4b2d69476193b48a7d384a Mon Sep 17 00:00:00 2001 From: Sam Phippen Date: Sun, 14 Jul 2013 20:24:15 +0100 Subject: [PATCH 5/5] Refine the any_instance passing implementation as per myron's suggestions. Signed-off-by: Sam Phippen --- .../mocks/any_instance/expectation_chain.rb | 2 +- lib/rspec/mocks/any_instance/stub_chain.rb | 2 +- lib/rspec/mocks/configuration.rb | 8 +-- spec/rspec/mocks/any_instance_spec.rb | 52 +++++++++---------- .../mocks/with_isolated_configuration_spec.rb | 22 ++++++++ spec/spec_helper.rb | 12 +++++ 6 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 spec/rspec/mocks/with_isolated_configuration_spec.rb diff --git a/lib/rspec/mocks/any_instance/expectation_chain.rb b/lib/rspec/mocks/any_instance/expectation_chain.rb index 234d3a7a4..97818eed2 100644 --- a/lib/rspec/mocks/any_instance/expectation_chain.rb +++ b/lib/rspec/mocks/any_instance/expectation_chain.rb @@ -26,7 +26,7 @@ def create_message_expectation_on(instance) proxy = ::RSpec::Mocks.proxy_for(instance) expected_from = IGNORED_BACKTRACE_LINE me = proxy.add_message_expectation(expected_from, *@expectation_args, &@expectation_block) - if RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs + if RSpec::Mocks.configuration.yield_instance_from_any_instance_implementation_blocks me.and_yield_receiver_to_implementation end diff --git a/lib/rspec/mocks/any_instance/stub_chain.rb b/lib/rspec/mocks/any_instance/stub_chain.rb index 75be639d9..ef25cfcb2 100644 --- a/lib/rspec/mocks/any_instance/stub_chain.rb +++ b/lib/rspec/mocks/any_instance/stub_chain.rb @@ -16,7 +16,7 @@ def create_message_expectation_on(instance) expected_from = IGNORED_BACKTRACE_LINE stub = proxy.add_stub(expected_from, *@expectation_args, &@expectation_block) - if RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs + if RSpec::Mocks.configuration.yield_instance_from_any_instance_implementation_blocks stub.and_yield_receiver_to_implementation end diff --git a/lib/rspec/mocks/configuration.rb b/lib/rspec/mocks/configuration.rb index 23a26b3b1..0ba9b820b 100644 --- a/lib/rspec/mocks/configuration.rb +++ b/lib/rspec/mocks/configuration.rb @@ -23,12 +23,12 @@ def add_stub_and_should_receive_to(*modules) end end - def pass_instance_to_any_instance_stubs - @pass_instance_to_any_instance_stubs ||= false + def yield_instance_from_any_instance_implementation_blocks + @yield_instance_from_any_instance_implementation_blocks ||= false end - def pass_instance_to_any_instance_stubs=(arg) - @pass_instance_to_any_instance_stubs = arg + def yield_instance_from_any_instance_implementation_blocks=(arg) + @yield_instance_from_any_instance_implementation_blocks = arg end def syntax=(values) diff --git a/spec/rspec/mocks/any_instance_spec.rb b/spec/rspec/mocks/any_instance_spec.rb index ec9ed3371..5b7e8ca32 100644 --- a/spec/rspec/mocks/any_instance_spec.rb +++ b/spec/rspec/mocks/any_instance_spec.rb @@ -830,63 +830,59 @@ def foo; end end end - context "passing self" do + context "passing the receiver to the implementation block" do context "when configured to pass the instance" do + include_context 'with isolated configuration' before(:each) do - @orig_pass = RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs - RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs = true - end - - after(:each) do - RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs = @orig_pass + RSpec::Mocks.configuration.yield_instance_from_any_instance_implementation_blocks = true end describe "an any instance stub" do - it "receives the instance" do - klass = Struct.new(:science) + it "passes the instance as the first arg of the implementation block" do instance = klass.new - klass.any_instance.stub(:bees) { |*args| expect(args.first).to eq(instance) } - instance.bees + + expect { |b| + klass.any_instance.should_receive(:bees).with(:sup, &b) + instance.bees(:sup) + }.to yield_with_args(instance, :sup) end end describe "an any instance expectation" do it "doesn't effect with" do - klass = Struct.new(:science) instance = klass.new klass.any_instance.should_receive(:bees).with(:sup) instance.bees(:sup) end - it "passes the instance" do - klass = Struct.new(:science) + it "passes the instance as the first arg of the implementation block" do instance = klass.new - klass.any_instance.should_receive(:bees).with(:sup) { |*args| expect(args.first).to eq(instance) } - instance.bees(:sup) + + expect { |b| + klass.any_instance.should_receive(:bees).with(:sup, &b) + instance.bees(:sup) + }.to yield_with_args(instance, :sup) end end end context "when configured not to pass the instance" do + include_context 'with isolated configuration' before(:each) do - @orig_pass = RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs - RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs = false - end - - after(:each) do - RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs = @orig_pass + RSpec::Mocks.configuration.yield_instance_from_any_instance_implementation_blocks = false end describe "an any instance stub" do - it "does not receive the instance" do - klass = Struct.new(:science) + it "does not pass the instance to the implementation block" do instance = klass.new - klass.any_instance.stub(:bees) { |*args| expect(args).to be_empty } - instance.bees + + expect { |b| + klass.any_instance.should_receive(:bees).with(:sup, &b) + instance.bees(:sup) + }.to yield_with_args(:sup) end - it "gets data from with correctly" do - klass = Struct.new(:science) + it "does not cause with to fail when the instance is passed" do instance = klass.new klass.any_instance.should_receive(:bees).with(:faces) instance.bees(:faces) diff --git a/spec/rspec/mocks/with_isolated_configuration_spec.rb b/spec/rspec/mocks/with_isolated_configuration_spec.rb new file mode 100644 index 000000000..0ddf046fc --- /dev/null +++ b/spec/rspec/mocks/with_isolated_configuration_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' +require 'pry' + +module RSpec + module Mocks + describe 'the with isolated configuration shared example group' do + @@c = describe '' do + include_context 'with isolated configuration' + end + it 'resets the configuration' do + @@c.before.first.block.call + RSpec::Mocks.configuration.instance_eval do + def this_method_wont_be_here + end + end + + @@c.after.last.block.call + expect(RSpec::Mocks.configuration.respond_to? :this_method_wont_be_here).to be false + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d160f68a6..3768bb797 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -68,3 +68,15 @@ def reset(object) end end + +shared_context "with isolated configuration" do + orig_configuration = nil + before do + orig_configuration = RSpec::Mocks.configuration + RSpec::Mocks.instance_variable_set(:@configuration, RSpec::Mocks::Configuration.new) + end + + after do + RSpec::Mocks.instance_variable_set(:@configuration, orig_configuration) + end +end