Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support IRB.conf[:BACKTRACE_FILTER] #917

Merged
merged 2 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading