rspec-mocks 3.0.4 → 3.12.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.document +1 -1
  4. data/.yardopts +1 -1
  5. data/Changelog.md +512 -2
  6. data/{License.txt → LICENSE.md} +5 -4
  7. data/README.md +113 -30
  8. data/lib/rspec/mocks/any_instance/chain.rb +5 -3
  9. data/lib/rspec/mocks/any_instance/error_generator.rb +31 -0
  10. data/lib/rspec/mocks/any_instance/expect_chain_chain.rb +1 -5
  11. data/lib/rspec/mocks/any_instance/expectation_chain.rb +9 -8
  12. data/lib/rspec/mocks/any_instance/message_chains.rb +7 -8
  13. data/lib/rspec/mocks/any_instance/proxy.rb +14 -5
  14. data/lib/rspec/mocks/any_instance/recorder.rb +61 -31
  15. data/lib/rspec/mocks/any_instance/stub_chain.rb +15 -11
  16. data/lib/rspec/mocks/any_instance/stub_chain_chain.rb +1 -5
  17. data/lib/rspec/mocks/any_instance.rb +1 -0
  18. data/lib/rspec/mocks/argument_list_matcher.rb +55 -10
  19. data/lib/rspec/mocks/argument_matchers.rb +88 -30
  20. data/lib/rspec/mocks/configuration.rb +61 -13
  21. data/lib/rspec/mocks/error_generator.rb +250 -107
  22. data/lib/rspec/mocks/example_methods.rb +151 -28
  23. data/lib/rspec/mocks/instance_method_stasher.rb +17 -6
  24. data/lib/rspec/mocks/matchers/have_received.rb +50 -20
  25. data/lib/rspec/mocks/matchers/receive.rb +39 -11
  26. data/lib/rspec/mocks/matchers/receive_message_chain.rb +22 -7
  27. data/lib/rspec/mocks/matchers/receive_messages.rb +12 -7
  28. data/lib/rspec/mocks/message_chain.rb +3 -7
  29. data/lib/rspec/mocks/message_expectation.rb +466 -307
  30. data/lib/rspec/mocks/method_double.rb +88 -29
  31. data/lib/rspec/mocks/method_reference.rb +85 -25
  32. data/lib/rspec/mocks/minitest_integration.rb +68 -0
  33. data/lib/rspec/mocks/mutate_const.rb +50 -109
  34. data/lib/rspec/mocks/object_reference.rb +89 -32
  35. data/lib/rspec/mocks/order_group.rb +4 -5
  36. data/lib/rspec/mocks/proxy.rb +156 -60
  37. data/lib/rspec/mocks/space.rb +52 -35
  38. data/lib/rspec/mocks/standalone.rb +1 -1
  39. data/lib/rspec/mocks/syntax.rb +26 -30
  40. data/lib/rspec/mocks/targets.rb +55 -28
  41. data/lib/rspec/mocks/test_double.rb +43 -7
  42. data/lib/rspec/mocks/verifying_double.rb +27 -33
  43. data/lib/rspec/mocks/{verifying_message_expecation.rb → verifying_message_expectation.rb} +11 -16
  44. data/lib/rspec/mocks/verifying_proxy.rb +77 -26
  45. data/lib/rspec/mocks/version.rb +1 -1
  46. data/lib/rspec/mocks.rb +8 -1
  47. data.tar.gz.sig +0 -0
  48. metadata +80 -43
  49. metadata.gz.sig +0 -0
@@ -2,8 +2,11 @@ module RSpec
2
2
  module Mocks
3
3
  # @private
4
4
  class MethodDouble
5
+ # @private TODO: drop in favor of FrozenError in ruby 2.5+
6
+ FROZEN_ERROR_MSG = /can't modify frozen/
7
+
5
8
  # @private
6
- attr_reader :method_name, :object, :expectations, :stubs
9
+ attr_reader :method_name, :object, :expectations, :stubs, :method_stasher
7
10
 
8
11
  # @private
9
12
  def initialize(object, method_name, proxy)
@@ -18,20 +21,31 @@ module RSpec
18
21
  @stubs = []
19
22
  end
20
23
 
21
- def original_method
24
+ def original_implementation_callable
22
25
  # If original method is not present, uses the `method_missing`
23
26
  # handler of the object. This accounts for cases where the user has not
24
27
  # correctly defined `respond_to?`, and also 1.8 which does not provide
25
28
  # method handles for missing methods even if `respond_to?` is correct.
29
+ @original_implementation_callable ||= original_method || method_missing_block
30
+ end
31
+
32
+ alias_method :save_original_implementation_callable!, :original_implementation_callable
33
+
34
+ def original_method
26
35
  @original_method ||=
27
36
  @method_stasher.original_method ||
28
- @proxy.original_method_handle_for(method_name) ||
29
- Proc.new do |*args, &block|
30
- @object.__send__(:method_missing, @method_name, *args, &block)
31
- end
37
+ @proxy.original_method_handle_for(method_name)
32
38
  end
33
39
 
34
- alias_method :save_original_method!, :original_method
40
+ # @private
41
+ def method_missing_block
42
+ block = Proc.new do |*args, &b|
43
+ @object.__send__(:method_missing, @method_name, *args, &b)
44
+ end
45
+ block.ruby2_keywords if block.respond_to?(:ruby2_keywords)
46
+
47
+ block
48
+ end
35
49
 
36
50
  # @private
37
51
  def visibility
@@ -45,7 +59,7 @@ module RSpec
45
59
 
46
60
  # @private
47
61
  def configure_method
48
- @original_visibility = [visibility, method_name]
62
+ @original_visibility = visibility
49
63
  @method_stasher.stash unless @method_is_proxied
50
64
  define_proxy_method
51
65
  end
@@ -54,44 +68,62 @@ module RSpec
54
68
  def define_proxy_method
55
69
  return if @method_is_proxied
56
70
 
57
- save_original_method!
58
- definition_target.class_exec(self, method_name, visibility) do |method_double, method_name, visibility|
71
+ save_original_implementation_callable!
72
+ definition_target.class_exec(self, method_name, @original_visibility || visibility) do |method_double, method_name, visibility|
59
73
  define_method(method_name) do |*args, &block|
60
74
  method_double.proxy_method_invoked(self, *args, &block)
61
75
  end
62
- self.__send__ visibility, method_name
76
+ # This can't be `if respond_to?(:ruby2_keywords, true)`,
77
+ # see https://github.com/rspec/rspec-mocks/pull/1385#issuecomment-755340298
78
+ ruby2_keywords(method_name) if Module.private_method_defined?(:ruby2_keywords)
79
+ __send__(visibility, method_name)
63
80
  end
64
81
 
65
82
  @method_is_proxied = true
83
+ rescue RuntimeError, TypeError => e
84
+ # TODO: drop in favor of FrozenError in ruby 2.5+
85
+ # RuntimeError (and FrozenError) for ruby 2.x
86
+ # TypeError for ruby 1.x
87
+ if (defined?(FrozenError) && e.is_a?(FrozenError)) || FROZEN_ERROR_MSG === e.message
88
+ raise ArgumentError, "Cannot proxy frozen objects, rspec-mocks relies on proxies for method stubbing and expectations."
89
+ end
90
+ raise
66
91
  end
67
92
 
68
93
  # The implementation of the proxied method. Subclasses may override this
69
94
  # method to perform additional operations.
70
95
  #
71
96
  # @private
72
- def proxy_method_invoked(obj, *args, &block)
97
+ def proxy_method_invoked(_obj, *args, &block)
73
98
  @proxy.message_received method_name, *args, &block
74
99
  end
100
+ ruby2_keywords :proxy_method_invoked if respond_to?(:ruby2_keywords, true)
75
101
 
76
102
  # @private
77
103
  def restore_original_method
78
- return show_frozen_warning if object_singleton_class.frozen?
79
104
  return unless @method_is_proxied
80
105
 
81
- definition_target.__send__(:remove_method, @method_name)
82
-
83
- if @method_stasher.method_is_stashed?
84
- @method_stasher.restore
85
- end
106
+ remove_method_from_definition_target
107
+ @method_stasher.restore if @method_stasher.method_is_stashed?
86
108
  restore_original_visibility
87
109
 
88
110
  @method_is_proxied = false
111
+ rescue RuntimeError, TypeError => e
112
+ # TODO: drop in favor of FrozenError in ruby 2.5+
113
+ # RuntimeError (and FrozenError) for ruby 2.x
114
+ # TypeError for ruby 1.x
115
+ if (defined?(FrozenError) && e.is_a?(FrozenError)) || FROZEN_ERROR_MSG === e.message
116
+ return show_frozen_warning
117
+ end
118
+ raise
89
119
  end
90
120
 
91
121
  # @private
92
122
  def show_frozen_warning
93
123
  RSpec.warn_with(
94
- "WARNING: rspec-mocks was unable to restore the original `#{@method_name}` method on #{@object.inspect} because it has been frozen. If you reuse this object, `#{@method_name}` will continue to respond with its stub implementation.",
124
+ "WARNING: rspec-mocks was unable to restore the original `#{@method_name}` " \
125
+ "method on #{@object.inspect} because it has been frozen. If you reuse this " \
126
+ "object, `#{@method_name}` will continue to respond with its stub implementation.",
95
127
  :call_site => nil,
96
128
  :use_spec_location_as_call_site => true
97
129
  )
@@ -102,12 +134,12 @@ module RSpec
102
134
  return unless @original_visibility &&
103
135
  MethodReference.method_defined_at_any_visibility?(object_singleton_class, @method_name)
104
136
 
105
- object_singleton_class.__send__(*@original_visibility)
137
+ object_singleton_class.__send__(@original_visibility, method_name)
106
138
  end
107
139
 
108
140
  # @private
109
141
  def verify
110
- expectations.each {|e| e.verify_messages_received}
142
+ expectations.each { |e| e.verify_messages_received }
111
143
  end
112
144
 
113
145
  # @private
@@ -134,7 +166,7 @@ module RSpec
134
166
  def add_expectation(error_generator, expectation_ordering, expected_from, opts, &implementation)
135
167
  configure_method
136
168
  expectation = message_expectation_class.new(error_generator, expectation_ordering,
137
- expected_from, self, :expectation, opts, &implementation)
169
+ expected_from, self, :expectation, opts, &implementation)
138
170
  expectations << expectation
139
171
  expectation
140
172
  end
@@ -149,7 +181,7 @@ module RSpec
149
181
  def add_stub(error_generator, expectation_ordering, expected_from, opts={}, &implementation)
150
182
  configure_method
151
183
  stub = message_expectation_class.new(error_generator, expectation_ordering, expected_from,
152
- self, :stub, opts, &implementation)
184
+ self, :stub, opts, &implementation)
153
185
  stubs.unshift stub
154
186
  stub
155
187
  end
@@ -172,7 +204,7 @@ module RSpec
172
204
  end
173
205
 
174
206
  # @private
175
- def setup_simple_method_double(method_name, response, collection, error_generator = nil, backtrace_line = nil)
207
+ def setup_simple_method_double(method_name, response, collection, error_generator=nil, backtrace_line=nil)
176
208
  define_proxy_method
177
209
 
178
210
  me = SimpleMessageExpectation.new(method_name, response, error_generator, backtrace_line)
@@ -199,15 +231,13 @@ module RSpec
199
231
 
200
232
  # @private
201
233
  def raise_method_not_stubbed_error
202
- raise MockExpectationError, "The method `#{method_name}` was not stubbed or was already unstubbed"
234
+ RSpec::Mocks.error_generator.raise_method_not_stubbed_error(method_name)
203
235
  end
204
236
 
205
- private
206
-
207
237
  # In Ruby 2.0.0 and above prepend will alter the method lookup chain.
208
238
  # We use an object's singleton class to define method doubles upon,
209
- # however if the object has had it's singleton class (as opposed to
210
- # it's actual class) prepended too then the the method lookup chain
239
+ # however if the object has had its singleton class (as opposed to
240
+ # its actual class) prepended too then the the method lookup chain
211
241
  # will look in the prepended module first, **before** the singleton
212
242
  # class.
213
243
  #
@@ -216,6 +246,9 @@ module RSpec
216
246
  # of our own.
217
247
  #
218
248
  if Support::RubyFeatures.module_prepends_supported?
249
+
250
+ private
251
+
219
252
  # We subclass `Module` in order to be able to easily detect our prepended module.
220
253
  RSpecPrependedModule = Class.new(Module)
221
254
 
@@ -247,11 +280,37 @@ module RSpec
247
280
 
248
281
  else
249
282
 
283
+ private
284
+
250
285
  def definition_target
251
286
  object_singleton_class
252
287
  end
253
288
 
254
289
  end
290
+
291
+ private
292
+
293
+ def remove_method_from_definition_target
294
+ definition_target.__send__(:remove_method, @method_name)
295
+ rescue NameError
296
+ # This can happen when the method has been monkeyed with by
297
+ # something outside RSpec. This happens, for example, when
298
+ # `file.write` has been stubbed, and then `file.reopen(other_io)`
299
+ # is later called, as `File#reopen` appears to redefine `write`.
300
+ #
301
+ # Note: we could avoid rescuing this by checking
302
+ # `definition_target.instance_method(@method_name).owner == definition_target`,
303
+ # saving us from the cost of the expensive exception, but this error is
304
+ # extremely rare (it was discovered on 2014-12-30, only happens on
305
+ # RUBY_VERSION < 2.0 and our spec suite only hits this condition once),
306
+ # so we'd rather avoid the cost of that check for every method double,
307
+ # and risk the rare situation where this exception will get raised.
308
+ RSpec.warn_with(
309
+ "WARNING: RSpec could not fully restore #{@object.inspect}." \
310
+ "#{@method_name}, possibly because the method has been redefined " \
311
+ "by something outside of RSpec."
312
+ )
313
+ end
255
314
  end
256
315
  end
257
316
  end
@@ -1,3 +1,5 @@
1
+ RSpec::Support.require_rspec_support 'comparable_version'
2
+
1
3
  module RSpec
2
4
  module Mocks
3
5
  # Represents a method on an object that may or may not be defined.
@@ -6,6 +8,10 @@ module RSpec
6
8
  #
7
9
  # @private
8
10
  class MethodReference
11
+ def self.for(object_reference, method_name)
12
+ new(object_reference, method_name)
13
+ end
14
+
9
15
  def initialize(object_reference, method_name)
10
16
  @object_reference = object_reference
11
17
  @method_name = method_name
@@ -27,7 +33,7 @@ module RSpec
27
33
  # cases when we don't know if a method is implemented and
28
34
  # both `implemented?` and `unimplemented?` will return false.
29
35
  def unimplemented?
30
- @object_reference.when_loaded do |m|
36
+ @object_reference.when_loaded do |_m|
31
37
  return !implemented?
32
38
  end
33
39
 
@@ -44,9 +50,8 @@ module RSpec
44
50
  end
45
51
 
46
52
  def with_signature
47
- if original = original_method
48
- yield Support::MethodSignature.new(original)
49
- end
53
+ return unless (original = original_method)
54
+ yield Support::MethodSignature.new(original)
50
55
  end
51
56
 
52
57
  def visibility
@@ -59,14 +64,6 @@ module RSpec
59
64
  :public
60
65
  end
61
66
 
62
- private
63
-
64
- def original_method
65
- @object_reference.when_loaded do |m|
66
- self.defined? && find_method(m)
67
- end
68
- end
69
-
70
67
  def self.instance_method_visibility_for(klass, method_name)
71
68
  if klass.public_method_defined?(method_name)
72
69
  :public
@@ -82,23 +79,40 @@ module RSpec
82
79
  end
83
80
 
84
81
  def self.method_visibility_for(object, method_name)
85
- instance_method_visibility_for(class << object; self; end, method_name).tap do |vis|
86
- # If the method is not defined on the class, `instance_method_visibility_for`
87
- # returns `nil`. However, it may be handled dynamically by `method_missing`,
88
- # so here we check `respond_to` (passing false to not check private methods).
89
- #
90
- # This only considers the public case, but I don't think it's possible to
91
- # write `method_missing` in such a way that it handles a dynamic message
92
- # with private or protected visibility. Ruby doesn't provide you with
93
- # the caller info.
94
- return :public if vis.nil? && object.respond_to?(method_name, false)
82
+ vis = instance_method_visibility_for(class << object; self; end, method_name)
83
+
84
+ # If the method is not defined on the class, `instance_method_visibility_for`
85
+ # returns `nil`. However, it may be handled dynamically by `method_missing`,
86
+ # so here we check `respond_to` (passing false to not check private methods).
87
+ #
88
+ # This only considers the public case, but I don't think it's possible to
89
+ # write `method_missing` in such a way that it handles a dynamic message
90
+ # with private or protected visibility. Ruby doesn't provide you with
91
+ # the caller info.
92
+ return vis unless vis.nil?
93
+
94
+ proxy = RSpec::Mocks.space.proxy_for(object)
95
+ respond_to = proxy.method_double_if_exists_for_message(:respond_to?)
96
+
97
+ visible = respond_to && respond_to.original_method.call(method_name) ||
98
+ object.respond_to?(method_name)
99
+
100
+ return :public if visible
101
+ end
102
+
103
+ private
104
+
105
+ def original_method
106
+ @object_reference.when_loaded do |m|
107
+ self.defined? && find_method(m)
95
108
  end
96
109
  end
97
110
  end
98
111
 
99
112
  # @private
100
113
  class InstanceMethodReference < MethodReference
101
- private
114
+ private
115
+
102
116
  def method_implemented?(mod)
103
117
  MethodReference.method_defined_at_any_visibility?(mod, @method_name)
104
118
  end
@@ -116,7 +130,7 @@ module RSpec
116
130
  #
117
131
  # This is necessary due to a bug in JRuby prior to 1.7.5 fixed in:
118
132
  # https://github.com/jruby/jruby/commit/99a0613fe29935150d76a9a1ee4cf2b4f63f4a27
119
- if RUBY_PLATFORM == 'java' && JRUBY_VERSION.split('.')[-1].to_i < 5
133
+ if RUBY_PLATFORM == 'java' && RSpec::Support::ComparableVersion.new(JRUBY_VERSION) < '1.7.5'
120
134
  def find_method(mod)
121
135
  mod.dup.instance_method(@method_name)
122
136
  end
@@ -133,7 +147,16 @@ module RSpec
133
147
 
134
148
  # @private
135
149
  class ObjectMethodReference < MethodReference
136
- private
150
+ def self.for(object_reference, method_name)
151
+ if ClassNewMethodReference.applies_to?(method_name) { object_reference.when_loaded { |o| o } }
152
+ ClassNewMethodReference.new(object_reference, method_name)
153
+ else
154
+ super
155
+ end
156
+ end
157
+
158
+ private
159
+
137
160
  def method_implemented?(object)
138
161
  object.respond_to?(@method_name, true)
139
162
  end
@@ -150,5 +173,42 @@ module RSpec
150
173
  MethodReference.method_visibility_for(object, @method_name)
151
174
  end
152
175
  end
176
+
177
+ # When a class's `.new` method is stubbed, we want to use the method
178
+ # signature from `#initialize` because `.new`'s signature is a generic
179
+ # `def new(*args)` and it simply delegates to `#initialize` and forwards
180
+ # all args...so the method with the actually used signature is `#initialize`.
181
+ #
182
+ # This method reference implementation handles that specific case.
183
+ # @private
184
+ class ClassNewMethodReference < ObjectMethodReference
185
+ def self.applies_to?(method_name)
186
+ return false unless method_name == :new
187
+ klass = yield
188
+ return false unless ::Class === klass && klass.respond_to?(:new, true)
189
+
190
+ # We only want to apply our special logic to normal `new` methods.
191
+ # Methods that the user has monkeyed with should be left as-is.
192
+ uses_class_new?(klass)
193
+ end
194
+
195
+ if RUBY_VERSION.to_i >= 3
196
+ CLASS_NEW = ::Class.instance_method(:new)
197
+
198
+ def self.uses_class_new?(klass)
199
+ ::RSpec::Support.method_handle_for(klass, :new) == CLASS_NEW.bind(klass)
200
+ end
201
+ else # Ruby 2's Method#== is too strict
202
+ def self.uses_class_new?(klass)
203
+ ::RSpec::Support.method_handle_for(klass, :new).owner == ::Class
204
+ end
205
+ end
206
+
207
+ def with_signature
208
+ @object_reference.when_loaded do |klass|
209
+ yield Support::MethodSignature.new(klass.instance_method(:initialize))
210
+ end
211
+ end
212
+ end
153
213
  end
154
214
  end
@@ -0,0 +1,68 @@
1
+ require 'rspec/mocks'
2
+
3
+ module RSpec
4
+ module Mocks
5
+ # @private
6
+ module MinitestIntegration
7
+ include ::RSpec::Mocks::ExampleMethods
8
+
9
+ def before_setup
10
+ ::RSpec::Mocks.setup
11
+ super
12
+ end
13
+
14
+ def after_teardown
15
+ super
16
+
17
+ # Only verify if there's not already an error. Otherwise
18
+ # we risk getting the same failure twice, since negative
19
+ # expectation violations raise both when the message is
20
+ # unexpectedly received, and also during `verify` (in case
21
+ # the first failure was caught by user code via a
22
+ # `rescue Exception`).
23
+ ::RSpec::Mocks.verify unless failures.any?
24
+ ensure
25
+ ::RSpec::Mocks.teardown
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ Minitest::Test.send(:include, RSpec::Mocks::MinitestIntegration)
32
+
33
+ if defined?(::Minitest::Expectation)
34
+ if defined?(::RSpec::Expectations) && ::Minitest::Expectation.method_defined?(:to)
35
+ # rspec/expectations/minitest_integration has already been loaded and
36
+ # has defined `to`/`not_to`/`to_not` on `Minitest::Expectation` so we do
37
+ # not want to here (or else we would interfere with rspec-expectations' definition).
38
+ else
39
+ # ...otherwise, define those methods now. If `rspec/expectations/minitest_integration`
40
+ # is loaded after this file, it'll override the definition here.
41
+ Minitest::Expectation.class_eval do
42
+ include RSpec::Mocks::ExpectationTargetMethods
43
+
44
+ def to(*args)
45
+ ctx.assertions += 1
46
+ super
47
+ end
48
+
49
+ def not_to(*args)
50
+ ctx.assertions += 1
51
+ super
52
+ end
53
+
54
+ def to_not(*args)
55
+ ctx.assertions += 1
56
+ super
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ module RSpec
63
+ module Mocks
64
+ remove_const :MockExpectationError
65
+ # Raised when a message expectation is not satisfied.
66
+ MockExpectationError = ::Minitest::Assertion
67
+ end
68
+ end