Skip to content

Commit 670fdb4

Browse files
committed
[GR-19220] Display "unhandled exception" as the message for RuntimeError instances with an empty message
PullRequest: truffleruby/4057
2 parents 20b9eaf + 25189e8 commit 670fdb4

File tree

4 files changed

+68
-6
lines changed

4 files changed

+68
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Compatibility:
1717
* Fix `rb_enc_vsprintf` and force String encoding instead of converting it (@andrykonchin).
1818
* Add `rb_gc_mark_movable` function (@andrykonchin).
1919
* Promote `File#path` and `File#to_path` to `IO#path` and `IO#to_path` and make IO#new accept an optional `path:` keyword argument (#3039, @moste00)
20+
* Display "unhandled exception" as the message for `RuntimeError` instances with an empty message (#3255, @nirvdrum).
2021

2122
Performance:
2223

spec/ruby/core/exception/full_message_spec.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,49 @@
4646
full_message[0].should.end_with?("': Some runtime error (RuntimeError)\n")
4747
end
4848

49+
describe "includes details about whether an exception was handled" do
50+
describe "RuntimeError" do
51+
it "should report as unhandled if message is empty" do
52+
err = RuntimeError.new("")
53+
54+
err.full_message.should =~ /unhandled exception/
55+
err.full_message(highlight: true).should =~ /unhandled exception/
56+
err.full_message(highlight: false).should =~ /unhandled exception/
57+
end
58+
59+
it "should not report as unhandled if the message is not empty" do
60+
err = RuntimeError.new("non-empty")
61+
62+
err.full_message.should !~ /unhandled exception/
63+
err.full_message(highlight: true).should !~ /unhandled exception/
64+
err.full_message(highlight: false).should !~ /unhandled exception/
65+
end
66+
67+
it "should not report as unhandled if the message is nil" do
68+
err = RuntimeError.new(nil)
69+
70+
err.full_message.should !~ /unhandled exception/
71+
err.full_message(highlight: true).should !~ /unhandled exception/
72+
err.full_message(highlight: false).should !~ /unhandled exception/
73+
end
74+
75+
it "should not report as unhandled if the message is not specified" do
76+
err = RuntimeError.new()
77+
78+
err.full_message.should !~ /unhandled exception/
79+
err.full_message(highlight: true).should !~ /unhandled exception/
80+
err.full_message(highlight: false).should !~ /unhandled exception/
81+
end
82+
end
83+
84+
describe "generic Error" do
85+
it "should not report as unhandled in any event" do
86+
StandardError.new("").full_message.should !~ /unhandled exception/
87+
StandardError.new("non-empty").full_message.should !~ /unhandled exception/
88+
end
89+
end
90+
end
91+
4992
it "shows the exception class at the end of the first line of the message when the message contains multiple lines" do
5093
begin
5194
line = __LINE__; raise "first line\nsecond line"

spec/tags/core/exception/detailed_message_tags.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

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

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

94+
# default implementation of Exception#detailed_message hook
9495
def self.detailed_message(exception, highlight)
9596
message = StringValue exception.message.to_s
9697

97-
klass = Primitive.class(exception).to_s
98+
exception_class = Primitive.class(exception)
99+
class_name = exception_class.to_s
100+
98101
if Primitive.is_a?(exception, Polyglot::ForeignException) and
99102
Truffle::Interop.has_meta_object?(exception)
100-
klass = "#{klass}: #{Truffle::Interop.meta_qualified_name Truffle::Interop.meta_object(exception)}"
103+
class_name = "#{class_name}: #{Truffle::Interop.meta_qualified_name Truffle::Interop.meta_object(exception)}"
101104
end
102105

103106
if message.empty?
104-
return highlight ? "\n\e[1m#{klass}\e[m" : klass
107+
message = Primitive.equal?(exception_class, RuntimeError) ? 'unhandled exception' : class_name
108+
message = "\n\e[1m#{message}\e[m" if highlight
109+
return message
105110
end
106111

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

109114
if highlight
110-
highlighted_class_string = !anonymous_class ? " (\e[1;4m#{klass}\e[m\e[1m)" : ''
115+
highlighted_class_string = !anonymous_class ? " (\e[1;4m#{class_name}\e[m\e[1m)" : ''
116+
111117
if message.include?("\n")
112118
first = true
113119
result = +''
120+
114121
message.each_line do |line|
115122
if first
116123
first = false
@@ -119,12 +126,13 @@ def self.detailed_message(exception, highlight)
119126
result << "\n\e[1m#{line.chomp}\e[m"
120127
end
121128
end
129+
122130
result
123131
else
124132
"\e[1m#{message}#{highlighted_class_string}\e[m"
125133
end
126134
else
127-
class_string = !anonymous_class ? " (#{klass})" : ''
135+
class_string = !anonymous_class ? " (#{class_name})" : ''
128136

129137
if i = message.index("\n")
130138
"#{message[0...i]}#{class_string}#{message[i..-1]}"
@@ -134,6 +142,9 @@ def self.detailed_message(exception, highlight)
134142
end
135143
end
136144

145+
# User can customise exception and override/undefine Exception#detailed_message method.
146+
# This way we need to handle corner cases when #detailed_message is undefined or
147+
# returns something other than String.
137148
def self.detailed_message_or_fallback(exception, options)
138149
unless Primitive.respond_to?(exception, :detailed_message, false)
139150
return detailed_message_fallback(exception, options)
@@ -177,29 +188,35 @@ def self.full_message(exception, **options)
177188

178189
result = ''.b
179190
bt = exception.backtrace || caller(2)
191+
180192
if reverse
181193
traceback_msg = if highlight
182194
"\e[1mTraceback\e[m (most recent call last):\n"
183195
else
184196
"Traceback (most recent call last):\n"
185197
end
198+
186199
result << traceback_msg
187200
append_causes(result, exception, {}.compare_by_identity, reverse, highlight, options)
188201
backtrace_message = backtrace_message(highlight, reverse, bt, exception, options)
202+
189203
if backtrace_message.empty?
190204
result << detailed_message_or_fallback(exception, options)
191205
else
192206
result << backtrace_message
193207
end
194208
else
195209
backtrace_message = backtrace_message(highlight, reverse, bt, exception, options)
210+
196211
if backtrace_message.empty?
197212
result << detailed_message_or_fallback(exception, options)
198213
else
199214
result << backtrace_message
200215
end
216+
201217
append_causes(result, exception, {}.compare_by_identity, reverse, highlight, options)
202218
end
219+
203220
result
204221
end
205222

@@ -255,6 +272,8 @@ def self.append_causes(str, err, causes, reverse, highlight, options)
255272
end
256273
end
257274

275+
# Return user provided message if it was specified.
276+
# A message might be computed (and assigned) lazily in some cases (e.g. for NoMethodError).
258277
def self.compute_message(exception)
259278
message = Primitive.exception_message(exception)
260279
# mimic CRuby behaviour and explicitly convert a user provided message to String

0 commit comments

Comments
 (0)