Class: TraceSpy::Method

Inherits:
Object
  • Object
show all
Defined in:
lib/trace_spy/method.rb

Overview

Note:

Tracer spies all rely on Qo for pattern-matching syntax. In order to more effectively leverage this gem it would be a good idea to look through the Qo documentation present here: github.com/baweaver/qo

Implements a TraceSpy on a Method

Examples:

A simple use-case would be monitoring for a line in which c happens to be
equal to 5. Now this value could be a range or other `===` respondant type
if desired, which gives quite a bit of flexibility in querying.

```ruby
def testing(a, b)
  c = 5

  a + b + c
end

trace_spy = TraceSpy::Method.new(:testing) do |spy|
  spy.on_locals do |m|
    m.when(c: 5) { |locals| p locals }
  end
end

trace_spy.enable
# => false

testing(1, 2)
# {:a=>1, :b=>2, :c=>5}
# => 8
```

Author:

  • baweaver

Since:

  • 0.0.1

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(method_name, from_class: Any) {|_self| ... } ⇒ TraceSpy::Method

Creates a new method trace

Parameters:

  • method_name (Symbol, String)

    Name of the method to watch, will be compared with ‘===` for flexibility which enables the use of regex and other more powerful matching techniques.

  • from_class: (defaults to: Any)

    Any [Any] Either a Class for type-matching, or other ‘===` respondant type for flexibility

  • &fn (Proc)

    Self-yielding proc used to initialize a spy in one block function

Yields:

  • (_self)

Yield Parameters:

Since:

  • 0.0.1



58
59
60
61
62
63
64
65
66
# File 'lib/trace_spy/method.rb', line 58

def initialize(method_name, from_class: Any, &fn)
  @method_name   = method_name
  @from_class    = from_class
  @spies         = Hash.new { |h,k| h[k] = [] }
  @tracepoint    = nil
  @current_trace = nil

  yield(self) if block_given?
end

Instance Attribute Details

#current_traceObject (readonly)

The current trace being executed upon, can be used in matcher blocks to get the entire trace context instead of just a part.

Since:

  • 0.0.1



40
41
42
# File 'lib/trace_spy/method.rb', line 40

def current_trace
  @current_trace
end

Instance Method Details

#current_argumentsHash[Symbol, Any]

Note:

This method will attempt to avoid running in contexts where argument retrieval will give a runtime error.

Returns the arguments of the currently active trace

Examples:

This is a utility function for use with `spy` inside the matcher
block.

```ruby
trace_spy = TraceSpy::Method.new(:testing) do |spy|
  spy.on_return do |m|
    m.when(String) do |v|
      binding.pry if spy.current_arguments[:a] == 'foo'
    end
  end
end
```

It's meant to expose the current arguments present in a trace's
scope.

Returns:

  • (Hash[Symbol, Any])

Since:

  • 0.0.2



290
291
292
293
294
295
# File 'lib/trace_spy/method.rb', line 290

def current_arguments
  return {} unless @current_trace
  return {} if RAISE_EVENT.include?(@current_trace.event)

  extract_args(@current_trace)
end

#current_local_variablesHash[Symbol, Any]

Returns the local variables of the currently active trace

Examples:

This is a utility function for use with `spy` inside the matcher
block.

```ruby
trace_spy = TraceSpy::Method.new(:testing) do |spy|
  spy.on_exception do |m|
    m.when(RuntimeError) do |v|
      p spy.current_local_variables
    end
  end
end
```

It's meant to be used to expose the current local variables
within a trace's scope in any type of matcher.

Returns:

  • (Hash[Symbol, Any])

Since:

  • 0.0.2



258
259
260
261
262
# File 'lib/trace_spy/method.rb', line 258

def current_local_variables
  return {} unless @current_trace

  extract_locals(@current_trace)
end

#disableBoolean

Disables the TracePoint, or pretends it did if one isn’t enabled yet

Returns:

  • (Boolean)

Since:

  • 0.0.1



232
233
234
# File 'lib/trace_spy/method.rb', line 232

def disable
  !!@tracepoint&.disable
end

#enableFalseClass

“Enables” the current tracepoint by defining it, caching it, and enabling it

Returns:

  • (FalseClass)

    Still not sure why TracePoint#enable returns false, but here we are

Since:

  • 0.0.1



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/trace_spy/method.rb', line 202

def enable
  @tracepoint = TracePoint.new do |trace|
    begin
      next unless matches?(trace)

      @current_trace = trace

      call_with  = -> with { -> spy { spy.call(with) } }


      @spies[:arguments].each(&call_with[extract_args(trace)])    if CALL_EVENT.include?(trace.event)
      @spies[:locals].each(&call_with[extract_locals(trace)])     if LINE_EVENT.include?(trace.event)
      @spies[:return].each(&call_with[trace.return_value])        if RETURN_EVENT.include?(trace.event)
      @spies[:exception].each(&call_with[trace.raised_exception]) if RAISE_EVENT.include?(trace.event)

      @current_trace = nil
    rescue RuntimeError => e
      # Stupid hack for now
      p e
    end
  end

  @tracepoint.enable
end

#on_arguments(&matcher_fn) ⇒ Array[Qo::Matcher]

Creates a Spy on function arguments

Examples:

Consider, you'd like to monitor if a particular argument is nil:

```ruby
def testing(a) a + 2 end

trace_spy = TraceSpy::Method.new(:testing) do |spy|
  spy.on_arguments do |m|
    m.when(a: nil) { |args| binding.pry }
  end
end
```

You could use this to find out if there's a type-mismatch, or what
the context is around a particular error due to an argument being
a particular value.

Parameters:

  • &matcher_fn (Proc)

    Qo Matcher

Returns:

  • (Array[Qo::Matcher])

    Currently added Qo matchers

Since:

  • 0.0.1



94
95
96
# File 'lib/trace_spy/method.rb', line 94

def on_arguments(&matcher_fn)
  @spies[:arguments] << Qo.match(&matcher_fn)
end

#on_exception(&matcher_fn) ⇒ Array[Qo::Matcher]

Creates a Spy on a certain type of exception

Examples:

Consider, you'd like to find out where that error is coming from in
your function:

```ruby
def testing(a)
  raise 'heck'
  a + 2
end

trace_spy = TraceSpy::Method.new(:testing) do |spy|
  spy.on_exception do |m|
    m.when(RuntimeError) { |args| binding.pry }
  end
end
```

Like return, you can use this to find out the context around why this
particular error occurred.

Parameters:

  • &matcher_fn (Proc)

    Qo Matcher

Returns:

  • (Array[Qo::Matcher])

    Currently added Qo matchers

Since:

  • 0.0.1



192
193
194
# File 'lib/trace_spy/method.rb', line 192

def on_exception(&matcher_fn)
  @spies[:exception] << Qo.match(&matcher_fn)
end

#on_locals(&matcher_fn) ⇒ Array[Qo::Matcher]

Creates a Spy on local method variables

Examples:

Consider, a local variable is inexplicably getting set equal to nil,
and you don't know where it's happening:

```ruby
def testing(a)
  b = nil
  a + 2
end

trace_spy = TraceSpy::Method.new(:testing) do |spy|
  spy.on_locals do |m|
    m.when(b: nil) { |args| binding.pry }
  end
end
```

You can use this to stop your program precisely where the offending code
is located without needing to know where it is beforehand.

Parameters:

  • &matcher_fn (Proc)

    Qo Matcher

Returns:

  • (Array[Qo::Matcher])

    Currently added Qo matchers

Since:

  • 0.0.2



127
128
129
# File 'lib/trace_spy/method.rb', line 127

def on_locals(&matcher_fn)
  @spies[:locals] << Qo.match(&matcher_fn)
end

#on_return(&matcher_fn) ⇒ Array[Qo::Matcher]

Creates a Spy on function returns

Examples:

Consider, you'd like to know when your logging method is returning
an empty string:

```ruby
def logger(msg)
  rand(10) < 5 ? msg : ""
end

trace_spy = TraceSpy::Method.new(:logger) do |spy|
  spy.on_return do |m|
    m.when("") { |v| binding.pry }
  end
end
```

This could be used to find out the remaining context around what caused
the blank message, like getting arguments from the `spy.current_trace`.

Parameters:

  • &matcher_fn (Proc)

    Qo Matcher

Returns:

  • (Array[Qo::Matcher])

    Currently added Qo matchers

Since:

  • 0.0.1



159
160
161
# File 'lib/trace_spy/method.rb', line 159

def on_return(&matcher_fn)
  @spies[:return] << Qo.match(&matcher_fn)
end