123456789_123456789_123456789_123456789_123456789_

Class: RSpec::Mocks::AnyInstance::Recorder Private

Relationships & Source Files
Inherits: Object
Defined in: rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb

Overview

Given a class TheClass, TheClass.any_instance returns a Recorder, which records stubs and message expectations for later playback on instances of TheClass.

Further constraints are stored in instances of [Chain](Chain).

Class Method Summary

Instance Attribute Summary

  • #klass readonly Internal use only Internal use only
  • #message_chains readonly Internal use only Internal use only
  • #stubs readonly Internal use only Internal use only

Instance Method Summary

Constructor Details

.new(klass) ⇒ Recorder

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 16

def initialize(klass)
  @message_chains = MessageChains.new
  @stubs = Hash.new { |hash, key| hash[key] = [] }
  @observed_methods = []
  @played_methods = {}
  @backed_up_method_owner = {}
  @klass = klass
  @expectation_set = false

  return unless RSpec::Mocks.configuration.verify_partial_doubles?
  RSpec::Mocks.configuration.verifying_double_callbacks.each do |block|
    block.call(ObjectReference.for(klass))
  end
end

Instance Attribute Details

#klass (readonly)

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 14

attr_reader :message_chains, :stubs, :klass

#message_chains (readonly)

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 14

attr_reader :message_chains, :stubs, :klass

#stubs (readonly)

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 14

attr_reader :message_chains, :stubs, :klass

Instance Method Details

#allow_no_prepended_module_definition_of(_method_name) (private)

See additional method definition at line 284.

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 292

def allow_no_prepended_module_definition_of(method_name)
  prepended_modules = RSpec::Mocks::Proxy.prepended_modules_of(@klass)
  problem_mod = prepended_modules.find { |mod| mod.method_defined?(method_name) }
  return unless problem_mod

  AnyInstance.error_generator.raise_not_supported_with_prepend_error(method_name, problem_mod)
end

#already_observing?(method_name) ⇒ Boolean

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 127

def already_observing?(method_name)
  @observed_methods.include?(method_name) || super_class_observing?(method_name)
end

#ancestor_is_an_observer?(ancestor, method_name) ⇒ Boolean (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 159

def ancestor_is_an_observer?(ancestor, method_name)
  return if ancestor == @klass

  ::RSpec::Mocks.space.
    any_instance_recorder_for(ancestor).already_observing?(method_name)
end

#backup_method!(method_name) (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 235

def backup_method!(method_name)
  return unless public_protected_or_private_method_defined?(method_name)

  alias_method_name = build_alias_method_name(method_name)
  @backed_up_method_owner[method_name.to_sym] ||= @klass.instance_method(method_name).owner
  @klass.class_exec do
    alias_method alias_method_name, method_name
  end
end

#build_alias_method_name(method_name)

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 122

def build_alias_method_name(method_name)
  "__#{method_name}_without_any_instance__"
end

#expect_chain(*method_names_and_optional_return_values, &block)

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 53

def expect_chain(*method_names_and_optional_return_values, &block)
  @expectation_set = true
  normalize_chain(*method_names_and_optional_return_values) do |method_name, args|
    observe!(method_name)
    message_chains.add(method_name, ExpectChainChain.new(self, *args, &block))
  end
end

#instance_that_received(method_name)

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 117

def instance_that_received(method_name)
  @played_methods[method_name]
end

#mark_invoked!(method_name) (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 271

def mark_invoked!(method_name)
  backup_method!(method_name)
  recorder = self
  @klass.__send__(:define_method, method_name) do |*_args, &_blk|
    invoked_instance = recorder.instance_that_received(method_name)
    inspect = "#<#{self.class}:#{object_id} #{instance_variables.map { |name| "#{name}=#{instance_variable_get name}" }.join(', ')}>"
    AnyInstance.error_generator.raise_message_already_received_by_other_instance_error(
      method_name, inspect, invoked_instance
    )
  end
end

#normalize_chain(*args) {|args.first, args| ... } (private)

Yields:

  • (args.first, args)
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 178

def normalize_chain(*args)
  args.shift.to_s.split('.').map { |s| s.to_sym }.reverse.each { |a| args.unshift a }
  yield args.first, args
end

#notify_received_message(_object, message, args, _blk)

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 132

def notify_received_message(_object, message, args, _blk)
  has_expectation = false

  message_chains.each_unfulfilled_expectation_matching(message, *args) do |expectation|
    has_expectation = true
    expectation.expectation_fulfilled!
  end

  return unless has_expectation

  restore_method!(message)
  mark_invoked!(message)
end

#observe!(method_name) (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 249

def observe!(method_name)
  allow_no_prepended_module_definition_of(method_name)

  if RSpec::Mocks.configuration.verify_partial_doubles? && !Mocks.configuration.temporarily_suppress_partial_double_verification
    unless public_protected_or_private_method_defined?(method_name)
      AnyInstance.error_generator.raise_does_not_implement_error(@klass, method_name)
    end
  end

  stop_observing!(method_name) if already_observing?(method_name)
  @observed_methods << method_name
  backup_method!(method_name)
  recorder = self
  method_was_private = @klass.private_method_defined?(method_name)
  @klass.__send__(:define_method, method_name) do |*args, &blk|
    recorder.playback!(self, method_name)
    __send__(method_name, *args, &blk)
  end
  @klass.__send__(:private, method_name) if method_was_private
  @klass.__send__(:ruby2_keywords, method_name) if @klass.respond_to?(:ruby2_keywords, true)
end

#playback!(instance, method_name)

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 109

def playback!(instance, method_name)
  RSpec::Mocks.space.ensure_registered(instance)
  message_chains.playback!(instance, method_name)
  @played_methods[method_name] = instance
  received_expected_message!(method_name) if message_chains.has_expectation?(method_name)
end

#public_protected_or_private_method_defined?(method_name) ⇒ Boolean (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 245

def public_protected_or_private_method_defined?(method_name)
  MethodReference.method_defined_at_any_visibility?(@klass, method_name)
end

#received_expected_message!(method_name) (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 183

def received_expected_message!(method_name)
  message_chains.received_expected_message!(method_name)
  restore_method!(method_name)
  mark_invoked!(method_name)
end

#remove_dummy_method!(method_name) (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 229

def remove_dummy_method!(method_name)
  @klass.class_exec do
    remove_method method_name
  end
end

#restore_method!(method_name) (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 189

def restore_method!(method_name)
  if public_protected_or_private_method_defined?(build_alias_method_name(method_name))
    restore_original_method!(method_name)
  else
    remove_dummy_method!(method_name)
  end
end

#restore_original_method!(method_name) (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 197

def restore_original_method!(method_name)
  return unless @klass.instance_method(method_name).owner == @klass

  alias_method_name = build_alias_method_name(method_name)
  @klass.class_exec(@backed_up_method_owner) do |backed_up_method_owner|
    remove_method method_name

    # A @klass can have methods implemented (see Method#owner) in @klass
    # or inherited from a superclass. In ruby 2.2 and earlier, we can copy
    # a method regardless of the 'owner' and restore it to @klass after
    # because a call to 'super' from @klass's copied method would end up
    # calling the original class's superclass's method.
    #
    # With the commit below, available starting in 2.3.0, ruby changed
    # this behavior and a call to 'super' from the method copied to @klass
    # will call @klass's superclass method, which is the original
    # implementer of this method!  This leads to very strange errors
    # if @klass's copied method calls 'super', since it would end up
    # calling itself, the original method implemented in @klass's
    # superclass.
    #
    # For ruby 2.3 and above, we need to only restore methods that
    # @klass originally owned.
    #
    # https://github.com/ruby/ruby/commit/c8854d2ca4be9ee6946e6d17b0e17d9ef130ee81
    if RUBY_VERSION < "2.3" || backed_up_method_owner[method_name.to_sym] == self
      alias_method method_name, alias_method_name
    end
    remove_method alias_method_name
  end
end

#should_not_receive(method_name, &block)

The opposite of #should_receive

See Also:

  • Methods#should_not_receive
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 75

def should_not_receive(method_name, &block)
  should_receive(method_name, &block).never
end

#should_receive(method_name, &block)

Initializes the recording a message expectation to be played back against any instance of this object that invokes the submitted method.

See Also:

  • Methods#should_receive
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 66

def should_receive(method_name, &block)
  @expectation_set = true
  observe!(method_name)
  message_chains.add(method_name, PositiveExpectationChain.new(self, method_name, &block))
end

#stop_all_observation!

This method is for internal use only.
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 104

def stop_all_observation!
  @observed_methods.each { |method_name| restore_method!(method_name) }
end

#stop_observing!(method_name) (protected)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 148

def stop_observing!(method_name)
  restore_method!(method_name)
  @observed_methods.delete(method_name)
  super_class_observers_for(method_name).each do |ancestor|
    ::RSpec::Mocks.space.
      any_instance_recorder_for(ancestor).stop_observing!(method_name)
  end
end

#stub(method_name, &block)

Initializes the recording a stub to be played back against any instance of this object that invokes the submitted method.

See Also:

  • Methods#stub
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 35

def stub(method_name, &block)
  observe!(method_name)
  message_chains.add(method_name, StubChain.new(self, method_name, &block))
end

#stub_chain(*method_names_and_optional_return_values, &block)

Initializes the recording a stub chain to be played back against any instance of this object that invokes the method matching the first argument.

See Also:

  • Methods#stub_chain
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 45

def stub_chain(*method_names_and_optional_return_values, &block)
  normalize_chain(*method_names_and_optional_return_values) do |method_name, args|
    observe!(method_name)
    message_chains.add(method_name, StubChainChain.new(self, *args, &block))
  end
end

#super_class_observers_for(method_name) (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 166

def super_class_observers_for(method_name)
  @klass.ancestors.select do |ancestor|
    ancestor_is_an_observer?(ancestor, method_name)
  end
end

#super_class_observing?(method_name) ⇒ Boolean (private)

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 172

def super_class_observing?(method_name)
  @klass.ancestors.any? do |ancestor|
    ancestor_is_an_observer?(ancestor, method_name)
  end
end

#unstub(method_name)

Removes any previously recorded stubs, stub_chains or message expectations that use method_name.

See Also:

  • Methods#unstub
[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 83

def unstub(method_name)
  unless @observed_methods.include?(method_name.to_sym)
    AnyInstance.error_generator.raise_method_not_stubbed_error(method_name)
  end
  message_chains.remove_stub_chains_for!(method_name)
  stubs[method_name].clear
  stop_observing!(method_name) unless message_chains.has_expectation?(method_name)
end

#verify

This method is for internal use only.

Used internally to verify that message expectations have been fulfilled.

[ GitHub ]

  
# File 'rspec-mocks/lib/rspec/mocks/any_instance/recorder.rb', line 96

def verify
  return unless @expectation_set
  return if message_chains.all_expectations_fulfilled?

  AnyInstance.error_generator.raise_second_instance_received_message_error(message_chains.unfulfilled_expectations)
end