-
-
Notifications
You must be signed in to change notification settings - Fork 359
/
method_double.rb
258 lines (215 loc) · 8.37 KB
/
method_double.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
module RSpec
module Mocks
# @private
class MethodDouble
# @private
attr_reader :method_name, :object, :expectations, :stubs
# @private
def initialize(object, method_name, proxy)
@method_name = method_name
@object = object
@proxy = proxy
@original_visibility = nil
@method_stasher = InstanceMethodStasher.new(object, method_name)
@method_is_proxied = false
@expectations = []
@stubs = []
end
def original_method
# If original method is not present, uses the `method_missing`
# handler of the object. This accounts for cases where the user has not
# correctly defined `respond_to?`, and also 1.8 which does not provide
# method handles for missing methods even if `respond_to?` is correct.
@original_method ||=
@method_stasher.original_method ||
@proxy.method_handle_for(method_name) ||
Proc.new do |*args, &block|
@object.__send__(:method_missing, @method_name, *args, &block)
end
end
alias_method :save_original_method!, :original_method
# @private
def visibility
@proxy.visibility_for(@method_name)
end
# @private
def object_singleton_class
class << @object; self; end
end
# @private
def configure_method
@original_visibility = [visibility, method_name]
@method_stasher.stash unless @method_is_proxied
define_proxy_method
end
# @private
def define_proxy_method
return if @method_is_proxied
save_original_method!
definition_target.class_exec(self, method_name, visibility) do |method_double, method_name, visibility|
define_method(method_name) do |*args, &block|
method_double.proxy_method_invoked(self, *args, &block)
end
self.__send__ visibility, method_name
end
@method_is_proxied = true
end
# The implementation of the proxied method. Subclasses may override this
# method to perform additional operations.
#
# @private
def proxy_method_invoked(obj, *args, &block)
@proxy.message_received method_name, *args, &block
end
# @private
def restore_original_method
return show_frozen_warning if object_singleton_class.frozen?
return unless @method_is_proxied
definition_target.__send__(:remove_method, @method_name)
if @method_stasher.method_is_stashed?
@method_stasher.restore
end
restore_original_visibility
@method_is_proxied = false
end
# @private
def show_frozen_warning
RSpec.warn_with(
"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.",
:call_site => nil,
:use_spec_location_as_call_site => true
)
end
# @private
def restore_original_visibility
return unless @original_visibility &&
MethodReference.method_defined_at_any_visibility?(object_singleton_class, @method_name)
object_singleton_class.__send__(*@original_visibility)
end
# @private
def verify
expectations.each {|e| e.verify_messages_received}
end
# @private
def reset
restore_original_method
clear
end
# @private
def clear
expectations.clear
stubs.clear
end
# The type of message expectation to create has been extracted to its own
# method so that subclasses can override it.
#
# @private
def message_expectation_class
MessageExpectation
end
# @private
def add_expectation(error_generator, expectation_ordering, expected_from, opts, &implementation)
configure_method
expectation = message_expectation_class.new(error_generator, expectation_ordering,
expected_from, self, 1, opts, &implementation)
expectations << expectation
expectation
end
# @private
def build_expectation(error_generator, expectation_ordering)
expected_from = IGNORED_BACKTRACE_LINE
message_expectation_class.new(error_generator, expectation_ordering, expected_from, self)
end
# @private
def add_stub(error_generator, expectation_ordering, expected_from, opts={}, &implementation)
configure_method
stub = message_expectation_class.new(error_generator, expectation_ordering, expected_from,
self, :any, opts, &implementation)
stubs.unshift stub
stub
end
# 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` / `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
me = SimpleMessageExpectation.new(method_name, response, error_generator, backtrace_line)
collection.unshift me
me
end
# @private
def add_default_stub(*args, &implementation)
return if stubs.any?
add_stub(*args, &implementation)
end
# @private
def remove_stub
raise_method_not_stubbed_error if stubs.empty?
expectations.empty? ? reset : stubs.clear
end
# @private
def remove_single_stub(stub)
stubs.delete(stub)
restore_original_method if stubs.empty? && expectations.empty?
end
# @private
def raise_method_not_stubbed_error
raise MockExpectationError, "The method `#{method_name}` was not stubbed or was already unstubbed"
end
private
# In Ruby 2.0.0 and above prepend will alter the method lookup chain.
# We use an object's singleton class to define method doubles upon,
# however if the object has had it's singleton class (as opposed to
# it's actual class) prepended too then the the method lookup chain
# will look in the prepended module first, **before** the singleton
# class.
#
# This code works around that by providing a mock definition target
# that is either the singleton class, or if necessary, a prepended module
# of our own.
#
if Support::RubyFeatures.module_prepends_supported?
# We subclass `Module` in order to be able to easily detect our prepended module.
RSpecPrependedModule = Class.new(Module)
def definition_target
@definition_target ||= usable_rspec_prepended_module || object_singleton_class
end
def usable_rspec_prepended_module
@proxy.prepended_modules_of_singleton_class.each do |mod|
# If we have one of our modules prepended before one of the user's
# modules that defines the method, use that, since our module's
# definition will take precedence.
return mod if RSpecPrependedModule === mod
# If we hit a user module with the method defined first,
# we must create a new prepend module, even if one exists later,
# because ours will only take precedence if it comes first.
return new_rspec_prepended_module if mod.method_defined?(method_name)
end
nil
end
def new_rspec_prepended_module
RSpecPrependedModule.new.tap do |mod|
object_singleton_class.__send__ :prepend, mod
end
end
else
def definition_target
object_singleton_class
end
end
end
end
end