diff options
Diffstat (limited to 'test/ruby/test_thread.rb')
| -rw-r--r-- | test/ruby/test_thread.rb | 1055 |
1 files changed, 852 insertions, 203 deletions
diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index a05123f81d..2a61fc3450 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -1,7 +1,8 @@ # -*- coding: us-ascii -*- +# frozen_string_literal: false require 'test/unit' -require 'thread' -require_relative 'envutil' +require "rbconfig/sizeof" +require "timeout" class TestThread < Test::Unit::TestCase class Thread < ::Thread @@ -27,6 +28,36 @@ class TestThread < Test::Unit::TestCase end end + def test_inspect + m = Thread::Mutex.new + m.lock + line = __LINE__+1 + th = Module.new {break module_eval("class C\u{30b9 30ec 30c3 30c9} < Thread; self; end")}.start do + m.synchronize {} + end + Thread.pass until th.stop? + s = th.inspect + assert_include(s, "::C\u{30b9 30ec 30c3 30c9}:") + assert_include(s, " #{__FILE__}:#{line} ") + assert_equal(s, th.to_s) + ensure + m.unlock + th.join + end + + def test_inspect_with_fiber + inspect1 = inspect2 = nil + + Thread.new{ + inspect1 = Thread.current.inspect + Fiber.new{ + inspect2 = Thread.current.inspect + }.resume + }.join + + assert_equal inspect1, inspect2, '[Bug #13689]' + end + def test_main_thread_variable_in_enumerator assert_equal Thread.main, Thread.current @@ -64,14 +95,14 @@ class TestThread < Test::Unit::TestCase end def test_thread_variable? - refute Thread.new { Thread.current.thread_variable?("foo") }.join.value + Thread.new { assert_not_send([Thread.current, :thread_variable?, "foo"]) }.value t = Thread.new { Thread.current.thread_variable_set("foo", "bar") }.join - assert t.thread_variable?("foo") - assert t.thread_variable?(:foo) - refute t.thread_variable?(:bar) + assert_send([t, :thread_variable?, "foo"]) + assert_send([t, :thread_variable?, :foo]) + assert_not_send([t, :thread_variable?, :bar]) end def test_thread_variable_strings_and_symbols_are_the_same_key @@ -83,53 +114,43 @@ class TestThread < Test::Unit::TestCase def test_thread_variable_frozen t = Thread.new { }.join t.freeze - assert_raises(RuntimeError) do + assert_raise(FrozenError) do t.thread_variable_set(:foo, "bar") end end - def test_thread_variable_security - t = Thread.new { sleep } - - assert_raises(SecurityError) do - Thread.new { $SAFE = 4; t.thread_variable_get(:foo) }.join - end - - assert_raises(SecurityError) do - Thread.new { $SAFE = 4; t.thread_variable_set(:foo, :baz) }.join - end - end - def test_mutex_synchronize - m = Mutex.new + m = Thread::Mutex.new r = 0 - max = 10 - (1..max).map{ + num_threads = 10 + loop=100 + (1..num_threads).map{ Thread.new{ - i=0 - while i<max*max - i+=1 + loop.times{ m.synchronize{ - r += 1 + tmp = r + # empty and waste loop for making thread preemption + 100.times { + } + r = tmp + 1 } - end + } } }.each{|e| e.join } - assert_equal(max * max * max, r) + assert_equal(num_threads*loop, r) end def test_mutex_synchronize_yields_no_block_params bug8097 = '[ruby-core:53424] [Bug #8097]' - assert_empty(Mutex.new.synchronize {|*params| break params}, bug8097) + assert_empty(Thread::Mutex.new.synchronize {|*params| break params}, bug8097) end def test_local_barrier dir = File.dirname(__FILE__) lbtest = File.join(dir, "lbtest.rb") $:.unshift File.join(File.dirname(dir), 'ruby') - require 'envutil' $:.shift 3.times { `#{EnvUtil.rubybin} #{lbtest}` @@ -139,20 +160,26 @@ class TestThread < Test::Unit::TestCase def test_priority c1 = c2 = 0 - t1 = Thread.new { loop { c1 += 1 } } + run = true + t1 = Thread.new { c1 += 1 while run } t1.priority = 3 - t2 = Thread.new { loop { c2 += 1 } } + t2 = Thread.new { c2 += 1 while run } t2.priority = -3 assert_equal(3, t1.priority) assert_equal(-3, t2.priority) sleep 0.5 5.times do + assert_not_predicate(t1, :stop?) + assert_not_predicate(t2, :stop?) break if c1 > c2 sleep 0.1 end + run = false t1.kill t2.kill assert_operator(c1, :>, c2, "[ruby-dev:33124]") # not guaranteed + t1.join + t2.join end def test_new @@ -171,34 +198,89 @@ class TestThread < Test::Unit::TestCase end ensure - t1.kill if t1 - t2.kill if t2 + t1&.kill&.join + t2&.kill&.join + end + + def test_new_symbol_proc + bug = '[ruby-core:80147] [Bug #13313]' + assert_ruby_status([], "#{<<-"begin;"}\n#{<<-'end;'}", bug) + begin; + exit("1" == Thread.start(1, &:to_s).value) + end; end def test_join t = Thread.new { sleep } - assert_nil(t.join(0.5)) + assert_nil(t.join(0.05)) ensure - t.kill if t + t&.kill&.join end def test_join2 - t1 = Thread.new { sleep(1.5) } + ok = false + t1 = Thread.new { ok = true; sleep } + Thread.pass until ok + Thread.pass until t1.stop? t2 = Thread.new do - t1.join(1) + Thread.pass while ok + t1.join(0.01) end t3 = Thread.new do - sleep 0.5 + ok = false t1.join end assert_nil(t2.value) + t1.wakeup assert_equal(t1, t3.value) ensure - t1.kill if t1 - t2.kill if t2 - t3.kill if t3 + t1&.kill&.join + t2&.kill&.join + t3&.kill&.join + end + + def test_join_argument_conversion + t = Thread.new {} + + # Make sure that the thread terminates + Thread.pass while t.status + + assert_raise(TypeError) {t.join(:foo)} + + limit = Struct.new(:to_f, :count).new(0.05) + assert_same(t, t.join(limit)) + end + + { 'FIXNUM_MAX' => RbConfig::LIMITS['FIXNUM_MAX'], + 'UINT64_MAX' => RbConfig::LIMITS['UINT64_MAX'], + 'INFINITY' => Float::INFINITY + }.each do |name, limit| + define_method("test_join_limit_#{name}") do + t = Thread.new {} + assert_same t, t.join(limit), "limit=#{limit.inspect}" + end + end + + { 'minus_1' => -1, + 'minus_0_1' => -0.1, + 'FIXNUM_MIN' => RbConfig::LIMITS['FIXNUM_MIN'], + 'INT64_MIN' => RbConfig::LIMITS['INT64_MIN'], + 'minus_INFINITY' => -Float::INFINITY + }.each do |name, limit| + define_method("test_join_limit_negative_#{name}") do + t = Thread.new { sleep } + begin + assert_nothing_raised(Timeout::Error) do + Timeout.timeout(30) do + assert_nil t.join(limit), "limit=#{limit.inspect}" + end + end + ensure + t.kill + end + end end def test_kill_main_thread @@ -244,15 +326,14 @@ class TestThread < Test::Unit::TestCase Thread.stop s += 1 end - sleep 0.5 + Thread.pass until t.stop? assert_equal(1, s) t.wakeup - sleep 0.5 + Thread.pass while t.alive? assert_equal(2, s) assert_raise(ThreadError) { t.wakeup } - ensure - t.kill if t + t&.kill&.join end def test_stop @@ -270,7 +351,7 @@ class TestThread < Test::Unit::TestCase assert_in_out_err([], <<-INPUT) do |r, e| t1 = Thread.new { sleep } Thread.pass - t2 = Thread.new { loop { } } + t2 = Thread.new { loop { Thread.pass } } Thread.new { }.join p [Thread.current, t1, t2].map{|t| t.object_id }.sort p Thread.list.map{|t| t.object_id }.sort @@ -291,8 +372,11 @@ class TestThread < Test::Unit::TestCase assert_in_out_err([], <<-INPUT, %w(false 1), []) p Thread.abort_on_exception begin - Thread.new { raise } - sleep 0.5 + t = Thread.new { + Thread.current.report_on_exception = false + raise + } + Thread.pass until t.stop? p 1 rescue p 2 @@ -303,7 +387,10 @@ class TestThread < Test::Unit::TestCase Thread.abort_on_exception = true p Thread.abort_on_exception begin - Thread.new { raise } + Thread.new { + Thread.current.report_on_exception = false + raise + } sleep 0.5 p 1 rescue @@ -311,11 +398,11 @@ class TestThread < Test::Unit::TestCase end INPUT - assert_in_out_err(%w(--disable-gems -d), <<-INPUT, %w(false 2), %r".+") + assert_in_out_err(%w(-d), <<-INPUT, %w(false 2), %r".+") p Thread.abort_on_exception begin - Thread.new { raise } - sleep 0.5 + t = Thread.new { raise } + Thread.pass until t.stop? p 1 rescue p 2 @@ -325,9 +412,15 @@ class TestThread < Test::Unit::TestCase assert_in_out_err([], <<-INPUT, %w(false true 2), []) p Thread.abort_on_exception begin - t = Thread.new { sleep 0.5; raise } + ok = false + t = Thread.new { + Thread.current.report_on_exception = false + Thread.pass until ok + raise + } t.abort_on_exception = true p t.abort_on_exception + ok = 1 sleep 1 p 1 rescue @@ -336,12 +429,106 @@ class TestThread < Test::Unit::TestCase INPUT end + def test_report_on_exception + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + q1 = Thread::Queue.new + q2 = Thread::Queue.new + + assert_equal(true, Thread.report_on_exception, + "global flag is true by default") + assert_equal(true, Thread.current.report_on_exception, + "the main thread has report_on_exception=true") + + Thread.report_on_exception = true + Thread.current.report_on_exception = false + assert_equal(true, + Thread.start {Thread.current.report_on_exception}.value, + "should not inherit from the parent thread but from the global flag") + + assert_warn("", "exception should be ignored silently when false") { + th = Thread.start { + Thread.current.report_on_exception = false + q1.push(Thread.current.report_on_exception) + raise "report 1" + } + assert_equal(false, q1.pop) + Thread.pass while th.alive? + assert_raise(RuntimeError) { th.join } + } + + assert_warn(/report 2/, "exception should be reported when true") { + th = Thread.start { + q1.push(Thread.current.report_on_exception = true) + raise "report 2" + } + assert_equal(true, q1.pop) + Thread.pass while th.alive? + assert_raise(RuntimeError) { th.join } + } + + assert_warn("", "the global flag should not affect already started threads") { + Thread.report_on_exception = false + th = Thread.start { + q2.pop + q1.push(Thread.current.report_on_exception) + raise "report 3" + } + q2.push(Thread.report_on_exception = true) + assert_equal(false, q1.pop) + Thread.pass while th.alive? + assert_raise(RuntimeError) { th.join } + } + + assert_warn(/report 4/, "should defaults to the global flag at the start") { + Thread.report_on_exception = true + th = Thread.start { + q1.push(Thread.current.report_on_exception) + raise "report 4" + } + assert_equal(true, q1.pop) + Thread.pass while th.alive? + assert_raise(RuntimeError) { th.join } + } + + assert_warn(/report 5/, "should first report and then raise with report_on_exception + abort_on_exception") { + th = Thread.start { + Thread.current.report_on_exception = true + Thread.current.abort_on_exception = true + q2.pop + raise "report 5" + } + assert_raise_with_message(RuntimeError, "report 5") { + q2.push(true) + Thread.pass while th.alive? + } + assert_raise(RuntimeError) { th.join } + } + end; + end + + def test_ignore_deadlock + if /mswin|mingw/ =~ RUBY_PLATFORM + omit "can't trap a signal from another process on Windows" + end + assert_in_out_err([], <<-INPUT, %w(false :sig), [], :signal=>:INT, timeout: 1, timeout_error: nil) + p Thread.ignore_deadlock + q = Thread::Queue.new + trap(:INT){q.push :sig} + Thread.ignore_deadlock = true + p q.pop + INPUT + end + def test_status_and_stop_p - a = ::Thread.new { raise("die now") } + a = ::Thread.new { + Thread.current.report_on_exception = false + raise("die now") + } b = Thread.new { Thread.stop } c = Thread.new { Thread.exit } e = Thread.current - sleep 0.5 + Thread.pass while a.alive? or !b.stop? or c.alive? assert_equal(nil, a.status) assert_predicate(a, :stop?) @@ -356,11 +543,10 @@ class TestThread < Test::Unit::TestCase es1 = e.status es2 = e.stop? assert_equal(["run", false], [es1, es2]) - + assert_raise(RuntimeError) { a.join } ensure - a.kill if a - b.kill if b - c.kill if c + b&.kill&.join + c&.join end def test_switch_while_busy_loop @@ -378,17 +564,7 @@ class TestThread < Test::Unit::TestCase end assert(!flag, bug1402) ensure - waiter.kill.join - end - - def test_safe_level - t = Thread.new { $SAFE = 3; sleep } - sleep 0.5 - assert_equal(0, Thread.current.safe_level) - assert_equal(3, t.safe_level) - - ensure - t.kill if t + waiter&.kill&.join end def test_thread_local @@ -405,43 +581,77 @@ class TestThread < Test::Unit::TestCase assert_equal(false, t.key?(:qux)) assert_equal(false, t.key?("qux")) - assert_equal([:foo, :bar, :baz], t.keys) + assert_equal([:foo, :bar, :baz].sort, t.keys.sort) ensure - t.kill if t + t&.kill&.join end - def test_thread_local_security + def test_thread_local_fetch t = Thread.new { sleep } - assert_raise(SecurityError) do - Thread.new { $SAFE = 4; t[:foo] }.join - end + assert_equal(false, t.key?(:foo)) - assert_raise(SecurityError) do - Thread.new { $SAFE = 4; t[:foo] = :baz }.join - end + t["foo"] = "foo" + t["bar"] = "bar" + t["baz"] = "baz" - assert_raise(RuntimeError) do - Thread.new do - Thread.current[:foo] = :bar - Thread.current.freeze + x = nil + assert_equal("foo", t.fetch(:foo, 0)) + assert_equal("foo", t.fetch(:foo) {x = true}) + assert_nil(x) + assert_equal("foo", t.fetch("foo", 0)) + assert_equal("foo", t.fetch("foo") {x = true}) + assert_nil(x) + + x = nil + assert_equal(0, t.fetch(:qux, 0)) + assert_equal(1, t.fetch(:qux) {x = 1}) + assert_equal(1, x) + assert_equal(2, t.fetch("qux", 2)) + assert_equal(3, t.fetch("qux") {x = 3}) + assert_equal(3, x) + + e = assert_raise(KeyError) {t.fetch(:qux)} + assert_equal(:qux, e.key) + assert_equal(t, e.receiver) + ensure + t&.kill&.join + end + + def test_thread_local_security + Thread.new do + Thread.current[:foo] = :bar + Thread.current.freeze + assert_raise(FrozenError) do Thread.current[:foo] = :baz - end.join - end + end + end.join + end + + def test_thread_local_dynamic_symbol + bug10667 = '[ruby-core:67185] [Bug #10667]' + t = Thread.new {}.join + key_str = "foo#{rand}" + key_sym = key_str.to_sym + t.thread_variable_set(key_str, "bar") + assert_equal("bar", t.thread_variable_get(key_str), "#{bug10667}: string key") + assert_equal("bar", t.thread_variable_get(key_sym), "#{bug10667}: symbol key") end def test_select_wait - assert_nil(IO.select(nil, nil, nil, 1)) + assert_nil(IO.select(nil, nil, nil, 0.001)) t = Thread.new do IO.select(nil, nil, nil, nil) end - sleep 0.5 - t.kill + Thread.pass until t.stop? + assert_predicate(t, :alive?) + ensure + t&.kill&.join end def test_mutex_deadlock - m = Mutex.new + m = Thread::Mutex.new m.synchronize do assert_raise(ThreadError) do m.synchronize do @@ -452,30 +662,30 @@ class TestThread < Test::Unit::TestCase end def test_mutex_interrupt - m = Mutex.new + m = Thread::Mutex.new m.lock t = Thread.new do m.lock :foo end - sleep 0.5 + Thread.pass until t.stop? t.kill assert_nil(t.value) end def test_mutex_illegal_unlock - m = Mutex.new + m = Thread::Mutex.new m.lock - assert_raise(ThreadError) do - Thread.new do + Thread.new do + assert_raise(ThreadError) do m.unlock - end.join - end + end + end.join end def test_mutex_fifo_like_lock - m1 = Mutex.new - m2 = Mutex.new + m1 = Thread::Mutex.new + m2 = Thread::Mutex.new m1.lock m2.lock m1.unlock @@ -483,7 +693,7 @@ class TestThread < Test::Unit::TestCase assert_equal(false, m1.locked?) assert_equal(false, m2.locked?) - m3 = Mutex.new + m3 = Thread::Mutex.new m1.lock m2.lock m3.lock @@ -496,7 +706,7 @@ class TestThread < Test::Unit::TestCase end def test_mutex_trylock - m = Mutex.new + m = Thread::Mutex.new assert_equal(true, m.try_lock) assert_equal(false, m.try_lock, '[ruby-core:20943]') @@ -530,24 +740,22 @@ class TestThread < Test::Unit::TestCase end def test_no_valid_cfp - skip 'with win32ole, cannot run this testcase because win32ole redefines Thread#intialize' if defined?(WIN32OLE) + omit 'with win32ole, cannot run this testcase because win32ole redefines Thread#initialize' if defined?(WIN32OLE) bug5083 = '[ruby-dev:44208]' - assert_equal([], Thread.new(&Module.method(:nesting)).value) - error = assert_raise(RuntimeError) do - Thread.new(:to_s, &Module.method(:undef_method)).join - end - assert_equal("Can't call on top of Fiber or Thread", error.message, bug5083) + assert_equal([], Thread.new(&Module.method(:nesting)).value, bug5083) + assert_instance_of(Thread, Thread.new(:to_s, &Class.new.method(:undef_method)).join, bug5083) end def make_handle_interrupt_test_thread1 flag r = [] - ready_p = false + ready_q = Thread::Queue.new + done_q = Thread::Queue.new th = Thread.new{ begin Thread.handle_interrupt(RuntimeError => flag){ begin - ready_p = true - sleep 0.5 + ready_q << true + done_q.pop rescue r << :c1 end @@ -556,9 +764,10 @@ class TestThread < Test::Unit::TestCase r << :c2 end } - Thread.pass until ready_p + ready_q.pop th.raise begin + done_q << true th.join rescue r << :c3 @@ -616,43 +825,44 @@ class TestThread < Test::Unit::TestCase end def test_handle_interrupt_blocking - r=:ng - e=Class.new(Exception) + r = nil + q = Thread::Queue.new + e = Class.new(Exception) th_s = Thread.current - begin - th = Thread.start{ + th = Thread.start { + assert_raise(RuntimeError) { Thread.handle_interrupt(Object => :on_blocking){ begin - Thread.current.raise RuntimeError - r=:ok + q.pop + Thread.current.raise RuntimeError, "will raise in sleep" + r = :ok sleep ensure - th_s.raise e + th_s.raise e, "raise from ensure", $@ end } } - sleep 1 - r=:ng - th.raise RuntimeError - th.join - rescue e - end - assert_equal(:ok,r) + } + assert_raise(e) {q << true; th.join} + assert_equal(:ok, r) end def test_handle_interrupt_and_io assert_in_out_err([], <<-INPUT, %w(ok), []) th_waiting = true + q = Thread::Queue.new t = Thread.new { + Thread.current.report_on_exception = false Thread.handle_interrupt(RuntimeError => :on_blocking) { + q << true nil while th_waiting # async interrupt should be raised _before_ writing puts arguments puts "ng" } } - sleep 0.1 + q.pop t.raise RuntimeError th_waiting = false t.join rescue nil @@ -662,10 +872,12 @@ class TestThread < Test::Unit::TestCase def test_handle_interrupt_and_p assert_in_out_err([], <<-INPUT, %w(:ok :ok), []) - th_waiting = true + th_waiting = false t = Thread.new { + Thread.current.report_on_exception = false Thread.handle_interrupt(RuntimeError => :on_blocking) { + th_waiting = true nil while th_waiting # p shouldn't provide interruptible point p :ok @@ -673,7 +885,7 @@ class TestThread < Test::Unit::TestCase } } - sleep 0.1 + Thread.pass until th_waiting t.raise RuntimeError th_waiting = false t.join rescue nil @@ -681,22 +893,23 @@ class TestThread < Test::Unit::TestCase end def test_handle_interrupted? - q = Queue.new + q = Thread::Queue.new Thread.handle_interrupt(RuntimeError => :never){ + done = false th = Thread.new{ q.push :e begin begin - sleep 0.5 - rescue => e + Thread.pass until done + rescue q.push :ng1 end begin Thread.handle_interrupt(Object => :immediate){} if Thread.pending_interrupt? - rescue RuntimeError => e + rescue RuntimeError q.push :ok end - rescue => e + rescue q.push :ng2 ensure q.push :ng3 @@ -704,13 +917,14 @@ class TestThread < Test::Unit::TestCase } q.pop th.raise + done = true th.join assert_equal(:ok, q.pop) } end def test_thread_timer_and_ensure - assert_normal_exit(<<_eom, 'r36492', timeout: 3) + assert_normal_exit(<<_eom, 'r36492', timeout: 10) flag = false t = Thread.new do begin @@ -730,9 +944,24 @@ _eom end def test_uninitialized - c = Class.new(Thread) - c.class_eval { def initialize; end } + c = Class.new(Thread) {def initialize; end} assert_raise(ThreadError) { c.new.start } + + bug11959 = '[ruby-core:72732] [Bug #11959]' + + c = Class.new(Thread) {def initialize; exit; end} + assert_raise(ThreadError, bug11959) { c.new } + + c = Class.new(Thread) {def initialize; raise; end} + assert_raise(ThreadError, bug11959) { c.new } + + c = Class.new(Thread) { + def initialize + pending = pending_interrupt? + super {pending} + end + } + assert_equal(false, c.new.value, bug11959) end def test_backtrace @@ -746,33 +975,41 @@ _eom end def test_thread_timer_and_interrupt + omit "[Bug #18613]" if /freebsd/ =~ RUBY_PLATFORM + bug5757 = '[ruby-dev:44985]' - t0 = Time.now.to_f pid = nil - cmd = 'r,=IO.pipe; Thread.start {Thread.pass until Thread.main.stop?; puts; STDOUT.flush}; r.read' + cmd = 'Signal.trap(:INT, "DEFAULT"); pipe=IO.pipe; Thread.start {Thread.pass until Thread.main.stop?; puts; STDOUT.flush}; pipe[0].read' opt = {} opt[:new_pgroup] = true if /mswin|mingw/ =~ RUBY_PLATFORM - s, _err = EnvUtil.invoke_ruby(['-e', cmd], "", true, true, opt) do |in_p, out_p, err_p, cpid| + s, t, _err = EnvUtil.invoke_ruby(['-e', cmd], "", true, true, **opt) do |in_p, out_p, err_p, cpid| + assert IO.select([out_p], nil, nil, 10), 'subprocess not ready' out_p.gets pid = cpid + t0 = Time.now.to_f Process.kill(:SIGINT, pid) - Process.wait(pid) - [$?, err_p.read] + begin + Timeout.timeout(10) { Process.wait(pid) } + rescue Timeout::Error + EnvUtil.terminate(pid) + raise + end + t1 = Time.now.to_f + [$?, t1 - t0, err_p.read] end - t1 = Time.now.to_f assert_equal(pid, s.pid, bug5757) - unless /mswin|mingw/ =~ RUBY_PLATFORM - # status of signal is not supported on Windows - assert_equal([false, true, false, Signal.list["INT"]], - [s.exited?, s.signaled?, s.stopped?, s.termsig], - "[s.exited?, s.signaled?, s.stopped?, s.termsig]") - end - assert_in_delta(t1 - t0, 1, 1, bug5757) + assert_equal([false, true, false, Signal.list["INT"]], + [s.exited?, s.signaled?, s.stopped?, s.termsig], + "[s.exited?, s.signaled?, s.stopped?, s.termsig]") + assert_include(0..2, t, bug5757) end def test_thread_join_in_trap + assert_separately [], <<-'EOS' + Signal.trap(:INT, "DEFAULT") + t0 = Thread.current assert_nothing_raised{ - t = Thread.new{ sleep 0.2; Process.kill(:INT, $$) } + t = Thread.new {Thread.pass until t0.stop?; Process.kill(:INT, $$)} Signal.trap :INT do t.join @@ -780,42 +1017,50 @@ _eom t.join } + EOS + end - assert_equal(:normal_end, - begin - t = Thread.new{ sleep 0.2; Process.kill(:INT, $$); :normal_end } + def test_thread_value_in_trap + assert_separately [], <<-'EOS' + Signal.trap(:INT, "DEFAULT") + t0 = Thread.current + t = Thread.new {Thread.pass until t0.stop?; Process.kill(:INT, $$); :normal_end} - Signal.trap :INT do - t.value - end - t.value - end - ) + Signal.trap :INT do + t.value + end + assert_equal(:normal_end, t.value) + EOS end def test_thread_join_current - assert_raises(ThreadError) do + assert_raise(ThreadError) do Thread.current.join end end def test_thread_join_main_thread - assert_raises(ThreadError) do - Thread.new(Thread.current) {|t| + Thread.new(Thread.current) {|t| + assert_raise(ThreadError) do t.join - }.join - end + end + }.join end def test_main_thread_status_at_exit - assert_in_out_err([], <<-INPUT, %w(false), []) + assert_in_out_err([], <<-'INPUT', ["false false aborting"], []) +q = Thread::Queue.new Thread.new(Thread.current) {|mth| begin - sleep 0.1 + q.push nil + mth.run + Thread.pass until mth.stop? + p :mth_stopped # don't run if killed by rb_thread_terminate_all ensure - p mth.alive? + puts "#{mth.alive?} #{mth.status} #{Thread.current.status}" end } +q.pop INPUT end @@ -825,14 +1070,16 @@ Thread.new(Thread.current) {|mth| assert_in_out_err([], <<-INPUT, %w(sleep run), []) Signal.trap(:INT) { puts Thread.current.status + exit } + t = Thread.current Thread.new(Thread.current) {|mth| - sleep 0.01 + Thread.pass until t.stop? puts mth.status Process.kill(:INT, $$) } - sleep 0.1 + sleep INPUT end @@ -841,33 +1088,34 @@ Thread.new(Thread.current) {|mth| ary = [] t = Thread.new { - begin - ary << Thread.current.status - sleep #1 - ensure + assert_raise(RuntimeError) do begin ary << Thread.current.status - sleep #2 + sleep #1 ensure - ary << Thread.current.status + begin + ary << Thread.current.status + sleep #2 + ensure + ary << Thread.current.status + end end end } - begin - sleep 0.01 - t.kill # wake up sleep #1 - sleep 0.01 - t.raise "wakeup" # wake up sleep #2 - sleep 0.01 - assert_equal(ary, ["run", "aborting", "aborting"]) - ensure - t.join rescue nil - end + Thread.pass until ary.size >= 1 + Thread.pass until t.stop? + t.kill # wake up sleep #1 + Thread.pass until ary.size >= 2 + Thread.pass until t.stop? + t.raise "wakeup" # wake up sleep #2 + Thread.pass while t.alive? + assert_equal(ary, ["run", "aborting", "aborting"]) + t.join end def test_mutex_owned - mutex = Mutex.new + mutex = Thread::Mutex.new assert_equal(mutex.owned?, false) mutex.synchronize { @@ -879,35 +1127,36 @@ Thread.new(Thread.current) {|mth| def test_mutex_owned2 begin - mutex = Mutex.new + mutex = Thread::Mutex.new th = Thread.new { # lock forever mutex.lock sleep } - sleep 0.01 until th.status == "sleep" - # acquired another thread. - assert_equal(mutex.locked?, true) + # acquired by another thread. + Thread.pass until mutex.locked? assert_equal(mutex.owned?, false) ensure - th.kill if th + th&.kill&.join end end def test_mutex_unlock_on_trap assert_in_out_err([], <<-INPUT, %w(locked unlocked false), []) - m = Mutex.new + m = Thread::Mutex.new + trapped = false Signal.trap("INT") { |signo| m.unlock + trapped = true puts "unlocked" } m.lock puts "locked" Process.kill("INT", $$) - sleep 0.2 + Thread.pass until trapped puts m.locked? INPUT end @@ -916,7 +1165,9 @@ Thread.new(Thread.current) {|mth| env = {} env['RUBY_THREAD_VM_STACK_SIZE'] = vm_stack_size.to_s if vm_stack_size env['RUBY_THREAD_MACHINE_STACK_SIZE'] = machine_stack_size.to_s if machine_stack_size - out, = EnvUtil.invoke_ruby([env, '-e', script], '', true, true, :timeout => 50) + out, err, status = EnvUtil.invoke_ruby([env, '-e', script], '', true, true) + assert_not_predicate(status, :signaled?, err) + use_length ? out.length : out end @@ -933,8 +1184,10 @@ Thread.new(Thread.current) {|mth| "0 thread_machine_stack_size") assert_operator(h_default[:thread_machine_stack_size], :<=, h_large[:thread_machine_stack_size], "large thread_machine_stack_size") + assert_equal("ok", invoke_rec('print :ok', 1024 * 1024 * 100, nil, false)) + end - # check VM machine stack size + def test_vm_machine_stack_size script = 'def rec; print "."; STDOUT.flush; rec; end; rec' size_default = invoke_rec script, nil, nil assert_operator(size_default, :>, 0, "default size") @@ -942,9 +1195,9 @@ Thread.new(Thread.current) {|mth| assert_operator(size_default, :>, size_0, "0 size") size_large = invoke_rec script, 1024 * 1024 * 10, nil assert_operator(size_default, :<, size_large, "large size") + end - return if /mswin|mingw/ =~ RUBY_PLATFORM - + def test_machine_stack_size # check machine stack size # Note that machine stack size may not change size (depend on OSs) script = 'def rec; print "."; STDOUT.flush; 1.times{1.times{1.times{rec}}}; end; Thread.new{rec}.join' @@ -954,18 +1207,16 @@ Thread.new(Thread.current) {|mth| assert_operator(size_default, :>=, size_0, "0 size") size_large = invoke_rec script, vm_stack_size, 1024 * 1024 * 10 assert_operator(size_default, :<=, size_large, "large size") - end + end unless /mswin|mingw/ =~ RUBY_PLATFORM def test_blocking_mutex_unlocked_on_fork bug8433 = '[ruby-core:55102] [Bug #8433]' - mutex = Mutex.new - flag = false + mutex = Thread::Mutex.new mutex.lock th = Thread.new do mutex.synchronize do - flag = true sleep end end @@ -993,9 +1244,9 @@ Thread.new(Thread.current) {|mth| end Process.wait2(f.pid) end - unless th.join(3) + unless th.join(EnvUtil.apply_timeout_scale(30)) Process.kill(:QUIT, f.pid) - Process.kill(:KILL, f.pid) unless th.join(1) + Process.kill(:KILL, f.pid) unless th.join(EnvUtil.apply_timeout_scale(1)) end _, status = th.value output = f.read @@ -1003,4 +1254,402 @@ Thread.new(Thread.current) {|mth| assert_not_predicate(status, :signaled?, FailDesc[status, bug9751, output]) assert_predicate(status, :success?, bug9751) end if Process.respond_to?(:fork) + + def test_fork_value + bug18902 = "[Bug #18902]" + th = Thread.start { sleep 2 } + begin + pid = fork do + th.value + end + _, status = Process.wait2(pid) + assert_predicate(status, :success?, bug18902) + ensure + th.kill + th.join + end + end if Process.respond_to?(:fork) + + def test_fork_while_locked + m = Thread::Mutex.new + thrs = [] + 3.times do |i| + thrs << Thread.new { m.synchronize { Process.waitpid2(fork{})[1] } } + end + thrs.each do |t| + assert_predicate t.value, :success?, '[ruby-core:85940] [Bug #14578]' + end + end if Process.respond_to?(:fork) + + def test_fork_while_parent_locked + omit 'needs fork' unless Process.respond_to?(:fork) + m = Thread::Mutex.new + nr = 1 + thrs = [] + m.synchronize do + thrs = nr.times.map { Thread.new { m.synchronize {} } } + thrs.each { Thread.pass } + pid = fork do + m.locked? or exit!(2) + thrs = nr.times.map { Thread.new { m.synchronize {} } } + m.unlock + thrs.each { |t| t.join(1) == t or exit!(1) } + exit!(0) + end + _, st = Process.waitpid2(pid) + assert_predicate st, :success?, '[ruby-core:90312] [Bug #15383]' + end + thrs.each { |t| assert_same t, t.join(1) } + end + + def test_fork_while_mutex_locked_by_forker + omit 'needs fork' unless Process.respond_to?(:fork) + m = Thread::Mutex.new + m.synchronize do + pid = fork do + exit!(2) unless m.locked? + m.unlock rescue exit!(3) + m.synchronize {} rescue exit!(4) + exit!(0) + end + _, st = Timeout.timeout(30) { Process.waitpid2(pid) } + assert_predicate st, :success?, '[ruby-core:90595] [Bug #15430]' + end + end + + def test_subclass_no_initialize + t = Module.new do + break eval("class C\u{30b9 30ec 30c3 30c9} < Thread; self; end") + end + t.class_eval do + def initialize + end + end + assert_raise_with_message(ThreadError, /C\u{30b9 30ec 30c3 30c9}/) do + t.new {} + end + end + + def test_thread_name + t = Thread.start {sleep} + sleep 0.001 until t.stop? + assert_nil t.name + s = t.inspect + t.name = 'foo' + assert_equal 'foo', t.name + t.name = nil + assert_nil t.name + assert_equal s, t.inspect + ensure + t.kill + t.join + end + + def test_thread_invalid_name + bug11756 = '[ruby-core:71774] [Bug #11756]' + t = Thread.start {} + assert_raise(ArgumentError, bug11756) {t.name = "foo\0bar"} + assert_raise(ArgumentError, bug11756) {t.name = "foo".encode(Encoding::UTF_32BE)} + ensure + t.kill + t.join + end + + def test_thread_invalid_object + bug11756 = '[ruby-core:71774] [Bug #11756]' + t = Thread.start {} + assert_raise(TypeError, bug11756) {t.name = []} + ensure + t.kill + t.join + end + + def test_yield_across_thread_through_enum + bug18649 = '[ruby-core:107980] [Bug #18649]' + @log = [] + + def self.p(arg) + @log << arg + end + + def self.synchronize + yield + end + + def self.execute(task) + success = true + value = reason = nil + end_sync = false + + synchronize do + begin + p :before + value = task.call + p :never_reached + success = true + rescue StandardError => ex + ex = ex.class + p [:rescue, ex] + reason = ex + success = false + end + + end_sync = true + p :end_sync + end + + p :should_not_reach_here! unless end_sync + [success, value, reason] + end + + def self.foo + Thread.new do + result = execute(-> { yield 42 }) + p [:result, result] + end.join + end + + value = to_enum(:foo).first + expected = [:before, + [:rescue, LocalJumpError], + :end_sync, + [:result, [false, nil, LocalJumpError]]] + + assert_equal(expected, @log, bug18649) + assert_equal(42, value, bug18649) + end + + def test_thread_setname_in_initialize + bug12290 = '[ruby-core:74963] [Bug #12290]' + c = Class.new(Thread) {def initialize() self.name = "foo"; super; end} + assert_equal("foo", c.new {Thread.current.name}.value, bug12290) + end + + def test_thread_native_thread_id + omit "don't support native_thread_id" unless Thread.method_defined?(:native_thread_id) + assert_instance_of Integer, Thread.main.native_thread_id + + th1 = Thread.start{sleep} + + # newly created thread which doesn't run yet returns nil or integer + assert_include [NilClass, Integer], th1.native_thread_id.class + + Thread.pass until th1.stop? + + # After a thread starts (and execute `sleep`), it returns native_thread_id + native_tid = th1.native_thread_id + assert_instance_of Integer, native_tid if native_tid # it can be nil + + th1.wakeup + Thread.pass while th1.alive? + + # dead thread returns nil + assert_nil th1.native_thread_id + end + + def test_thread_native_thread_id_across_fork_on_linux + begin + require '-test-/thread/id' + rescue LoadError + omit "this test is only for Linux" + else + extend Bug::ThreadID + end + + parent_thread_id = Thread.main.native_thread_id + real_parent_thread_id = gettid + + assert_equal real_parent_thread_id, parent_thread_id + + child_lines = nil + IO.popen('-') do |pipe| + if pipe + # parent + child_lines = pipe.read.lines + else + # child + puts Thread.main.native_thread_id + puts gettid + end + end + child_thread_id = child_lines[0].chomp.to_i + real_child_thread_id = child_lines[1].chomp.to_i + + assert_equal real_child_thread_id, child_thread_id + refute_equal parent_thread_id, child_thread_id + end + + def test_thread_interrupt_for_killed_thread + pend "hang-up" if /mswin|mingw/ =~ RUBY_PLATFORM + + opts = { timeout: 5, timeout_error: nil } + + assert_normal_exit(<<-_end, '[Bug #8996]', **opts) + Thread.report_on_exception = false + trap(:TERM){exit} + while true + t = Thread.new{sleep 0} + t.raise Interrupt + Thread.pass # allow t to finish + end + _end + end + + def test_signal_at_join + if /mswin|mingw/ =~ RUBY_PLATFORM + omit "can't trap a signal from another process on Windows" + # opt = {new_pgroup: true} + end + + if /freebsd/ =~ RUBY_PLATFORM + omit "[Bug #18613]" + end + + assert_separately([], "#{<<~"{#"}\n#{<<~'};'}", timeout: 120) + {# + n = 1000 + sig = :INT + trap(sig) {} + IO.popen([EnvUtil.rubybin, "-e", "#{<<~"{#1"}\n#{<<~'};#1'}"], "r+") do |f| + tpid = #{$$} + sig = :#{sig} + {#1 + STDOUT.sync = true + while gets + puts + Process.kill(sig, tpid) + end + };#1 + assert_nothing_raised do + n.times do + w = Thread.start do + sleep 30 + end + begin + f.puts + f.gets + ensure + w.kill + w.join + end + end + end + n.times do + w = Thread.start { sleep 30 } + begin + f.puts + f.gets + ensure + w.kill + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + w.join(30) + t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + diff = t1 - t0 + assert_operator diff, :<=, 2 + end + end + end + }; + end + + def test_pending_interrupt? + t = Thread.handle_interrupt(Exception => :never) { Thread.new { Thread.stop } } + t.raise(StandardError) + assert_equal(true, t.pending_interrupt?) + assert_equal(true, t.pending_interrupt?(Exception)) + assert_equal(false, t.pending_interrupt?(ArgumentError)) + end + + def test_deadlock_backtrace + bug21127 = '[ruby-core:120930] [Bug #21127]' + + expected_stderr = [ + /-:12:in 'Thread#join': No live threads left. Deadlock\? \(fatal\)\n/, + /2 threads, 2 sleeps current:\w+ main thread:\w+\n/, + /\* #<Thread:\w+ sleep_forever>\n/, + :*, + /^\s*-:6:in 'Object#frame_for_deadlock_test_2'/, + :*, + /\* #<Thread:\w+ -:10 sleep_forever>\n/, + :*, + /^\s*-:2:in 'Object#frame_for_deadlock_test_1'/, + :*, + ] + + assert_in_out_err([], <<-INPUT, [], expected_stderr, bug21127) + def frame_for_deadlock_test_1 + yield + end + + def frame_for_deadlock_test_2 + yield + end + + q = Thread::Queue.new + t = Thread.new { frame_for_deadlock_test_1 { q.pop } } + + frame_for_deadlock_test_2 { t.join } + INPUT + end + + # [Bug #21342] + def test_unlock_locked_mutex_with_collected_fiber + bug21127 = '[ruby-core:120930] [Bug #21127]' + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + 5.times do + m = Mutex.new + Thread.new do + m.synchronize do + end + end.join + Fiber.new do + GC.start + m.lock + end.resume + end + end; + end + + def test_unlock_locked_mutex_with_collected_fiber2 + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + MUTEXES = [] + 5.times do + m = Mutex.new + Fiber.new do + GC.start + m.lock + end.resume + MUTEXES << m + end + 10.times do + MUTEXES.clear + GC.start + end + end; + end + + def test_mutexes_locked_in_fiber_dont_have_aba_issue_with_new_fibers + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + mutexes = 1000.times.map do + Mutex.new + end + + mutexes.map do |m| + Fiber.new do + m.lock + end.resume + end + + GC.start + + 1000.times.map do + Fiber.new do + raise "FAILED!" if mutexes.any?(&:owned?) + end.resume + end + end; + end end |
