[ruby/irb] Support IRB.conf[:BACKTRACE_FILTER]

(https://github.com/ruby/irb/pull/917)

* Use 'irbtest-' instead if 'irb-' as prefix of test files.

Otherwise IRB would mis-recognize exceptions raised in test files as
exceptions raised in IRB itself.

* Support `IRB.conf[:BACKTRACE_FILTER]``

This config allows users to customize the backtrace of exceptions raised
and displayed in IRB sessions. This is useful for filtering out library
frames from the backtrace.

IRB expects the given value to response to `call` method and return
the filtered backtrace.

https://github.com/ruby/irb/commit/6f6e87d769
This commit is contained in:
Stan Lo 2024-05-01 22:23:05 +08:00 committed by git
parent 2a978ee047
commit 1000c27db8
3 changed files with 115 additions and 18 deletions

View File

@ -1242,27 +1242,33 @@ module IRB
irb_bug = true irb_bug = true
else else
irb_bug = false irb_bug = false
# This is mostly to make IRB work nicely with Rails console's backtrace filtering, which patches WorkSpace#filter_backtrace # To support backtrace filtering while utilizing Exception#full_message, we need to clone
# In such use case, we want to filter the exception's backtrace before its displayed through Exception#full_message # the exception to avoid modifying the original exception's backtrace.
# And we clone the exception object in order to avoid mutating the original exception
# TODO: introduce better API to expose exception backtrace externally
backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact
exc = exc.clone exc = exc.clone
exc.set_backtrace(backtrace) filtered_backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact
backtrace_filter = IRB.conf[:BACKTRACE_FILTER]
if backtrace_filter
if backtrace_filter.respond_to?(:call)
filtered_backtrace = backtrace_filter.call(filtered_backtrace)
else
warn "IRB.conf[:BACKTRACE_FILTER] #{backtrace_filter} should respond to `call` method"
end
end
exc.set_backtrace(filtered_backtrace)
end end
if RUBY_VERSION < '3.0.0' highlight = Color.colorable?
if STDOUT.tty?
message = exc.full_message(order: :bottom) order =
order = :bottom if RUBY_VERSION < '3.0.0'
else STDOUT.tty? ? :bottom : :top
message = exc.full_message(order: :top) else # '3.0.0' <= RUBY_VERSION
order = :top :top
end end
else # '3.0.0' <= RUBY_VERSION
message = exc.full_message(order: :top) message = exc.full_message(order: order, highlight: highlight)
order = :top
end
message = convert_invalid_byte_sequence(message, exc.message.encoding) message = convert_invalid_byte_sequence(message, exc.message.encoding)
message = encode_with_invalid_byte_sequence(message, IRB.conf[:LC_MESSAGES].encoding) unless message.encoding.to_s.casecmp?(IRB.conf[:LC_MESSAGES].encoding.to_s) message = encode_with_invalid_byte_sequence(message, IRB.conf[:LC_MESSAGES].encoding) unless message.encoding.to_s.casecmp?(IRB.conf[:LC_MESSAGES].encoding.to_s)
message = message.gsub(/((?:^\t.+$\n)+)/) { |m| message = message.gsub(/((?:^\t.+$\n)+)/) { |m|

View File

@ -196,7 +196,7 @@ module TestIRB
end end
def write_ruby(program) def write_ruby(program)
@ruby_file = Tempfile.create(%w{irb- .rb}) @ruby_file = Tempfile.create(%w{irbtest- .rb})
@tmpfiles << @ruby_file @tmpfiles << @ruby_file
@ruby_file.write(program) @ruby_file.write(program)
@ruby_file.close @ruby_file.close

View File

@ -823,4 +823,95 @@ module TestIRB
IRB::Irb.new(workspace, TestInputMethod.new) IRB::Irb.new(workspace, TestInputMethod.new)
end end
end end
class BacktraceFilteringTest < TestIRB::IntegrationTestCase
def test_backtrace_filtering
write_ruby <<~'RUBY'
def foo
raise "error"
end
def bar
foo
end
binding.irb
RUBY
output = run_ruby_file do
type "bar"
type "exit"
end
assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output)
frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip)
expected_traces = if RUBY_VERSION >= "3.3.0"
[
/from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/,
/from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/,
/from <internal:kernel>:\d+:in (`|'Kernel#)loop'/,
/from <internal:prelude>:\d+:in (`|'Binding#)irb'/,
/from .*\/irbtest-.*.rb:9:in [`']<main>'/
]
else
[
/from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/,
/from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/,
/from <internal:prelude>:\d+:in (`|'Binding#)irb'/,
/from .*\/irbtest-.*.rb:9:in [`']<main>'/
]
end
expected_traces.reverse! if RUBY_VERSION < "3.0.0"
expected_traces.each_with_index do |expected_trace, index|
assert_match(expected_trace, frame_traces[index])
end
end
def test_backtrace_filtering_with_backtrace_filter
write_rc <<~'RUBY'
class TestBacktraceFilter
def self.call(backtrace)
backtrace.reject { |line| line.include?("internal") }
end
end
IRB.conf[:BACKTRACE_FILTER] = TestBacktraceFilter
RUBY
write_ruby <<~'RUBY'
def foo
raise "error"
end
def bar
foo
end
binding.irb
RUBY
output = run_ruby_file do
type "bar"
type "exit"
end
assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output)
frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip)
expected_traces = [
/from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/,
/from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/,
/from .*\/irbtest-.*.rb:9:in [`']<main>'/
]
expected_traces.reverse! if RUBY_VERSION < "3.0.0"
expected_traces.each_with_index do |expected_trace, index|
assert_match(expected_trace, frame_traces[index])
end
end
end
end end