class TraceWrapper

Wraps methods on given classes or modules to output a call/return tree.

Constants

VERSION

Current version of TraceWrapper

Public Class Methods

new(output: $stdout, colour: nil) click to toggle source

Create a new TraceWrapper

Options:

:output - IO object to write trace output to (default STDOUT) :colour - True to use shell colours in output (default nil will colour if output is a TTY)

# File lib/trace_wrapper.rb, line 35
def initialize(output: $stdout, colour: nil)
  @output = output
  @colour = colour
  @unwrappers = []
  @main_process_id = process_id
  @processes = {}
  process
end
wrap(*receivers, **options) { |a_trace_wrapper| ... } click to toggle source

Wraps methods on given receivers with tracing

options will be passed to .new and wrap respectively

If a block is given, it will be passed to wrap

# File lib/trace_wrapper.rb, line 18
def wrap(*receivers, **options, &block) # :yields: a_trace_wrapper
  init_keys = %i[output colour]
  init_args = options.select { |k, _| init_keys.include?(k) }
  wrap_args = options.reject { |k, _| init_keys.include?(k) }

  new(**init_args).wrap(*receivers, **wrap_args, &block)
end

Public Instance Methods

unwrap() click to toggle source

Remove any wrappers set by this tracer

# File lib/trace_wrapper.rb, line 88
def unwrap
  @unwrappers.each(&:call)
  @unwrappers = []
end
wrap(*receivers, method_type: :all, visibility: :protected) { |a_trace_wrapper| ... } click to toggle source

Wraps methods on given receivers with tracing

Options

:method_type - Types of methods to wrap (default: :all). Choices are: :instance (for instance methods), :self (for receiver methods, i.e. class/module functions), :all for both

:visibility - Lowest method visibility level to wrap (default: :protected). Choices are: :public, :protected, :private.

If a block is given, the wrappers will be created just around the block and the block's result will be returned. The TraceWrapper instance will be yielded to the block to allow further wraps to be added.

If no block is given, you should call unwrap after use.

# File lib/trace_wrapper.rb, line 63
def wrap(*receivers,
         method_type: :all,
         visibility: :protected) # :yields: a_trace_wrapper
  unwrappers = []
  Array(*receivers).each do |receiver|
    if %i[all self].include?(method_type)
      unwrappers += wrap_methods(receiver, visibility: visibility)
    end
    if %i[all instance].include?(method_type)
      unwrappers += wrap_instance_methods(receiver, visibility: visibility)
    end
  end
  if block_given?
    begin
      yield(self)
    ensure
      unwrappers.each(&:call)
    end
  else
    @unwrappers += unwrappers
    self
  end
end

Private Instance Methods

decr_indent() click to toggle source
# File lib/trace_wrapper.rb, line 216
def decr_indent
  process[:indent] = [process[:indent] - 1, 0].max
end
function(receiver, dot, name) click to toggle source
# File lib/trace_wrapper.rb, line 202
def function(receiver, dot, name)
  return colour(id, :teal) if main == receiver

  "#{colour(receiver, :b_green)}#{dot}#{colour(name, :teal)}"
end
get_methods(receiver, method_type, visibility) click to toggle source
# File lib/trace_wrapper.rb, line 168
def get_methods(receiver, method_type, visibility)
  visibilities = %i[public protected private]
  unless visibilities.include?(visibility)
    raise "visibility option not recognised: #{visibility.inspect}"
  end
  visibilities = visibilities[0..visibilities.find_index(visibility)]

  visibilities.map do |vis|
    lister = LIST_METHODS[method_type][vis]
    receiver.public_send(lister, false) - Object.public_send(lister)
  end.compact.flatten
end
incr_indent() click to toggle source
# File lib/trace_wrapper.rb, line 212
def incr_indent
  process[:indent] += 1
end
indent() click to toggle source
# File lib/trace_wrapper.rb, line 220
def indent
  '  ' * (process[:indent] + 1)
end
key_args?(method) click to toggle source
# File lib/trace_wrapper.rb, line 194
def key_args?(method)
  method.parameters.any? { |k, _| %i[keyrest key].include?(k) }
end
main() click to toggle source
# File lib/trace_wrapper.rb, line 198
def main
  TOPLEVEL_BINDING.receiver
end
process() click to toggle source
# File lib/trace_wrapper.rb, line 224
def process
  proc_colours = %i[b_purple orange blue red purple cyan yellow b_blue b_red]
  @processes[process_id.join(':')] ||= {
    colour: proc_colours[@processes.size],
    indent: 0
  }
end
process_id() click to toggle source
# File lib/trace_wrapper.rb, line 232
def process_id
  [Process.pid, Thread.current.hash]
end
process_label() click to toggle source
# File lib/trace_wrapper.rb, line 236
def process_label
  return if process_id == @main_process_id
  pid, tid = process_id
  return pid if Thread.current == Thread.main
  pid = '' if pid == @main_process_id.first
  "#{pid}:#{tid.to_s[-4..-1]}"
end
short_inspect(obj, limit = 20) click to toggle source
# File lib/trace_wrapper.rb, line 260
def short_inspect(obj, limit = 20)
  text = obj.inspect
  return text if text.length <= limit
  text[0...limit] + ELLIPSIS + text[-1]
end
show_args(*args, **kwargs) click to toggle source
# File lib/trace_wrapper.rb, line 249
def show_args(*args, **kwargs)
  return if args.empty? && kwargs.empty?
  parts = args.map do |v|
    colour(short_inspect(v), :purple)
  end
  parts += kwargs.map do |k, v|
    "#{k}: #{colour(short_inspect(v), :purple)}"
  end
  parts.join(', ')
end
show_pid() click to toggle source
# File lib/trace_wrapper.rb, line 244
def show_pid
  return if process_id == @main_process_id
  colour("[#{process_label}]", process[:colour])
end
trace_call(receiver, dot, method_name, *args, **kwargs) click to toggle source
# File lib/trace_wrapper.rb, line 181
def trace_call(receiver, dot, method_name, *args, **kwargs)
  writeln("#{show_pid}#{function(receiver, dot, method_name)}" \
          "(#{show_args(*args, **kwargs)})")
  incr_indent
end
trace_return(receiver, dot, method_name, result) click to toggle source
# File lib/trace_wrapper.rb, line 187
def trace_return(receiver, dot, method_name, result)
  decr_indent
  writeln("#{show_pid}#{function(receiver, dot, method_name)} " \
          "#{colour('return', :yellow)} " \
          "#{colour(short_inspect(result), :purple)}")
end
wrap_instance_methods(*receivers, visibility: :protected) click to toggle source

Wrap instance methods (called on an instance of the class given) with tracing

# File lib/trace_wrapper.rb, line 108
def wrap_instance_methods(*receivers, visibility: :protected)
  unwrappers = []
  Array(*receivers).each do |receiver|
    mod, unwrapper = wrapping_module(receiver, :instance, visibility)
    unwrappers << unwrapper
    receiver.send(:prepend, mod)
  end
  unwrappers
end
wrap_methods(*receivers, visibility: :protected) click to toggle source

Wrap standard methods (methods on the object given) with tracing

# File lib/trace_wrapper.rb, line 96
def wrap_methods(*receivers, visibility: :protected)
  unwrappers = []
  Array(*receivers).each do |receiver|
    mod, unwrapper = wrapping_module(receiver, :self, visibility)
    unwrappers << unwrapper
    receiver.singleton_class.send(:prepend, mod)
  end
  unwrappers
end
wrapping_module(receiver, method_type, visibility) click to toggle source
Calls superclass method
# File lib/trace_wrapper.rb, line 118
def wrapping_module(receiver, method_type, visibility)
  method_names = get_methods(receiver, method_type, visibility)
  get_method = method_type == :instance ? :instance_method : :method
  dot = method_type == :instance ? '#' : '.'
  trace_call = method(:trace_call)
  trace_return = method(:trace_return)
  key_args = method(:key_args?)

  mod = Module.new do
    method_names.each do |name|
      wrap_method =
        if key_args.call(receiver.public_send(get_method, name))
          lambda do |*args, **kwargs, &block|
            trace_call.call(receiver, dot, name, *args, **kwargs)
            result = super(*args, **kwargs, &block)
            trace_return.call(receiver, dot, name, result)
            result
          end
        else
          lambda do |*args, &block|
            trace_call.call(receiver, dot, name, *args)
            result = super(*args, &block)
            trace_return.call(receiver, dot, name, result)
            result
          end
        end
      define_method(name, wrap_method)
    end
  end
  unwrapper = lambda do
    method_names.each do |name|
      mod.send(:remove_method, name) if mod.method_defined?(name)
    end
  end
  [mod, unwrapper]
end
writeln(text) click to toggle source
# File lib/trace_wrapper.rb, line 208
def writeln(text)
  @output.write("#{indent}#{text}\n")
end