diff options
Diffstat (limited to 'test/ruby/test_exception.rb')
| -rw-r--r-- | test/ruby/test_exception.rb | 1000 |
1 files changed, 948 insertions, 52 deletions
diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb index 99cacc21f9..31e5aa9f6b 100644 --- a/test/ruby/test_exception.rb +++ b/test/ruby/test_exception.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false require 'test/unit' require 'tempfile' @@ -19,8 +20,8 @@ class TestException < Test::Unit::TestCase if bad bad = false retry - assert(false) end + assert(!bad) end assert(true) end @@ -77,6 +78,77 @@ class TestException < Test::Unit::TestCase assert(!bad) end + def test_exception_in_ensure_with_next + string = "[ruby-core:82936] [Bug #13930]" + assert_raise_with_message(RuntimeError, string) do + lambda do + next + rescue + assert(false) + ensure + raise string + end.call + assert(false) + end + + assert_raise_with_message(RuntimeError, string) do + flag = true + while flag + flag = false + begin + next + rescue + assert(false) + ensure + raise string + end + end + end + + iseq = RubyVM::InstructionSequence.compile(<<-RUBY) + begin + while true + break + end + rescue + end + RUBY + + assert_equal false, iseq.to_a[13].any?{|(e,_)| e == :throw} + end + + def test_exception_in_ensure_with_redo + string = "[ruby-core:82936] [Bug #13930]" + + assert_raise_with_message(RuntimeError, string) do + i = 0 + lambda do + i += 1 + redo if i < 2 + rescue + assert(false) + ensure + raise string + end.call + assert(false) + end + end + + def test_exception_in_ensure_with_return + @string = "[ruby-core:97104] [Bug #16618]" + def self.meow + return if true # This if modifier suppresses "warning: statement not reached" + assert(false) + rescue + assert(false) + ensure + raise @string + end + assert_raise_with_message(RuntimeError, @string) do + meow + end + end + def test_errinfo_in_debug bug9568 = EnvUtil.labeled_class("[ruby-core:61091] [Bug #9568]", RuntimeError) do def to_s @@ -180,6 +252,37 @@ class TestException < Test::Unit::TestCase } end + def test_catch_throw_in_require_cant_be_rescued + bug18562 = '[ruby-core:107403]' + Tempfile.create(["dep", ".rb"]) {|t| + t.puts("throw :extdep, 42") + t.close + + rescue_all = Class.new(Exception) + def rescue_all.===(_) + raise "should not reach here" + end + + v = assert_throw(:extdep, bug18562) do + require t.path + rescue rescue_all + assert(false, "should not reach here") + end + + assert_equal(42, v, bug18562) + } + end + + def test_throw_false + bug12743 = '[ruby-core:77229] [Bug #12743]' + Thread.start { + e = assert_raise_with_message(UncaughtThrowError, /false/, bug12743) { + throw false + } + assert_same(false, e.tag, bug12743) + }.join + end + def test_else_no_exception begin assert(true) @@ -302,6 +405,10 @@ class TestException < Test::Unit::TestCase assert_raise_with_message(TypeError, /C\u{4032}/) do [*o] end + obj = eval("class C\u{1f5ff}; self; end").new + assert_raise_with_message(TypeError, /C\u{1f5ff}/) do + Class.new {include obj} + end end def test_errat @@ -309,7 +416,7 @@ class TestException < Test::Unit::TestCase assert_in_out_err([], "$@ = 1", [], /\$! not set \(ArgumentError\)$/) - assert_in_out_err([], <<-INPUT, [], /backtrace must be Array of String \(TypeError\)$/) + assert_in_out_err([], <<-INPUT, [], /backtrace must be an Array of String or an Array of Thread::Backtrace::Location \(TypeError\)$/) begin raise rescue @@ -337,8 +444,10 @@ class TestException < Test::Unit::TestCase end def test_thread_signal_location - _, stderr, _ = EnvUtil.invoke_ruby("--disable-gems -d", <<-RUBY, false, true) + # pend('TODO: a known bug [Bug #14474]') + _, stderr, _ = EnvUtil.invoke_ruby(%w"--disable-gems -d", <<-RUBY, false, true) Thread.start do + Thread.current.report_on_exception = false begin Process.kill(:INT, $$) ensure @@ -369,6 +478,12 @@ end.join def to_s; ""; end end assert_equal(e.inspect, e.new.inspect) + + # https://bugs.ruby-lang.org/issues/18170#note-13 + assert_equal('#<Exception:"foo\nbar">', Exception.new("foo\nbar").inspect) + assert_equal('#<Exception: foo bar>', Exception.new("foo bar").inspect) + assert_equal('#<Exception: foo\bar>', Exception.new("foo\\bar").inspect) + assert_equal('#<Exception: "foo\nbar">', Exception.new('"foo\nbar"').inspect) end def test_to_s @@ -393,6 +508,16 @@ end.join assert_raise(TypeError) { e.set_backtrace(1) } assert_raise(TypeError) { e.set_backtrace([1]) } + + error = assert_raise(TypeError) do + e.set_backtrace(caller_locations(1, 1) + ["foo"]) + end + assert_include error.message, "backtrace must be an Array of String or an Array of Thread::Backtrace::Location" + + error = assert_raise(TypeError) do + e.set_backtrace(["foo"] + caller_locations(1, 1)) + end + assert_include error.message, "backtrace must be an Array of String or an Array of Thread::Backtrace::Location" end def test_exit_success_p @@ -421,20 +546,18 @@ end.join assert_not_send([e, :success?], "abort means failure") end - def test_nomethoderror - bug3237 = '[ruby-core:29948]' - str = "\u2600" - id = :"\u2604" - msg = "undefined method `#{id}' for #{str.inspect}:String" - assert_raise_with_message(NoMethodError, msg, bug3237) do - str.__send__(id) - end - end - def test_errno assert_equal(Encoding.find("locale"), Errno::EINVAL.new.message.encoding) end + def test_errno_constants + assert_equal [:NOERROR], Errno.constants.grep_v(/\AE/) + all_assertions_foreach("should be a subclass of SystemCallError", *Errno.constants) do |c| + e = Errno.const_get(c) + assert_operator e, :<, SystemCallError, proc {e.ancestors.inspect} + end + end + def test_too_many_args_in_eval bug5720 = '[ruby-core:41520]' arg_string = (0...140000).to_a.join(", ") @@ -471,20 +594,44 @@ end.join end def test_exception_in_name_error_to_str + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") bug5575 = '[ruby-core:41612]' - Tempfile.create(["test_exception_in_name_error_to_str", ".rb"]) do |t| - t.puts <<-EOC + begin; begin BasicObject.new.inspect rescue - $!.inspect - end - EOC - t.close - assert_nothing_raised(NameError, bug5575) do - load(t.path) + assert_nothing_raised(NameError, bug5575) {$!.inspect} end + end; + end + + def test_ensure_after_nomemoryerror + omit "Forcing NoMemoryError causes problems in some environments" + assert_separately([], "$_ = 'a' * 1_000_000_000_000_000_000") + rescue NoMemoryError + assert_raise(NoMemoryError) do + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + bug15779 = bug15779 = '[ruby-core:92342]' + begin; + require 'open-uri' + + begin + 'a' * 1_000_000_000_000_000_000 + ensure + URI.open('http://www.ruby-lang.org/') + end + end; end + rescue Test::Unit::AssertionFailedError + # Possibly compiled with -DRUBY_DEBUG, in which + # case rb_bug is used instead of NoMemoryError, + # and we cannot test ensure after NoMemoryError. + rescue RangeError + # MingW can raise RangeError instead of NoMemoryError, + # so we cannot test this case. + rescue Timeout::Error + # Solaris 11 CI times out instead of raising NoMemoryError, + # so we cannot test this case. end def test_equal @@ -494,19 +641,28 @@ end.join end def test_exception_in_exception_equal + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") bug5865 = '[ruby-core:41979]' - Tempfile.create(["test_exception_in_exception_equal", ".rb"]) do |t| - t.puts <<-EOC + begin; o = Object.new def o.exception(arg) end - _ = RuntimeError.new("a") == o - EOC - t.close assert_nothing_raised(ArgumentError, bug5865) do - load(t.path) + RuntimeError.new("a") == o end + end; + end + + def test_backtrace_by_exception + begin + line = __LINE__; raise "foo" + rescue => e end + e2 = e.exception("bar") + assert_not_equal(e.message, e2.message) + assert_equal(e.backtrace, e2.backtrace) + loc = e2.backtrace_locations[0] + assert_equal([__FILE__, line], [loc.path, loc.lineno]) end Bug4438 = '[ruby-core:35364]' @@ -529,28 +685,6 @@ end.join end end - def test_to_s_taintness_propagation - for exc in [Exception, NameError] - m = "abcdefg" - e = exc.new(m) - e.taint - s = e.to_s - assert_equal(false, m.tainted?, - "#{exc}#to_s should not propagate taintness") - assert_equal(false, s.tainted?, - "#{exc}#to_s should not propagate taintness") - end - - o = Object.new - def o.to_str - "foo" - end - o.taint - e = NameError.new(o) - s = e.to_s - assert_equal(false, s.tainted?) - end - def m m(&->{return 0}) 42 @@ -569,7 +703,7 @@ end.join def test_machine_stackoverflow bug9109 = '[ruby-dev:47804] [Bug #9109]' - assert_separately(%w[--disable-gem], <<-SRC) + assert_separately([], <<-SRC) assert_raise(SystemStackError, #{bug9109.dump}) { h = {a: ->{h[:a].call}} h[:a].call @@ -580,7 +714,7 @@ end.join def test_machine_stackoverflow_by_define_method bug9454 = '[ruby-core:60113] [Bug #9454]' - assert_separately(%w[--disable-gem], <<-SRC) + assert_separately([], <<-SRC) assert_raise(SystemStackError, #{bug9454.dump}) { define_method(:foo) {self.foo} self.foo @@ -589,6 +723,30 @@ end.join rescue SystemStackError end + def test_machine_stackoverflow_by_trace + assert_normal_exit("#{<<-"begin;"}\n#{<<~"end;"}", timeout: 60) + begin; + require 'timeout' + require 'tracer' + class HogeError < StandardError + def to_s + message.upcase # disable tailcall optimization + end + end + Tracer.stdout = open(IO::NULL, "w") + begin + Timeout.timeout(5) do + Tracer.on + HogeError.new.to_s + end + rescue Timeout::Error + # ok. there are no SEGV or critical error + rescue SystemStackError => e + # ok. + end + end; + end + def test_cause msg = "[Feature #8257]" cause = nil @@ -618,11 +776,62 @@ end.join assert_not_same(e, e.cause, "#{msg}: should not be recursive") end + def test_cause_raised_in_rescue + a = nil + e = assert_raise_with_message(RuntimeError, 'b') { + begin + raise 'a' + rescue => a + begin + raise 'b' + rescue => b + assert_same(a, b.cause) + begin + raise 'c' + rescue + raise b + end + end + end + } + assert_same(a, e.cause, 'cause should not be overwritten by reraise') + end + + def test_cause_at_raised + a = nil + e = assert_raise_with_message(RuntimeError, 'b') { + begin + raise 'a' + rescue => a + b = RuntimeError.new('b') + assert_nil(b.cause) + begin + raise 'c' + rescue + raise b + end + end + } + assert_equal('c', e.cause.message, 'cause should be the exception at raised') + assert_same(a, e.cause.cause) + end + + def test_cause_at_end + errs = [ + /-: unexpected return\n/, + /.*undefined local variable or method 'n'.*\n/, + ] + assert_in_out_err([], <<-'end;', [], errs) + END{n}; END{return} + end; + end + def test_raise_with_cause msg = "[Feature #8257]" cause = ArgumentError.new("foobar") e = assert_raise(RuntimeError) {raise msg, cause: cause} assert_same(cause, e.cause) + assert_raise(TypeError) {raise msg, {cause: cause}} end def test_cause_with_no_arguments @@ -632,6 +841,77 @@ end.join end end + def test_raise_with_cause_in_rescue + e = assert_raise_with_message(RuntimeError, 'b') { + begin + raise 'a' + rescue => a + begin + raise 'b' + rescue => b + assert_same(a, b.cause) + begin + raise 'c' + rescue + raise b, cause: ArgumentError.new('d') + end + end + end + } + assert_equal('d', e.cause.message, 'cause option should be honored always') + assert_nil(e.cause.cause) + end + + def test_cause_thread_no_cause + bug12741 = '[ruby-core:77222] [Bug #12741]' + + x = Thread.current + a = false + y = Thread.start do + Thread.pass until a + x.raise "stop" + end + + begin + raise bug12741 + rescue + e = assert_raise_with_message(RuntimeError, "stop") do + a = true + sleep 1 + end + end + assert_nil(e.cause) + ensure + y.join + end + + def test_cause_thread_with_cause + bug12741 = '[ruby-core:77222] [Bug #12741]' + + x = Thread.current + q = Thread::Queue.new + y = Thread.start do + q.pop + begin + raise "caller's cause" + rescue + x.raise "stop" + end + end + + begin + raise bug12741 + rescue + e = assert_raise_with_message(RuntimeError, "stop") do + q.push(true) + sleep 1 + end + ensure + y.join + end + assert_equal("caller's cause", e.cause.message) + end + def test_unknown_option bug = '[ruby-core:63203] [Feature #8257] should pass unknown options' @@ -646,14 +926,630 @@ end.join e = assert_raise(exc, bug) {raise exc, "foo" => "bar", foo: "bar"} assert_equal({"foo" => "bar", foo: "bar"}, e.arg, bug) - e = assert_raise(exc, bug) {raise exc, "foo" => "bar", foo: "bar", cause: "zzz"} + e = assert_raise(exc, bug) {raise exc, "foo" => "bar", foo: "bar", cause: RuntimeError.new("zzz")} assert_equal({"foo" => "bar", foo: "bar"}, e.arg, bug) e = assert_raise(exc, bug) {raise exc, {}} assert_equal({}, e.arg, bug) end + def test_circular_cause + bug13043 = '[ruby-core:78688] [Bug #13043]' + begin + begin + raise "error 1" + ensure + orig_error = $! + begin + raise "error 2" + rescue => err + raise orig_error + end + end + rescue => x + end + assert_equal(orig_error, x) + assert_equal(orig_error, err.cause) + assert_nil(orig_error.cause, bug13043) + end + + def test_cause_with_frozen_exception + exc = ArgumentError.new("foo").freeze + assert_raise_with_message(ArgumentError, exc.message) { + raise exc, cause: RuntimeError.new("bar") + } + end + + def test_cause_exception_in_cause_message + assert_in_out_err([], "#{<<~"begin;"}\n#{<<~'end;'}") do |outs, errs, status| + begin; + exc = Class.new(StandardError) do + def initialize(obj, cnt) + super(obj) + @errcnt = cnt + end + def to_s + return super if @errcnt <= 0 + @errcnt -= 1 + raise "xxx" + end + end.new("ok", 10) + raise "[Bug #17033]", cause: exc + end; + assert_equal(1, errs.count {|m| m.include?("[Bug #17033]")}, proc {errs.pretty_inspect}) + end + end + def test_anonymous_message assert_in_out_err([], "raise Class.new(RuntimeError), 'foo'", [], /foo\n/) end + + def test_output_string_encoding + # "\x82\xa0" in cp932 is "\u3042" (Japanese hiragana 'a') + # change $stderr to force calling rb_io_write() instead of fwrite() + assert_in_out_err(["-Eutf-8:cp932"], '# coding: cp932 +$stderr = $stdout; raise "\x82\xa0"') do |outs, errs, status| + assert_equal 1, outs.size + assert_equal 0, errs.size + err = outs.first.force_encoding('utf-8') + assert_predicate err, :valid_encoding? + assert_match %r/\u3042/, err + end + end + + def test_multibyte_and_newline + bug10727 = '[ruby-core:67473] [Bug #10727]' + assert_in_out_err([], <<-'end;', [], /\u{306b 307b 3093 3054} \(E\)\n\u{6539 884c}/, bug10727, encoding: "UTF-8") + class E < StandardError + def initialize + super("\u{306b 307b 3093 3054}\n\u{6539 884c}") + end + end + raise E + end; + end + + def assert_null_char(src, *args, **opts) + begin + eval(src) + rescue => e + end + assert_not_nil(e) + assert_include(e.message, "\0") + # Disabled by [Feature #18367] + #assert_in_out_err([], src, [], [], *args, **opts) do |_, err,| + # err.each do |e| + # assert_not_include(e, "\0") + # end + #end + e + end + + def test_control_in_message + bug7574 = '[ruby-dev:46749]' + assert_null_char("#{<<~"begin;"}\n#{<<~'end;'}", bug7574) + begin; + Object.const_defined?("String\0") + end; + assert_null_char("#{<<~"begin;"}\n#{<<~'end;'}", bug7574) + begin; + Object.const_get("String\0") + end; + end + + def test_encoding_in_message + name = "\u{e9}t\u{e9}" + e = EnvUtil.with_default_external("US-ASCII") do + assert_raise(NameError) do + Object.const_get(name) + end + end + assert_include(e.message, name) + end + + def test_method_missing_reason_clear + bug10969 = '[ruby-core:68515] [Bug #10969]' + a = Class.new {def method_missing(*) super end}.new + assert_raise(NameError) {a.instance_eval("foo")} + assert_raise(NoMethodError, bug10969) {a.public_send("bar", true)} + end + + def test_message_of_name_error + assert_raise_with_message(NameError, /\Aundefined method 'foo' for module '#<Module:.*>'$/) do + Module.new do + module_function :foo + end + end + end + + def capture_warning_warn(category: false) + verbose = $VERBOSE + categories = Warning.categories.to_h {|cat| [cat, Warning[cat]]} + warning = [] + + ::Warning.class_eval do + alias_method :warn2, :warn + remove_method :warn + + if category + define_method(:warn) do |str, category: nil| + warning << [str, category] + end + else + define_method(:warn) do |str, category: nil| + warning << str + end + end + end + + $VERBOSE = true + Warning.categories.each {|cat| Warning[cat] = true} + yield + + return warning + ensure + $VERBOSE = verbose + categories.each {|cat, flag| Warning[cat] = flag} + + ::Warning.class_eval do + remove_method :warn + alias_method :warn, :warn2 + remove_method :warn2 + end + end + + def test_warning_warn + warning = capture_warning_warn {$asdfasdsda_test_warning_warn} + assert_match(/global variable '\$asdfasdsda_test_warning_warn' not initialized/, warning[0]) + + assert_equal(["a\nz\n"], capture_warning_warn {warn "a\n", "z"}) + assert_equal([], capture_warning_warn {warn}) + assert_equal(["\n"], capture_warning_warn {warn ""}) + end + + def test_warn_deprecated_backwards_compatibility_category + (message, category), = capture_warning_warn(category: true) do + $; = "www" + $; = nil + end + + assert_include message, 'deprecated' + assert_equal :deprecated, category + end + + def test_kernel_warn_uplevel + warning = capture_warning_warn {warn("test warning", uplevel: 0)} + assert_equal("#{__FILE__}:#{__LINE__-1}: warning: test warning\n", warning[0]) + def (obj = Object.new).w(n) warn("test warning", uplevel: n) end + warning = capture_warning_warn {obj.w(0)} + assert_equal("#{__FILE__}:#{__LINE__-2}: warning: test warning\n", warning[0]) + warning = capture_warning_warn {obj.w(1)} + assert_equal("#{__FILE__}:#{__LINE__-1}: warning: test warning\n", warning[0]) + assert_raise(ArgumentError) {warn("test warning", uplevel: -1)} + assert_in_out_err(["-e", "warn 'ok', uplevel: 1"], '', [], /warning:/) + warning = capture_warning_warn {warn("test warning", {uplevel: 0})} + assert_match(/test warning.*{uplevel: 0}/m, warning[0]) + warning = capture_warning_warn {warn("test warning", **{uplevel: 0})} + assert_equal("#{__FILE__}:#{__LINE__-1}: warning: test warning\n", warning[0]) + warning = capture_warning_warn {warn("test warning", {uplevel: 0}, **{})} + assert_equal("test warning\n{uplevel: 0}\n", warning[0]) + assert_raise(ArgumentError) {warn("test warning", foo: 1)} + end + + def test_warning_warn_invalid_argument + assert_raise(TypeError) do + ::Warning.warn nil + end + assert_raise(TypeError) do + ::Warning.warn 1 + end + assert_raise(Encoding::CompatibilityError) do + ::Warning.warn "\x00a\x00b\x00c".force_encoding("utf-16be") + end + end + + def test_warning_warn_circular_require_backtrace + warning = nil + path = nil + Tempfile.create(%w[circular .rb]) do |t| + path = File.realpath(t.path) + basename = File.basename(path) + t.puts "require '#{basename}'" + t.close + $LOAD_PATH.push(File.dirname(t)) + warning = capture_warning_warn { + assert require(basename) + } + ensure + $LOAD_PATH.pop + $LOADED_FEATURES.delete(t.path) + end + assert_equal(1, warning.size) + assert_match(/circular require/, warning.first) + assert_match(/^\tfrom #{Regexp.escape(path)}:1:/, warning.first) + end + + def test_warning_warn_super + assert_in_out_err(%[-W0], "#{<<~"{#"}\n#{<<~'};'}", [], /global variable '\$asdfiasdofa_test_warning_warn_super' not initialized/) + {# + module Warning + def warn(message) + super + end + end + + $VERBOSE = true + $asdfiasdofa_test_warning_warn_super + }; + end + + def test_warning_category + assert_raise(TypeError) {Warning[nil]} + assert_raise(ArgumentError) {Warning[:XXXX]} + + all_assertions_foreach("categories", *Warning.categories) do |cat| + value = Warning[cat] + assert_include([true, false], value) + + enabled = EnvUtil.verbose_warning do + Warning[cat] = true + Warning.warn "#{cat} feature", category: cat + end + disabled = EnvUtil.verbose_warning do + Warning[cat] = false + Warning.warn "#{cat} feature", category: cat + end + ensure + Warning[cat] = value + assert_equal "#{cat} feature", enabled + assert_empty disabled + end + end + + def test_undef_Warning_warn + assert_separately([], "#{<<-"begin;"}\n#{<<-"end;"}") + begin; + Warning.undef_method(:warn) + assert_raise(NoMethodError) { warn "" } + end; + end + + def test_undefined_backtrace + assert_separately([], "#{<<-"begin;"}\n#{<<-"end;"}") + begin; + class Exception + undef backtrace + end + + assert_raise(RuntimeError) { + raise RuntimeError, "hello" + } + end; + end + + def test_redefined_backtrace + assert_separately([], "#{<<-"begin;"}\n#{<<-"end;"}") + begin; + $exc = nil + + class Exception + undef backtrace + def backtrace + $exc = self + end + end + + e = assert_raise(RuntimeError) { + raise RuntimeError, "hello" + } + assert_same(e, $exc) + end; + end + + def test_blocking_backtrace + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class Bug < RuntimeError + def backtrace + File.readlines(IO::NULL) + end + end + bug = Bug.new '[ruby-core:85939] [Bug #14577]' + n = 10000 + i = 0 + n.times do + begin + raise bug + rescue Bug + i += 1 + end + end + assert_equal(n, i) + end; + end + + def test_wrong_backtrace + assert_separately([], "#{<<-"begin;"}\n#{<<-"end;"}") + begin; + class Exception + undef backtrace + def backtrace(a) + end + end + + assert_raise(RuntimeError) { + raise RuntimeError, "hello" + } + end; + + error_class = Class.new(StandardError) do + def backtrace; :backtrace; end + end + begin + raise error_class + rescue error_class => e + assert_raise(TypeError) {$@} + assert_raise(TypeError) {e.full_message} + end + end + + def test_backtrace_in_eval + bug = '[ruby-core:84434] [Bug #14229]' + assert_in_out_err(['-e', 'eval("raise")'], "", [], /^\(eval at .*\):1:/, bug) + end + + def test_full_message + message = RuntimeError.new("testerror").full_message + assert_operator(message, :end_with?, "\n") + + test_method = "def foo; raise 'testerror'; end" + + out1, err1, status1 = EnvUtil.invoke_ruby(['-e', "#{test_method}; begin; foo; rescue => e; puts e.full_message; end"], '', true, true) + assert_predicate(status1, :success?) + assert_empty(err1, "expected nothing wrote to $stdout by #full_message") + + _, err2, status1 = EnvUtil.invoke_ruby(['-e', "#{test_method}; begin; foo; end"], '', true, true) + assert_equal(err2, out1) + + e = RuntimeError.new("a\n") + message = assert_nothing_raised(ArgumentError, proc {e.pretty_inspect}) do + e.full_message + end + assert_operator(message, :end_with?, "\n") + message = message.gsub(/\e\[[\d;]*m/, '') + assert_not_operator(message, :end_with?, "\n\n") + e = RuntimeError.new("a\n\nb\n\nc") + message = assert_nothing_raised(ArgumentError, proc {e.pretty_inspect}) do + e.full_message + end + assert_all?(message.lines) do |m| + /\e\[\d[;\d]*m[^\e]*\n/ !~ m + end + + e = RuntimeError.new("testerror") + message = e.full_message(highlight: false) + assert_not_match(/\e/, message) + + bt = ["test:100", "test:99", "test:98", "test:1"] + e = assert_raise(RuntimeError) {raise RuntimeError, "testerror", bt} + + bottom = "test:100: testerror (RuntimeError)\n" + top = "test:1\n" + remark = "Traceback (most recent call last):" + + message = e.full_message(highlight: false, order: :top) + assert_not_match(/\e/, message) + assert_operator(message.count("\n"), :>, 2) + assert_operator(message, :start_with?, bottom) + assert_operator(message, :end_with?, top) + + message = e.full_message(highlight: false, order: :bottom) + assert_not_match(/\e/, message) + assert_operator(message.count("\n"), :>, 2) + assert_operator(message, :start_with?, remark) + assert_operator(message, :end_with?, bottom) + + assert_raise_with_message(ArgumentError, /:top or :bottom/) { + e.full_message(highlight: false, order: :middle) + } + + message = e.full_message(highlight: true) + assert_match(/\e/, message) + assert_not_match(/(\e\[1)m\1/, message) + e2 = assert_raise(RuntimeError) {raise RuntimeError, "", bt} + assert_not_match(/(\e\[1)m\1/, e2.full_message(highlight: true)) + + message = e.full_message + if Exception.to_tty? + assert_match(/\e/, message) + message = message.gsub(/\e\[[\d;]*m/, '') + else + assert_not_match(/\e/, message) + end + assert_operator(message, :start_with?, bottom) + assert_operator(message, :end_with?, top) + end + + def test_exception_in_message + code = "#{<<~"begin;"}\n#{<<~'end;'}" + begin; + class Bug14566 < StandardError + def message; raise self.class; end + end + raise Bug14566 + end; + assert_in_out_err([], code, [], /Bug14566/, success: false, timeout: 2) + end + + def test_non_exception_cause + assert_raise_with_message(TypeError, /exception/) do + raise "foo", cause: 1 + end; + end + + def test_circular_cause_handle + assert_raise_with_message(ArgumentError, /circular cause/) do + begin + raise "error 1" + rescue => e1 + raise "error 2" rescue raise e1, cause: $! + end + end; + end + + def test_marshal_circular_cause + dump = "\x04\bo:\x11RuntimeError\b:\tmesgI\"\berr\x06:\x06ET:\abt[\x00:\ncause@\x05" + assert_raise_with_message(ArgumentError, /circular cause/, ->{dump.inspect}) do + e = Marshal.load(dump) + assert_same(e, e.cause) + end + end + + def test_super_in_method_missing + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + $VERBOSE = nil + class Object + def method_missing(name, *args, &block) + super + end + end + + bug14670 = '[ruby-dev:50522] [Bug #14670]' + assert_raise_with_message(NoMethodError, /'foo'/, bug14670) do + Object.new.foo + end + end; + end + + def test_detailed_message + e = RuntimeError.new("message") + assert_equal("message (RuntimeError)", e.detailed_message) + assert_equal("\e[1mmessage (\e[1;4mRuntimeError\e[m\e[1m)\e[m", e.detailed_message(highlight: true)) + + e = RuntimeError.new("foo\nbar\nbaz") + assert_equal("foo (RuntimeError)\nbar\nbaz", e.detailed_message) + assert_equal("\e[1mfoo (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n\e[1mbar\e[m\n\e[1mbaz\e[m", e.detailed_message(highlight: true)) + + e = RuntimeError.new("") + assert_equal("unhandled exception", e.detailed_message) + assert_equal("\e[1;4munhandled exception\e[m", e.detailed_message(highlight: true)) + + e = RuntimeError.new + assert_equal("RuntimeError (RuntimeError)", e.detailed_message) + assert_equal("\e[1mRuntimeError (\e[1;4mRuntimeError\e[m\e[1m)\e[m", e.detailed_message(highlight: true)) + end + + def test_detailed_message_under_gc_compact_stress + omit "compaction doesn't work well on s390x" if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + EnvUtil.under_gc_compact_stress do + e = RuntimeError.new("foo\nbar\nbaz") + assert_equal("foo (RuntimeError)\nbar\nbaz", e.detailed_message) + assert_equal("\e[1mfoo (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n\e[1mbar\e[m\n\e[1mbaz\e[m", e.detailed_message(highlight: true)) + end + end + + def test_full_message_with_custom_detailed_message + e = RuntimeError.new("message") + opt_ = nil + e.define_singleton_method(:detailed_message) do |**opt| + opt_ = opt + "BOO!" + end + assert_match("BOO!", e.full_message.lines.first) + assert_equal({ highlight: Exception.to_tty? }, opt_) + end + + def test_full_message_with_encoding + message = "\u{dc}bersicht" + begin + begin + raise message + rescue => e + raise "\n#{e.message}" + end + rescue => e + end + assert_include(e.full_message, message) + end + + def test_syntax_error_detailed_message + Dir.mktmpdir do |dir| + File.write(File.join(dir, "detail.rb"), "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class SyntaxError + def detailed_message(**) + Thread.new {}.join + "<#{super}>\n""<#{File.basename(__FILE__)}>" + rescue ThreadError => e + e.message + end + end + end; + pattern = /^<detail\.rb>/ + assert_in_out_err(%W[-r#{dir}/detail -], "1+", [], pattern) + + File.write(File.join(dir, "main.rb"), "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + 1 + + end; + assert_in_out_err(%W[-r#{dir}/detail #{dir}/main.rb]) do |stdout, stderr,| + assert_empty(stdout) + assert_not_empty(stderr.grep(pattern)) + error, = stderr.grep(/unexpected end-of-input/) + assert_not_nil(error) + assert_match(/<.*unexpected end-of-input.*>|\^ unexpected end-of-input,/, error) + end + end + end + + def test_syntax_error_path + e = assert_raise(SyntaxError) { + eval("1+", nil, "test_syntax_error_path.rb") + } + assert_equal("test_syntax_error_path.rb", e.path) + + Dir.mktmpdir do |dir| + lib = File.join(dir, "syntax_error-path.rb") + File.write(lib, "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + class SyntaxError + def detailed_message(**) + STDERR.puts "\n""path=#{path}\n" + super + end + end + end; + main = File.join(dir, "syntax_error.rb") + File.write(main, "1+\n") + assert_in_out_err(%W[-r#{lib} #{main}], "", [], [:*, "\n""path=#{main}\n", :*]) + end + end + + class Ex; end + + def test_exception_message_for_unexpected_implicit_conversion_type + a = Ex.new + def self.x(a) = nil + + assert_raise_with_message(TypeError, "no implicit conversion of TestException::Ex into Hash") do + x(**a) + end + assert_raise_with_message(TypeError, "no implicit conversion of TestException::Ex into Proc") do + x(&a) + end + + def a.to_a = 1 + def a.to_hash = 1 + def a.to_proc = 1 + assert_raise_with_message(TypeError, "can't convert TestException::Ex to Array (TestException::Ex#to_a gives Integer)") do + x(*a) + end + assert_raise_with_message(TypeError, "can't convert TestException::Ex to Hash (TestException::Ex#to_hash gives Integer)") do + x(**a) + end + assert_raise_with_message(TypeError, "can't convert TestException::Ex to Proc (TestException::Ex#to_proc gives Integer)") do + x(&a) + end + end end |
