From 8c62b821ef7f22b6d7735e8db9be9aeeca1e6197 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sun, 7 Apr 2024 12:21:02 +0100 Subject: [PATCH 1/2] 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. --- test/irb/helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/irb/helper.rb b/test/irb/helper.rb index 1614b42ad..591bd05b7 100644 --- a/test/irb/helper.rb +++ b/test/irb/helper.rb @@ -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 From b9aad76734481be361aef2743de6fa84ad328025 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sun, 7 Apr 2024 12:57:15 +0100 Subject: [PATCH 2/2] 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. --- lib/irb.rb | 40 ++++++++++--------- test/irb/test_irb.rb | 91 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 17 deletions(-) diff --git a/lib/irb.rb b/lib/irb.rb index 5cb91a293..45a59087b 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -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| diff --git a/test/irb/test_irb.rb b/test/irb/test_irb.rb index b05fc80f7..28be74408 100644 --- a/test/irb/test_irb.rb +++ b/test/irb/test_irb.rb @@ -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 [`']
'/, + /from :\d+:in (`|'Kernel#)loop'/, + /from :\d+:in (`|'Binding#)irb'/, + /from .*\/irbtest-.*.rb:9:in [`']
'/ + ] + else + [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']
'/, + /from :\d+:in (`|'Binding#)irb'/, + /from .*\/irbtest-.*.rb:9:in [`']
'/ + ] + 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 [`']
'/, + /from .*\/irbtest-.*.rb:9:in [`']
'/ + ] + + 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