Skip to content

Commit

Permalink
Merge d46944e into 3ac6f4e
Browse files Browse the repository at this point in the history
  • Loading branch information
JonRowe committed Sep 9, 2013
2 parents 3ac6f4e + d46944e commit 16b9211
Show file tree
Hide file tree
Showing 12 changed files with 314 additions and 34 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Enhancements:
attributes. (Xavier Shay)
* Default value of `transfer_nested_constants` option for constant stubbing can
be configured. (Xavier Shay)
* Messages can be allowed or expected on in bulk via
`receive_messages(:message => :value)` (Jon Rowe)

Bug Fixes:

Expand Down
1 change: 1 addition & 0 deletions lib/rspec/mocks/framework.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
require 'rspec/mocks/mutate_const'
require 'rspec/mocks/matchers/have_received'
require 'rspec/mocks/matchers/receive'
require 'rspec/mocks/matchers/receive_messages'
require 'rspec/mocks/stub_chain'
require 'rspec/mocks/targets'
require 'rspec/mocks/syntax'
Expand Down
4 changes: 4 additions & 0 deletions lib/rspec/mocks/matchers/have_received.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ def initialize(method_name, &block)
@subject = nil
end

def name
"have_received"
end

def matches?(subject, &block)
@block ||= block
@subject = subject
Expand Down
8 changes: 5 additions & 3 deletions lib/rspec/mocks/matchers/receive.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ def initialize(message, block)
# reports an extra "in `new'" line in the backtrace that the
# others do not include. The safest way to find the right
# line is to search for the first line BEFORE rspec/mocks/syntax.rb.
@backtrace_line = caller.find do |line|
!line.split(':').first.end_with?('rspec/mocks/syntax.rb')
end
@backtrace_line = CallerFilter.first_non_rspec_line
end

def name
"receive"
end

def setup_expectation(subject, &block)
Expand Down
64 changes: 64 additions & 0 deletions lib/rspec/mocks/matchers/receive_messages.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
module RSpec
module Mocks
module Matchers
class ReceiveMessages

def initialize(message_return_value_hash)
@message_return_value_hash = message_return_value_hash
@backtrace_line = CallerFilter.first_non_rspec_line
end

def name
"receive_messages"
end

def setup_expectation(subject, &block)
each_message_on( proxy_on(subject) ) do |host, message, return_value|
host.add_simple_expectation(message, return_value, @backtrace_line)
end
end
alias matches? setup_expectation

def setup_negative_expectation(subject, &block)
raise NegationUnsupportedError,
"`expect(...).to_not receive_messages` is not supported since it " +
"doesn't really make sense. What would it even mean?"
end
alias does_not_match? setup_negative_expectation

def setup_allowance(subject)
each_message_on( proxy_on(subject) ) do |host, message, return_value|
host.add_simple_stub(message, return_value)
end
end

def setup_any_instance_expectation(subject)
each_message_on( any_instance_of(subject) ) do |host, message, return_value|
host.should_receive(message).and_return(return_value)
end
end

def setup_any_instance_allowance(subject)
any_instance_of(subject).stub(@message_return_value_hash)
end

private

def proxy_on(subject)
::RSpec::Mocks.proxy_for(subject)
end

def any_instance_of(subject)
::RSpec::Mocks.any_instance_recorder_for(subject)
end

def each_message_on(host)
@message_return_value_hash.each do |message, value|
yield host, message, value
end
end

end
end
end
end
44 changes: 37 additions & 7 deletions lib/rspec/mocks/message_expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,32 @@ module Mocks
# for a message. While this same effect can be achieved using a standard
# MessageExpecation, this version is much faster and so can be used as an
# optimization.
SimpleMessageExpectation = Struct.new(:message, :response) do
class SimpleMessageExpectation

def initialize(message, response, error_generator, backtrace_line = nil)
@message, @response, @error_generator, @backtrace_line = message, response, error_generator, backtrace_line
@received = false
end

def invoke(*_)
response
@received = true
@response
end

def matches?(message, *_)
self.message == message
@message == message
end

def called_max_times?
false
end

def verify_messages_received
BacktrackRestore.with(@backtrace_line) do
unless @received
@error_generator.raise_expectation_error(@message, 1, ArgumentListMatcher::MATCH_ALL, 0, nil)
end
end
end
end

Expand Down Expand Up @@ -245,10 +264,9 @@ def matches_name_but_not_args(message, *args)

# @private
def verify_messages_received
generate_error unless expected_messages_received? || failed_fast?
rescue RSpec::Mocks::MockExpectationError => error
error.backtrace.insert(0, @expected_from)
Kernel::raise error
BacktrackRestore.with(@expected_from) do
generate_error unless expected_messages_received? || failed_fast?
end
end

# @private
Expand Down Expand Up @@ -585,5 +603,17 @@ def cannot_modify_further_error
"to call the original implementation, and cannot be modified further."
end
end

# Insert original locations into stacktraces
# @api private
class BacktrackRestore
def self.with(location)
yield
rescue RSpec::Mocks::MockExpectationError => error
error.backtrace.insert(0, location)
Kernel::raise error
end
end

end
end
22 changes: 16 additions & 6 deletions lib/rspec/mocks/method_double.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,28 @@ def add_stub(error_generator, expectation_ordering, expected_from, opts={}, &imp

# A simple stub can only return a concrete value for a message, and
# cannot match on arguments. It is used as an optimization over
# `add_stub` where it is known in advance that this is all that will be
# required of a stub, such as when passing attributes to the `double`
# example method. They do not stash or restore existing method
# `add_stub` / `add_expectation` where it is known in advance that this
# is all that will be required of a stub, such as when passing attributes
# to the `double` example method. They do not stash or restore existing method
# definitions.
#
# @private
def add_simple_stub(method_name, response)
setup_simple_method_double method_name, response, stubs
end

# @private
def add_simple_expectation(method_name, response, error_generator, backtrace_line)
setup_simple_method_double method_name, response, expectations, error_generator, backtrace_line
end

# @private
def setup_simple_method_double(method_name, response, collection, error_generator = nil, backtrace_line = nil)
define_proxy_method

stub = SimpleMessageExpectation.new(method_name, response)
stubs.unshift stub
stub
me = SimpleMessageExpectation.new(method_name, response, error_generator, backtrace_line)
collection.unshift me
me
end

# @private
Expand Down
5 changes: 5 additions & 0 deletions lib/rspec/mocks/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ def add_message_expectation(location, method_name, opts={}, &block)
meth_double.add_expectation @error_generator, @expectation_ordering, location, opts, &block
end

# @private
def add_simple_expectation(method_name, response, location)
method_double[method_name].add_simple_expectation method_name, response, @error_generator, location
end

# @private
def build_expectation(method_name)
meth_double = method_double[method_name]
Expand Down
20 changes: 20 additions & 0 deletions lib/rspec/mocks/syntax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ def receive(method_name, &block)
Matchers::Receive.new(method_name, block)
end

def receive_messages(message_return_value_hash)
raise "Implementation blocks aren't supported with `receive_messages`" if block_given?
Matchers::ReceiveMessages.new(message_return_value_hash)
end

def allow(target)
AllowanceTarget.new(target)
end
Expand Down Expand Up @@ -119,6 +124,7 @@ def self.disable_expect(syntax_host = ::RSpec::Mocks::ExampleMethods)

syntax_host.class_exec do
undef receive
undef receive_messages
undef allow
undef expect_any_instance_of
undef allow_any_instance_of
Expand Down Expand Up @@ -321,6 +327,20 @@ def self.default_should_syntax_host
# expect(obj).to receive(:hello).with("world").exactly(3).times
#
# @note This is only available when you have enabled the `expect` syntax.
#
# @method receive_messages
# Shorthand syntax used to setup message(s), and their return value(s),
# that you expect or allow an object to receive. The method takes a hash
# of messages and their respective return values. Unlike with `receive`,
# you cannot apply further customizations using a block or the fluent
# interface.
#
# @example
#
# allow(obj).to receive_messages(:speak => "Hello World")
# allow(obj).to receive_messages(:speak => "Hello", :meow => "Meow")
#
# @note This is only available when you have enabled the `expect` syntax.
end
end
end
Expand Down
57 changes: 40 additions & 17 deletions lib/rspec/mocks/targets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,51 @@ def initialize(target)
@target = target
end

def self.delegate_to(matcher_method, options = {})
method_name = options.fetch(:from) { :to }
define_method(method_name) do |matcher, &block|
unless Matchers::Receive === matcher
raise UnsupportedMatcherError, "only the `receive` matcher is supported " +
"with `#{expression}(...).#{method_name}`, but you have provided: #{matcher}"
def self.delegate_to(matcher_method)
define_method(:to) do |matcher, &block|
unless Matchers::Receive === matcher || Matchers::ReceiveMessages === matcher
raise_unsupported_matcher(:to, matcher)
end
define_matcher(matcher, matcher_method, &block)
end
end

matcher.__send__(matcher_method, @target, &block)
def self.delegate_not_to(matcher_method, options = {})
method_name = options.fetch(:from)
define_method(method_name) do |matcher, &block|
case matcher
when Matchers::Receive then define_matcher(matcher, matcher_method, &block)
when Matchers::ReceiveMessages then raise_negation_unsupported(method_name, matcher)
else
raise_unsupported_matcher(method_name, matcher)
end
end
end

def self.disallow_negation(method)
define_method method do |*args|
raise NegationUnsupportedError,
"`#{expression}(...).#{method} receive` is not supported since it " +
"doesn't really make sense. What would it even mean?"
def self.disallow_negation(method_name)
define_method(method_name) do |matcher, *args|
raise_negation_unsupported(method_name, matcher)
end
end

private

def define_matcher(matcher, name, &block)
matcher.__send__(name, @target, &block)
end

def raise_unsupported_matcher(method_name, matcher)
raise UnsupportedMatcherError,
"only the `receive` or `receive_messages` matchers are supported " +
"with `#{expression}(...).#{method_name}`, but you have provided: #{matcher}"
end

def raise_negation_unsupported(method_name, matcher)
raise NegationUnsupportedError,
"`#{expression}(...).#{method_name} #{matcher.name}` is not supported since it " +
"doesn't really make sense. What would it even mean?"
end

def expression
self.class::EXPRESSION
end
Expand All @@ -45,12 +68,12 @@ class AllowanceTarget < TargetBase
class ExpectationTarget < TargetBase
EXPRESSION = :expect
delegate_to :setup_expectation
delegate_to :setup_negative_expectation, :from => :not_to
delegate_to :setup_negative_expectation, :from => :to_not
delegate_not_to :setup_negative_expectation, :from => :not_to
delegate_not_to :setup_negative_expectation, :from => :to_not
end

class AnyInstanceAllowanceTarget < TargetBase
EXPRESSION = :expect_any_instance_of
EXPRESSION = :allow_any_instance_of
delegate_to :setup_any_instance_allowance
disallow_negation :not_to
disallow_negation :to_not
Expand All @@ -59,8 +82,8 @@ class AnyInstanceAllowanceTarget < TargetBase
class AnyInstanceExpectationTarget < TargetBase
EXPRESSION = :expect_any_instance_of
delegate_to :setup_any_instance_expectation
delegate_to :setup_any_instance_negative_expectation, :from => :not_to
delegate_to :setup_any_instance_negative_expectation, :from => :to_not
delegate_not_to :setup_any_instance_negative_expectation, :from => :not_to
delegate_not_to :setup_any_instance_negative_expectation, :from => :to_not
end
end
end
Expand Down

0 comments on commit 16b9211

Please sign in to comment.