diff options
Diffstat (limited to 'test/ruby/test_exception.rb')
| -rw-r--r-- | test/ruby/test_exception.rb | 1218 |
1 files changed, 1101 insertions, 117 deletions
diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb index ea733b3add..31e5aa9f6b 100644 --- a/test/ruby/test_exception.rb +++ b/test/ruby/test_exception.rb @@ -1,6 +1,6 @@ +# frozen_string_literal: false require 'test/unit' require 'tempfile' -require_relative 'envutil' class TestException < Test::Unit::TestCase def test_exception_rescued @@ -20,15 +20,15 @@ class TestException < Test::Unit::TestCase if bad bad = false retry - assert(false) end + assert(!bad) end assert(true) end def test_exception_in_rescue string = "this must be handled no.3" - e = assert_raise(RuntimeError) do + assert_raise_with_message(RuntimeError, string) do begin raise "exception in rescue clause" rescue @@ -36,12 +36,11 @@ class TestException < Test::Unit::TestCase end assert(false) end - assert_equal(string, e.message) end def test_exception_in_ensure string = "exception in ensure clause" - e = assert_raise(RuntimeError) do + assert_raise_with_message(RuntimeError, string) do begin raise "this must be handled no.4" ensure @@ -51,7 +50,6 @@ class TestException < Test::Unit::TestCase end assert(false) end - assert_equal(string, e.message) end def test_exception_ensure @@ -80,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 @@ -131,28 +200,87 @@ class TestException < Test::Unit::TestCase assert(!bad) end + def test_catch_no_throw + assert_equal(:foo, catch {:foo}) + end + def test_catch_throw - assert(catch(:foo) { - loop do - loop do - throw :foo, true - break - end - break - assert(false) # should no reach here - end - false - }) + result = catch(:foo) { + loop do + loop do + throw :foo, true + break + end + assert(false, "should not reach here") + end + false + } + assert(result) + end + + def test_catch_throw_noarg + assert_nothing_raised(UncaughtThrowError) { + result = catch {|obj| + throw obj, :ok + assert(false, "should not reach here") + } + assert_equal(:ok, result) + } + end + + def test_uncaught_throw + tag = nil + e = assert_raise_with_message(UncaughtThrowError, /uncaught throw/) { + catch("foo") {|obj| + tag = obj.dup + throw tag, :ok + assert(false, "should not reach here") + } + assert(false, "should not reach here") + } + assert_not_nil(tag) + assert_same(tag, e.tag) + assert_equal(:ok, e.value) end def test_catch_throw_in_require bug7185 = '[ruby-dev:46234]' - t = Tempfile.open(["dep", ".rb"]) - t.puts("throw :extdep, 42") - t.close - assert_equal(42, catch(:extdep) {require t.path}, bug7185) - ensure - t.close! if t + Tempfile.create(["dep", ".rb"]) {|t| + t.puts("throw :extdep, 42") + t.close + assert_equal(42, assert_throw(:extdep, bug7185) {require t.path}, bug7185) + } + 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 @@ -266,20 +394,21 @@ class TestException < Test::Unit::TestCase def test_type_error_message_encoding c = eval("Module.new do break class C\u{4032}; self; end; end") o = c.new - e = assert_raise(TypeError) do + assert_raise_with_message(TypeError, /C\u{4032}/) do ""[o] end - assert_match(/C\u{4032}/, e.message) c.class_eval {def to_int; self; end} - e = assert_raise(TypeError) do + assert_raise_with_message(TypeError, /C\u{4032}/) do ""[o] end - assert_match(/C\u{4032}/, e.message) c.class_eval {def to_a; self; end} - assert_raise(TypeError) do + assert_raise_with_message(TypeError, /C\u{4032}/) do [*o] end - assert_match(/C\u{4032}/, e.message) + 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 @@ -287,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 @@ -314,28 +443,11 @@ class TestException < Test::Unit::TestCase INPUT end - def test_safe4 - cmd = proc{raise SystemExit} - safe0_p = proc{|*args| args} - - test_proc = proc { - $SAFE = 4 - begin - cmd.call - rescue SystemExit => e - safe0_p["SystemExit: #{e.inspect}"] - raise e - rescue Exception => e - safe0_p["Exception (NOT SystemExit): #{e.inspect}"] - raise e - end - } - assert_raise(SystemExit, '[ruby-dev:38760]') {test_proc.call} - end - def test_thread_signal_location - _, stderr, _ = EnvUtil.invoke_ruby("-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 @@ -366,6 +478,23 @@ 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 + e = StandardError.new("foo") + assert_equal("foo", e.to_s) + + def (s = Object.new).to_s + "bar" + end + e = StandardError.new(s) + assert_equal("bar", e.to_s) end def test_set_backtrace @@ -379,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 @@ -407,18 +546,18 @@ end.join assert_not_send([e, :success?], "abort means failure") end - def test_nomethoderror - bug3237 = '[ruby-core:29948]' - str = "\u2600" - id = :"\u2604" - e = assert_raise(NoMethodError) {str.__send__(id)} - assert_equal("undefined method `#{id}' for #{str.inspect}:String", e.message, bug3237) - 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(", ") @@ -455,23 +594,44 @@ end.join end def test_exception_in_name_error_to_str + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") bug5575 = '[ruby-core:41612]' - t = nil - Tempfile.open(["test_exception_in_name_error_to_str", ".rb"]) do |f| - t = f - t.puts <<-EOC + begin; begin BasicObject.new.inspect rescue - $!.inspect + assert_nothing_raised(NameError, bug5575) {$!.inspect} end - EOC - end - assert_nothing_raised(NameError, bug5575) do - load(t.path) + 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 - ensure - t.close(true) if t + 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 @@ -481,22 +641,28 @@ end.join end def test_exception_in_exception_equal + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") bug5865 = '[ruby-core:41979]' - t = nil - Tempfile.open(["test_exception_in_exception_equal", ".rb"]) do |f| - t = f - t.puts <<-EOC + begin; o = Object.new def o.exception(arg) end - _ = RuntimeError.new("a") == o - EOC - end - assert_nothing_raised(ArgumentError, bug5865) do - load(t.path) + assert_nothing_raised(ArgumentError, bug5865) do + RuntimeError.new("a") == o + end + end; + end + + def test_backtrace_by_exception + begin + line = __LINE__; raise "foo" + rescue => e end - ensure - t.close(true) if t + 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]' @@ -519,53 +685,871 @@ 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") + def m + m(&->{return 0}) + 42 + end + + def test_stackoverflow + feature6216 = '[ruby-core:43794] [Feature #6216]' + e = assert_raise(SystemStackError, feature6216) {m} + level = e.backtrace.size + assert_operator(level, :>, 10, feature6216) + + feature6216 = '[ruby-core:63377] [Feature #6216]' + e = assert_raise(SystemStackError, feature6216) {raise e} + assert_equal(level, e.backtrace.size, feature6216) + end + + def test_machine_stackoverflow + bug9109 = '[ruby-dev:47804] [Bug #9109]' + assert_separately([], <<-SRC) + assert_raise(SystemStackError, #{bug9109.dump}) { + h = {a: ->{h[:a].call}} + h[:a].call + } + SRC + rescue SystemStackError + end + + def test_machine_stackoverflow_by_define_method + bug9454 = '[ruby-core:60113] [Bug #9454]' + assert_separately([], <<-SRC) + assert_raise(SystemStackError, #{bug9454.dump}) { + define_method(:foo) {self.foo} + self.foo + } + SRC + 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 + e = assert_raise(StandardError) { + begin + raise msg + rescue => e + cause = e.cause + raise StandardError + end + } + assert_nil(cause, msg) + cause = e.cause + assert_instance_of(RuntimeError, cause, msg) + assert_equal(msg, cause.message, msg) + end + + def test_cause_reraised + msg = "[Feature #8257]" + e = assert_raise(RuntimeError) { + begin + raise msg + rescue => e + raise e + end + } + 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 + cause = ArgumentError.new("foobar") + assert_raise_with_message(ArgumentError, /with no arguments/) do + raise cause: cause + 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 - o = Object.new - def o.to_str - "foo" + begin + raise bug12741 + rescue + e = assert_raise_with_message(RuntimeError, "stop") do + a = true + sleep 1 + end end - o.taint - e = NameError.new(o) - s = e.to_s - assert_equal(false, s.tainted?) + assert_nil(e.cause) + ensure + y.join end - def test_exception_to_s_should_not_propagate_untrustedness - favorite_lang = "Ruby" + 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 - for exc in [Exception, NameError] - assert_raise(SecurityError) do - lambda { - $SAFE = 4 - exc.new(favorite_lang).to_s - favorite_lang.replace("Python") - }.call + 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 - assert_raise(SecurityError) do - lambda { - $SAFE = 4 - o = Object.new - o.singleton_class.send(:define_method, :to_str) { - favorite_lang - } - NameError.new(o).to_s - favorite_lang.replace("Python") - }.call + def test_unknown_option + bug = '[ruby-core:63203] [Feature #8257] should pass unknown options' + + exc = Class.new(RuntimeError) do + attr_reader :arg + def initialize(msg = nil) + @arg = msg + super(msg) + end end - assert_equal("Ruby", favorite_lang) + 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: 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 |
