spy 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ require 'singleton'
2
+
3
+ module Spy
4
+ class Agency
5
+ include Singleton
6
+
7
+ attr_reader :subroutines, :constants, :doubles
8
+
9
+ def initialize
10
+ clear!
11
+ end
12
+
13
+ def recruit(spy)
14
+ case spy
15
+ when Subroutine
16
+ subroutines << spy
17
+ when Constant
18
+ constants << spy
19
+ when Double
20
+ doubles << spy
21
+ else
22
+ raise "Not a spy"
23
+ end
24
+ spy
25
+ end
26
+
27
+ def retire(spy)
28
+ case spy
29
+ when Subroutine
30
+ subroutines.delete(spy)
31
+ when Constant
32
+ constants.delete(spy)
33
+ when Double
34
+ doubles.delete(spy)
35
+ else
36
+ raise "Not a spy"
37
+ end
38
+ spy
39
+ end
40
+
41
+ def active?(spy)
42
+ case spy
43
+ when Subroutine
44
+ subroutines.include?(spy)
45
+ when Constant
46
+ constants.include?(spy)
47
+ when Double
48
+ doubles.include?(spy)
49
+ end
50
+ end
51
+
52
+ def dissolve!
53
+ subroutines.each(&:unhook)
54
+ constants.each(&:unhook)
55
+ clear!
56
+ end
57
+
58
+ def clear!
59
+ @subroutines = []
60
+ @constants = []
61
+ @doubles = []
62
+ self
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,67 @@
1
+ module Spy
2
+ class Constant
3
+ attr_reader :base_module, :constant_name, :original_value, :new_value
4
+
5
+ def initialize(base_module, constant_name)
6
+ raise "#{base_module.inspect} is not a kind of Module" unless base_module.is_a? Module
7
+ raise "#{constant_name.inspect} is not a kind of Symbol" unless constant_name.is_a? Symbol
8
+ @base_module, @constant_name = base_module, constant_name.to_sym
9
+ @original_value = nil
10
+ @new_value = nil
11
+ @was_defined = nil
12
+ end
13
+
14
+ def hook(opts = {})
15
+ opts[:force] ||= false
16
+ @was_defined = base_module.const_defined?(constant_name, false)
17
+ if @was_defined || !opts[:force]
18
+ @original_value = base_module.const_get(constant_name, false)
19
+ end
20
+ and_return(@new_value)
21
+ Nest.fetch(base_module).add(self)
22
+ Agency.instance.recruit(self)
23
+ self
24
+ end
25
+
26
+ def unhook
27
+ if @was_defined
28
+ and_return(@original_value)
29
+ end
30
+ @original_value = nil
31
+
32
+ Agency.instance.retire(self)
33
+ Nest.fetch(base_module).remove(self)
34
+ self
35
+ end
36
+
37
+ def and_hide
38
+ base_module.send(:remove_const, constant_name)
39
+ self
40
+ end
41
+
42
+ def and_return(value)
43
+ @new_value = value
44
+ base_module.send(:remove_const, constant_name) if base_module.const_defined?(constant_name, false)
45
+ base_module.const_set(constant_name, @new_value)
46
+ self
47
+ end
48
+
49
+ def hooked?
50
+ Nest.get(base_module).hooked?(constant_name)
51
+ end
52
+
53
+ class << self
54
+ def on(base_module, constant_name)
55
+ new(base_module, constant_name).hook
56
+ end
57
+
58
+ def off(base_module, constant_name)
59
+ get(base_module, constant_name).unhook
60
+ end
61
+
62
+ def get(base_module, constant_name)
63
+ Nest.get(base_module).hooked_constants[constant_name]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,28 @@
1
+ module Marshal
2
+ class << self
3
+ def dump_with_mocks(*args)
4
+ object = args.shift
5
+ spies = Spy::Subroutine.get_spies(object)
6
+ if spies.empty?
7
+ return dump_without_mocks(*args.unshift(object))
8
+ end
9
+
10
+ spy_hook_options = spies.map do |spy|
11
+ opts = spy.opts
12
+ [spy.unhook, opts]
13
+ end
14
+
15
+ begin
16
+ dump_without_mocks(*args.unshift(object.dup))
17
+ ensure
18
+ spy_hook_options.each do |spy, opts|
19
+ spy.hook(opts)
20
+ end
21
+ end
22
+ end
23
+
24
+ alias_method :dump_without_mocks, :dump
25
+ undef_method :dump
26
+ alias_method :dump, :dump_with_mocks
27
+ end
28
+ end
@@ -1,4 +1,4 @@
1
- class Spy
1
+ module Spy
2
2
  class Double
3
3
  def initialize(name, *args)
4
4
  @name = name
@@ -0,0 +1,45 @@
1
+ module Spy
2
+ class Nest
3
+ attr_reader :base_module, :hooked_constants
4
+
5
+ def initialize(base_module)
6
+ raise "#{base_module} is not a kind of Module" unless base_module.is_a?(Module)
7
+ @base_module = base_module
8
+ @hooked_constants = {}
9
+ end
10
+
11
+ def add(spy)
12
+ if @hooked_constants[spy.constant_name]
13
+ raise "#{spy.constant_name} has already been stubbed"
14
+ else
15
+ @hooked_constants[spy.constant_name] = spy
16
+ end
17
+ self
18
+ end
19
+
20
+ def remove(spy)
21
+ if @hooked_constants[spy.constant_name] == spy
22
+ @hooked_constants.delete(spy.constant_name)
23
+ end
24
+ self
25
+ end
26
+
27
+ def hooked?(constant_name)
28
+ !!@hooked_constants[constant_name]
29
+ end
30
+
31
+ class << self
32
+ def get(base_module)
33
+ all[base_module.name]
34
+ end
35
+
36
+ def fetch(base_module)
37
+ all[base_module.name] ||= self.new(base_module)
38
+ end
39
+
40
+ def all
41
+ @all ||= {}
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,239 @@
1
+ module Spy
2
+ class Subroutine
3
+ CallLog = Struct.new(:object, :args, :block, :result)
4
+ attr_reader :base_object, :method_name, :calls, :original_method, :opts
5
+
6
+ # set what object and method the spy should watch
7
+ # @param object
8
+ # @param method_name <Symbol>
9
+ def initialize(object, method_name)
10
+ @was_hooked = false
11
+ @base_object, @method_name = object, method_name
12
+ reset!
13
+ end
14
+
15
+ # hooks the method into the object and stashes original method if it exists
16
+ # @param opts [Hash{force => false, visibility => nil}] set :force => true if you want it to ignore if the method exists, or visibility to [:public, :protected, :private] to overwride current visibility
17
+ # @return self
18
+ def hook(opts = {})
19
+ @opts = opts
20
+ raise "#{base_object} method '#{method_name}' has already been hooked" if hooked?
21
+ opts[:force] ||= base_object.is_a?(Double)
22
+ if base_object.respond_to?(method_name, true) || !opts[:force]
23
+ @original_method = base_object.method(method_name)
24
+ end
25
+
26
+ opts[:visibility] ||= method_visibility
27
+
28
+ __method_spy__ = self
29
+ base_object.define_singleton_method(method_name) do |*__spy_args, &block|
30
+ if __spy_args.first === SECRET_SPY_KEY
31
+ __method_spy__
32
+ else
33
+ __method_spy__.invoke(self,__spy_args,block)
34
+ end
35
+ end
36
+
37
+ base_object.singleton_class.send(opts[:visibility], method_name) if opts[:visibility]
38
+
39
+ Agency.instance.recruit(self)
40
+ @was_hooked = true
41
+ self
42
+ end
43
+
44
+ # unhooks method from object
45
+ # @return self
46
+ def unhook
47
+ raise "#{method_name} method has not been hooked" unless hooked?
48
+ if original_method && original_method.owner == base_object.singleton_class
49
+ base_object.define_singleton_method(method_name, original_method)
50
+ base_object.singleton_class.send(method_visibility, method_name) if method_visibility
51
+ else
52
+ base_object.singleton_class.send(:remove_method, method_name)
53
+ end
54
+ clear_method!
55
+ Agency.instance.retire(self)
56
+ self
57
+ end
58
+
59
+ # is the spy hooked?
60
+ # @return Boolean
61
+ def hooked?
62
+ self == self.class.get(base_object, method_name)
63
+ end
64
+
65
+ # @overload and_return(value)
66
+ # @overload and_return(&block)
67
+ #
68
+ # Tells the spy to return a value when the method is called.
69
+ #
70
+ # @return self
71
+ def and_return(value = nil)
72
+ if block_given?
73
+ @plan = Proc.new
74
+ if value.nil? || value.is_a?(Hash) && value.has_key?(:force)
75
+ if !(value.is_a?(Hash) && value[:force]) &&
76
+ original_method &&
77
+ original_method.arity >=0 &&
78
+ @plan.arity > original_method.arity
79
+ raise ArgumentError.new "The original method only has an arity of #{original_method.arity} you have an arity of #{@plan.arity}"
80
+ end
81
+ else
82
+ raise ArgumentError.new("value and block conflict. Choose one") if !value.nil?
83
+ end
84
+ else
85
+ @plan = Proc.new { value }
86
+ end
87
+ self
88
+ end
89
+
90
+ # Tells the object to yield one or more args to a block when the message is received.
91
+ def and_yield(*args)
92
+ yield eval_context = Object.new if block_given?
93
+ @plan = Proc.new do |&block|
94
+ eval_context.instance_exec(*args, &block)
95
+ end
96
+ self
97
+ end
98
+
99
+ # tells the spy to call the original method
100
+ # @return self
101
+ def and_call_through
102
+ raise "can only call through if original method is set" unless method_visibility
103
+ if original_method
104
+ @plan = original_method
105
+ else
106
+ @plan = Proc.new do |*args, &block|
107
+ base_object.send(:method_missing, method_name, *args, &block)
108
+ end
109
+ end
110
+ self
111
+ end
112
+
113
+ def and_raise(exception = RuntimeError, message = nil)
114
+ if exception.respond_to?(:exception)
115
+ exception = message ? exception.exception(message) : exception.exception
116
+ end
117
+
118
+ @plan = Proc.new { raise exception }
119
+ end
120
+
121
+ def and_throw(*args)
122
+ @plan = Proc.new { throw(*args) }
123
+ self
124
+ end
125
+
126
+ def has_been_called?
127
+ raise "was never hooked" unless @was_hooked
128
+ calls.size > 0
129
+ end
130
+
131
+ # check if the method was called with the exact arguments
132
+ def has_been_called_with?(*args)
133
+ raise "was never hooked" unless @was_hooked
134
+ calls.any? do |call_log|
135
+ call_log.args == args
136
+ end
137
+ end
138
+
139
+ # invoke that the method has been called. You really shouldn't use this
140
+ # method.
141
+ def invoke(object, args, block)
142
+ check_arity!(args.size)
143
+ result = @plan ? @plan.call(*args, &block) : nil
144
+ calls << CallLog.new(object, args, block, result)
145
+ result
146
+ end
147
+
148
+ # reset the call log
149
+ def reset!
150
+ @calls = []
151
+ clear_method!
152
+ true
153
+ end
154
+
155
+ private
156
+
157
+ def call_with_yield(&block)
158
+ raise "no block sent" unless block
159
+ value = nil
160
+ @args_to_yield.each do |args|
161
+ if block.arity > -1 && args.length != block.arity
162
+ @error_generator.raise_wrong_arity_error args, block.arity
163
+ end
164
+ value = @eval_context ? @eval_context.instance_exec(*args, &block) : block.call(*args)
165
+ end
166
+ value
167
+ end
168
+
169
+ def clear_method!
170
+ @hooked = false
171
+ @opts = @original_method = @arity_range = @method_visibility = nil
172
+ end
173
+
174
+ def method_visibility
175
+ @method_visibility ||=
176
+ if base_object.respond_to?(method_name)
177
+ if original_method && original_method.owner.protected_method_defined?(method_name)
178
+ :protected
179
+ else
180
+ :public
181
+ end
182
+ elsif base_object.respond_to?(method_name, true)
183
+ :private
184
+ end
185
+ end
186
+
187
+ def check_arity!(arity)
188
+ self.class.check_arity_against_range!(arity_range, arity)
189
+ end
190
+
191
+ def arity_range
192
+ @arity_range ||= self.class.arity_range_of(original_method) if original_method
193
+ end
194
+
195
+ class << self
196
+ def arity_range_of(block)
197
+ raise "#{block.inspect} does not respond to :parameters" unless block.respond_to?(:parameters)
198
+ min = max = 0
199
+ block.parameters.each do |type,_|
200
+ case type
201
+ when :req
202
+ min += 1
203
+ max += 1
204
+ when :opt
205
+ max += 1
206
+ when :rest
207
+ max = Float::INFINITY
208
+ end
209
+ end
210
+ (min..max)
211
+ end
212
+
213
+ def check_arity_against_range!(arity_range, arity)
214
+ return unless arity_range
215
+ if arity < arity_range.min
216
+ raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.min})")
217
+ elsif arity > arity_range.max
218
+ raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.max})")
219
+ end
220
+ end
221
+
222
+ SPY_METHOD_PARAMS = [[:rest, :__spy_args], [:block, :block]]
223
+
224
+ def get(base_object, method_name)
225
+ if (base_object.singleton_methods + base_object.singleton_class.private_instance_methods(false)).include?(method_name.to_sym) && base_object.method(method_name).parameters == SPY_METHOD_PARAMS
226
+ base_object.send(method_name, SECRET_SPY_KEY)
227
+ end
228
+ end
229
+
230
+ def get_spies(base_object)
231
+ base_object.singleton_methods.map do |method_name|
232
+ if base_object.method(method_name).parameters == SPY_METHOD_PARAMS
233
+ base_object.send(method_name, SECRET_SPY_KEY)
234
+ end
235
+ end.compact
236
+ end
237
+ end
238
+ end
239
+ end