Skip to content

Commit

Permalink
Merge pull request #519 from rspec/lifecycle-space-fixups
Browse files Browse the repository at this point in the history
Lifecycle/space fixups
  • Loading branch information
myronmarston committed Jan 9, 2014
2 parents e31f355 + c08a408 commit 8615dc2
Show file tree
Hide file tree
Showing 35 changed files with 802 additions and 473 deletions.
7 changes: 7 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Breaking Changes for 3.0.0:
* Remove the `host` argument of `RSpec::Mocks.setup`. Instead
`RSpec::Mocks::ExampleMethods` should be included directly in the scope where
RSpec's mocking capabilities are used. (Sam Phippen)
* Make test doubles raise errors if you attempt to use them after they
get reset, to help surface issues when you accidentally retain
references to test doubles and attempt to reuse them in another
example. (Myron Marston)

Bug Fixes:

Expand Down Expand Up @@ -48,6 +52,9 @@ Enhancements:
* Change argument matchers to use `===` as their primary matching
protocol, as their semantics mirror that of a case or rescue statement
(which uses `===` for matching). (Myron Marston)
* Add `RSpec::Mocks.with_temporary_scope`, which allows you to create
temporary rspec-mocks scopes in arbitrary places (such as a
`before(:all)` hook). (Myron Marston)

### 3.0.0.beta1 / 2013-11-07
[full changelog](http://github.com/rspec/rspec-mocks/compare/v2.99.0.beta1...v3.0.0.beta1)
Expand Down
48 changes: 16 additions & 32 deletions lib/rspec/mocks.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
require 'rspec/mocks/framework'
require 'rspec/mocks/version'
require 'rspec/support'
require "rspec/mocks/error_space"

module RSpec
# Contains top-level utility methods. While this contains a few
# public methods, these are not generally meant to be called from
# a test or example. They exist primarily for integration with
# test frameworks (such as rspec-core).
module Mocks
ERROR_SPACE = RSpec::Mocks::ErrorSpace.new
MOCK_SPACE = RSpec::Mocks::Space.new

class << self
# Stores rspec-mocks' global state.
# @api private
attr_accessor :space
end

self.space = ERROR_SPACE

# Performs per-test/example setup. This should be called before
# an test or example begins.
def self.setup
self.space = MOCK_SPACE
@space_stack << (@space = space.new_scope)
end

# Verifies any message expectations that were set during the
Expand All @@ -37,7 +25,8 @@ def self.verify
# each example, even if an error was raised during the example.
def self.teardown
space.reset_all
self.space = ERROR_SPACE
@space_stack.pop
@space = @space_stack.last || @root_space
end

# Adds an allowance (stub) on `subject`
Expand All @@ -56,8 +45,7 @@ def self.allow_message(subject, message, opts={}, &block)
orig_caller = opts.fetch(:expected_from) {
CallerFilter.first_non_rspec_line
}
::RSpec::Mocks.proxy_for(subject).
add_stub(orig_caller, message, opts, &block)
space.proxy_for(subject).add_stub(orig_caller, message, opts, &block)
end

# Sets a message expectation on `subject`.
Expand All @@ -75,27 +63,23 @@ def self.expect_message(subject, message, opts={}, &block)
orig_caller = opts.fetch(:expected_from) {
CallerFilter.first_non_rspec_line
}
::RSpec::Mocks.proxy_for(subject).
add_message_expectation(orig_caller, message, opts, &block)
space.proxy_for(subject).add_message_expectation(orig_caller, message, opts, &block)
end

# @api private
# Returns the mock proxy for the given object.
def self.proxy_for(object)
space.proxy_for(object)
end
def self.with_temporary_scope
setup

# @api private
# Returns the mock proxies for instances of the given class.
def self.proxies_of(klass)
space.proxies_of(klass)
begin
yield
verify
ensure
teardown
end
end

# @api private
# Returns the any instance recorder for the given class.
def self.any_instance_recorder_for(klass)
space.any_instance_recorder_for(klass)
end
class << self; attr_reader :space; end
@space_stack = []
@root_space = @space = RSpec::Mocks::RootSpace.new

# @private
IGNORED_BACKTRACE_LINE = 'this backtrace line is ignored'
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/mocks/any_instance/expectation_chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class PositiveExpectationChain < ExpectationChain
private

def create_message_expectation_on(instance)
proxy = ::RSpec::Mocks.proxy_for(instance)
proxy = ::RSpec::Mocks.space.proxy_for(instance)
expected_from = IGNORED_BACKTRACE_LINE
me = proxy.add_message_expectation(expected_from, *@expectation_args, &@expectation_block)
if RSpec::Mocks.configuration.yield_receiver_to_any_instance_implementation_blocks?
Expand Down
12 changes: 7 additions & 5 deletions lib/rspec/mocks/any_instance/recorder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def unstub(method_name)
raise RSpec::Mocks::MockExpectationError, "The method `#{method_name}` was not stubbed or was already unstubbed"
end
message_chains.remove_stub_chains_for!(method_name)
::RSpec::Mocks.proxies_of(@klass).each do |proxy|
::RSpec::Mocks.space.proxies_of(@klass).each do |proxy|
stubs[method_name].each { |stub| proxy.remove_single_stub(method_name, stub) }
end
stubs[method_name].clear
Expand Down Expand Up @@ -134,7 +134,8 @@ 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.any_instance_recorder_for(ancestor).stop_observing!(method_name)
::RSpec::Mocks.space.
any_instance_recorder_for(ancestor).stop_observing!(method_name)
end
end

Expand All @@ -143,7 +144,8 @@ def stop_observing!(method_name)
def ancestor_is_an_observer?(method_name)
lambda do |ancestor|
unless ancestor == @klass
::RSpec::Mocks.any_instance_recorder_for(ancestor).already_observing?(method_name)
::RSpec::Mocks.space.
any_instance_recorder_for(ancestor).already_observing?(method_name)
end
end
end
Expand Down Expand Up @@ -213,7 +215,7 @@ def observe!(method_name)
backup_method!(method_name)
@klass.__send__(:define_method, method_name) do |*args, &blk|
klass = ::RSpec::Support.method_handle_for(self, method_name).owner
::RSpec::Mocks.any_instance_recorder_for(klass).playback!(self, method_name)
::RSpec::Mocks.space.any_instance_recorder_for(klass).playback!(self, method_name)
self.__send__(method_name, *args, &blk)
end
end
Expand All @@ -222,7 +224,7 @@ def mark_invoked!(method_name)
backup_method!(method_name)
@klass.__send__(:define_method, method_name) do |*args, &blk|
klass = ::RSpec::Support.method_handle_for(self, method_name).owner
invoked_instance = ::RSpec::Mocks.any_instance_recorder_for(klass).instance_that_received(method_name)
invoked_instance = ::RSpec::Mocks.space.any_instance_recorder_for(klass).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
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/mocks/any_instance/stub_chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def expectation_fulfilled?
private

def create_message_expectation_on(instance)
proxy = ::RSpec::Mocks.proxy_for(instance)
proxy = ::RSpec::Mocks.space.proxy_for(instance)
expected_from = IGNORED_BACKTRACE_LINE
stub = proxy.add_stub(expected_from, *@expectation_args, &@expectation_block)
@recorder.stubs[stub.message] << stub
Expand Down
34 changes: 30 additions & 4 deletions lib/rspec/mocks/error_generator.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
module RSpec
module Mocks
# @public
# Raised when a message expectation is not satisfied.
MockExpectationError = Class.new(Exception)

# @public
# Raised when a test double is used after it has been torn
# down (typically at the end of an rspec-core example).
ExpiredTestDoubleError = Class.new(MockExpectationError)

# @public
# Raised when doubles or partial doubles are used outside of the per-test lifecycle.
OutsideOfExampleError = Class.new(StandardError)

# @private
UnsupportedMatcherError = Class.new(StandardError)
# @private
NegationUnsupportedError = Class.new(StandardError)

# @private
class ErrorGenerator
attr_writer :opts
Expand Down Expand Up @@ -63,6 +81,14 @@ def raise_arity_error(calculator, actual)
]
end

def raise_expired_test_double_error
raise ExpiredTestDoubleError,
"#{intro} was originally created in one example but has leaked into " +
"another example and can no longer be used. rspec-mocks' doubles are " +
"designed to only last for one example, and you need to create a new " +
"one in each example you wish to use it for."
end

# @private
def received_part_of_expectation_error(actual_received_count, *args)
"received: #{count_message(actual_received_count)}" +
Expand Down Expand Up @@ -189,12 +215,12 @@ def received_arg_list(*args)
end

def count_message(count, expectation_count_type=nil)
return "at least #{pretty_print(count.abs)}" if count < 0 || expectation_count_type == :at_least
return "at most #{pretty_print(count)}" if expectation_count_type == :at_most
return pretty_print(count)
return "at least #{times(count.abs)}" if count < 0 || expectation_count_type == :at_least
return "at most #{times(count)}" if expectation_count_type == :at_most
return times(count)
end

def pretty_print(count)
def times(count)
"#{count} time#{count == 1 ? '' : 's'}"
end

Expand Down
35 changes: 0 additions & 35 deletions lib/rspec/mocks/error_space.rb

This file was deleted.

12 changes: 0 additions & 12 deletions lib/rspec/mocks/errors.rb

This file was deleted.

2 changes: 1 addition & 1 deletion lib/rspec/mocks/extensions/marshal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class << self
# Duplicates any mock objects before serialization. Otherwise,
# serialization will fail because methods exist on the singleton class.
def dump_with_mocks(object, *rest)
if ::RSpec::Mocks.space.nil? || !::RSpec::Mocks.space.registered?(object) || NilClass === object
if !::RSpec::Mocks.space.registered?(object) || NilClass === object
dump_without_mocks(object, *rest)
else
dump_without_mocks(object.dup, *rest)
Expand Down
2 changes: 0 additions & 2 deletions lib/rspec/mocks/framework.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@
require 'rspec/mocks/argument_matchers'
require 'rspec/mocks/example_methods'
require 'rspec/mocks/proxy'
require 'rspec/mocks/proxy_for_nil'
require 'rspec/mocks/test_double'
require 'rspec/mocks/argument_list_matcher'
require 'rspec/mocks/message_expectation'
require 'rspec/mocks/order_group'
require 'rspec/mocks/errors'
require 'rspec/mocks/error_generator'
require 'rspec/mocks/space'
require 'rspec/mocks/extensions/marshal'
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/mocks/matchers/have_received.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def expected_messages_received_in_order?
end

def mock_proxy
RSpec::Mocks.proxy_for(@subject)
RSpec::Mocks.space.proxy_for(@subject)
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/rspec/mocks/matchers/receive.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ def warn_if_any_instance(expression, subject)
end

def setup_mock_proxy_method_substitute(subject, method, block)
proxy = ::RSpec::Mocks.proxy_for(subject)
proxy = ::RSpec::Mocks.space.proxy_for(subject)
setup_method_substitute(proxy, method, block, @backtrace_line)
end

def setup_any_instance_method_substitute(subject, method, block)
any_instance_recorder = ::RSpec::Mocks.any_instance_recorder_for(subject)
any_instance_recorder = ::RSpec::Mocks.space.any_instance_recorder_for(subject)
setup_method_substitute(any_instance_recorder, method, block)
end

Expand Down
4 changes: 2 additions & 2 deletions lib/rspec/mocks/matchers/receive_message_chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ def setup_allowance(subject, &block)
end

def setup_any_instance_allowance(subject, &block)
recorder = ::RSpec::Mocks.any_instance_recorder_for(subject)
recorder = ::RSpec::Mocks.space.any_instance_recorder_for(subject)
chain = recorder.stub_chain(*@chain, &(@block || block))
replay_customizations(chain)
end

def setup_any_instance_expectation(subject, &block)
recorder = ::RSpec::Mocks.any_instance_recorder_for(subject)
recorder = ::RSpec::Mocks.space.any_instance_recorder_for(subject)
chain = recorder.expect_chain(*@chain, &(@block || block))
replay_customizations(chain)
end
Expand Down
4 changes: 2 additions & 2 deletions lib/rspec/mocks/matchers/receive_messages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ def warn_about_block
private

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

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

def each_message_on(host)
Expand Down
4 changes: 2 additions & 2 deletions lib/rspec/mocks/message_chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ def format_chain(*chain, &blk)
end

def find_matching_stub
::RSpec::Mocks.proxy_for(object).
::RSpec::Mocks.space.proxy_for(object).
__send__(:find_matching_method_stub, chain.first.to_sym)
end

def find_matching_expectation
::RSpec::Mocks.proxy_for(object).
::RSpec::Mocks.space.proxy_for(object).
__send__(:find_matching_expectation, chain.first.to_sym)
end
end
Expand Down
Loading

0 comments on commit 8615dc2

Please sign in to comment.