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 +4 -4
- data/README.md +22 -6
- data/lib/trace_spy.rb +15 -2
- data/lib/trace_spy/method.rb +320 -7
- data/lib/trace_spy/version.rb +1 -1
- data/trace_spy.gemspec +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3831d476b8d255dbec62c68b430fbd5733a8ff9a517c16d61a5be2ccac842a59
|
4
|
+
data.tar.gz: 4247e3304fd6cb2ef1e81187e1a29e8457ca4225c6414d5a171aed34775739ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
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
|
-
|
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'
|
data/lib/trace_spy/method.rb
CHANGED
@@ -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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
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
|
-
|
233
|
+
!!@tracepoint&.disable
|
44
234
|
end
|
45
235
|
|
46
|
-
|
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
|
data/lib/trace_spy/version.rb
CHANGED
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.
|
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-
|
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
|