trace_spy 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a36db56361a6223b879ca27adfedfbb65660a7617bd5e24fca290a876be6df92
4
- data.tar.gz: 68506cf1d4bb171fc998761190a680547d238418e43e04f51a432af10be5f9ff
3
+ metadata.gz: 3831d476b8d255dbec62c68b430fbd5733a8ff9a517c16d61a5be2ccac842a59
4
+ data.tar.gz: 4247e3304fd6cb2ef1e81187e1a29e8457ca4225c6414d5a171aed34775739ca
5
5
  SHA512:
6
- metadata.gz: 6155fea20309cba45610af5d966eab12e348769dfb1d376894e97eda9b7bd7fda593fa0f3fdd4947a2637016bb6a3a1b3f3b454318a28bd2c85465e7cd0ada7c
7
- data.tar.gz: 543c16db8f7d8055137d22b1959f97f216b8f117136d7e1eb5453a33263ce13aa4de108c38cca56caa0cedd0ea93efaa1796b43ed2178554cdaa4d4f2daea6bf
6
+ metadata.gz: 69c58ab72a6908eee036ecebbae4ee3b1337ef6d8baa921d2d885bc017781a86fa5ea09a4892b6a575b9d82e4190c8785c43b8af9825543e71c2c5c0208b4b9a
7
+ data.tar.gz: 3226f8d53fd8cceb8403bfda5f90a5cfa3938d77c71bfbdcb5f131098e55fee9642492bd60028fb78d42cceb78423abbc5aedfa368328ea36957b020c953764c
data/README.md CHANGED
@@ -13,10 +13,15 @@ I can create a nice API, and we'll work from there.
13
13
 
14
14
  ## Usage
15
15
 
16
+ The methods themselves are documented, and I'll work on expanding this section later with more examples and ideas
17
+ as I can.
18
+
16
19
  ```ruby
17
20
  def testing(a, b, c)
18
21
  raise 'heck' if a.is_a?(Numeric) && a > 20
19
22
 
23
+ d = 5 if c.is_a?(Numeric) && c > 3
24
+
20
25
  a + b + c
21
26
  end
22
27
 
@@ -42,31 +47,42 @@ testing_spy = TraceSpy::Method.new(:testing) do |spy|
42
47
  # On a return value, will yield the return to the block
43
48
  spy.on_return do |m|
44
49
  m.when(String) do |v|
45
- puts "Strings in, Strings out no?: #{v}"
50
+ puts "Strings in, Strings out no?: #{v}. I got this in though: #{spy.current_arguments}"
46
51
  end
47
52
 
48
53
  m.when(:even?) do |v|
49
54
  puts "I got an even return: #{v}"
50
55
  end
51
56
  end
57
+
58
+ # On a local variable being present:
59
+ spy.on_locals do |m|
60
+ m.when(d: 5) do |v|
61
+ puts "I saw d was a local in here!: #{v}. I could also ask this: #{spy.current_local_variables}"
62
+ end
63
+ end
52
64
  end
53
65
 
54
66
  testing_spy.enable
55
67
  # => false
56
68
 
57
- testing(1, 2, 3)
69
+ p testing(1, 2, 3)
58
70
  # My args were 1, 2, 3: {:a=>1, :b=>2, :c=>3}
59
71
  # I got an even return: 6
60
72
  # => 6
61
73
 
62
- testing(21, 2, 3)
74
+ p testing(21, 2, 3) rescue 'nope'
63
75
  # I encountered an error: heck
64
- # RuntimeError: heck
65
- # from (pry):2:in `testing'
76
+ # => 'nope'
66
77
 
67
- testing(*%w(foo bar baz))
78
+ p testing(*%w(foo bar baz))
68
79
  # Oh hey! You called me with strings: {:a=>"foo", :b=>"bar", :c=>"baz"}
69
80
  # Strings in, Strings out no?: foobarbaz
81
+ # => 'foobarbaz'
82
+
83
+ p testing(1, 2, 4)
84
+ # I saw d was a local in here!: {:a=>1, :b=>2, :c=>4, :d=>5}
85
+ # => 7
70
86
  ```
71
87
 
72
88
  ## Installation
data/lib/trace_spy.rb CHANGED
@@ -2,12 +2,25 @@ require "trace_spy/version"
2
2
 
3
3
  require 'qo'
4
4
 
5
+ # A Wrapper around TracePoint to provide a more flexible API
6
+ #
7
+ # @author baweaver
8
+ # @since 0.0.1
9
+ #
5
10
  module TraceSpy
6
- class Error < StandardError; end
7
-
11
+ # Method call events
8
12
  CALL_EVENT = Set.new([:call, :c_call])
13
+
14
+ # Method return events
9
15
  RETURN_EVENT = Set.new([:return, :c_return])
16
+
17
+ # Exception events
10
18
  RAISE_EVENT = Set.new([:raise])
19
+
20
+ # Line execution events
21
+ LINE_EVENT = Set.new([:line])
22
+
23
+ # TODO: Implement other event types
11
24
  end
12
25
 
13
26
  require 'trace_spy/method'
@@ -1,35 +1,220 @@
1
1
  module TraceSpy
2
+ # Implements a TraceSpy on a Method
3
+ #
4
+ # @author baweaver
5
+ # @since 0.0.1
6
+ #
7
+ # @note
8
+ # Tracer spies all rely on Qo for pattern-matching syntax. In order to more
9
+ # effectively leverage this gem it would be a good idea to look through
10
+ # the Qo documentation present here: https://github.com/baweaver/qo
11
+ #
12
+ # @example
13
+ # A simple use-case would be monitoring for a line in which c happens to be
14
+ # equal to 5. Now this value could be a range or other `===` respondant type
15
+ # if desired, which gives quite a bit of flexibility in querying.
16
+ #
17
+ # ```ruby
18
+ # def testing(a, b)
19
+ # c = 5
20
+ #
21
+ # a + b + c
22
+ # end
23
+ #
24
+ # trace_spy = TraceSpy::Method.new(:testing) do |spy|
25
+ # spy.on_locals do |m|
26
+ # m.when(c: 5) { |locals| p locals }
27
+ # end
28
+ # end
29
+ #
30
+ # trace_spy.enable
31
+ # # => false
32
+ #
33
+ # testing(1, 2)
34
+ # # {:a=>1, :b=>2, :c=>5}
35
+ # # => 8
36
+ # ```
2
37
  class Method
3
- def initialize(method_name, &fn)
4
- @method_name = method_name
5
- @spies = Hash.new { |h,k| h[k] = [] }
6
- @tracepoint = nil
38
+ # The current trace being executed upon, can be used in matcher
39
+ # blocks to get the entire trace context instead of just a part.
40
+ attr_reader :current_trace
41
+
42
+ # Creates a new method trace
43
+ #
44
+ # @param method_name [Symbol, String]
45
+ # Name of the method to watch, will be compared with `===` for flexibility
46
+ # which enables the use of regex and other more powerful matching
47
+ # techniques.
48
+ #
49
+ # @param from_class: Any [Any]
50
+ # Either a Class for type-matching, or other `===` respondant type for flexibility
51
+ #
52
+ # @param &fn [Proc]
53
+ # Self-yielding proc used to initialize a spy in one block function
54
+ #
55
+ # @yields self
56
+ #
57
+ # @return [TraceSpy::Method]
58
+ def initialize(method_name, from_class: Any, &fn)
59
+ @method_name = method_name
60
+ @from_class = from_class
61
+ @spies = Hash.new { |h,k| h[k] = [] }
62
+ @tracepoint = nil
63
+ @current_trace = nil
7
64
 
8
65
  yield(self) if block_given?
9
66
  end
10
67
 
68
+ # Creates a Spy on function arguments
69
+ #
70
+ # @since 0.0.1
71
+ #
72
+ # @example
73
+ # Consider, you'd like to monitor if a particular argument is nil:
74
+ #
75
+ # ```ruby
76
+ # def testing(a) a + 2 end
77
+ #
78
+ # trace_spy = TraceSpy::Method.new(:testing) do |spy|
79
+ # spy.on_arguments do |m|
80
+ # m.when(a: nil) { |args| binding.pry }
81
+ # end
82
+ # end
83
+ # ```
84
+ #
85
+ # You could use this to find out if there's a type-mismatch, or what
86
+ # the context is around a particular error due to an argument being
87
+ # a particular value.
88
+ #
89
+ # @param &matcher_fn [Proc]
90
+ # Qo Matcher
91
+ #
92
+ # @return [Array[Qo::Matcher]]
93
+ # Currently added Qo matchers
11
94
  def on_arguments(&matcher_fn)
12
95
  @spies[:arguments] << Qo.match(&matcher_fn)
13
96
  end
14
97
 
98
+ # Creates a Spy on local method variables
99
+ #
100
+ # @since 0.0.2
101
+ #
102
+ # @example
103
+ # Consider, a local variable is inexplicably getting set equal to nil,
104
+ # and you don't know where it's happening:
105
+ #
106
+ # ```ruby
107
+ # def testing(a)
108
+ # b = nil
109
+ # a + 2
110
+ # end
111
+ #
112
+ # trace_spy = TraceSpy::Method.new(:testing) do |spy|
113
+ # spy.on_locals do |m|
114
+ # m.when(b: nil) { |args| binding.pry }
115
+ # end
116
+ # end
117
+ # ```
118
+ #
119
+ # You can use this to stop your program precisely where the offending code
120
+ # is located without needing to know where it is beforehand.
121
+ #
122
+ # @param &matcher_fn [Proc]
123
+ # Qo Matcher
124
+ #
125
+ # @return [Array[Qo::Matcher]]
126
+ # Currently added Qo matchers
127
+ def on_locals(&matcher_fn)
128
+ @spies[:locals] << Qo.match(&matcher_fn)
129
+ end
130
+
131
+ # Creates a Spy on function returns
132
+ #
133
+ # @since 0.0.1
134
+ #
135
+ # @example
136
+ # Consider, you'd like to know when your logging method is returning
137
+ # an empty string:
138
+ #
139
+ # ```ruby
140
+ # def logger(msg)
141
+ # rand(10) < 5 ? msg : ""
142
+ # end
143
+ #
144
+ # trace_spy = TraceSpy::Method.new(:logger) do |spy|
145
+ # spy.on_return do |m|
146
+ # m.when("") { |v| binding.pry }
147
+ # end
148
+ # end
149
+ # ```
150
+ #
151
+ # This could be used to find out the remaining context around what caused
152
+ # the blank message, like getting arguments from the `spy.current_trace`.
153
+ #
154
+ # @param &matcher_fn [Proc]
155
+ # Qo Matcher
156
+ #
157
+ # @return [Array[Qo::Matcher]]
158
+ # Currently added Qo matchers
15
159
  def on_return(&matcher_fn)
16
160
  @spies[:return] << Qo.match(&matcher_fn)
17
161
  end
18
162
 
163
+ # Creates a Spy on a certain type of exception
164
+ #
165
+ # @since 0.0.1
166
+ #
167
+ # @example
168
+ # Consider, you'd like to find out where that error is coming from in
169
+ # your function:
170
+ #
171
+ # ```ruby
172
+ # def testing(a)
173
+ # raise 'heck'
174
+ # a + 2
175
+ # end
176
+ #
177
+ # trace_spy = TraceSpy::Method.new(:testing) do |spy|
178
+ # spy.on_exception do |m|
179
+ # m.when(RuntimeError) { |args| binding.pry }
180
+ # end
181
+ # end
182
+ # ```
183
+ #
184
+ # Like return, you can use this to find out the context around why this
185
+ # particular error occurred.
186
+ #
187
+ # @param &matcher_fn [Proc]
188
+ # Qo Matcher
189
+ #
190
+ # @return [Array[Qo::Matcher]]
191
+ # Currently added Qo matchers
19
192
  def on_exception(&matcher_fn)
20
193
  @spies[:exception] << Qo.match(&matcher_fn)
21
194
  end
22
195
 
196
+ # "Enables" the current tracepoint by defining it, caching it, and enabling it
197
+ #
198
+ # @since 0.0.1
199
+ #
200
+ # @return [FalseClass]
201
+ # Still not sure why TracePoint#enable returns `false`, but here we are
23
202
  def enable
24
203
  @tracepoint = TracePoint.new do |trace|
25
204
  begin
26
- next unless trace.method_id == @method_name
205
+ next unless matches?(trace)
206
+
207
+ @current_trace = trace
27
208
 
28
209
  call_with = -> with { -> spy { spy.call(with) } }
29
210
 
211
+
30
212
  @spies[:arguments].each(&call_with[extract_args(trace)]) if CALL_EVENT.include?(trace.event)
213
+ @spies[:locals].each(&call_with[extract_locals(trace)]) if LINE_EVENT.include?(trace.event)
31
214
  @spies[:return].each(&call_with[trace.return_value]) if RETURN_EVENT.include?(trace.event)
32
215
  @spies[:exception].each(&call_with[trace.raised_exception]) if RAISE_EVENT.include?(trace.event)
216
+
217
+ @current_trace = nil
33
218
  rescue RuntimeError => e
34
219
  # Stupid hack for now
35
220
  p e
@@ -39,14 +224,142 @@ module TraceSpy
39
224
  @tracepoint.enable
40
225
  end
41
226
 
227
+ # Disables the TracePoint, or pretends it did if one isn't enabled yet
228
+ #
229
+ # @since 0.0.1
230
+ #
231
+ # @return [Boolean]
42
232
  def disable
43
- @tracepoint&.disable
233
+ !!@tracepoint&.disable
44
234
  end
45
235
 
46
- def extract_args(trace)
236
+ # Returns the local variables of the currently active trace
237
+ #
238
+ # @since 0.0.2
239
+ #
240
+ # @example
241
+ # This is a utility function for use with `spy` inside the matcher
242
+ # block.
243
+ #
244
+ # ```ruby
245
+ # trace_spy = TraceSpy::Method.new(:testing) do |spy|
246
+ # spy.on_exception do |m|
247
+ # m.when(RuntimeError) do |v|
248
+ # p spy.current_local_variables
249
+ # end
250
+ # end
251
+ # end
252
+ # ```
253
+ #
254
+ # It's meant to be used to expose the current local variables
255
+ # within a trace's scope in any type of matcher.
256
+ #
257
+ # @return [Hash[Symbol, Any]]
258
+ def current_local_variables
259
+ return {} unless @current_trace
260
+
261
+ extract_locals(@current_trace)
262
+ end
263
+
264
+ # Returns the arguments of the currently active trace
265
+ #
266
+ # @since 0.0.2
267
+ #
268
+ # @note
269
+ # This method will attempt to avoid running in contexts where
270
+ # argument retrieval will give a runtime error.
271
+ #
272
+ # @example
273
+ # This is a utility function for use with `spy` inside the matcher
274
+ # block.
275
+ #
276
+ # ```ruby
277
+ # trace_spy = TraceSpy::Method.new(:testing) do |spy|
278
+ # spy.on_return do |m|
279
+ # m.when(String) do |v|
280
+ # binding.pry if spy.current_arguments[:a] == 'foo'
281
+ # end
282
+ # end
283
+ # end
284
+ # ```
285
+ #
286
+ # It's meant to expose the current arguments present in a trace's
287
+ # scope.
288
+ #
289
+ # @return [Hash[Symbol, Any]]
290
+ def current_arguments
291
+ return {} unless @current_trace
292
+ return {} if RAISE_EVENT.include?(@current_trace.event)
293
+
294
+ extract_args(@current_trace)
295
+ end
296
+
297
+ # Whether the current trace matches our current preconditions
298
+ #
299
+ # @since 0.0.1
300
+ #
301
+ # @param trace [Trace]
302
+ # Currently active Trace
303
+ #
304
+ # @return [Boolean]
305
+ # Whether or not the trace matches
306
+ private def matches?(trace)
307
+ method_matches?(trace) && class_matches?(trace)
308
+ end
309
+
310
+ # Whether the current trace fits the class constraints
311
+ #
312
+ # @since 0.0.1
313
+ #
314
+ # @param trace [Trace]
315
+ # Currently active Trace
316
+ #
317
+ # @return [Boolean]
318
+ # Whether or not the trace matches
319
+ private def class_matches?(trace)
320
+ return true if @from_class == Any
321
+
322
+ @from_class == trace.defined_class || @from_class === trace.defined_class
323
+ end
324
+
325
+ # Whether the current trace fits the method constraints
326
+ #
327
+ # @since 0.0.1
328
+ #
329
+ # @param trace [Trace]
330
+ # Currently active Trace
331
+ #
332
+ # @return [Boolean]
333
+ # Whether or not the trace matches
334
+ private def method_matches?(trace)
335
+ @method_name === trace.method_id
336
+ end
337
+
338
+ # Extracts the arguments from a given trace
339
+ #
340
+ # @since 0.0.1
341
+ #
342
+ # @param trace [Trace]
343
+ #
344
+ # @return [Hash[Symbol, Any]]
345
+ # Hash mapping argument names to their respective values
346
+ private def extract_args(trace)
47
347
  param_names = trace.parameters.map(&:last)
48
348
 
49
349
  param_names.map { |n| [n, trace.binding.eval(n.to_s)] }.to_h
50
350
  end
351
+
352
+ # Extracts the local variables from a given trace
353
+ #
354
+ # @since 0.0.1
355
+ #
356
+ # @param trace [Trace]
357
+ #
358
+ # @return [Hash[Symbol, Any]]
359
+ # Hash mapping local variable names to their respective values
360
+ private def extract_locals(trace)
361
+ local_names = trace.binding.eval('local_variables')
362
+ local_names.map { |n| [n, trace.binding.eval(n.to_s)] }.to_h
363
+ end
51
364
  end
52
365
  end
@@ -1,3 +1,3 @@
1
1
  module TraceSpy
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/trace_spy.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_runtime_dependency "qo"
25
+ spec.add_runtime_dependency "qo", "~> 0.5"
26
26
 
27
27
  spec.add_development_dependency "bundler", "~> 1.17"
28
28
  spec.add_development_dependency "rake", "~> 10.0"
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trace_spy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Weaver
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-01-26 00:00:00.000000000 Z
11
+ date: 2019-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: qo
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '0.5'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '0.5'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement