diff options
Diffstat (limited to 'test/ruby/test_thread.rb')
| -rw-r--r-- | test/ruby/test_thread.rb | 351 |
1 files changed, 302 insertions, 49 deletions
diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index 3283e30122..2a61fc3450 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -29,9 +29,19 @@ class TestThread < Test::Unit::TestCase end def test_inspect - th = Module.new {break module_eval("class C\u{30b9 30ec 30c3 30c9} < Thread; self; end")}.start{} - assert_match(/::C\u{30b9 30ec 30c3 30c9}:/, th.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 @@ -226,9 +236,21 @@ class TestThread < Test::Unit::TestCase assert_equal(t1, t3.value) ensure - t1&.kill - t2&.kill - t3&.kill + 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'], @@ -305,7 +327,6 @@ class TestThread < Test::Unit::TestCase s += 1 end Thread.pass until t.stop? - sleep 1 if RubyVM::MJIT.enabled? # t.stop? behaves unexpectedly with --jit-wait assert_equal(1, s) t.wakeup Thread.pass while t.alive? @@ -377,7 +398,7 @@ 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 t = Thread.new { raise } @@ -486,6 +507,19 @@ class TestThread < Test::Unit::TestCase 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 { Thread.current.report_on_exception = false @@ -533,23 +567,6 @@ class TestThread < Test::Unit::TestCase waiter&.kill&.join end - def test_safe_level - ok = false - t = Thread.new do - EnvUtil.suppress_warning do - $SAFE = 1 - end - ok = true - sleep - end - Thread.pass until ok - assert_equal($SAFE, Thread.current.safe_level) - assert_equal($SAFE, t.safe_level) - ensure - $SAFE = 0 - t&.kill&.join - end - def test_thread_local t = Thread.new { sleep } @@ -630,7 +647,7 @@ class TestThread < Test::Unit::TestCase Thread.pass until t.stop? assert_predicate(t, :alive?) ensure - t&.kill + t&.kill&.join end def test_mutex_deadlock @@ -723,7 +740,7 @@ class TestThread < Test::Unit::TestCase end def test_no_valid_cfp - skip 'with win32ole, cannot run this testcase because win32ole redefines Thread#initialize' 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, bug5083) assert_instance_of(Thread, Thread.new(:to_s, &Class.new.method(:undef_method)).join, bug5083) @@ -731,8 +748,8 @@ class TestThread < Test::Unit::TestCase def make_handle_interrupt_test_thread1 flag r = [] - ready_q = Queue.new - done_q = Queue.new + ready_q = Thread::Queue.new + done_q = Thread::Queue.new th = Thread.new{ begin Thread.handle_interrupt(RuntimeError => flag){ @@ -808,14 +825,15 @@ 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 - th = Thread.start{ + th = Thread.start { assert_raise(RuntimeError) { Thread.handle_interrupt(Object => :on_blocking){ begin - Thread.pass until r == :wait + q.pop Thread.current.raise RuntimeError, "will raise in sleep" r = :ok sleep @@ -825,25 +843,26 @@ class TestThread < Test::Unit::TestCase } } } - assert_raise(e) {r = :wait; sleep 0.2} - th.join - 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" } } - Thread.pass while t.stop? + q.pop t.raise RuntimeError th_waiting = false t.join rescue nil @@ -956,12 +975,14 @@ _eom end def test_thread_timer_and_interrupt + omit "[Bug #18613]" if /freebsd/ =~ RUBY_PLATFORM + bug5757 = '[ruby-dev:44985]' pid = nil 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, t, _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 @@ -1058,7 +1079,7 @@ q.pop puts mth.status Process.kill(:INT, $$) } - sleep 0.1 + sleep INPUT end @@ -1117,7 +1138,7 @@ q.pop Thread.pass until mutex.locked? assert_equal(mutex.owned?, false) ensure - th&.kill + th&.kill&.join end end @@ -1144,7 +1165,9 @@ q.pop 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) + out, err, status = EnvUtil.invoke_ruby([env, '-e', script], '', true, true) + assert_not_predicate(status, :signaled?, err) + use_length ? out.length : out end @@ -1161,6 +1184,7 @@ q.pop "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 def test_vm_machine_stack_size @@ -1231,8 +1255,23 @@ q.pop 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 = Mutex.new + m = Thread::Mutex.new thrs = [] 3.times do |i| thrs << Thread.new { m.synchronize { Process.waitpid2(fork{})[1] } } @@ -1243,7 +1282,7 @@ q.pop end if Process.respond_to?(:fork) def test_fork_while_parent_locked - skip 'needs fork' unless Process.respond_to?(:fork) + omit 'needs fork' unless Process.respond_to?(:fork) m = Thread::Mutex.new nr = 1 thrs = [] @@ -1264,8 +1303,8 @@ q.pop end def test_fork_while_mutex_locked_by_forker - skip 'needs fork' unless Process.respond_to?(:fork) - m = Mutex.new + omit 'needs fork' unless Process.respond_to?(:fork) + m = Thread::Mutex.new m.synchronize do pid = fork do exit!(2) unless m.locked? @@ -1325,19 +1364,127 @@ q.pop 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 - opts = { timeout: 5, timeout_error: nil } + pend "hang-up" if /mswin|mingw/ =~ RUBY_PLATFORM - # prevent SIGABRT from slow shutdown with MJIT - opts[:reprieve] = 3 if RubyVM::MJIT.enabled? + opts = { timeout: 5, timeout_error: nil } - assert_normal_exit(<<-_end, '[Bug #8996]', opts) + assert_normal_exit(<<-_end, '[Bug #8996]', **opts) Thread.report_on_exception = false trap(:TERM){exit} while true @@ -1350,9 +1497,14 @@ q.pop def test_signal_at_join if /mswin|mingw/ =~ RUBY_PLATFORM - skip "can't trap a signal from another process on Windows" + 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 @@ -1399,4 +1551,105 @@ q.pop 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 |
