Permalink
Browse files

Restore @kaiwren any_instance implementation.

  • Loading branch information...
1 parent 27753c8 commit 628eb9154f0bfdfc199ffab91770b85e780c12a8 @justinko justinko committed May 31, 2011
View
1 features/Changelog.md
@@ -1,7 +1,6 @@
### Dev
* Bug fixes
- * any_instance is instance agnostic. (Justin Ko)
* pass a hash to `any_instance.stub`. (Justin Ko)
### 2.6.0 / 2011-05-12
View
262 lib/rspec/mocks/any_instance.rb
@@ -1,32 +1,268 @@
-require 'rspec/mocks/any_instance/recorder'
-require 'rspec/mocks/any_instance/player'
-require 'rspec/mocks/any_instance/chains'
-require 'rspec/mocks/any_instance/message'
-require 'rspec/mocks/any_instance/chain'
-require 'rspec/mocks/any_instance/expectation'
-require 'rspec/mocks/any_instance/stub'
-
module RSpec
module Mocks
module AnyInstance
+ class Chain
+ [
+ :with, :and_return, :and_raise, :and_yield,
+ :once, :twice, :any_number_of_times,
+ :exactly, :times, :never,
+ :at_least, :at_most
+ ].each do |method_name|
+ class_eval(<<-EOM, __FILE__, __LINE__)
+ def #{method_name}(*args, &block)
+ record(:#{method_name}, *args, &block)
+ end
+ EOM
+ end
+
+ def playback!(instance)
+ messages.inject(instance) do |instance, message|
+ instance.send(*message.first, &message.last)
+ end
+ end
+
+ def constrained_to_any_of?(*constraints)
+ constraints.any? do |constraint|
+ messages.any? do |message|
+ message.first.first == constraint
+ end
+ end
+ end
+
+ def messages
+ @messages ||= []
+ end
+
+ def last_message
+ messages.last.first.first unless messages.empty?
+ end
+
+ def record(rspec_method_name, *args, &block)
+ verify_invocation_order(rspec_method_name, *args, &block)
+ messages << [args.unshift(rspec_method_name), block]
+ self
+ end
+ end
+
+ class StubChain < Chain
+ def initialize(*args, &block)
+ record(:stub, *args, &block)
+ end
+
+ def invocation_order
+ @invocation_order ||= {
+ :stub => [nil],
+ :with => [:stub],
+ :and_return => [:with, :stub],
+ :and_raise => [:with, :stub],
+ :and_yield => [:with, :stub]
+ }
+ end
+
+ def expectation_filfilled?
+ true
+ end
+
+ private
+ def verify_invocation_order(rspec_method_name, *args, &block)
+ unless invocation_order[rspec_method_name].include?(last_message)
+ raise(NoMethodError, "Undefined method #{rspec_method_name}")
+ end
+ end
+ end
+
+ class ExpectationChain < Chain
+ def initialize(*args, &block)
+ record(:should_receive, *args, &block)
+ @expectation_fulfilled = false
+ end
+
+ def invocation_order
+ @invocation_order ||= {
+ :should_receive => [nil],
+ :with => [:should_receive],
+ :and_return => [:with, :should_receive],
+ :and_raise => [:with, :should_receive]
+ }
+ end
+
+ def expectation_fulfilled!
+ @expectation_fulfilled = true
+ end
+
+ def expectation_filfilled?
+ @expectation_fulfilled || constrained_to_any_of?(:never, :any_number_of_times)
+ end
+
+ private
+ def verify_invocation_order(rspec_method_name, *args, &block)
+ end
+ end
+
+ class Recorder
+ def initialize(klass)
+ @message_chains = {}
+ @observed_methods = []
+ @played_methods = {}
+ @klass = klass
+ @expectation_set = false
+ end
+
+ def stub(method_name, *args, &block)
+ if Hash === method_name
+ method_name.each do |method_name, value|
+ chain = add_chain(:stub, method_name, args, &block)
+ chain.messages << [[:and_return, value], block]
+ chain
+ end
+ else
+ add_chain(:stub, method_name, *args, &block)
+ end
+ end
+
+ def should_receive(method_name, *args, &block)
+ @expectation_set = true
+ add_chain(:expectation, method_name, *args, &block)
+ end
+
+ def stop_all_observation!
+ @observed_methods.each do |method_name|
+ restore_method!(method_name)
+ end
+ end
+
+ def playback!(instance, method_name)
+ RSpec::Mocks::space.add(instance)
+ @message_chains[method_name].playback!(instance)
+ @played_methods[method_name] = instance
+ received_expected_message!(method_name) if has_expectation?(method_name)
+ end
+
+ def instance_that_received(method_name)
+ @played_methods[method_name]
+ end
+
+ def verify
+ if @expectation_set && !each_expectation_filfilled?
+ raise RSpec::Mocks::MockExpectationError, "Exactly one instance should have received the following message(s) but didn't: #{unfulfilled_expectations.sort.join(', ')}"
+ end
+ end
+
+ private
+ def each_expectation_filfilled?
+ @message_chains.all? do |method_name, chain|
+ chain.expectation_filfilled?
+ end
+ end
+
+ def has_expectation?(method_name)
+ @message_chains[method_name].is_a?(ExpectationChain)
+ end
+
+ def unfulfilled_expectations
+ @message_chains.map do |method_name, chain|
+ method_name.to_s if chain.is_a?(ExpectationChain) unless chain.expectation_filfilled?
+ end.compact
+ end
+
+ def received_expected_message!(method_name)
+ @message_chains[method_name].expectation_fulfilled!
+ restore_method!(method_name)
+ mark_invoked!(method_name)
+ end
+
+ def restore_method!(method_name)
+ if @klass.method_defined?(build_alias_method_name(method_name))
+ restore_original_method!(method_name)
+ else
+ remove_dummy_method!(method_name)
+ end
+ end
+
+ def build_alias_method_name(method_name)
+ "__#{method_name}_without_any_instance__"
+ end
+
+ def restore_original_method!(method_name)
+ alias_method_name = build_alias_method_name(method_name)
+ @klass.class_eval do
+ alias_method method_name, alias_method_name
+ remove_method alias_method_name
+ end
+ end
+
+ def remove_dummy_method!(method_name)
+ @klass.class_eval do
+ remove_method method_name
+ end
+ end
+
+ def backup_method!(method_name)
+ alias_method_name = build_alias_method_name(method_name)
+ @klass.class_eval do
+ if method_defined?(method_name)
+ alias_method alias_method_name, method_name
+ end
+ end
+ end
+
+ def stop_observing!(method_name)
+ restore_method!(method_name)
+ @observed_methods.delete(method_name)
+ end
+
+ def already_observing?(method_name)
+ @observed_methods.include?(method_name)
+ end
+
+ def add_chain(chain_type, method_name, *args, &block)
+ observe!(method_name)
+ chain_class = StubChain if chain_type == :stub
+ chain_class = ExpectationChain if chain_type == :expectation
+ @message_chains[method_name] = chain_class.new(method_name, *args, &block)
+ end
+
+ def observe!(method_name)
+ stop_observing!(method_name) if already_observing?(method_name)
+ @observed_methods << method_name
+ backup_method!(method_name)
+ @klass.class_eval(<<-EOM, __FILE__, __LINE__)
+ def #{method_name}(*args, &blk)
+ self.class.__recorder.playback!(self, :#{method_name})
+ self.send(:#{method_name}, *args, &blk)
+ end
+ EOM
+ end
+
+ def mark_invoked!(method_name)
+ backup_method!(method_name)
+ @klass.class_eval(<<-EOM, __FILE__, __LINE__)
+ def #{method_name}(*args, &blk)
+ method_name = :#{method_name}
+ current_instance = self
+ invoked_instance = self.class.__recorder.instance_that_received(method_name)
+ raise RSpec::Mocks::MockExpectationError, "The message '#{method_name}' was received by \#{self.inspect} but has already been received by \#{invoked_instance}"
+ end
+ EOM
+ end
+ end
def any_instance
- Mocks.space.add(self)
+ RSpec::Mocks::space.add(self)
__recorder
end
def rspec_verify
__recorder.verify
super
ensure
- __recorder.remove_chains
+ __recorder.stop_all_observation!
@__recorder = nil
end
def __recorder
- @__recorder ||= Recorder.new(self)
+ @__recorder ||= AnyInstance::Recorder.new(self)
end
-
end
end
-end
+end
View
142 lib/rspec/mocks/any_instance/chain.rb
@@ -1,142 +0,0 @@
-module RSpec
- module Mocks
- module AnyInstance
-
- class Chain
-
- InvocationOrder = {
- :stub => [nil],
- :should_receive => [nil],
- :with => [:stub, :should_receive],
- :and_return => [:with, :stub, :should_receive],
- :and_raise => [:with, :stub, :should_receive],
- :and_yield => [:with, :stub]
- }
-
- attr_accessor :recorded_class, :instance, :dummy
-
- [ :with, :and_return, :and_raise, :and_yield,
- :once, :twice, :any_number_of_times,
- :exactly, :times, :never,
- :at_least, :at_most
- ].each do |method_name|
- class_eval(<<-EOM, __FILE__, __LINE__)
- def #{method_name}(*args, &block)
- record(:#{method_name}, *args, &block)
- end
- EOM
- end
-
- def playback
- messages.inject(instance) {|i, message| message.invoke(i) }
- end
-
- def attach(cls)
- self.recorded_class = cls
- backup unless backed_up?
- hijack
- end
-
- def backup
- recorded_class_eval <<-RUBY
- if method_defined?(:#{method_name})
- alias_method :#{alias_method_name}, :#{method_name}
- else
- #{dummy!}
- end
- RUBY
- end
-
- def restore_original
- recorded_class_eval <<-RUBY
- alias_method :#{method_name}, :#{alias_method_name}
- remove_method :#{alias_method_name}
- RUBY
- end
-
- def hijack
- recorded_class_eval <<-RUBY
- def #{method_name}(*args, &blk)
- Mocks.space.add(self)
- self.class.__recorder.playback(self, #{object_id}, *args, &blk)
- end
- RUBY
- end
-
- def backed_up?
- recorded_class.method_defined?(alias_method_name)
- end
-
- def restore
- backed_up? ? restore_original : remove if dummy?
- end
-
- def remove
- recorded_class_eval "remove_method(:#{method_name})"
- end
-
- def expectation?
- is_a?(Expectation)
- end
-
- def dummy!
- self.dummy = true
- end
-
- def dummy?
- !!dummy
- end
-
- def method_name
- messages.first.args.first
- end
-
- def alias_method_name
- "__#{method_name}_without_any_instance__"
- end
-
- def constrained?(*constraints)
- constraints.any? do |constraint|
- messages.any? {|message| message.name == constraint }
- end
- end
-
- def record(method_name, *args, &block)
- verify_invocation_order(method_name, *args, &block)
- add_message(method_name, *args, &block)
- self
- end
-
- def verify_invocation_order(rspec_method_name, *args, &block)
- if invocation_order = InvocationOrder[rspec_method_name]
- unless invocation_order.include?(last_message)
- raise NoMethodError, "Undefined method #{rspec_method_name}"
- end
- end
- end
-
- def recorded_class_eval(code)
- recorded_class.class_eval(code, __FILE__, __LINE__)
- end
-
- def with_siblings
- recorded_class.__recorder.chains.find_with_siblings(self)
- end
-
- def add_message(method_name, *args, &block)
- messages.push Message.new(method_name, args, block)
- end
-
- def last_message
- messages.last.name unless messages.empty?
- end
-
- def messages
- @messages ||= []
- end
-
- end
-
- end
- end
-end
View
42 lib/rspec/mocks/any_instance/chains.rb
@@ -1,42 +0,0 @@
-module RSpec
- module Mocks
- module AnyInstance
-
- class Chains < Array
-
- def add(chain)
- push(chain)
- chain
- end
-
- def find_by_id(id)
- find {|chain| chain.object_id == id }
- end
-
- def unplayed
- chainable { reject {|chain| chain.played? } }
- end
-
- def expectations
- chainable { select {|chain| chain.is_a?(Expectation) } }
- end
-
- def find_with_siblings(chain)
- chainable { select {|c| c.method_name == chain.method_name } }
- end
-
- def playable(args)
- chainable { reject {|chain| chain.any_unplayable_messages?(args) } }
- end
-
- private
-
- def chainable
- self.class.new yield
- end
-
- end
-
- end
- end
-end
View
33 lib/rspec/mocks/any_instance/expectation.rb
@@ -1,33 +0,0 @@
-module RSpec
- module Mocks
- module AnyInstance
-
- class Expectation < Chain
-
- attr_accessor :fulfilled, :played
-
- def initialize(*args, &block)
- record(:should_receive, *args, &block)
- end
-
- def played?
- !!played
- end
-
- def fulfilled?
- fulfilled || constrained?(:never, :any_number_of_times)
- end
-
- def any_unplayable_messages?(args)
- messages.any? {|message| message.unplayable?(args) }
- end
-
- def any_single_instance_messages?
- messages.any? {|message| message.single_instance? }
- end
-
- end
-
- end
- end
-end
View
34 lib/rspec/mocks/any_instance/message.rb
@@ -1,34 +0,0 @@
-module RSpec
- module Mocks
- module AnyInstance
-
- class Message < Struct.new(:name, :args, :block)
-
- SingleInstanceMethods = [
- :once,
- :twice,
- :any_number_of_times,
- :exactly,
- :times,
- :never,
- :at_least,
- :at_most
- ]
-
- def invoke(instance)
- instance.send(name, *args, &block)
- end
-
- def unplayable?(arguments)
- name == :with && args != arguments
- end
-
- def single_instance?
- SingleInstanceMethods.include?(name)
- end
-
- end
-
- end
- end
-end
View
70 lib/rspec/mocks/any_instance/player.rb
@@ -1,70 +0,0 @@
-module RSpec
- module Mocks
- module AnyInstance
-
- class Player
-
- attr_accessor :chain, :instance, :args, :block
-
- def play
- chain.expectation? ? playback_expectation : playback_stub
- end
-
- private
-
- def playback_stub
- chain.with_siblings.each(&playback_chain)
- invoke(chain.method_name)
- end
-
- def playback_expectation
- playable_chains.empty? ? invoke_backup : play_playable_chains
- end
-
- def play_playable_chains
- playable_chains.each(&playback_chain)
- result = invoke(chain.method_name)
- expect_once if expected_once?
- result
- end
-
- def expected_once?
- not chain.any_single_instance_messages?
- end
-
- def expect_once
- instance.instance_eval(<<-EOM, __FILE__, __LINE__)
- def #{chain.method_name}(*args, &blk)
- raise MockExpectationError,
- "The message :#{chain.method_name} has already been received by #{chain.instance}"
- end
- EOM
- end
-
- def playable_chains
- @playable_chains ||= chain.with_siblings.unplayed.playable(args)
- end
-
- def playback_chain
- lambda do |chain|
- chain.instance = instance
- chain.playback
- chain.played = chain.fulfilled = true if chain.expectation?
- end
- end
-
- def invoke_backup
- invoke(chain.alias_method_name)
- rescue NoMethodError => ex
- raise ex.class, "undefined method `#{chain.method_name}' for #{instance}", ex.backtrace
- end
-
- def invoke(name)
- instance.send(name, *args, &block)
- end
-
- end
-
- end
- end
-end
View
69 lib/rspec/mocks/any_instance/recorder.rb
@@ -1,69 +0,0 @@
-module RSpec
- module Mocks
- module AnyInstance
-
- class Recorder
-
- attr_reader :chains
-
- def initialize(class_to_record)
- @class, @chains = class_to_record, Chains.new
- end
-
- def should_receive(sym, *args, &block)
- add_chain Expectation.new(sym, *args, &block)
- end
-
- def stub(sym_or_hash, *args, &block)
- if Hash === sym_or_hash
- sym_or_hash.each do |method, value|
- chain = Stub.new(method, *args, &block)
- chain.add_message(:and_return, value)
- add_chain(chain)
- end
- else
- add_chain Stub.new(sym_or_hash, *args, &block)
- end
- end
-
- def playback(instance, chain_id, *args, &block)
- player = Player.new
- player.chain = chains.find_by_id(chain_id)
- player.instance = instance
- player.args = args
- player.block = block
- player.play
- end
-
- def verify
- if unfulfilled_expectations.any?
- raise MockExpectationError,
- "Exactly one instance should have received the following message(s)" +
- " but didn't: #{unfulfilled_expectation_names}"
- end
- end
-
- def remove_chains
- chains.each {|chain| chain.restore }
- end
-
- private
-
- def unfulfilled_expectation_names
- unfulfilled_expectations.map {|expectation| expectation.method_name.to_s }.sort.join(', ')
- end
-
- def unfulfilled_expectations
- chains.expectations.reject {|expectation| expectation.fulfilled? }
- end
-
- def add_chain(chain)
- chain.attach(@class)
- chains.add(chain)
- end
-
- end
-
- end
- end
-end
View
15 lib/rspec/mocks/any_instance/stub.rb
@@ -1,15 +0,0 @@
-module RSpec
- module Mocks
- module AnyInstance
-
- class Stub < Chain
-
- def initialize(*args, &block)
- record(:stub, *args, &block)
- end
-
- end
-
- end
- end
-end
View
89 spec/rspec/mocks/any_instance_spec.rb
@@ -3,17 +3,15 @@
module RSpec
module Mocks
describe "#any_instance" do
-
- CustomErrorForAnyInstanceSpec = Class.new(StandardError)
+ class CustomErrorForAnyInstanceSpec < StandardError;end
let(:klass) do
Class.new do
- def existing_method; :existing_method_return_value end
+ def existing_method; :existing_method_return_value; end
def another_existing_method; end
end
end
-
- let(:existing_method_return_value) { :existing_method_return_value }
+ let(:existing_method_return_value){ :existing_method_return_value }
context "invocation order" do
context "#stub" do
@@ -73,7 +71,7 @@ def another_existing_method; end
context "with a block" do
before { klass.any_instance.stub(:foo => 1, :bar => 2) }
-
+
it "stubs a method" do
instance = klass.new
instance.foo.should eq(1)
@@ -103,13 +101,6 @@ def another_existing_method; end
klass.new.foo.should be(return_value)
klass.new.foo.should be(return_value)
end
-
- it "returns the object depending on what arguments are passed" do
- klass.any_instance.stub(:foo).with(1).and_return(:one)
- klass.any_instance.stub(:foo).with(2).and_return(:two)
- klass.new.foo(1).should eq(:one)
- klass.new.foo(2).should eq(:two)
- end
end
context "with #and_yield" do
@@ -145,6 +136,11 @@ def another_existing_method; end
end
context "core ruby objects" do
+ it "works uniformly across *everything*" do
+ Object.any_instance.stub(:foo).and_return(1)
+ Object.new.foo.should eq(1)
+ end
+
it "works with the non-standard constructor []" do
Array.any_instance.stub(:foo).and_return(1)
[].foo.should eq(1)
@@ -184,15 +180,6 @@ class RSpec::SampleRspecTestClass;end
let(:existing_method_expectation_error_message) { 'Exactly one instance should have received the following message(s) but didn\'t: existing_method' }
context "with an expectation is set on a method which does not exist" do
- it 'raises an error if invoked twice' do
- klass.any_instance.should_receive(:bar)
- instance = klass.new
- instance.bar
- expect { instance.bar }.to raise_error(
- RSpec::Mocks::MockExpectationError, "The message :bar has already been received by #{instance}"
- )
- end
-
it "returns the expected value" do
klass.any_instance.should_receive(:foo).and_return(1)
klass.new.foo(1).should eq(1)
@@ -240,14 +227,12 @@ class RSpec::SampleRspecTestClass;end
it "fails if the method is invoked on a second instance" do
instance_one = klass.new
instance_two = klass.new
-
- klass.any_instance.should_receive(:foo)
-
- instance_one.foo
+ expect do
+ klass.any_instance.should_receive(:foo)
- expect { instance_two.foo }.to raise_error(
- NoMethodError, "undefined method `foo' for #{instance_two}"
- )
+ instance_one.foo
+ instance_two.foo
+ end.to raise_error(RSpec::Mocks::MockExpectationError, "The message 'foo' was received by #{instance_two.inspect} but has already been received by #{instance_one.inspect}")
end
end
@@ -263,6 +248,7 @@ class RSpec::SampleRspecTestClass;end
end)
end
+
it "pass when expectations are met" do
klass.any_instance.should_receive(:foo)
klass.should_receive(:woot).and_return(result = Object.new)
@@ -298,20 +284,7 @@ class RSpec::SampleRspecTestClass;end
klass.any_instance.should_receive(:existing_method)
klass.any_instance.should_receive(:another_existing_method)
klass.rspec_verify
- end.to raise_error(RSpec::Mocks::MockExpectationError,
- 'Exactly one instance should have received the following message(s) but didn\'t: another_existing_method, existing_method')
- end
-
- it "allows multiple expectations on the same method if the arguments are different" do
- klass.any_instance.should_receive(:existing_method).with(1).and_return(:one)
- klass.any_instance.should_receive(:existing_method).with(2).and_return(:two)
-
- klass.new.existing_method.should eq(:existing_method_return_value)
-
- klass.new.existing_method(1).should eq(:one)
- klass.new.existing_method(2).should eq(:two)
-
- klass.new.existing_method.should eq(:existing_method_return_value)
+ end.to raise_error(RSpec::Mocks::MockExpectationError, 'Exactly one instance should have received the following message(s) but didn\'t: another_existing_method, existing_method')
end
context "after any one instance has received a message" do
@@ -321,10 +294,15 @@ class RSpec::SampleRspecTestClass;end
klass.new
end
- it "passes if the method is invoked on a second instance" do
- klass.any_instance.should_receive(:existing_method)
- klass.new.existing_method
- klass.new.existing_method
+ it "fails if the method is invoked on a second instance" do
+ instance_one = klass.new
+ instance_two = klass.new
+ expect do
+ klass.any_instance.should_receive(:existing_method)
+
+ instance_one.existing_method
+ instance_two.existing_method
+ end.to raise_error(RSpec::Mocks::MockExpectationError, "The message 'existing_method' was received by #{instance_two.inspect} but has already been received by #{instance_one.inspect}")
end
end
end
@@ -604,7 +582,6 @@ class RSpec::SampleRspecTestClass;end
klass.any_instance.stub(:existing_method).and_return(true)
klass.rspec_verify
-
klass.new.should respond_to(:existing_method)
klass.new.existing_method.should eq(existing_method_return_value)
end
@@ -622,21 +599,7 @@ class RSpec::SampleRspecTestClass;end
RSpec::Mocks::space.send(:mocks).should include(instance)
end
end
-
- context 'with #stub and #should_receive used in the same example' do
- it 'does not conflict with each other' do
- klass.any_instance.stub(:foo).and_return(:foo)
- klass.any_instance.stub(:foo).with(:arg).and_return(:foo_with_arg)
- klass.any_instance.should_receive(:bar).and_return(:bar)
- klass.any_instance.should_receive(:bar).with(:arg).and_return(:bar_with_arg)
-
- klass.new.foo.should eq(:foo)
- klass.new.foo(:arg).should eq(:foo_with_arg)
- klass.new.bar.should eq(:bar)
- klass.new.bar(:arg).should eq(:bar_with_arg)
- end
- end
end
end
-end
+end

0 comments on commit 628eb91

Please sign in to comment.