Как получить ruby ​​для печати полной обратной трассировки, содержащей аргументы, переданные функциям?

Иногда для диагностики проблемы достаточно обратного следа. Но иногда причина сбоя не очевидна без знания того, что было передано в работу.

Получение информации о том, что было передано функции, вызвавшей сбой, было бы весьма полезно, особенно в случаях, когда воспроизведение неочевидно, потому что это было вызвано, например, исключением при сетевом подключении, странным вводом данных пользователем или потому, что программа зависит от рандомизации или обрабатывает данные из внешнего источника. датчик.

Допустим, есть следующая программа

def handle_changed_input(changed_input)
    raise 'ops' if changed_input =~ /magic/
end

def do_something_with_user_input(input)
    input = "#{input.strip}c"
    handle_changed_input(input)
end

input = gets
do_something_with_user_input(input)

где пользователь ввел "волшебный" в качестве ввода. Обычно

test.rb:2:in `handle_changed_input': ops (RuntimeError)
    from test.rb:7:in `do_something_with_user_input'
    from test.rb:11:in `<main>'

в качестве вывода. Что можно сделать, чтобы показать также, что было передано в функцию? Что-то вроде

test.rb:2:in `handle_changed_input("magic")': ops (RuntimeError)
    from test.rb:7:in `do_something_with_user_input("magi\n")'
    from test.rb:11:in `<main>'

Это было бы полезно во многих ситуациях (и не очень полезно, когда параметры не могут быть представлены в виде строк разумного размера, есть веская причина, почему он не включен по умолчанию).

Как можно добавить эту функциональность? Необходимо, чтобы программа работала как обычно во время нормальной работы, и желательно, чтобы перед сбоем не было дополнительных выходных данных.

Я пробовал например

def do_something_with_user_input(input)
    method(__method__).parameters.map do |_, name|
        puts "#{name}=#{binding.local_variable_get(name)}"
    end
    raise 'ops' if input =~ /magic/
end

input = gets

найти в Есть ли способ получить доступ к аргументам метода в Ruby? но он будет печатать на каждом входе, чтобы функционировать так же, как и поток данных, и значительно замедлять работу программы.

4 ответа

У меня нет полного решения, но... Но вы можете получить аргументы всех вызываемых методов в контролируемой среде с помощью класса TracePoint из Ruby core lib.

Посмотрите на пример:

trace = TracePoint.new(:call) do |tp|
  puts "===================== #{tp.method_id}"
  b_self = tp.binding.eval('self')
  names = b_self.method(tp.method_id).parameters.map(&:last)
  values = names.map { |name| tp.binding.eval(name.to_s) }
  p names.zip(values)
end

trace.enable

def method_a(p1, p2, p3)
end

method_a(1, "foobar", false)

#=> ===================== method_a
#=> [[:p1, 1], [:p2, "foobar"], [:p3, false]]

Я думаю, что это возможно. Приведенный ниже код не идеален и потребует некоторой дополнительной работы, но он опровергает основную идею трассировки стека со значениями аргументов. Обратите внимание, что для того, чтобы узнать сайт вызова, я заархивировал оригинальную трассировку стека с сайтами входа, отслеживаемыми функцией трассировки. Чтобы различать эти записи, я использую ">" и "<" соответственно.

class Reporting
  def self.info(arg1)
    puts "*** #{arg1} ***"
  end
end


def read_byte(arg1)
  Reporting.info(arg1)
  raise Exception.new("File not found")
end

def read_input(arg1)
  read_byte(arg1)
end

def main(arg1)
  read_input(arg1)
end


class BetterStacktrace
  def self.enable
    set_trace_func -> (event, file, line, id, binding, classname) do
      case event
      when 'call'
        receiver_type = binding.eval('self.class')
        if receiver_type == Object
          meth = binding.eval('__method__')
          params = binding.method(meth).parameters.select{|e| e[0] != :block}
          values = params.map{|_, var| [var, binding.local_variable_get(var)]}
          self.push(event, file, line, id, classname, values)
        else
          self.push(event, file, line, id, classname)
        end
      when 'return'
        self.pop
      when 'raise'
        self.push(event, file, line, id, classname)
        Thread.current[:_keep_stacktrace] = true
      end
    end
  end

  def self.push(event, file, line, id, classname, values=nil)
    Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
    unless Thread.current[:_keep_stacktrace]
      if values
        values_msg = values.map(&:last).join(", ")
        msg = "%s:%d:in `%s(%s)'" % [file, line, id, values_msg]
      else
        msg = "%s:%d:in `%s'" % [file, line, id]
      end
      Thread.current[:_saved_stacktrace] << msg
    end
  end

  def self.pop()
    Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
    unless Thread.current[:_keep_stacktrace]
      value = Thread.current[:_saved_stacktrace].pop
    end
  end

  def self.disable
    set_trace_func nil
  end

  def self.print_stacktrace(calls)
    enters = Thread.current[:_saved_stacktrace].reverse
    calls.zip(enters).each do |call, enter|
      STDERR.puts "> #{enter}"
      STDERR.puts "< #{call}"
    end
    Thread.current[:_saved_stacktrace] = []
  end
end

BetterStacktrace.enable

begin
  main(10)
rescue Exception => ex
  puts "--- Catched ---"
  puts ex
  BetterStacktrace.print_stacktrace(ex.backtrace)
end


BetterStacktrace.disable
begin
  main(10)
rescue Exception
  puts "--- Catched ---"
  puts ex
  puts ex.backtrace
end

Вывод приведенного выше кода выглядит следующим образом:

*** 10 ***
--- Catched ---
File not found
> work/tracing_with_params.rb:10:in `read_byte'
< work/tracing_with_params.rb:10:in `read_byte'
> work/tracing_with_params.rb:8:in `read_byte(10)'
< work/tracing_with_params.rb:14:in `read_input'
> work/tracing_with_params.rb:13:in `read_input(10)'
< work/tracing_with_params.rb:18:in `main'
> work/tracing_with_params.rb:17:in `main(10)'
< work/tracing_with_params.rb:82:in `<main>'
*** 10 ***
--- Catched ---
File not found
work/tracing_with_params.rb:10:in `read_byte'
work/tracing_with_params.rb:14:in `read_input'
work/tracing_with_params.rb:18:in `main'
work/tracing_with_params.rb:82:in `<main>'

РЕДАКТИРОВАТЬ:

Вызовы функций класса не записываются. Это должно быть исправлено, чтобы функция печати стековой трассировки не получала неправильный вывод. Более того, я использовал STDERR в качестве вывода, чтобы легко получить тот или иной вывод. Вы можете изменить его, если хотите.

Для печати следов исключений Ruby использует функцию C exc_backtrace от error.c ( exc_backtrace на github). Если вы не исправите Ruby нужной вам функциональностью, я не думаю, что есть способ изменить выходные данные обратной трассировки исключений. Вот фрагмент (trace.rb), который вам может пригодиться:

set_trace_func -> (event, file, line, id, binding, classname) do
  if event == 'call' && meth = binding.eval('__method__')
    params = binding.method(meth).parameters.select{|e| e[0] != :block}
    values = params.map{|_, var| [var, binding.local_variable_get(var)]}
    printf "%8s %s:%-2d %15s %8s %s\n", event, file, line, id, classname, values.inspect
  else
    printf "%8s %s:%-2d %15s %8s\n", event, file, line, id, classname
  end
end


def foo(a,b = 0)
  bar(a, foo: true)
end

def bar(c, d = {})
  puts "!!!buz!!!\n"
end

foo('lol')

Результат этого фрагмента:

c-return /path/to/trace.rb:1   set_trace_func   Kernel
    line /path/to/trace.rb:12
  c-call /path/to/trace.rb:12    method_added   Module
c-return /path/to/trace.rb:12    method_added   Module
    line /path/to/trace.rb:16
  c-call /path/to/trace.rb:16    method_added   Module
c-return /path/to/trace.rb:16    method_added   Module
    line /path/to/trace.rb:20
    call /path/to/trace.rb:12             foo   Object [[:a, "lol"], [:b, 0]]
    line /path/to/trace.rb:13             foo   Object
    call /path/to/trace.rb:16             bar   Object [[:c, "lol"], [:d, {:foo=>true}]]
    line /path/to/trace.rb:17             bar   Object
  c-call /path/to/trace.rb:17            puts   Kernel
  c-call /path/to/trace.rb:17            puts       IO
  c-call /path/to/trace.rb:17           write       IO
!!!buz!!!
c-return /path/to/trace.rb:17           write       IO
c-return /path/to/trace.rb:17            puts       IO
c-return /path/to/trace.rb:17            puts   Kernel
  return /path/to/trace.rb:18             bar   Object
  return /path/to/trace.rb:14             foo   Object

Я надеюсь, что это поможет вам так же, как и мне.

      MAX_STACK_SIZE = 200
tracer = proc do |event|
  if event == 'call' && caller_locations.length > MAX_STACK_SIZE
    fail "Probable Stack Overflow"
  end
end
set_trace_func(tracer)
Другие вопросы по тегам