Skip to content

Commit

Permalink
[GR-18163] Add Exception#detailed_message
Browse files Browse the repository at this point in the history
PullRequest: truffleruby/4004
  • Loading branch information
andrykonchin authored and eregon committed Sep 15, 2023
2 parents 0e3726d + 59d7c03 commit b4a05b5
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Bug fixes:

Compatibility:

* Add `Exception#detailed_message` method (#3257, @andrykonchin).

Performance:

Expand Down
12 changes: 10 additions & 2 deletions spec/ruby/core/exception/detailed_message_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
RuntimeError.new("new error").detailed_message.should == "new error (RuntimeError)"
end

it "is called by #full_message to allow message customization" do
exception = Exception.new("new error")
def exception.detailed_message(**)
"<prefix>#{message}<suffix>"
end
exception.full_message(highlight: false).should.include? "<prefix>new error<suffix>"
end

it "accepts highlight keyword argument and adds escape control sequences" do
RuntimeError.new("new error").detailed_message(highlight: true).should == "\e[1mnew error (\e[1;4mRuntimeError\e[m\e[1m)\e[m"
end
Expand All @@ -23,13 +31,13 @@
RuntimeError.new("").detailed_message.should == "unhandled exception"
end

it "returns just class name for an instance of RuntimeError sublass with empty message" do
it "returns just class name for an instance of RuntimeError subclass with empty message" do
DetailedMessageSpec::C.new("").detailed_message.should == "DetailedMessageSpec::C"
end

it "returns a generated class name for an instance of RuntimeError anonymous subclass with empty message" do
klass = Class.new(RuntimeError)
klass.new("").detailed_message.should =~ /\A#<Class:0x\h+>\z/
klass.new("").detailed_message.should =~ /\A#<Class:0x\h+>\z/
end
end
end
40 changes: 34 additions & 6 deletions spec/ruby/core/exception/full_message_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,49 @@
ruby_version_is "3.2" do
it "relies on #detailed_message" do
e = RuntimeError.new("new error")
e.define_singleton_method(:detailed_message) { |**opt| "DETAILED MESSAGE" }
e.define_singleton_method(:detailed_message) { |**| "DETAILED MESSAGE" }

e.full_message.lines.first.should =~ /DETAILED MESSAGE/
end

it "passes all its own keyword arguments to #detailed_message" do
it "passes all its own keyword arguments (with :highlight default value and without :order default value) to #detailed_message" do
e = RuntimeError.new("new error")
opt_ = nil
e.define_singleton_method(:detailed_message) do |**opt|
opt_ = opt
options_passed = nil
e.define_singleton_method(:detailed_message) do |**options|
options_passed = options
"DETAILED MESSAGE"
end

e.full_message(foo: "bar")
opt_.should == { foo: "bar", highlight: Exception.to_tty? }
options_passed.should == { foo: "bar", highlight: Exception.to_tty? }
end

it "converts #detailed_message returned value to String if it isn't a String" do
message = Object.new
def message.to_str; "DETAILED MESSAGE"; end

e = RuntimeError.new("new error")
e.define_singleton_method(:detailed_message) { |**| message }

e.full_message.lines.first.should =~ /DETAILED MESSAGE/
end

it "uses class name if #detailed_message returns nil" do
e = RuntimeError.new("new error")
e.define_singleton_method(:detailed_message) { |**| nil }

e.full_message(highlight: false).lines.first.should =~ /RuntimeError/
e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/
end

it "uses class name if exception object doesn't respond to #detailed_message" do
e = RuntimeError.new("new error")
class << e
undef :detailed_message
end

e.full_message(highlight: false).lines.first.should =~ /RuntimeError/
e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/
end
end
end
6 changes: 0 additions & 6 deletions spec/tags/core/exception/detailed_message_tags.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
fails:Exception#detailed_message returns decorated message
fails:Exception#detailed_message accepts highlight keyword argument and adds escape control sequences
fails:Exception#detailed_message allows and ignores other keyword arguments
fails:Exception#detailed_message returns just a message if exception class is anonymous
fails:Exception#detailed_message returns 'unhandled exception' for an instance of RuntimeError with empty message
fails:Exception#detailed_message returns just class name for an instance of RuntimeError sublass with empty message
fails:Exception#detailed_message returns a generated class name for an instance of RuntimeError anonymous subclass with empty message
2 changes: 0 additions & 2 deletions spec/tags/core/exception/full_message_tags.txt

This file was deleted.

12 changes: 10 additions & 2 deletions src/main/ruby/truffleruby/core/exception.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,16 @@ def inspect
end
end

def full_message(highlight: nil, order: :top)
Truffle::ExceptionOperations.full_message(self, highlight, order)
def full_message(**options)
Truffle::ExceptionOperations.full_message(self, **options)
end

def detailed_message(highlight: nil, **options)
unless Primitive.true?(highlight) || Primitive.false?(highlight) || Primitive.nil?(highlight)
raise ArgumentError, "expected true of false as highlight: #{highlight}"
end

Truffle::ExceptionOperations.detailed_message(self, highlight)
end

class << self
Expand Down
87 changes: 63 additions & 24 deletions src/main/ruby/truffleruby/core/truffle/exception_operations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def self.receiver_string(receiver)
end
end

def self.message_and_class(exception, highlight)
def self.detailed_message(exception, highlight)
message = StringValue exception.message.to_s

klass = Primitive.class(exception).to_s
Expand All @@ -100,40 +100,78 @@ def self.message_and_class(exception, highlight)
klass = "#{klass}: #{Truffle::Interop.meta_qualified_name Truffle::Interop.meta_object(exception)}"
end

if message.empty?
return highlight ? "\n\e[1m#{klass}\e[m" : klass
end

anonymous_class = Primitive.module_anonymous?(Primitive.class(exception))

if highlight
highlighted_class = " (\e[1;4m#{klass}\e[m\e[1m)"
highlighted_class_string = !anonymous_class ? " (\e[1;4m#{klass}\e[m\e[1m)" : ''
if message.include?("\n")
first = true
result = +''
message.each_line do |line|
if first
first = false
result << "\e[1m#{line.chomp}#{highlighted_class}\e[m"
result << "\e[1m#{line.chomp}#{highlighted_class_string}\e[m"
else
result << "\n\e[1m#{line.chomp}\e[m"
end
end
result
else
"\e[1m#{message}#{highlighted_class}\e[m"
"\e[1m#{message}#{highlighted_class_string}\e[m"
end
else
class_string = !anonymous_class ? " (#{klass})" : ''

if i = message.index("\n")
"#{message[0...i]} (#{klass})#{message[i..-1]}"
"#{message[0...i]}#{class_string}#{message[i..-1]}"
else
"#{message} (#{klass})"
"#{message}#{class_string}"
end
end
end

def self.full_message(exception, highlight, order)
def self.detailed_message_or_fallback(exception, options)
unless Primitive.respond_to?(exception, :detailed_message, false)
return detailed_message_fallback(exception, options)
end

detailed_message = exception.detailed_message(**options)
detailed_message = Truffle::Type.rb_check_convert_type(detailed_message, String, :to_str)

if !Primitive.nil?(detailed_message)
detailed_message
else
detailed_message_fallback(exception, options)
end
end

def self.detailed_message_fallback(exception, options)
class_name = Primitive.class(exception).to_s

if options[:highlight]
"\e[1;4m#{class_name}\e[m\e[1m"
else
class_name
end
end

def self.full_message(exception, **options)
highlight = options[:highlight]
highlight = if Primitive.nil?(highlight)
Exception.to_tty?
else
raise ArgumentError, "expected true of false as highlight: #{highlight}" unless Primitive.true?(highlight) || Primitive.false?(highlight)
!Primitive.false?(highlight)
end

options[:highlight] = highlight

order = options[:order]
order = :top if Primitive.nil?(order)
raise ArgumentError, "expected :top or :bottom as order: #{order}" unless Primitive.equal?(order, :top) || Primitive.equal?(order, :bottom)
reverse = !Primitive.equal?(order, :top)

Expand All @@ -146,28 +184,29 @@ def self.full_message(exception, highlight, order)
"Traceback (most recent call last):\n"
end
result << traceback_msg
append_causes(result, exception, {}.compare_by_identity, reverse, highlight)
backtrace_message = backtrace_message(highlight, reverse, bt, exception)
append_causes(result, exception, {}.compare_by_identity, reverse, highlight, options)
backtrace_message = backtrace_message(highlight, reverse, bt, exception, options)
if backtrace_message.empty?
result << message_and_class(exception, highlight)
result << detailed_message_or_fallback(exception, options)
else
result << backtrace_message
end
else
backtrace_message = backtrace_message(highlight, reverse, bt, exception)
backtrace_message = backtrace_message(highlight, reverse, bt, exception, options)
if backtrace_message.empty?
result << message_and_class(exception, highlight)
result << detailed_message_or_fallback(exception, options)
else
result << backtrace_message
end
append_causes(result, exception, {}.compare_by_identity, reverse, highlight)
append_causes(result, exception, {}.compare_by_identity, reverse, highlight, options)
end
result
end

def self.backtrace_message(highlight, reverse, bt, exc)
message = message_and_class(exc, highlight)
def self.backtrace_message(highlight, reverse, bt, exc, options)
message = detailed_message_or_fallback(exc, options)
message = message.end_with?("\n") ? message : "#{message}\n"

return '' if Primitive.nil?(bt) || bt.empty?
limit = Primitive.exception_backtrace_limit
limit = limit >= 0 && bt.size - 1 >= limit + 2 ? limit : -1
Expand All @@ -192,26 +231,26 @@ def self.backtrace?(exc)
end
end

def self.append_causes(str, err, causes, reverse, highlight)
def self.append_causes(str, err, causes, reverse, highlight, options)
cause = err.cause
if !Primitive.nil?(cause) && Primitive.is_a?(cause, Exception) && !causes.has_key?(cause)
causes[cause] = true
if reverse
append_causes(str, cause, causes, reverse, highlight)
backtrace_message = backtrace_message(highlight, reverse, cause.backtrace, cause)
append_causes(str, cause, causes, reverse, highlight, options)
backtrace_message = backtrace_message(highlight, reverse, cause.backtrace, cause, options)
if backtrace_message.empty?
str << message_and_class(err, highlight)
str << detailed_message_or_fallback(exception, options)
else
str << backtrace_message
end
else
backtrace_message = backtrace_message(highlight, reverse, cause.backtrace, cause)
backtrace_message = backtrace_message(highlight, reverse, cause.backtrace, cause, options)
if backtrace_message.empty?
str << message_and_class(err, highlight)
str << detailed_message_or_fallback(exception, options)
else
str << backtrace_message
end
append_causes(str, cause, causes, reverse, highlight)
append_causes(str, cause, causes, reverse, highlight, options)
end
end
end
Expand Down Expand Up @@ -250,8 +289,8 @@ def self.to_class_name(val)
end
end

def self.get_formatted_backtrace(exc)
full_message(exc, nil, :top)
def self.get_formatted_backtrace(exception)
full_message(exception, highlight: nil, order: :top)
end

def self.comparison_error_message(x, y)
Expand Down

0 comments on commit b4a05b5

Please sign in to comment.