Skip to content

Commit b4a05b5

Browse files
andrykonchineregon
authored andcommitted
[GR-18163] Add Exception#detailed_message
PullRequest: truffleruby/4004
2 parents 0e3726d + 59d7c03 commit b4a05b5

File tree

7 files changed

+118
-42
lines changed

7 files changed

+118
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Bug fixes:
88

99
Compatibility:
1010

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

1213
Performance:
1314

spec/ruby/core/exception/detailed_message_spec.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
RuntimeError.new("new error").detailed_message.should == "new error (RuntimeError)"
88
end
99

10+
it "is called by #full_message to allow message customization" do
11+
exception = Exception.new("new error")
12+
def exception.detailed_message(**)
13+
"<prefix>#{message}<suffix>"
14+
end
15+
exception.full_message(highlight: false).should.include? "<prefix>new error<suffix>"
16+
end
17+
1018
it "accepts highlight keyword argument and adds escape control sequences" do
1119
RuntimeError.new("new error").detailed_message(highlight: true).should == "\e[1mnew error (\e[1;4mRuntimeError\e[m\e[1m)\e[m"
1220
end
@@ -23,13 +31,13 @@
2331
RuntimeError.new("").detailed_message.should == "unhandled exception"
2432
end
2533

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

3038
it "returns a generated class name for an instance of RuntimeError anonymous subclass with empty message" do
3139
klass = Class.new(RuntimeError)
32-
klass.new("").detailed_message.should =~ /\A#<Class:0x\h+>\z/
40+
klass.new("").detailed_message.should =~ /\A#<Class:0x\h+>\z/
3341
end
3442
end
3543
end

spec/ruby/core/exception/full_message_spec.rb

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,49 @@
107107
ruby_version_is "3.2" do
108108
it "relies on #detailed_message" do
109109
e = RuntimeError.new("new error")
110-
e.define_singleton_method(:detailed_message) { |**opt| "DETAILED MESSAGE" }
110+
e.define_singleton_method(:detailed_message) { |**| "DETAILED MESSAGE" }
111111

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

115-
it "passes all its own keyword arguments to #detailed_message" do
115+
it "passes all its own keyword arguments (with :highlight default value and without :order default value) to #detailed_message" do
116116
e = RuntimeError.new("new error")
117-
opt_ = nil
118-
e.define_singleton_method(:detailed_message) do |**opt|
119-
opt_ = opt
117+
options_passed = nil
118+
e.define_singleton_method(:detailed_message) do |**options|
119+
options_passed = options
120120
"DETAILED MESSAGE"
121121
end
122122

123123
e.full_message(foo: "bar")
124-
opt_.should == { foo: "bar", highlight: Exception.to_tty? }
124+
options_passed.should == { foo: "bar", highlight: Exception.to_tty? }
125+
end
126+
127+
it "converts #detailed_message returned value to String if it isn't a String" do
128+
message = Object.new
129+
def message.to_str; "DETAILED MESSAGE"; end
130+
131+
e = RuntimeError.new("new error")
132+
e.define_singleton_method(:detailed_message) { |**| message }
133+
134+
e.full_message.lines.first.should =~ /DETAILED MESSAGE/
135+
end
136+
137+
it "uses class name if #detailed_message returns nil" do
138+
e = RuntimeError.new("new error")
139+
e.define_singleton_method(:detailed_message) { |**| nil }
140+
141+
e.full_message(highlight: false).lines.first.should =~ /RuntimeError/
142+
e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/
143+
end
144+
145+
it "uses class name if exception object doesn't respond to #detailed_message" do
146+
e = RuntimeError.new("new error")
147+
class << e
148+
undef :detailed_message
149+
end
150+
151+
e.full_message(highlight: false).lines.first.should =~ /RuntimeError/
152+
e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/
125153
end
126154
end
127155
end
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
1-
fails:Exception#detailed_message returns decorated message
2-
fails:Exception#detailed_message accepts highlight keyword argument and adds escape control sequences
3-
fails:Exception#detailed_message allows and ignores other keyword arguments
4-
fails:Exception#detailed_message returns just a message if exception class is anonymous
51
fails:Exception#detailed_message returns 'unhandled exception' for an instance of RuntimeError with empty message
6-
fails:Exception#detailed_message returns just class name for an instance of RuntimeError sublass with empty message
7-
fails:Exception#detailed_message returns a generated class name for an instance of RuntimeError anonymous subclass with empty message

spec/tags/core/exception/full_message_tags.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/main/ruby/truffleruby/core/exception.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,16 @@ def inspect
7777
end
7878
end
7979

80-
def full_message(highlight: nil, order: :top)
81-
Truffle::ExceptionOperations.full_message(self, highlight, order)
80+
def full_message(**options)
81+
Truffle::ExceptionOperations.full_message(self, **options)
82+
end
83+
84+
def detailed_message(highlight: nil, **options)
85+
unless Primitive.true?(highlight) || Primitive.false?(highlight) || Primitive.nil?(highlight)
86+
raise ArgumentError, "expected true of false as highlight: #{highlight}"
87+
end
88+
89+
Truffle::ExceptionOperations.detailed_message(self, highlight)
8290
end
8391

8492
class << self

src/main/ruby/truffleruby/core/truffle/exception_operations.rb

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def self.receiver_string(receiver)
9191
end
9292
end
9393

94-
def self.message_and_class(exception, highlight)
94+
def self.detailed_message(exception, highlight)
9595
message = StringValue exception.message.to_s
9696

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

103+
if message.empty?
104+
return highlight ? "\n\e[1m#{klass}\e[m" : klass
105+
end
106+
107+
anonymous_class = Primitive.module_anonymous?(Primitive.class(exception))
108+
103109
if highlight
104-
highlighted_class = " (\e[1;4m#{klass}\e[m\e[1m)"
110+
highlighted_class_string = !anonymous_class ? " (\e[1;4m#{klass}\e[m\e[1m)" : ''
105111
if message.include?("\n")
106112
first = true
107113
result = +''
108114
message.each_line do |line|
109115
if first
110116
first = false
111-
result << "\e[1m#{line.chomp}#{highlighted_class}\e[m"
117+
result << "\e[1m#{line.chomp}#{highlighted_class_string}\e[m"
112118
else
113119
result << "\n\e[1m#{line.chomp}\e[m"
114120
end
115121
end
116122
result
117123
else
118-
"\e[1m#{message}#{highlighted_class}\e[m"
124+
"\e[1m#{message}#{highlighted_class_string}\e[m"
119125
end
120126
else
127+
class_string = !anonymous_class ? " (#{klass})" : ''
128+
121129
if i = message.index("\n")
122-
"#{message[0...i]} (#{klass})#{message[i..-1]}"
130+
"#{message[0...i]}#{class_string}#{message[i..-1]}"
123131
else
124-
"#{message} (#{klass})"
132+
"#{message}#{class_string}"
125133
end
126134
end
127135
end
128136

129-
def self.full_message(exception, highlight, order)
137+
def self.detailed_message_or_fallback(exception, options)
138+
unless Primitive.respond_to?(exception, :detailed_message, false)
139+
return detailed_message_fallback(exception, options)
140+
end
141+
142+
detailed_message = exception.detailed_message(**options)
143+
detailed_message = Truffle::Type.rb_check_convert_type(detailed_message, String, :to_str)
144+
145+
if !Primitive.nil?(detailed_message)
146+
detailed_message
147+
else
148+
detailed_message_fallback(exception, options)
149+
end
150+
end
151+
152+
def self.detailed_message_fallback(exception, options)
153+
class_name = Primitive.class(exception).to_s
154+
155+
if options[:highlight]
156+
"\e[1;4m#{class_name}\e[m\e[1m"
157+
else
158+
class_name
159+
end
160+
end
161+
162+
def self.full_message(exception, **options)
163+
highlight = options[:highlight]
130164
highlight = if Primitive.nil?(highlight)
131165
Exception.to_tty?
132166
else
133167
raise ArgumentError, "expected true of false as highlight: #{highlight}" unless Primitive.true?(highlight) || Primitive.false?(highlight)
134168
!Primitive.false?(highlight)
135169
end
136170

171+
options[:highlight] = highlight
172+
173+
order = options[:order]
174+
order = :top if Primitive.nil?(order)
137175
raise ArgumentError, "expected :top or :bottom as order: #{order}" unless Primitive.equal?(order, :top) || Primitive.equal?(order, :bottom)
138176
reverse = !Primitive.equal?(order, :top)
139177

@@ -146,28 +184,29 @@ def self.full_message(exception, highlight, order)
146184
"Traceback (most recent call last):\n"
147185
end
148186
result << traceback_msg
149-
append_causes(result, exception, {}.compare_by_identity, reverse, highlight)
150-
backtrace_message = backtrace_message(highlight, reverse, bt, exception)
187+
append_causes(result, exception, {}.compare_by_identity, reverse, highlight, options)
188+
backtrace_message = backtrace_message(highlight, reverse, bt, exception, options)
151189
if backtrace_message.empty?
152-
result << message_and_class(exception, highlight)
190+
result << detailed_message_or_fallback(exception, options)
153191
else
154192
result << backtrace_message
155193
end
156194
else
157-
backtrace_message = backtrace_message(highlight, reverse, bt, exception)
195+
backtrace_message = backtrace_message(highlight, reverse, bt, exception, options)
158196
if backtrace_message.empty?
159-
result << message_and_class(exception, highlight)
197+
result << detailed_message_or_fallback(exception, options)
160198
else
161199
result << backtrace_message
162200
end
163-
append_causes(result, exception, {}.compare_by_identity, reverse, highlight)
201+
append_causes(result, exception, {}.compare_by_identity, reverse, highlight, options)
164202
end
165203
result
166204
end
167205

168-
def self.backtrace_message(highlight, reverse, bt, exc)
169-
message = message_and_class(exc, highlight)
206+
def self.backtrace_message(highlight, reverse, bt, exc, options)
207+
message = detailed_message_or_fallback(exc, options)
170208
message = message.end_with?("\n") ? message : "#{message}\n"
209+
171210
return '' if Primitive.nil?(bt) || bt.empty?
172211
limit = Primitive.exception_backtrace_limit
173212
limit = limit >= 0 && bt.size - 1 >= limit + 2 ? limit : -1
@@ -192,26 +231,26 @@ def self.backtrace?(exc)
192231
end
193232
end
194233

195-
def self.append_causes(str, err, causes, reverse, highlight)
234+
def self.append_causes(str, err, causes, reverse, highlight, options)
196235
cause = err.cause
197236
if !Primitive.nil?(cause) && Primitive.is_a?(cause, Exception) && !causes.has_key?(cause)
198237
causes[cause] = true
199238
if reverse
200-
append_causes(str, cause, causes, reverse, highlight)
201-
backtrace_message = backtrace_message(highlight, reverse, cause.backtrace, cause)
239+
append_causes(str, cause, causes, reverse, highlight, options)
240+
backtrace_message = backtrace_message(highlight, reverse, cause.backtrace, cause, options)
202241
if backtrace_message.empty?
203-
str << message_and_class(err, highlight)
242+
str << detailed_message_or_fallback(exception, options)
204243
else
205244
str << backtrace_message
206245
end
207246
else
208-
backtrace_message = backtrace_message(highlight, reverse, cause.backtrace, cause)
247+
backtrace_message = backtrace_message(highlight, reverse, cause.backtrace, cause, options)
209248
if backtrace_message.empty?
210-
str << message_and_class(err, highlight)
249+
str << detailed_message_or_fallback(exception, options)
211250
else
212251
str << backtrace_message
213252
end
214-
append_causes(str, cause, causes, reverse, highlight)
253+
append_causes(str, cause, causes, reverse, highlight, options)
215254
end
216255
end
217256
end
@@ -250,8 +289,8 @@ def self.to_class_name(val)
250289
end
251290
end
252291

253-
def self.get_formatted_backtrace(exc)
254-
full_message(exc, nil, :top)
292+
def self.get_formatted_backtrace(exception)
293+
full_message(exception, highlight: nil, order: :top)
255294
end
256295

257296
def self.comparison_error_message(x, y)

0 commit comments

Comments
 (0)