spy 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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