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



321
322
323
324
325
326
# File 'lib/trace_spy/method.rb', line 321

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



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

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



263
264
265
# File 'lib/trace_spy/method.rb', line 263

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



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/trace_spy/method.rb', line 233

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



125
126
127
# File 'lib/trace_spy/method.rb', line 125

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



223
224
225
# File 'lib/trace_spy/method.rb', line 223

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



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

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



190
191
192
# File 'lib/trace_spy/method.rb', line 190

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

#with_tracing(&traced_function) ⇒ TrueClass

Allows to run a block of code in the context of a tracer with the convenient side-effect of not having to remember to turn it off afterwards.

Tracer will only be active within the block, and will be disabled afterwards

Examples:

The tracer will only be active within the block:

```ruby
tracer.with_tracing do
  # tasks
end
```

Parameters:

  • &traced_function (Proc)

    Function to execute with tracing enabled

Returns:

  • (TrueClass)

    Result of disabling the tracer

Since:

  • 0.0.3



91
92
93
94
95
96
97
# File 'lib/trace_spy/method.rb', line 91

def with_tracing(&traced_function)
  self.enable

  yield

  self.disable
end