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

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

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

Предположим, что существует следующая программа

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>'

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

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

Я попытался, например,

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?, но он будет печатать на каждом отдельном входе, чтобы функционировать, что оба будут наводнения на выходе и значительно замедлить работу программы.

Ответ 1

У меня нет полного решения, но... Но вы можете получить аргументы метода всех вызываемых методов в управляемой среде с помощью 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]]

Ответ 2

Чтобы распечатать исключения, 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

Я надеюсь, что это поможет вам в той мере, в какой это помогло мне.

Ответ 3

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

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>'

EDIT:

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