Как получить 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)