Skip to content

Commit 6f6e87d

Browse files
authored
Support IRB.conf[:BACKTRACE_FILTER] (#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.
1 parent 07d13a3 commit 6f6e87d

File tree

3 files changed

+115
-18
lines changed

3 files changed

+115
-18
lines changed

lib/irb.rb

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,27 +1242,33 @@ def handle_exception(exc)
12421242
irb_bug = true
12431243
else
12441244
irb_bug = false
1245-
# This is mostly to make IRB work nicely with Rails console's backtrace filtering, which patches WorkSpace#filter_backtrace
1246-
# In such use case, we want to filter the exception's backtrace before its displayed through Exception#full_message
1247-
# And we clone the exception object in order to avoid mutating the original exception
1248-
# TODO: introduce better API to expose exception backtrace externally
1249-
backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact
1245+
# To support backtrace filtering while utilizing Exception#full_message, we need to clone
1246+
# the exception to avoid modifying the original exception's backtrace.
12501247
exc = exc.clone
1251-
exc.set_backtrace(backtrace)
1252-
end
1248+
filtered_backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact
1249+
backtrace_filter = IRB.conf[:BACKTRACE_FILTER]
12531250

1254-
if RUBY_VERSION < '3.0.0'
1255-
if STDOUT.tty?
1256-
message = exc.full_message(order: :bottom)
1257-
order = :bottom
1258-
else
1259-
message = exc.full_message(order: :top)
1260-
order = :top
1251+
if backtrace_filter
1252+
if backtrace_filter.respond_to?(:call)
1253+
filtered_backtrace = backtrace_filter.call(filtered_backtrace)
1254+
else
1255+
warn "IRB.conf[:BACKTRACE_FILTER] #{backtrace_filter} should respond to `call` method"
1256+
end
12611257
end
1262-
else # '3.0.0' <= RUBY_VERSION
1263-
message = exc.full_message(order: :top)
1264-
order = :top
1258+
1259+
exc.set_backtrace(filtered_backtrace)
12651260
end
1261+
1262+
highlight = Color.colorable?
1263+
1264+
order =
1265+
if RUBY_VERSION < '3.0.0'
1266+
STDOUT.tty? ? :bottom : :top
1267+
else # '3.0.0' <= RUBY_VERSION
1268+
:top
1269+
end
1270+
1271+
message = exc.full_message(order: order, highlight: highlight)
12661272
message = convert_invalid_byte_sequence(message, exc.message.encoding)
12671273
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)
12681274
message = message.gsub(/((?:^\t.+$\n)+)/) { |m|

test/irb/helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def type(command)
196196
end
197197

198198
def write_ruby(program)
199-
@ruby_file = Tempfile.create(%w{irb- .rb})
199+
@ruby_file = Tempfile.create(%w{irbtest- .rb})
200200
@tmpfiles << @ruby_file
201201
@ruby_file.write(program)
202202
@ruby_file.close

test/irb/test_irb.rb

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,4 +823,95 @@ def build_irb
823823
IRB::Irb.new(workspace, TestInputMethod.new)
824824
end
825825
end
826+
827+
class BacktraceFilteringTest < TestIRB::IntegrationTestCase
828+
def test_backtrace_filtering
829+
write_ruby <<~'RUBY'
830+
def foo
831+
raise "error"
832+
end
833+
834+
def bar
835+
foo
836+
end
837+
838+
binding.irb
839+
RUBY
840+
841+
output = run_ruby_file do
842+
type "bar"
843+
type "exit"
844+
end
845+
846+
assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output)
847+
frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip)
848+
849+
expected_traces = if RUBY_VERSION >= "3.3.0"
850+
[
851+
/from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/,
852+
/from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/,
853+
/from <internal:kernel>:\d+:in (`|'Kernel#)loop'/,
854+
/from <internal:prelude>:\d+:in (`|'Binding#)irb'/,
855+
/from .*\/irbtest-.*.rb:9:in [`']<main>'/
856+
]
857+
else
858+
[
859+
/from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/,
860+
/from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/,
861+
/from <internal:prelude>:\d+:in (`|'Binding#)irb'/,
862+
/from .*\/irbtest-.*.rb:9:in [`']<main>'/
863+
]
864+
end
865+
866+
expected_traces.reverse! if RUBY_VERSION < "3.0.0"
867+
868+
expected_traces.each_with_index do |expected_trace, index|
869+
assert_match(expected_trace, frame_traces[index])
870+
end
871+
end
872+
873+
def test_backtrace_filtering_with_backtrace_filter
874+
write_rc <<~'RUBY'
875+
class TestBacktraceFilter
876+
def self.call(backtrace)
877+
backtrace.reject { |line| line.include?("internal") }
878+
end
879+
end
880+
881+
IRB.conf[:BACKTRACE_FILTER] = TestBacktraceFilter
882+
RUBY
883+
884+
write_ruby <<~'RUBY'
885+
def foo
886+
raise "error"
887+
end
888+
889+
def bar
890+
foo
891+
end
892+
893+
binding.irb
894+
RUBY
895+
896+
output = run_ruby_file do
897+
type "bar"
898+
type "exit"
899+
end
900+
901+
assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output)
902+
frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip)
903+
904+
expected_traces = [
905+
/from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/,
906+
/from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/,
907+
/from .*\/irbtest-.*.rb:9:in [`']<main>'/
908+
]
909+
910+
expected_traces.reverse! if RUBY_VERSION < "3.0.0"
911+
912+
expected_traces.each_with_index do |expected_trace, index|
913+
assert_match(expected_trace, frame_traces[index])
914+
end
915+
end
916+
end
826917
end

0 commit comments

Comments
 (0)