Skip to content

Commit

Permalink
Support IRB.conf[:BACKTRACE_FILTER] (#917)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
st0012 committed May 1, 2024
1 parent 07d13a3 commit 6f6e87d
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 18 deletions.
40 changes: 23 additions & 17 deletions lib/irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1242,27 +1242,33 @@ def handle_exception(exc)
irb_bug = true
else
irb_bug = false
# This is mostly to make IRB work nicely with Rails console's backtrace filtering, which patches WorkSpace#filter_backtrace
# In such use case, we want to filter the exception's backtrace before its displayed through Exception#full_message
# 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
# To support backtrace filtering while utilizing Exception#full_message, we need to clone
# the exception to avoid modifying the original exception's backtrace.
exc = exc.clone
exc.set_backtrace(backtrace)
end
filtered_backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact
backtrace_filter = IRB.conf[:BACKTRACE_FILTER]

if RUBY_VERSION < '3.0.0'
if STDOUT.tty?
message = exc.full_message(order: :bottom)
order = :bottom
else
message = exc.full_message(order: :top)
order = :top
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
else # '3.0.0' <= RUBY_VERSION
message = exc.full_message(order: :top)
order = :top

exc.set_backtrace(filtered_backtrace)
end

highlight = Color.colorable?

order =
if RUBY_VERSION < '3.0.0'
STDOUT.tty? ? :bottom : :top
else # '3.0.0' <= RUBY_VERSION
:top
end

message = exc.full_message(order: order, highlight: highlight)
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 = message.gsub(/((?:^\t.+$\n)+)/) { |m|
Expand Down
2 changes: 1 addition & 1 deletion test/irb/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def type(command)
end

def write_ruby(program)
@ruby_file = Tempfile.create(%w{irb- .rb})
@ruby_file = Tempfile.create(%w{irbtest- .rb})
@tmpfiles << @ruby_file
@ruby_file.write(program)
@ruby_file.close
Expand Down
91 changes: 91 additions & 0 deletions test/irb/test_irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -823,4 +823,95 @@ def build_irb
IRB::Irb.new(workspace, TestInputMethod.new)
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

0 comments on commit 6f6e87d

Please sign in to comment.