diff options
Diffstat (limited to 'spec/ruby/core/thread')
50 files changed, 2066 insertions, 0 deletions
diff --git a/spec/ruby/core/thread/abort_on_exception_spec.rb b/spec/ruby/core/thread/abort_on_exception_spec.rb new file mode 100644 index 0000000000..e424b2fd26 --- /dev/null +++ b/spec/ruby/core/thread/abort_on_exception_spec.rb @@ -0,0 +1,106 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#abort_on_exception" do + before do + ThreadSpecs.clear_state + @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit } + end + + after do + ThreadSpecs.state = :exit + @thread.join + end + + it "is false by default" do + @thread.abort_on_exception.should be_false + end + + it "returns true when #abort_on_exception= is passed true" do + @thread.abort_on_exception = true + @thread.abort_on_exception.should be_true + end +end + +describe :thread_abort_on_exception, shared: true do + before do + @thread = Thread.new do + Thread.pass until ThreadSpecs.state == :run + raise RuntimeError, "Thread#abort_on_exception= specs" + end + end + + it "causes the main thread to raise the exception raised in the thread" do + begin + ScratchPad << :before + + @thread.abort_on_exception = true if @object + lambda do + ThreadSpecs.state = :run + # Wait for the main thread to be interrupted + sleep + end.should raise_error(RuntimeError, "Thread#abort_on_exception= specs") + + ScratchPad << :after + rescue Exception => e + ScratchPad << [:rescue, e] + end + + ScratchPad.recorded.should == [:before, :after] + end +end + +describe "Thread#abort_on_exception=" do + describe "when enabled and the thread dies due to an exception" do + before do + ScratchPad.record [] + ThreadSpecs.clear_state + @stderr, $stderr = $stderr, IOStub.new + end + + after do + $stderr = @stderr + end + + it_behaves_like :thread_abort_on_exception, nil, true + end +end + +describe "Thread.abort_on_exception" do + before do + @abort_on_exception = Thread.abort_on_exception + end + + after do + Thread.abort_on_exception = @abort_on_exception + end + + it "is false by default" do + Thread.abort_on_exception.should == false + end + + it "returns true when .abort_on_exception= is passed true" do + Thread.abort_on_exception = true + Thread.abort_on_exception.should be_true + end +end + +describe "Thread.abort_on_exception=" do + describe "when enabled and a non-main thread dies due to an exception" do + before :each do + ScratchPad.record [] + ThreadSpecs.clear_state + @stderr, $stderr = $stderr, IOStub.new + + @abort_on_exception = Thread.abort_on_exception + Thread.abort_on_exception = true + end + + after :each do + Thread.abort_on_exception = @abort_on_exception + $stderr = @stderr + end + + it_behaves_like :thread_abort_on_exception, nil, false + end +end diff --git a/spec/ruby/core/thread/add_trace_func_spec.rb b/spec/ruby/core/thread/add_trace_func_spec.rb new file mode 100644 index 0000000000..c2010ef317 --- /dev/null +++ b/spec/ruby/core/thread/add_trace_func_spec.rb @@ -0,0 +1,5 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Thread#add_trace_func" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/thread/alive_spec.rb b/spec/ruby/core/thread/alive_spec.rb new file mode 100644 index 0000000000..c1459ac693 --- /dev/null +++ b/spec/ruby/core/thread/alive_spec.rb @@ -0,0 +1,58 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#alive?" do + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.alive?.should == true + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.alive?.should == true + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.alive?.should == true + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.alive?.should == true + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.alive?.should == false + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.alive?.should == false + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.alive?.should == false + end + + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.alive?.should == true + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.alive?.should == true + end + + it "returns true for a killed but still running thread" do + exit = false + t = Thread.new do + begin + sleep + ensure + Thread.pass until exit + end + end + + ThreadSpecs.spin_until_sleeping(t) + + t.kill + t.alive?.should == true + exit = true + t.join + end +end diff --git a/spec/ruby/core/thread/allocate_spec.rb b/spec/ruby/core/thread/allocate_spec.rb new file mode 100644 index 0000000000..1db05878ba --- /dev/null +++ b/spec/ruby/core/thread/allocate_spec.rb @@ -0,0 +1,9 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Thread.allocate" do + it "raises a TypeError" do + lambda { + Thread.allocate + }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb b/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb new file mode 100644 index 0000000000..6810bdcd78 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb @@ -0,0 +1,12 @@ +require File.expand_path('../../../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe 'Thread::Backtrace::Location#absolute_path' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + end + + it 'returns the absolute path of the call frame' do + @frame.absolute_path.should == File.realpath(__FILE__) + end +end diff --git a/spec/ruby/core/thread/backtrace/location/base_label_spec.rb b/spec/ruby/core/thread/backtrace/location/base_label_spec.rb new file mode 100644 index 0000000000..cba7e3f34c --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/base_label_spec.rb @@ -0,0 +1,12 @@ +require File.expand_path('../../../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe 'Thread::Backtrace::Location#base_label' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + end + + it 'returns the base label of the call frame' do + @frame.base_label.should == '<top (required)>' + end +end diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb b/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb new file mode 100644 index 0000000000..3e42d8cf81 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb @@ -0,0 +1,17 @@ +module ThreadBacktraceLocationSpecs + MODULE_LOCATION = caller_locations(0) rescue nil + + def self.locations + caller_locations + end + + def self.method_location + caller_locations(0) + end + + def self.block_location + 1.times do + return caller_locations(0) + end + end +end diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/main.rb b/spec/ruby/core/thread/backtrace/location/fixtures/main.rb new file mode 100644 index 0000000000..d2d14ac957 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/main.rb @@ -0,0 +1,5 @@ +def example + caller_locations[0].path +end + +print example diff --git a/spec/ruby/core/thread/backtrace/location/inspect_spec.rb b/spec/ruby/core/thread/backtrace/location/inspect_spec.rb new file mode 100644 index 0000000000..56d440c04a --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/inspect_spec.rb @@ -0,0 +1,13 @@ +require File.expand_path('../../../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe 'Thread::Backtrace::Location#inspect' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + @line = __LINE__ - 1 + end + + it 'converts the call frame to a String' do + @frame.inspect.should include("#{__FILE__}:#{@line}:in ") + end +end diff --git a/spec/ruby/core/thread/backtrace/location/label_spec.rb b/spec/ruby/core/thread/backtrace/location/label_spec.rb new file mode 100644 index 0000000000..4e67509d0f --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/label_spec.rb @@ -0,0 +1,20 @@ +require File.expand_path('../../../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe 'Thread::Backtrace::Location#label' do + it 'returns the base label of the call frame' do + ThreadBacktraceLocationSpecs.locations[0].label.should include('<top (required)>') + end + + it 'returns the method name for a method location' do + ThreadBacktraceLocationSpecs.method_location[0].label.should == "method_location" + end + + it 'returns the block name for a block location' do + ThreadBacktraceLocationSpecs.block_location[0].label.should == "block in block_location" + end + + it 'returns the module name for a module location' do + ThreadBacktraceLocationSpecs::MODULE_LOCATION[0].label.should include "ThreadBacktraceLocationSpecs" + end +end diff --git a/spec/ruby/core/thread/backtrace/location/lineno_spec.rb b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb new file mode 100644 index 0000000000..7d203008e5 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb @@ -0,0 +1,13 @@ +require File.expand_path('../../../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe 'Thread::Backtrace::Location#lineno' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + @line = __LINE__ - 1 + end + + it 'returns the absolute path of the call frame' do + @frame.lineno.should == @line + end +end diff --git a/spec/ruby/core/thread/backtrace/location/path_spec.rb b/spec/ruby/core/thread/backtrace/location/path_spec.rb new file mode 100644 index 0000000000..c2f2058990 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/path_spec.rb @@ -0,0 +1,91 @@ +require File.expand_path('../../../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe 'Thread::Backtrace::Location#path' do + context 'outside a main script' do + it 'returns an absolute path' do + frame = ThreadBacktraceLocationSpecs.locations[0] + + frame.path.should == __FILE__ + end + end + + context 'in a main script' do + before do + @script = fixture(__FILE__, 'main.rb') + end + + context 'when the script is in the working directory' do + before do + @directory = File.dirname(@script) + end + + context 'when using a relative script path' do + it 'returns a path relative to the working directory' do + Dir.chdir(@directory) { + ruby_exe('main.rb') + }.should == 'main.rb' + end + end + + context 'when using an absolute script path' do + it 'returns an absolute path' do + Dir.chdir(@directory) { + ruby_exe(@script) + }.should == @script + end + end + end + + context 'when the script is in a sub directory of the working directory' do + context 'when using a relative script path' do + it 'returns a path relative to the working directory' do + path = 'fixtures/main.rb' + directory = File.dirname(__FILE__) + Dir.chdir(directory) { + ruby_exe(path) + }.should == path + end + end + + context 'when using an absolute script path' do + it 'returns an absolute path' do + ruby_exe(@script).should == @script + end + end + end + + context 'when the script is outside of the working directory' do + before do + @parent_dir = tmp('path_outside_pwd') + @sub_dir = File.join(@parent_dir, 'sub') + @script = File.join(@parent_dir, 'main.rb') + source = fixture(__FILE__, 'main.rb') + + mkdir_p(@sub_dir) + + cp(source, @script) + end + + after do + rm_r(@script) + rm_r(@sub_dir) + rm_r(@parent_dir) + end + + context 'when using a relative script path' do + it 'returns a path relative to the working directory' do + Dir.chdir(@sub_dir) { + ruby_exe('../main.rb') + }.should == '../main.rb' + end + end + + context 'when using an absolute path' do + it 'returns an absolute path' do + ruby_exe(@script).should == @script + end + end + end + end +end diff --git a/spec/ruby/core/thread/backtrace/location/to_s_spec.rb b/spec/ruby/core/thread/backtrace/location/to_s_spec.rb new file mode 100644 index 0000000000..486d7da4c9 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/to_s_spec.rb @@ -0,0 +1,13 @@ +require File.expand_path('../../../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe 'Thread::Backtrace::Location#to_s' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + @line = __LINE__ - 1 + end + + it 'converts the call frame to a String' do + @frame.to_s.should include("#{__FILE__}:#{@line}:in ") + end +end diff --git a/spec/ruby/core/thread/backtrace_spec.rb b/spec/ruby/core/thread/backtrace_spec.rb new file mode 100644 index 0000000000..a20fdee956 --- /dev/null +++ b/spec/ruby/core/thread/backtrace_spec.rb @@ -0,0 +1,27 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Thread#backtrace" do + it "returns the current backtrace of a thread" do + t = Thread.new do + begin + sleep + rescue + end + end + + Thread.pass while t.status && t.status != 'sleep' + + backtrace = t.backtrace + backtrace.should be_kind_of(Array) + backtrace.first.should =~ /`sleep'/ + + t.raise 'finish the thread' + t.join + end + + it "returns nil for dead thread" do + t = Thread.new {} + t.join + t.backtrace.should == nil + end +end diff --git a/spec/ruby/core/thread/current_spec.rb b/spec/ruby/core/thread/current_spec.rb new file mode 100644 index 0000000000..cc969b71c4 --- /dev/null +++ b/spec/ruby/core/thread/current_spec.rb @@ -0,0 +1,15 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread.current" do + it "returns a thread" do + current = Thread.current + current.should be_kind_of(Thread) + end + + it "returns the current thread" do + t = Thread.new { Thread.current } + t.value.should equal(t) + Thread.current.should_not equal(t.value) + end +end diff --git a/spec/ruby/core/thread/element_reference_spec.rb b/spec/ruby/core/thread/element_reference_spec.rb new file mode 100644 index 0000000000..81b11d2c09 --- /dev/null +++ b/spec/ruby/core/thread/element_reference_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#[]" do + it "gives access to thread local values" do + th = Thread.new do + Thread.current[:value] = 5 + end + th.join + th[:value].should == 5 + Thread.current[:value].should == nil + end + + it "is not shared across threads" do + t1 = Thread.new do + Thread.current[:value] = 1 + end + t2 = Thread.new do + Thread.current[:value] = 2 + end + [t1,t2].each {|x| x.join} + t1[:value].should == 1 + t2[:value].should == 2 + end + + it "is accessible using strings or symbols" do + t1 = Thread.new do + Thread.current[:value] = 1 + end + t2 = Thread.new do + Thread.current["value"] = 2 + end + [t1,t2].each {|x| x.join} + t1[:value].should == 1 + t1["value"].should == 1 + t2[:value].should == 2 + t2["value"].should == 2 + end + + it "raises exceptions on the wrong type of keys" do + lambda { Thread.current[nil] }.should raise_error(TypeError) + lambda { Thread.current[5] }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/thread/element_set_spec.rb b/spec/ruby/core/thread/element_set_spec.rb new file mode 100644 index 0000000000..47b4d06328 --- /dev/null +++ b/spec/ruby/core/thread/element_set_spec.rb @@ -0,0 +1,52 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#[]=" do + after :each do + Thread.current[:value] = nil + end + + it "raises a RuntimeError if the thread is frozen" do + running = false + t = Thread.new do + Thread.pass until running + t.freeze + t[:foo] = "bar" + end + running = true + lambda { t.join }.should raise_error(RuntimeError) + end + + it "raises exceptions on the wrong type of keys" do + lambda { Thread.current[nil] = true }.should raise_error(TypeError) + lambda { Thread.current[5] = true }.should raise_error(TypeError) + end + + it "is not shared across fibers" do + fib = Fiber.new do + Thread.current[:value] = 1 + Fiber.yield + Thread.current[:value].should == 1 + end + fib.resume + Thread.current[:value].should be_nil + Thread.current[:value] = 2 + fib.resume + Thread.current[:value] = 2 + end + + it "stores a local in another thread when in a fiber" do + fib = Fiber.new do + t = Thread.new do + sleep + Thread.current[:value].should == 1 + end + + Thread.pass while t.status and t.status != "sleep" + t[:value] = 1 + t.wakeup + t.join + end + fib.resume + end +end diff --git a/spec/ruby/core/thread/exclusive_spec.rb b/spec/ruby/core/thread/exclusive_spec.rb new file mode 100644 index 0000000000..66c87f4713 --- /dev/null +++ b/spec/ruby/core/thread/exclusive_spec.rb @@ -0,0 +1,18 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Thread.exclusive" do + before :each do + ScratchPad.clear + end + + it "yields to the block" do + Thread.exclusive { ScratchPad.record true } + ScratchPad.recorded.should == true + end + + it "returns the result of yielding" do + Thread.exclusive { :result }.should == :result + end + + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/thread/exit_spec.rb b/spec/ruby/core/thread/exit_spec.rb new file mode 100644 index 0000000000..0fb329e66f --- /dev/null +++ b/spec/ruby/core/thread/exit_spec.rb @@ -0,0 +1,15 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/exit', __FILE__) + +describe "Thread#exit!" do + it "needs to be reviewed for spec completeness" +end + +describe "Thread.exit" do + it "causes the current thread to exit" do + thread = Thread.new { Thread.exit; sleep } + thread.join + thread.status.should be_false + end +end diff --git a/spec/ruby/core/thread/fixtures/classes.rb b/spec/ruby/core/thread/fixtures/classes.rb new file mode 100644 index 0000000000..b572c8dd82 --- /dev/null +++ b/spec/ruby/core/thread/fixtures/classes.rb @@ -0,0 +1,298 @@ +unless defined? Channel + require 'thread' + class Channel < Queue + alias receive shift + end +end + +module ThreadSpecs + + class SubThread < Thread + def initialize(*args) + super { args.first << 1 } + end + end + + class Status + attr_reader :thread, :inspect, :status + def initialize(thread) + @thread = thread + @alive = thread.alive? + @inspect = thread.inspect + @status = thread.status + @stop = thread.stop? + end + + def alive? + @alive + end + + def stop? + @stop + end + end + + # TODO: In the great Thread spec rewrite, abstract this + class << self + attr_accessor :state + end + + def self.clear_state + @state = nil + end + + def self.spin_until_sleeping(t) + Thread.pass while t.status and t.status != "sleep" + end + + def self.sleeping_thread + Thread.new do + begin + sleep + ScratchPad.record :woken + rescue Object => e + ScratchPad.record e + end + end + end + + def self.running_thread + Thread.new do + begin + ThreadSpecs.state = :running + loop { Thread.pass } + ScratchPad.record :woken + rescue Object => e + ScratchPad.record e + end + end + end + + def self.completed_thread + Thread.new {} + end + + def self.status_of_current_thread + Thread.new { Status.new(Thread.current) }.value + end + + def self.status_of_running_thread + t = running_thread + Thread.pass while t.status and t.status != "run" + status = Status.new t + t.kill + t.join + status + end + + def self.status_of_completed_thread + t = completed_thread + t.join + Status.new t + end + + def self.status_of_sleeping_thread + t = sleeping_thread + Thread.pass while t.status and t.status != 'sleep' + status = Status.new t + t.run + t.join + status + end + + def self.status_of_blocked_thread + m = Mutex.new + m.lock + t = Thread.new { m.lock } + Thread.pass while t.status and t.status != 'sleep' + status = Status.new t + m.unlock + t.join + status + end + + def self.status_of_killed_thread + t = Thread.new { sleep } + Thread.pass while t.status and t.status != 'sleep' + t.kill + t.join + Status.new t + end + + def self.status_of_thread_with_uncaught_exception + t = Thread.new { raise "error" } + begin + t.join + rescue RuntimeError + end + Status.new t + end + + def self.status_of_dying_running_thread + status = nil + t = dying_thread_ensures { status = Status.new Thread.current } + t.join + status + end + + def self.status_of_dying_sleeping_thread + t = dying_thread_ensures { Thread.stop; } + Thread.pass while t.status and t.status != 'sleep' + status = Status.new t + t.wakeup + t.join + status + end + + def self.status_of_dying_thread_after_sleep + status = nil + t = dying_thread_ensures { + Thread.stop + status = Status.new(Thread.current) + } + Thread.pass while t.status and t.status != 'sleep' + t.wakeup + Thread.pass while t.status and t.status == 'sleep' + t.join + status + end + + def self.dying_thread_ensures(kill_method_name=:kill) + Thread.new do + begin + Thread.current.send(kill_method_name) + ensure + yield + end + end + end + + def self.dying_thread_with_outer_ensure(kill_method_name=:kill) + Thread.new do + begin + begin + Thread.current.send(kill_method_name) + ensure + raise "In dying thread" + end + ensure + yield + end + end + end + + def self.join_dying_thread_with_outer_ensure(kill_method_name=:kill) + t = dying_thread_with_outer_ensure(kill_method_name) { yield } + lambda { t.join }.should raise_error(RuntimeError, "In dying thread") + return t + end + + def self.wakeup_dying_sleeping_thread(kill_method_name=:kill) + t = ThreadSpecs.dying_thread_ensures(kill_method_name) { yield } + Thread.pass while t.status and t.status != 'sleep' + t.wakeup + t.join + end + + def self.critical_is_reset + # Create another thread to verify that it can call Thread.critical= + t = Thread.new do + initial_critical = Thread.critical + Thread.critical = true + Thread.critical = false + initial_critical == false && Thread.critical == false + end + v = t.value + t.join + v + end + + def self.counter + @@counter + end + + def self.counter= c + @@counter = c + end + + def self.increment_counter(incr) + incr.times do + begin + Thread.critical = true + @@counter += 1 + ensure + Thread.critical = false + end + end + end + + def self.critical_thread1 + Thread.critical = true + Thread.current.key?(:thread_specs).should == false + end + + def self.critical_thread2(is_thread_stop) + Thread.current[:thread_specs].should == 101 + Thread.critical.should == !is_thread_stop + unless is_thread_stop + Thread.critical = false + end + end + + def self.main_thread1(critical_thread, is_thread_sleep, is_thread_stop) + # Thread.stop resets Thread.critical. Also, with native threads, the Thread.Stop may not have executed yet + # since the main thread will race with the critical thread + unless is_thread_stop + Thread.critical.should == true + end + critical_thread[:thread_specs] = 101 + if is_thread_sleep or is_thread_stop + # Thread#wakeup calls are not queued up. So we need to ensure that the thread is sleeping before calling wakeup + Thread.pass while critical_thread.status and critical_thread.status != "sleep" + critical_thread.wakeup + end + end + + def self.main_thread2(critical_thread) + Thread.pass # The join below seems to cause a deadlock with CRuby unless Thread.pass is called first + critical_thread.join + Thread.critical.should == false + end + + def self.critical_thread_yields_to_main_thread(is_thread_sleep=false, is_thread_stop=false) + @@after_first_sleep = false + + critical_thread = Thread.new do + Thread.pass while Thread.main.status and Thread.main.status != "sleep" + critical_thread1() + Thread.main.wakeup + yield + Thread.pass while @@after_first_sleep != true # Need to ensure that the next statement does not see the first sleep itself + Thread.pass while Thread.main.status and Thread.main.status != "sleep" + critical_thread2(is_thread_stop) + Thread.main.wakeup + end + + sleep 5 + @@after_first_sleep = true + main_thread1(critical_thread, is_thread_sleep, is_thread_stop) + sleep 5 + main_thread2(critical_thread) + end + + def self.create_critical_thread + Thread.new do + Thread.critical = true + yield + Thread.critical = false + end + end + + def self.create_and_kill_critical_thread(pass_after_kill=false) + ThreadSpecs.create_critical_thread do + Thread.current.kill + Thread.pass if pass_after_kill + ScratchPad.record("status=" + Thread.current.status) + end + end +end diff --git a/spec/ruby/core/thread/fork_spec.rb b/spec/ruby/core/thread/fork_spec.rb new file mode 100644 index 0000000000..d321230812 --- /dev/null +++ b/spec/ruby/core/thread/fork_spec.rb @@ -0,0 +1,9 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/start', __FILE__) + +describe "Thread.fork" do + describe "Thread.start" do + it_behaves_like :thread_start, :fork + end +end diff --git a/spec/ruby/core/thread/group_spec.rb b/spec/ruby/core/thread/group_spec.rb new file mode 100644 index 0000000000..aecc1422ba --- /dev/null +++ b/spec/ruby/core/thread/group_spec.rb @@ -0,0 +1,5 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +describe "Thread#group" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/thread/initialize_spec.rb b/spec/ruby/core/thread/initialize_spec.rb new file mode 100644 index 0000000000..b6345f03de --- /dev/null +++ b/spec/ruby/core/thread/initialize_spec.rb @@ -0,0 +1,27 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#initialize" do + + describe "already initialized" do + + before do + @t = Thread.new { sleep } + end + + after do + @t.kill + @t.join + end + + it "raises a ThreadError" do + lambda { + @t.instance_eval do + initialize {} + end + }.should raise_error(ThreadError) + end + + end + +end diff --git a/spec/ruby/core/thread/inspect_spec.rb b/spec/ruby/core/thread/inspect_spec.rb new file mode 100644 index 0000000000..95e598eb6a --- /dev/null +++ b/spec/ruby/core/thread/inspect_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#inspect" do + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.inspect.should include('run') + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.inspect.should include('run') + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.inspect.should include('sleep') + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.inspect.should include('sleep') + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.inspect.should include('dead') + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.inspect.should include('dead') + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.inspect.should include('dead') + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.inspect.should include('sleep') + end + + it "reports aborting on a killed thread" do + ThreadSpecs.status_of_dying_running_thread.inspect.should include('aborting') + end + + it "reports aborting on a killed thread after sleep" do + ThreadSpecs.status_of_dying_thread_after_sleep.inspect.should include('aborting') + end +end diff --git a/spec/ruby/core/thread/join_spec.rb b/spec/ruby/core/thread/join_spec.rb new file mode 100644 index 0000000000..a6dd2da9a3 --- /dev/null +++ b/spec/ruby/core/thread/join_spec.rb @@ -0,0 +1,62 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#join" do + it "returns the thread when it is finished" do + t = Thread.new {} + t.join.should equal(t) + end + + it "returns the thread when it is finished when given a timeout" do + t = Thread.new {} + t.join + t.join(0).should equal(t) + end + + it "coerces timeout to a Float if it is not nil" do + t = Thread.new {} + t.join + t.join(0).should equal(t) + t.join(0.0).should equal(t) + t.join(nil).should equal(t) + lambda { t.join(:foo) }.should raise_error TypeError + lambda { t.join("bar") }.should raise_error TypeError + end + + it "returns nil if it is not finished when given a timeout" do + c = Channel.new + t = Thread.new { c.receive } + begin + t.join(0).should == nil + ensure + c << true + end + t.join.should == t + end + + it "accepts a floating point timeout length" do + c = Channel.new + t = Thread.new { c.receive } + begin + t.join(0.01).should == nil + ensure + c << true + end + t.join.should == t + end + + it "raises any exceptions encountered in the thread body" do + t = Thread.new { raise NotImplementedError.new("Just kidding") } + lambda { t.join }.should raise_error(NotImplementedError) + end + + it "returns the dead thread" do + t = Thread.new { Thread.current.kill } + t.join.should equal(t) + end + + it "raises any uncaught exception encountered in ensure block" do + t = ThreadSpecs.dying_thread_ensures { raise NotImplementedError.new("Just kidding") } + lambda { t.join }.should raise_error(NotImplementedError) + end +end diff --git a/spec/ruby/core/thread/key_spec.rb b/spec/ruby/core/thread/key_spec.rb new file mode 100644 index 0000000000..6cdfc3ca7f --- /dev/null +++ b/spec/ruby/core/thread/key_spec.rb @@ -0,0 +1,53 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#key?" do + before :each do + @th = Thread.new do + Thread.current[:oliver] = "a" + end + @th.join + end + + it "tests for existance of thread local variables using symbols or strings" do + @th.key?(:oliver).should == true + @th.key?("oliver").should == true + @th.key?(:stanley).should == false + @th.key?(:stanley.to_s).should == false + end + + it "raises exceptions on the wrong type of keys" do + lambda { Thread.current.key? nil }.should raise_error(TypeError) + lambda { Thread.current.key? 5 }.should raise_error(TypeError) + end + + it "is not shared across fibers" do + fib = Fiber.new do + Thread.current[:val1] = 1 + Fiber.yield + Thread.current.key?(:val1).should be_true + Thread.current.key?(:val2).should be_false + end + Thread.current.key?(:val1).should_not be_true + fib.resume + Thread.current[:val2] = 2 + fib.resume + Thread.current.key?(:val1).should be_false + Thread.current.key?(:val2).should be_true + end + + it "stores a local in another thread when in a fiber" do + fib = Fiber.new do + t = Thread.new do + sleep + Thread.current.key?(:value).should be_true + end + + Thread.pass while t.status and t.status != "sleep" + t[:value] = 1 + t.wakeup + t.join + end + fib.resume + end +end diff --git a/spec/ruby/core/thread/keys_spec.rb b/spec/ruby/core/thread/keys_spec.rb new file mode 100644 index 0000000000..0fc8184e06 --- /dev/null +++ b/spec/ruby/core/thread/keys_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#keys" do + it "returns an array of the names of the thread-local variables as symbols" do + th = Thread.new do + Thread.current["cat"] = 'woof' + Thread.current[:cat] = 'meow' + Thread.current[:dog] = 'woof' + end + th.join + th.keys.sort_by {|x| x.to_s}.should == [:cat,:dog] + end + + it "is not shared across fibers" do + fib = Fiber.new do + Thread.current[:val1] = 1 + Fiber.yield + Thread.current.keys.should include(:val1) + Thread.current.keys.should_not include(:val2) + end + Thread.current.keys.should_not include(:val1) + fib.resume + Thread.current[:val2] = 2 + fib.resume + Thread.current.keys.should include(:val2) + Thread.current.keys.should_not include(:val1) + end + + it "stores a local in another thread when in a fiber" do + fib = Fiber.new do + t = Thread.new do + sleep + Thread.current.keys.should include(:value) + end + + Thread.pass while t.status and t.status != "sleep" + t[:value] = 1 + t.wakeup + t.join + end + fib.resume + end +end diff --git a/spec/ruby/core/thread/kill_spec.rb b/spec/ruby/core/thread/kill_spec.rb new file mode 100644 index 0000000000..cf71307af5 --- /dev/null +++ b/spec/ruby/core/thread/kill_spec.rb @@ -0,0 +1,21 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/exit', __FILE__) + +describe "Thread#kill" do + it_behaves_like :thread_exit, :kill +end + +describe "Thread#kill!" do + it "needs to be reviewed for spec completeness" +end + +describe "Thread.kill" do + it "causes the given thread to exit" do + thread = Thread.new { sleep } + Thread.pass while thread.status and thread.status != "sleep" + Thread.kill(thread).should == thread + thread.join + thread.status.should be_false + end +end diff --git a/spec/ruby/core/thread/list_spec.rb b/spec/ruby/core/thread/list_spec.rb new file mode 100644 index 0000000000..b8deb98260 --- /dev/null +++ b/spec/ruby/core/thread/list_spec.rb @@ -0,0 +1,42 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread.list" do + it "includes the current and main thread" do + Thread.list.should include(Thread.current) + Thread.list.should include(Thread.main) + end + + it "includes threads of non-default thread groups" do + t = Thread.new { sleep } + begin + ThreadGroup.new.add(t) + Thread.list.should include(t) + ensure + t.kill + t.join + end + end + + it "does not include deceased threads" do + t = Thread.new { 1; } + t.join + Thread.list.should_not include(t) + end + + it "includes waiting threads" do + c = Channel.new + t = Thread.new { c.receive } + begin + Thread.pass while t.status and t.status != 'sleep' + Thread.list.should include(t) + ensure + c << nil + t.join + end + end +end + +describe "Thread.list" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/thread/main_spec.rb b/spec/ruby/core/thread/main_spec.rb new file mode 100644 index 0000000000..0cada8f59d --- /dev/null +++ b/spec/ruby/core/thread/main_spec.rb @@ -0,0 +1,10 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread.main" do + it "returns the main thread" do + Thread.new { @main = Thread.main ; @current = Thread.current}.join + @main.should_not == @current + @main.should == Thread.current + end +end diff --git a/spec/ruby/core/thread/name_spec.rb b/spec/ruby/core/thread/name_spec.rb new file mode 100644 index 0000000000..0417d7a500 --- /dev/null +++ b/spec/ruby/core/thread/name_spec.rb @@ -0,0 +1,56 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +ruby_version_is '2.3' do + describe "Thread#name" do + before :each do + @thread = Thread.new {} + end + + after :each do + @thread.join + end + + it "is nil initially" do + @thread.name.should == nil + end + + it "returns the thread name" do + @thread.name = "thread_name" + @thread.name.should == "thread_name" + end + end + + describe "Thread#name=" do + before :each do + @thread = Thread.new {} + end + + after :each do + @thread.join + end + + it "can be set to a String" do + @thread.name = "new thread name" + @thread.name.should == "new thread name" + end + + it "raises an ArgumentError if the name includes a null byte" do + lambda { + @thread.name = "new thread\0name" + }.should raise_error(ArgumentError) + end + + it "can be reset to nil" do + @thread.name = nil + @thread.name.should == nil + end + + it "calls #to_str to convert name to String" do + name = mock("Thread#name") + name.should_receive(:to_str).and_return("a thread name") + + @thread.name = name + @thread.name.should == "a thread name" + end + end +end diff --git a/spec/ruby/core/thread/new_spec.rb b/spec/ruby/core/thread/new_spec.rb new file mode 100644 index 0000000000..b1ed5560a1 --- /dev/null +++ b/spec/ruby/core/thread/new_spec.rb @@ -0,0 +1,56 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread.new" do + it "creates a thread executing the given block" do + c = Channel.new + Thread.new { c << true }.join + c << false + c.receive.should == true + end + + it "can pass arguments to the thread block" do + arr = [] + a, b, c = 1, 2, 3 + t = Thread.new(a,b,c) {|d,e,f| arr << d << e << f } + t.join + arr.should == [a,b,c] + end + + it "raises an exception when not given a block" do + lambda { Thread.new }.should raise_error(ThreadError) + end + + it "creates a subclass of thread calls super with a block in initialize" do + arr = [] + t = ThreadSpecs::SubThread.new(arr) + t.join + arr.should == [1] + end + + it "calls #initialize and raises an error if super not used" do + c = Class.new(Thread) do + def initialize + end + end + + lambda { + c.new + }.should raise_error(ThreadError) + end + + it "calls and respects #initialize for the block to use" do + c = Class.new(Thread) do + def initialize + ScratchPad.record [:good] + super { ScratchPad << :in_thread } + end + end + + t = c.new + t.join + + ScratchPad.recorded.should == [:good, :in_thread] + end + +end diff --git a/spec/ruby/core/thread/pass_spec.rb b/spec/ruby/core/thread/pass_spec.rb new file mode 100644 index 0000000000..128de934ac --- /dev/null +++ b/spec/ruby/core/thread/pass_spec.rb @@ -0,0 +1,8 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread.pass" do + it "returns nil" do + Thread.pass.should == nil + end +end diff --git a/spec/ruby/core/thread/priority_spec.rb b/spec/ruby/core/thread/priority_spec.rb new file mode 100644 index 0000000000..b986fb7a0d --- /dev/null +++ b/spec/ruby/core/thread/priority_spec.rb @@ -0,0 +1,68 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#priority" do + before do + @current_priority = Thread.current.priority + ThreadSpecs.clear_state + @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit } + end + + after do + ThreadSpecs.state = :exit + @thread.join + end + + it "inherits the priority of the current thread while running" do + @thread.alive?.should be_true + @thread.priority.should == @current_priority + end + + it "maintain the priority of the current thread after death" do + ThreadSpecs.state = :exit + @thread.join + @thread.alive?.should be_false + @thread.priority.should == @current_priority + end + + it "returns an integer" do + @thread.priority.should be_kind_of(Integer) + end +end + +describe "Thread#priority=" do + before do + ThreadSpecs.clear_state + @thread = Thread.new {} + end + + after do + @thread.join + end + + describe "when set with an integer" do + it "returns an integer" do + value = (@thread.priority = 3) + value.should == 3 + end + + it "clamps the priority to -3..3" do + @thread.priority = 42 + @thread.priority.should == 3 + @thread.priority = -42 + @thread.priority.should == -3 + end + end + + describe "when set with a non-integer" do + it "raises a type error" do + lambda{ @thread.priority = Object.new }.should raise_error(TypeError) + end + end + + it "sets priority even when the thread has died" do + @thread.join + @thread.priority = 3 + @thread.priority.should == 3 + end +end diff --git a/spec/ruby/core/thread/raise_spec.rb b/spec/ruby/core/thread/raise_spec.rb new file mode 100644 index 0000000000..93e0f048b1 --- /dev/null +++ b/spec/ruby/core/thread/raise_spec.rb @@ -0,0 +1,175 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../../../shared/kernel/raise', __FILE__) + +describe "Thread#raise" do + it "ignores dead threads" do + t = Thread.new { :dead } + Thread.pass while t.alive? + lambda {t.raise("Kill the thread")}.should_not raise_error + lambda {t.value}.should_not raise_error + end +end + +describe "Thread#raise on a sleeping thread" do + before :each do + ScratchPad.clear + @thr = ThreadSpecs.sleeping_thread + Thread.pass while @thr.status and @thr.status != "sleep" + end + + after :each do + @thr.kill + @thr.join + end + + it "raises a RuntimeError if no exception class is given" do + @thr.raise + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(RuntimeError) + end + + it "raises the given exception" do + @thr.raise Exception + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + end + + it "raises the given exception with the given message" do + @thr.raise Exception, "get to work" + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + ScratchPad.recorded.message.should == "get to work" + end + + it "raises the given exception and the backtrace is the one of the interrupted thread" do + @thr.raise Exception + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + ScratchPad.recorded.backtrace[0].should include("sleep") + end + + it "is captured and raised by Thread#value" do + t = Thread.new do + sleep + end + + ThreadSpecs.spin_until_sleeping(t) + + t.raise + lambda { t.value }.should raise_error(RuntimeError) + end + + it "raises a RuntimeError when called with no arguments inside rescue" do + t = Thread.new do + begin + 1/0 + rescue ZeroDivisionError + sleep + end + end + begin + raise RangeError + rescue + ThreadSpecs.spin_until_sleeping(t) + t.raise + end + lambda {t.value}.should raise_error(RuntimeError) + end +end + +describe "Thread#raise on a running thread" do + before :each do + ScratchPad.clear + ThreadSpecs.clear_state + + @thr = ThreadSpecs.running_thread + Thread.pass until ThreadSpecs.state == :running + end + + after :each do + @thr.kill + @thr.join + end + + it "raises a RuntimeError if no exception class is given" do + @thr.raise + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(RuntimeError) + end + + it "raises the given exception" do + @thr.raise Exception + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + end + + it "raises the given exception with the given message" do + @thr.raise Exception, "get to work" + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + ScratchPad.recorded.message.should == "get to work" + end + + it "can go unhandled" do + t = Thread.new do + loop { Thread.pass } + end + + t.raise + lambda {t.value}.should raise_error(RuntimeError) + end + + it "raises the given argument even when there is an active exception" do + raised = false + t = Thread.new do + begin + 1/0 + rescue ZeroDivisionError + raised = true + loop { Thread.pass } + end + end + begin + raise "Create an active exception for the current thread too" + rescue + Thread.pass until raised + t.raise RangeError + lambda {t.value}.should raise_error(RangeError) + end + end + + it "raises a RuntimeError when called with no arguments inside rescue" do + raised = false + t = Thread.new do + begin + 1/0 + rescue ZeroDivisionError + raised = true + loop { } + end + end + begin + raise RangeError + rescue + Thread.pass until raised + t.raise + end + lambda {t.value}.should raise_error(RuntimeError) + end +end + +describe "Thread#raise on same thread" do + it_behaves_like :kernel_raise, :raise, Thread.current + + it "raises a RuntimeError when called with no arguments inside rescue" do + t = Thread.new do + begin + 1/0 + rescue ZeroDivisionError + Thread.current.raise + end + end + lambda {t.value}.should raise_error(RuntimeError) + end +end diff --git a/spec/ruby/core/thread/run_spec.rb b/spec/ruby/core/thread/run_spec.rb new file mode 100644 index 0000000000..26ed9ed961 --- /dev/null +++ b/spec/ruby/core/thread/run_spec.rb @@ -0,0 +1,9 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +require File.expand_path('../shared/wakeup', __FILE__) + +describe "Thread#run" do + it_behaves_like :thread_wakeup, :run +end + diff --git a/spec/ruby/core/thread/set_trace_func_spec.rb b/spec/ruby/core/thread/set_trace_func_spec.rb new file mode 100644 index 0000000000..6dd5448d79 --- /dev/null +++ b/spec/ruby/core/thread/set_trace_func_spec.rb @@ -0,0 +1,5 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Thread#set_trace_func" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/thread/shared/exit.rb b/spec/ruby/core/thread/shared/exit.rb new file mode 100644 index 0000000000..f15da360fd --- /dev/null +++ b/spec/ruby/core/thread/shared/exit.rb @@ -0,0 +1,176 @@ +describe :thread_exit, shared: true do + before :each do + ScratchPad.clear + end + + it "kills sleeping thread" do + sleeping_thread = Thread.new do + sleep + ScratchPad.record :after_sleep + end + Thread.pass while sleeping_thread.status and sleeping_thread.status != "sleep" + sleeping_thread.send(@method) + sleeping_thread.join + ScratchPad.recorded.should == nil + end + + it "kills current thread" do + thread = Thread.new do + Thread.current.send(@method) + ScratchPad.record :after_sleep + end + thread.join + ScratchPad.recorded.should == nil + end + + it "runs ensure clause" do + thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record :in_ensure_clause } + thread.join + ScratchPad.recorded.should == :in_ensure_clause + end + + it "runs nested ensure clauses" do + ScratchPad.record [] + @outer = Thread.new do + begin + @inner = Thread.new do + begin + sleep + ensure + ScratchPad << :inner_ensure_clause + end + end + sleep + ensure + ScratchPad << :outer_ensure_clause + @inner.send(@method) + @inner.join + end + end + Thread.pass while @outer.status and @outer.status != "sleep" + Thread.pass until @inner + Thread.pass while @inner.status and @inner.status != "sleep" + @outer.send(@method) + @outer.join + ScratchPad.recorded.should include(:inner_ensure_clause) + ScratchPad.recorded.should include(:outer_ensure_clause) + end + + it "does not set $!" do + thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record $! } + thread.join + ScratchPad.recorded.should == nil + end + + it "cannot be rescued" do + thread = Thread.new do + begin + Thread.current.send(@method) + rescue Exception + ScratchPad.record :in_rescue + end + ScratchPad.record :end_of_thread_block + end + + thread.join + ScratchPad.recorded.should == nil + end + + with_feature :fiber do + it "kills the entire thread when a fiber is active" do + t = Thread.new do + Fiber.new do + sleep + end.resume + ScratchPad.record :fiber_resumed + end + Thread.pass while t.status and t.status != "sleep" + t.send(@method) + t.join + ScratchPad.recorded.should == nil + end + end + + # This spec is a mess. It fails randomly, it hangs on MRI, it needs to be removed + quarantine! do + it "killing dying running does nothing" do + in_ensure_clause = false + exit_loop = true + t = ThreadSpecs.dying_thread_ensures do + in_ensure_clause = true + loop { if exit_loop then break end } + ScratchPad.record :after_stop + end + + Thread.pass until in_ensure_clause == true + 10.times { t.send(@method); Thread.pass } + exit_loop = true + t.join + ScratchPad.recorded.should == :after_stop + end + end + + quarantine! do + + it "propogates inner exception to Thread.join if there is an outer ensure clause" do + thread = ThreadSpecs.dying_thread_with_outer_ensure(@method) { } + lambda { thread.join }.should raise_error(RuntimeError, "In dying thread") + end + + it "runs all outer ensure clauses even if inner ensure clause raises exception" do + ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record :in_outer_ensure_clause } + ScratchPad.recorded.should == :in_outer_ensure_clause + end + + it "sets $! in outer ensure clause if inner ensure clause raises exception" do + ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record $! } + ScratchPad.recorded.to_s.should == "In dying thread" + end + end + + it "can be rescued by outer rescue clause when inner ensure clause raises exception" do + thread = Thread.new do + begin + begin + Thread.current.send(@method) + ensure + raise "In dying thread" + end + rescue Exception + ScratchPad.record $! + end + :end_of_thread_block + end + + thread.value.should == :end_of_thread_block + ScratchPad.recorded.to_s.should == "In dying thread" + end + + it "is deferred if ensure clause does Thread.stop" do + ThreadSpecs.wakeup_dying_sleeping_thread(@method) { Thread.stop; ScratchPad.record :after_sleep } + ScratchPad.recorded.should == :after_sleep + end + + # Hangs on 1.8.6.114 OS X, possibly also on Linux + quarantine! do + it "is deferred if ensure clause sleeps" do + ThreadSpecs.wakeup_dying_sleeping_thread(@method) { sleep; ScratchPad.record :after_sleep } + ScratchPad.recorded.should == :after_sleep + end + end + + # This case occurred in JRuby where native threads are used to provide + # the same behavior as MRI green threads. Key to this issue was the fact + # that the thread which called #exit in its block was also being explicitly + # sent #join from outside the thread. The 100.times provides a certain + # probability that the deadlock will occur. It was sufficient to reliably + # reproduce the deadlock in JRuby. + it "does not deadlock when called from within the thread while being joined from without" do + 100.times do + t = Thread.new { Thread.stop; Thread.current.send(@method) } + Thread.pass while t.status and t.status != "sleep" + t.wakeup.should == t + t.join.should == t + end + end +end diff --git a/spec/ruby/core/thread/shared/start.rb b/spec/ruby/core/thread/shared/start.rb new file mode 100644 index 0000000000..80ce063a0e --- /dev/null +++ b/spec/ruby/core/thread/shared/start.rb @@ -0,0 +1,41 @@ +describe :thread_start, shared: true do + before :each do + ScratchPad.clear + end + + it "raises an ArgumentError if not passed a block" do + lambda { + Thread.send(@method) + }.should raise_error(ArgumentError) + end + + it "spawns a new Thread running the block" do + run = false + t = Thread.send(@method) { run = true } + t.should be_kind_of(Thread) + t.join + + run.should be_true + end + + it "respects Thread subclasses" do + c = Class.new(Thread) + t = c.send(@method) { } + t.should be_kind_of(c) + + t.join + end + + it "does not call #initialize" do + c = Class.new(Thread) do + def initialize + ScratchPad.record :bad + end + end + + t = c.send(@method) { } + t.join + + ScratchPad.recorded.should == nil + end +end diff --git a/spec/ruby/core/thread/shared/wakeup.rb b/spec/ruby/core/thread/shared/wakeup.rb new file mode 100644 index 0000000000..71838d88e5 --- /dev/null +++ b/spec/ruby/core/thread/shared/wakeup.rb @@ -0,0 +1,61 @@ +describe :thread_wakeup, shared: true do + it "can interrupt Kernel#sleep" do + exit_loop = false + after_sleep1 = false + after_sleep2 = false + + t = Thread.new do + while true + break if exit_loop == true + Thread.pass + end + + sleep + after_sleep1 = true + + sleep + after_sleep2 = true + end + + 10.times { t.send(@method); Thread.pass } + t.status.should_not == "sleep" + + exit_loop = true + + 10.times { sleep 0.1 if t.status and t.status != "sleep" } + after_sleep1.should == false # t should be blocked on the first sleep + t.send(@method) + + 10.times { sleep 0.1 if after_sleep1 != true } + 10.times { sleep 0.1 if t.status and t.status != "sleep" } + after_sleep2.should == false # t should be blocked on the second sleep + t.send(@method) + + t.join + end + + it "does not result in a deadlock" do + t = Thread.new do + 100.times { Thread.stop } + end + + while t.status + begin + t.send(@method) + rescue ThreadError + # The thread might die right after. + t.status.should == false + end + Thread.pass + end + + t.status.should == false + t.join + end + + it "raises a ThreadError when trying to wake up a dead thread" do + t = Thread.new { 1 } + t.join + lambda { t.send @method }.should raise_error(ThreadError) + end +end diff --git a/spec/ruby/core/thread/start_spec.rb b/spec/ruby/core/thread/start_spec.rb new file mode 100644 index 0000000000..932e782382 --- /dev/null +++ b/spec/ruby/core/thread/start_spec.rb @@ -0,0 +1,9 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/start', __FILE__) + +describe "Thread.start" do + describe "Thread.start" do + it_behaves_like :thread_start, :start + end +end diff --git a/spec/ruby/core/thread/status_spec.rb b/spec/ruby/core/thread/status_spec.rb new file mode 100644 index 0000000000..6cfdf0be40 --- /dev/null +++ b/spec/ruby/core/thread/status_spec.rb @@ -0,0 +1,60 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#status" do + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.status.should == 'run' + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.status.should == 'run' + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.status.should == 'sleep' + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.status.should == 'sleep' + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.status.should == false + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.status.should == false + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.status.should == nil + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.status.should == 'sleep' + end + + it "reports aborting on a killed thread" do + ThreadSpecs.status_of_dying_running_thread.status.should == 'aborting' + end + + it "reports aborting on a killed thread after sleep" do + ThreadSpecs.status_of_dying_thread_after_sleep.status.should == 'aborting' + end + + it "reports aborting on an externally killed thread that sleeps" do + q = Queue.new + t = Thread.new do + begin + q.push nil + sleep + ensure + q.push Thread.current.status + end + end + q.pop + t.kill + t.join + q.pop.should == 'aborting' + end +end diff --git a/spec/ruby/core/thread/stop_spec.rb b/spec/ruby/core/thread/stop_spec.rb new file mode 100644 index 0000000000..0bc99487fd --- /dev/null +++ b/spec/ruby/core/thread/stop_spec.rb @@ -0,0 +1,54 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread.stop" do + it "causes the current thread to sleep indefinitely" do + t = Thread.new { Thread.stop; 5 } + Thread.pass while t.status and t.status != 'sleep' + t.status.should == 'sleep' + t.run + t.value.should == 5 + end +end + +describe "Thread#stop?" do + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.stop?.should == false + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.stop?.should == false + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.stop?.should == true + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.stop?.should == true + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.stop?.should == true + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.stop?.should == true + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.stop?.should == true + end + + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.stop?.should == false + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.stop?.should == true + end + + it "describes a dying thread after sleep" do + ThreadSpecs.status_of_dying_thread_after_sleep.stop?.should == false + end +end diff --git a/spec/ruby/core/thread/terminate_spec.rb b/spec/ruby/core/thread/terminate_spec.rb new file mode 100644 index 0000000000..bb89d87762 --- /dev/null +++ b/spec/ruby/core/thread/terminate_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/exit', __FILE__) + +describe "Thread#terminate" do + it_behaves_like :thread_exit, :terminate +end diff --git a/spec/ruby/core/thread/thread_variable_get_spec.rb b/spec/ruby/core/thread/thread_variable_get_spec.rb new file mode 100644 index 0000000000..0e02c30fad --- /dev/null +++ b/spec/ruby/core/thread/thread_variable_get_spec.rb @@ -0,0 +1,25 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Thread#thread_variable_get" do + before :each do + @t = Thread.new { } + end + + after :each do + @t.join + end + + it "returns nil if the variable is not set" do + @t.thread_variable_get(:a).should be_nil + end + + it "returns the value previously set by #[]=" do + @t.thread_variable_set :a, 49 + @t.thread_variable_get(:a).should == 49 + end + + it "returns a value private to self" do + @t.thread_variable_set :thread_variable_get_spec, 82 + Thread.current.thread_variable_get(:thread_variable_get_spec).should be_nil + end +end diff --git a/spec/ruby/core/thread/thread_variable_set_spec.rb b/spec/ruby/core/thread/thread_variable_set_spec.rb new file mode 100644 index 0000000000..0f55341132 --- /dev/null +++ b/spec/ruby/core/thread/thread_variable_set_spec.rb @@ -0,0 +1,26 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Thread#thread_variable_set" do + before :each do + @t = Thread.new { } + end + + after :each do + @t.join + end + + it "returns the value set" do + (@t.thread_variable_set :a, 2).should == 2 + end + + it "sets a value that will be returned by #thread_variable_get" do + @t.thread_variable_set :a, 49 + @t.thread_variable_get(:a).should == 49 + end + + it "sets a value private to self" do + @t.thread_variable_set :thread_variable_get_spec, 82 + @t.thread_variable_get(:thread_variable_get_spec).should == 82 + Thread.current.thread_variable_get(:thread_variable_get_spec).should be_nil + end +end diff --git a/spec/ruby/core/thread/thread_variable_spec.rb b/spec/ruby/core/thread/thread_variable_spec.rb new file mode 100644 index 0000000000..b409b3abfc --- /dev/null +++ b/spec/ruby/core/thread/thread_variable_spec.rb @@ -0,0 +1,21 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Thread#thread_variable?" do + before :each do + @t = Thread.new { } + end + + after :each do + @t.join + end + + it "returns false if the thread variables do not contain 'key'" do + @t.thread_variable_set :a, 2 + @t.thread_variable?(:b).should be_false + end + + it "returns true if the thread variables contain 'key'" do + @t.thread_variable_set :a, 2 + @t.thread_variable?(:a).should be_true + end +end diff --git a/spec/ruby/core/thread/thread_variables_spec.rb b/spec/ruby/core/thread/thread_variables_spec.rb new file mode 100644 index 0000000000..538c85c5e4 --- /dev/null +++ b/spec/ruby/core/thread/thread_variables_spec.rb @@ -0,0 +1,24 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Thread#thread_variables" do + before :each do + @t = Thread.new { } + end + + after :each do + @t.join + end + + it "returns the keys of all the values set" do + @t.thread_variable_set :a, 2 + @t.thread_variable_set :b, 4 + @t.thread_variable_set :c, 6 + @t.thread_variables.sort.should == [:a, :b, :c] + end + + it "sets a value private to self" do + @t.thread_variable_set :thread_variables_spec_a, 82 + @t.thread_variable_set :thread_variables_spec_b, 82 + Thread.current.thread_variables.should == [] + end +end diff --git a/spec/ruby/core/thread/value_spec.rb b/spec/ruby/core/thread/value_spec.rb new file mode 100644 index 0000000000..82c0cbf762 --- /dev/null +++ b/spec/ruby/core/thread/value_spec.rb @@ -0,0 +1,18 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Thread#value" do + it "returns the result of the block" do + Thread.new { 3 }.value.should == 3 + end + + it "re-raises an error for an uncaught exception" do + t = Thread.new { raise "Hello" } + lambda { t.value }.should raise_error(RuntimeError, "Hello") + end + + it "is nil for a killed thread" do + t = Thread.new { Thread.current.exit } + t.value.should == nil + end +end diff --git a/spec/ruby/core/thread/wakeup_spec.rb b/spec/ruby/core/thread/wakeup_spec.rb new file mode 100644 index 0000000000..5197a03a35 --- /dev/null +++ b/spec/ruby/core/thread/wakeup_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/wakeup', __FILE__) + +describe "Thread#wakeup" do + it_behaves_like :thread_wakeup, :wakeup +end |