-
-
Notifications
You must be signed in to change notification settings - Fork 356
/
recorder.rb
194 lines (171 loc) · 7.09 KB
/
recorder.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
module RSpec
module Mocks
module AnyInstance
# 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).
#
# @see AnyInstance
# @see Chain
class Recorder
# @private
attr_reader :message_chains
def initialize(klass)
@message_chains = MessageChains.new
@observed_methods = []
@played_methods = {}
@klass = klass
@expectation_set = false
end
# Initializes the recording a stub to be played back against any
# instance of this object that invokes the submitted method.
#
# @see Methods#stub
def stub(method_name_or_method_map, &block)
if method_name_or_method_map.is_a?(Hash)
method_name_or_method_map.each do |method_name, return_value|
stub(method_name).and_return(return_value)
end
else
observe!(method_name_or_method_map)
message_chains.add(method_name_or_method_map, StubChain.new(method_name_or_method_map, &block))
end
end
# 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 Methods#stub_chain
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(*args, &block))
end
end
# Initializes the recording a message expectation to be played back
# against any instance of this object that invokes the submitted
# method.
#
# @see Methods#should_receive
def should_receive(method_name, &block)
@expectation_set = true
observe!(method_name, true)
message_chains.add(method_name, PositiveExpectationChain.new(method_name, &block))
end
def should_not_receive(method_name, &block)
should_receive(method_name, &block).never
end
# Removes any previously recorded stubs, stub_chains or message
# expectations that use `method_name`.
#
# @see Methods#unstub
def unstub(method_name)
unless @observed_methods.include?(method_name.to_sym)
raise RSpec::Mocks::MockExpectationError, "The method `#{method_name}` was not stubbed or was already unstubbed"
end
message_chains.remove_stub_chains_for!(method_name)
stop_observing!(method_name) unless message_chains.has_expectation?(method_name)
end
# @api private
#
# Used internally to verify that message expectations have been
# fulfilled.
def verify
if @expectation_set && !message_chains.all_expectations_fulfilled?
raise RSpec::Mocks::MockExpectationError, "Exactly one instance should have received the following message(s) but didn't: #{message_chains.unfulfilled_expectations.sort.join(', ')}"
end
ensure
stop_all_observation!
::RSpec::Mocks.space.remove_any_instance_recorder_for(@klass)
end
# @private
def stop_all_observation!
@observed_methods.each {|method_name| restore_method!(method_name)}
end
# @private
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
# @private
def instance_that_received(method_name)
@played_methods[method_name]
end
def build_alias_method_name(method_name)
"__#{method_name}_without_any_instance__"
end
def already_observing?(method_name)
@observed_methods.include?(method_name)
end
private
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
def received_expected_message!(method_name)
message_chains.received_expected_message!(method_name)
restore_method!(method_name)
mark_invoked!(method_name)
end
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
def restore_original_method!(method_name)
alias_method_name = build_alias_method_name(method_name)
@klass.class_eval do
remove_method method_name
alias_method method_name, alias_method_name
remove_method alias_method_name
end
end
def remove_dummy_method!(method_name)
@klass.class_eval do
remove_method method_name
end
end
def backup_method!(method_name)
alias_method_name = build_alias_method_name(method_name)
@klass.class_eval do
alias_method alias_method_name, method_name
end if public_protected_or_private_method_defined?(method_name)
end
def public_protected_or_private_method_defined?(method_name)
@klass.method_defined?(method_name) || @klass.private_method_defined?(method_name)
end
def stop_observing!(method_name)
restore_method!(method_name)
@observed_methods.delete(method_name)
end
def observe!(method_name, ignore_instance=false)
stop_observing!(method_name) if already_observing?(method_name)
@observed_methods << method_name
backup_method!(method_name)
@klass.__send__(:define_method, method_name) do |*args, &blk|
klass = ::RSpec::Mocks.method_handle_for(self, method_name).owner
::RSpec::Mocks.any_instance_recorder_for(klass).playback!(self, method_name)
if ::RSpec::Mocks.configuration.pass_instance_to_any_instance_stubs
args.unshift(self)
end
self.__send__(method_name, *args, &blk)
end
end
def mark_invoked!(method_name)
backup_method!(method_name)
@klass.__send__(:define_method, method_name) do |*args, &blk|
klass = ::RSpec::Mocks.method_handle_for(self, method_name).owner
invoked_instance = ::RSpec::Mocks.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
end
end
end
end