trace_spy 0.0.1 → 0.0.2

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.
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