diff options
Diffstat (limited to 'spec/ruby/core/thread')
68 files changed, 3298 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..49be84ea9f --- /dev/null +++ b/spec/ruby/core/thread/abort_on_exception_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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 + -> 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..0abae81a78 --- /dev/null +++ b/spec/ruby/core/thread/add_trace_func_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +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..c2f5f5371d --- /dev/null +++ b/spec/ruby/core/thread/alive_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#alive?" do + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.should.alive? + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.should.alive? + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.should.alive? + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.should.alive? + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.should_not.alive? + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.should_not.alive? + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.should_not.alive? + end + + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.should.alive? + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.should.alive? + 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.should.alive? + 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..cfd556812f --- /dev/null +++ b/spec/ruby/core/thread/allocate_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Thread.allocate" do + it "raises a TypeError" do + -> { + Thread.allocate + }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/thread/backtrace/limit_spec.rb b/spec/ruby/core/thread/backtrace/limit_spec.rb new file mode 100644 index 0000000000..b55ca67ea0 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/limit_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../../spec_helper' + +describe "Thread::Backtrace.limit" do + it "returns maximum backtrace length set by --backtrace-limit command-line option" do + out = ruby_exe("print Thread::Backtrace.limit", options: "--backtrace-limit=2") + out.should == "2" + end + + it "returns -1 when --backtrace-limit command-line option is not set" do + out = ruby_exe("print Thread::Backtrace.limit") + out.should == "-1" + 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..68a69049d9 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb @@ -0,0 +1,93 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +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 + + it 'returns an absolute path when using a relative main script path' do + script = fixture(__FILE__, 'absolute_path_main.rb') + Dir.chdir(File.dirname(script)) do + ruby_exe('absolute_path_main.rb').should == "absolute_path_main.rb\n#{script}\n" + end + end + + it 'returns the correct absolute path when using a relative main script path and changing CWD' do + script = fixture(__FILE__, 'subdir/absolute_path_main_chdir.rb') + sibling = fixture(__FILE__, 'subdir/sibling.rb') + subdir = File.dirname script + Dir.chdir(fixture(__FILE__)) do + ruby_exe('subdir/absolute_path_main_chdir.rb').should == "subdir/absolute_path_main_chdir.rb\n#{subdir}\n#{subdir}\n#{script}\n#{sibling}\n" + end + end + + context "when used in eval with a given filename" do + it "returns nil with absolute_path" do + code = "caller_locations(0)[0].absolute_path" + + eval(code, nil, "foo.rb").should == nil + eval(code, nil, "foo/bar.rb").should == nil + end + end + + context "when used in #method_added" do + it "returns the user filename that defined the method" do + path = fixture(__FILE__, "absolute_path_method_added.rb") + load path + locations = ScratchPad.recorded + locations[0].absolute_path.should == path + # Make sure it's from the class body, not from the file top-level + locations[0].label.should include 'MethodAddedAbsolutePath' + end + end + + context "when used in a core method" do + it "returns nil" do + location = nil + tap { location = caller_locations(1, 1)[0] } + location.label.should =~ /\A(?:Kernel#)?tap\z/ + if location.path.start_with?("<internal:") + location.absolute_path.should == nil + else + location.absolute_path.should == File.realpath(__FILE__) + end + end + end + + context "canonicalization" do + platform_is_not :windows do + before :each do + @file = fixture(__FILE__, "absolute_path.rb") + @symlink = tmp("symlink.rb") + File.symlink(@file, @symlink) + ScratchPad.record [] + end + + after :each do + rm_r @symlink + end + + it "returns a canonical path without symlinks, even when __FILE__ does not" do + realpath = File.realpath(@symlink) + realpath.should_not == @symlink + + load @symlink + ScratchPad.recorded.should == [@symlink, realpath] + end + + it "returns a canonical path without symlinks, even when __FILE__ is removed" do + realpath = File.realpath(@symlink) + realpath.should_not == @symlink + + ScratchPad << -> { rm_r(@symlink) } + load @symlink + ScratchPad.recorded.should == [@symlink, realpath] + end + end + 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..739f62f42f --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/base_label_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +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 + + describe 'when call frame is inside a block' do + before :each do + @frame = ThreadBacktraceLocationSpecs.block_location[0] + end + + it 'returns the name of the method that contains the block' do + @frame.base_label.should == 'block_location' + end + end + + it "is <module:A> for a module body" do + module ThreadBacktraceLocationSpecs + module ModuleLabel + ScratchPad.record caller_locations(0, 1)[0].base_label + end + end + ScratchPad.recorded.should == '<module:ModuleLabel>' + end + + it "is <class:A> for a class body" do + module ThreadBacktraceLocationSpecs + class ClassLabel + ScratchPad.record caller_locations(0, 1)[0].base_label + end + end + ScratchPad.recorded.should == '<class:ClassLabel>' + end + + it "is 'singleton class' for a singleton class body" do + module ThreadBacktraceLocationSpecs + class << Object.new + ScratchPad.record caller_locations(0, 1)[0].base_label + end + end + ScratchPad.recorded.should =~ /\A(singleton class|<singleton class>)\z/ + end +end diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb new file mode 100644 index 0000000000..875e97ffac --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb @@ -0,0 +1,4 @@ +action = ScratchPad.recorded.pop +ScratchPad << __FILE__ +action.call if action +ScratchPad << caller_locations(0)[0].absolute_path diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb new file mode 100644 index 0000000000..d2b23393d4 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb @@ -0,0 +1,2 @@ +puts __FILE__ +puts caller_locations(0)[0].absolute_path diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb new file mode 100644 index 0000000000..26d6298a19 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb @@ -0,0 +1,10 @@ +module ThreadBacktraceLocationSpecs + class MethodAddedAbsolutePath + def self.method_added(name) + ScratchPad.record caller_locations + end + + def foo + end + 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..e903c3e450 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb @@ -0,0 +1,35 @@ +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 + + def self.locations_inside_nested_blocks + first_level_location = nil + second_level_location = nil + third_level_location = nil + + 1.times do + first_level_location = locations[0] + 1.times do + second_level_location = locations[0] + 1.times do + third_level_location = locations[0] + end + end + end + + [first_level_location, second_level_location, third_level_location] + end +end diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb new file mode 100644 index 0000000000..b124c8161c --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb @@ -0,0 +1,5 @@ +1.times do + puts Thread.current.backtrace_locations(1..1)[0].label +end + +require_relative 'locations_in_required' diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb new file mode 100644 index 0000000000..5f5ed89e98 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb @@ -0,0 +1,3 @@ +1.times do + puts Thread.current.backtrace_locations(1..1)[0].label +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..bde208a059 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/main.rb @@ -0,0 +1,5 @@ +def backtrace_location_example + caller_locations[0].path +end + +print backtrace_location_example diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/path.rb b/spec/ruby/core/thread/backtrace/location/fixtures/path.rb new file mode 100644 index 0000000000..fba34cb0bc --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/path.rb @@ -0,0 +1,2 @@ +ScratchPad << __FILE__ +ScratchPad << caller_locations(0)[0].path diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb new file mode 100644 index 0000000000..33c8fb36ef --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb @@ -0,0 +1,11 @@ +puts __FILE__ +puts __dir__ +Dir.chdir __dir__ + +# Check __dir__ is still correct after chdir +puts __dir__ + +puts caller_locations(0)[0].absolute_path + +# require_relative also needs to know the absolute path of the current file so we test it here too +require_relative 'sibling' diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb new file mode 100644 index 0000000000..2a854ddccd --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb @@ -0,0 +1 @@ +puts __FILE__ 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..20e477a5a6 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/inspect_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +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..85ddccc8e3 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/label_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +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 =~ /\A(?:ThreadBacktraceLocationSpecs\.)?method_location\z/ + end + + it 'returns the block name for a block location' do + ThreadBacktraceLocationSpecs.block_location[0].label.should =~ /\Ablock in (?:ThreadBacktraceLocationSpecs\.)?block_location\z/ + end + + it 'returns the module name for a module location' do + ThreadBacktraceLocationSpecs::MODULE_LOCATION[0].label.should include "ThreadBacktraceLocationSpecs" + end + + it 'includes the nesting level of a block as part of the location label' do + first_level_location, second_level_location, third_level_location = + ThreadBacktraceLocationSpecs.locations_inside_nested_blocks + + first_level_location.label.should =~ /\Ablock in (?:ThreadBacktraceLocationSpecs\.)?locations_inside_nested_blocks\z/ + second_level_location.label.should =~ /\Ablock \(2 levels\) in (?:ThreadBacktraceLocationSpecs\.)?locations_inside_nested_blocks\z/ + third_level_location.label.should =~ /\Ablock \(3 levels\) in (?:ThreadBacktraceLocationSpecs\.)?locations_inside_nested_blocks\z/ + end + + it 'sets the location label for a top-level block differently depending on it being in the main file or a required file' do + path = fixture(__FILE__, "locations_in_main.rb") + main_label, required_label = ruby_exe(path).lines + + main_label.should == "block in <main>\n" + required_label.should == "block in <top (required)>\n" + 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..10457f80f0 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Thread::Backtrace::Location#lineno' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + @line = __LINE__ - 1 + end + + it 'returns the line number of the call frame' do + @frame.lineno.should == @line + end + + it 'should be the same line number as in #to_s, including for core methods' do + # Get the caller_locations from a call made into a core library method + locations = [:non_empty].map { caller_locations }[0] + + locations.each do |location| + line_number = location.to_s[/:(\d+):/, 1] + location.lineno.should == Integer(line_number) + end + 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..75f76833a9 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/path_spec.rb @@ -0,0 +1,124 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +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 = __dir__ + 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 :each 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 :each do + 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 + + it 'should be the same path as in #to_s, including for core methods' do + # Get the caller_locations from a call made into a core library method + locations = [:non_empty].map { caller_locations }[0] + + locations.each do |location| + filename = location.to_s[/^(.+):\d+:/, 1] + path = location.path + + path.should == filename + end + end + + context "canonicalization" do + platform_is_not :windows do + before :each do + @file = fixture(__FILE__, "path.rb") + @symlink = tmp("symlink.rb") + File.symlink(@file, @symlink) + ScratchPad.record [] + end + + after :each do + rm_r @symlink + end + + it "returns a non-canonical path with symlinks, the same as __FILE__" do + realpath = File.realpath(@symlink) + realpath.should_not == @symlink + + load @symlink + ScratchPad.recorded.should == [@symlink, @symlink] + 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..5911cdced0 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/to_s_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +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_locations_spec.rb b/spec/ruby/core/thread/backtrace_locations_spec.rb new file mode 100644 index 0000000000..09fe622e0d --- /dev/null +++ b/spec/ruby/core/thread/backtrace_locations_spec.rb @@ -0,0 +1,79 @@ +require_relative '../../spec_helper' + +describe "Thread#backtrace_locations" do + it "returns an Array" do + locations = Thread.current.backtrace_locations + locations.should be_an_instance_of(Array) + locations.should_not be_empty + end + + it "sets each element to a Thread::Backtrace::Location" do + locations = Thread.current.backtrace_locations + locations.each { |loc| loc.should be_an_instance_of(Thread::Backtrace::Location) } + end + + it "can be called on any Thread" do + locations = Thread.new { Thread.current.backtrace_locations }.value + locations.should be_an_instance_of(Array) + locations.should_not be_empty + locations.each { |loc| loc.should be_an_instance_of(Thread::Backtrace::Location) } + end + + it "can be called with a number of locations to omit" do + locations1 = Thread.current.backtrace_locations + locations2 = Thread.current.backtrace_locations(2) + locations2.length.should == locations1[2..-1].length + locations2.map(&:to_s).should == locations1[2..-1].map(&:to_s) + end + + it "can be called with a maximum number of locations to return as second parameter" do + locations1 = Thread.current.backtrace_locations + locations2 = Thread.current.backtrace_locations(2, 3) + locations2.map(&:to_s).should == locations1[2..4].map(&:to_s) + end + + it "can be called with a range" do + locations1 = Thread.current.backtrace_locations + locations2 = Thread.current.backtrace_locations(2..4) + locations2.map(&:to_s).should == locations1[2..4].map(&:to_s) + end + + it "can be called with a range whose end is negative" do + Thread.current.backtrace_locations(2..-1).map(&:to_s).should == Thread.current.backtrace_locations[2..-1].map(&:to_s) + Thread.current.backtrace_locations(2..-2).map(&:to_s).should == Thread.current.backtrace_locations[2..-2].map(&:to_s) + end + + it "can be called with an endless range" do + locations1 = Thread.current.backtrace_locations(0) + locations2 = Thread.current.backtrace_locations(eval("(2..)")) + locations2.map(&:to_s).should == locations1[2..-1].map(&:to_s) + end + + it "can be called with an beginless range" do + locations1 = Thread.current.backtrace_locations(0) + locations2 = Thread.current.backtrace_locations((..5)) + locations2.map(&:to_s)[eval("(2..)")].should == locations1[(..5)].map(&:to_s)[eval("(2..)")] + end + + it "returns nil if omitting more locations than available" do + Thread.current.backtrace_locations(100).should == nil + Thread.current.backtrace_locations(100..-1).should == nil + end + + it "returns [] if omitting exactly the number of locations available" do + omit = Thread.current.backtrace_locations.length + Thread.current.backtrace_locations(omit).should == [] + end + + it "without argument is the same as showing all locations with 0..-1" do + Thread.current.backtrace_locations.map(&:to_s).should == Thread.current.backtrace_locations(0..-1).map(&:to_s) + end + + it "the first location reports the call to #backtrace_locations" do + Thread.current.backtrace_locations(0..0)[0].to_s.should =~ /\A#{__FILE__ }:#{__LINE__ }:in [`'](?:Thread#)?backtrace_locations'\z/ + end + + it "[1..-1] is the same as #caller_locations(0..-1) for Thread.current" do + Thread.current.backtrace_locations(1..-1).map(&:to_s).should == caller_locations(0..-1).map(&:to_s) + 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..15bb29a349 --- /dev/null +++ b/spec/ruby/core/thread/backtrace_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' + +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 =~ /[`'](?:Kernel#)?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 + + it "returns an array (which may be empty) immediately after the thread is created" do + t = Thread.new { sleep } + backtrace = t.backtrace + t.kill + t.join + backtrace.should be_kind_of(Array) + end + + it "can be called with a number of locations to omit" do + locations1 = Thread.current.backtrace + locations2 = Thread.current.backtrace(2) + locations1[2..-1].length.should == locations2.length + locations1[2..-1].map(&:to_s).should == locations2.map(&:to_s) + end + + it "can be called with a maximum number of locations to return as second parameter" do + locations1 = Thread.current.backtrace + locations2 = Thread.current.backtrace(2, 3) + locations1[2..4].map(&:to_s).should == locations2.map(&:to_s) + end + + it "can be called with a range" do + locations1 = Thread.current.backtrace + locations2 = Thread.current.backtrace(2..4) + locations1[2..4].map(&:to_s).should == locations2.map(&:to_s) + end + + it "can be called with a range whose end is negative" do + Thread.current.backtrace(2..-1).should == Thread.current.backtrace[2..-1] + Thread.current.backtrace(2..-2).should == Thread.current.backtrace[2..-2] + end + + it "returns nil if omitting more locations than available" do + Thread.current.backtrace(100).should == nil + Thread.current.backtrace(100..-1).should == nil + end + + it "returns [] if omitting exactly the number of locations available" do + omit = Thread.current.backtrace.length + Thread.current.backtrace(omit).should == [] + 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..f5ed1d95cd --- /dev/null +++ b/spec/ruby/core/thread/current_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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 + + it "returns the correct thread in a Fiber" do + # This catches a bug where Fibers are running on a thread-pool + # and Fibers from a different Ruby Thread reuse the same native thread. + # Caching the Ruby Thread based on the native thread is not correct in that case. + 2.times do + t = Thread.new { + cur = Thread.current + Fiber.new { + Thread.current + }.resume.should equal cur + cur + } + t.value.should equal t + end + end +end diff --git a/spec/ruby/core/thread/each_caller_location_spec.rb b/spec/ruby/core/thread/each_caller_location_spec.rb new file mode 100644 index 0000000000..aa7423675b --- /dev/null +++ b/spec/ruby/core/thread/each_caller_location_spec.rb @@ -0,0 +1,47 @@ +require_relative '../../spec_helper' + +describe "Thread.each_caller_location" do + it "iterates through the current execution stack and matches caller_locations content and type" do + ScratchPad.record [] + Thread.each_caller_location { |l| ScratchPad << l; } + + ScratchPad.recorded.map(&:to_s).should == caller_locations.map(&:to_s) + ScratchPad.recorded[0].should be_kind_of(Thread::Backtrace::Location) + end + + it "returns subset of 'Thread.to_enum(:each_caller_location)' locations" do + ar = [] + ecl = Thread.each_caller_location { |x| ar << x } + + (ar.map(&:to_s) - Thread.to_enum(:each_caller_location).to_a.map(&:to_s)).should.empty? + end + + it "stops the backtrace iteration if 'break' occurs" do + i = 0 + ar = [] + ecl = Thread.each_caller_location do |x| + ar << x + i += 1 + break x if i == 2 + end + + ar.map(&:to_s).should == caller_locations(1, 2).map(&:to_s) + ecl.should be_kind_of(Thread::Backtrace::Location) + end + + it "returns nil" do + Thread.each_caller_location {}.should == nil + end + + it "raises LocalJumpError when called without a block" do + -> { + Thread.each_caller_location + }.should raise_error(LocalJumpError, "no block given") + end + + it "doesn't accept keyword arguments" do + -> { + Thread.each_caller_location(12, foo: 10) {} + }.should raise_error(ArgumentError); + 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..fde9d1f440 --- /dev/null +++ b/spec/ruby/core/thread/element_reference_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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 "converts a key that is neither String nor Symbol with #to_str" do + key = mock('value') + key.should_receive(:to_str).and_return('value') + + th = Thread.new do + Thread.current[:value] = 1 + end.join + + th[key].should == 1 + end + + it "raises exceptions on the wrong type of keys" do + -> { Thread.current[nil] }.should raise_error(TypeError) + -> { 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..f205177304 --- /dev/null +++ b/spec/ruby/core/thread/element_set_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#[]=" do + after :each do + Thread.current[:value] = nil + end + + it "raises a FrozenError if the thread is frozen" do + Thread.new do + th = Thread.current + th.freeze + -> { + th[:foo] = "bar" + }.should raise_error(FrozenError, "can't modify frozen thread locals") + end.join + end + + it "accepts Strings and Symbols" do + t1 = Thread.new do + Thread.current[:value] = 1 + end.join + t2 = Thread.new do + Thread.current["value"] = 2 + end.join + + t1[:value].should == 1 + t2[:value].should == 2 + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('value') + key.should_receive(:to_str).and_return('value') + + th = Thread.new do + Thread.current[key] = 1 + end.join + + th[:value].should == 1 + end + + it "raises exceptions on the wrong type of keys" do + -> { Thread.current[nil] = true }.should raise_error(TypeError) + -> { 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/exit_spec.rb b/spec/ruby/core/thread/exit_spec.rb new file mode 100644 index 0000000000..c3f710920e --- /dev/null +++ b/spec/ruby/core/thread/exit_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exit' + +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/fetch_spec.rb b/spec/ruby/core/thread/fetch_spec.rb new file mode 100644 index 0000000000..85ffb71874 --- /dev/null +++ b/spec/ruby/core/thread/fetch_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' + +describe 'Thread#fetch' do + describe 'with 2 arguments' do + it 'returns the value of the fiber-local variable if value has been assigned' do + th = Thread.new { Thread.current[:cat] = 'meow' } + th.join + th.fetch(:cat, true).should == 'meow' + end + + it "returns the default value if fiber-local variable hasn't been assigned" do + th = Thread.new {} + th.join + th.fetch(:cat, true).should == true + end + end + + describe 'with 1 argument' do + it 'raises a KeyError when the Thread does not have a fiber-local variable of the same name' do + th = Thread.new {} + th.join + -> { th.fetch(:cat) }.should raise_error(KeyError) + end + + it 'returns the value of the fiber-local variable if value has been assigned' do + th = Thread.new { Thread.current[:cat] = 'meow' } + th.join + th.fetch(:cat).should == 'meow' + end + end + + describe 'with a block' do + it 'returns the value of the fiber-local variable if value has been assigned' do + th = Thread.new { Thread.current[:cat] = 'meow' } + th.join + th.fetch(:cat) { true }.should == 'meow' + end + + it "returns the block value if fiber-local variable hasn't been assigned" do + th = Thread.new {} + th.join + th.fetch(:cat) { true }.should == true + end + + it "does not call the block if value has been assigned" do + th = Thread.new { Thread.current[:cat] = 'meow' } + th.join + var = :not_updated + th.fetch(:cat) { var = :updated }.should == 'meow' + var.should == :not_updated + end + + it "uses the block if a default is given and warns about it" do + th = Thread.new {} + th.join + -> { + th.fetch(:cat, false) { true }.should == true + }.should complain(/warning: block supersedes default value argument/) + end + end + + it 'raises an ArgumentError when not passed one or two arguments' do + -> { Thread.current.fetch() }.should raise_error(ArgumentError) + -> { Thread.current.fetch(1, 2, 3) }.should raise_error(ArgumentError) + 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..7c485660a8 --- /dev/null +++ b/spec/ruby/core/thread/fixtures/classes.rb @@ -0,0 +1,322 @@ +module ThreadSpecs + + class SubThread < Thread + def initialize(*args) + super { args.first << 1 } + end + end + + class NewThreadToRaise + def self.raise(*args, **kwargs, &block) + thread = Thread.new do + Thread.current.report_on_exception = false + + if block_given? + block.call do + sleep + end + else + sleep + end + end + + Thread.pass until thread.stop? + + thread.raise(*args, **kwargs) + + thread.join + ensure + thread.kill if thread.alive? + Thread.pass while thread.alive? # Thread#kill may not terminate a thread immediately so it may be detected as a leaked one + end + end + + class Status + attr_reader :thread, :inspect, :status, :to_s + def initialize(thread) + @thread = thread + @alive = thread.alive? + @inspect = thread.inspect + @to_s = thread.to_s + @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 { + Thread.current.report_on_exception = false + 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 + Thread.current.report_on_exception = false + 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 + Thread.current.report_on_exception = false + 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 } + -> { 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..a2f4181298 --- /dev/null +++ b/spec/ruby/core/thread/fork_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/start' + +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..d0d4704b66 --- /dev/null +++ b/spec/ruby/core/thread/group_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "Thread#group" do + it "returns the default thread group for the main thread" do + Thread.main.group.should == ThreadGroup::Default + end + + it "returns the thread group explicitly set for this thread" do + thread = Thread.new { nil } + thread_group = ThreadGroup.new + thread_group.add(thread) + thread.group.should == thread_group + ensure + thread.join if thread + end +end diff --git a/spec/ruby/core/thread/handle_interrupt_spec.rb b/spec/ruby/core/thread/handle_interrupt_spec.rb new file mode 100644 index 0000000000..ea7e81cb98 --- /dev/null +++ b/spec/ruby/core/thread/handle_interrupt_spec.rb @@ -0,0 +1,125 @@ +require_relative '../../spec_helper' + +describe "Thread.handle_interrupt" do + def make_handle_interrupt_thread(interrupt_config, blocking = true) + interrupt_class = Class.new(RuntimeError) + + ScratchPad.record [] + + in_handle_interrupt = Queue.new + can_continue = Queue.new + + thread = Thread.new do + begin + Thread.handle_interrupt(interrupt_config) do + begin + in_handle_interrupt << true + if blocking + Thread.pass # Make it clearer the other thread needs to wait for this one to be in #pop + can_continue.pop + else + begin + can_continue.pop(true) + rescue ThreadError + Thread.pass + retry + end + end + rescue interrupt_class + ScratchPad << :interrupted + end + end + rescue interrupt_class + ScratchPad << :deferred + end + end + + in_handle_interrupt.pop + if blocking + # Ensure the thread is inside Thread#pop, as if thread.raise is done before it would be deferred + Thread.pass until thread.stop? + end + thread.raise interrupt_class, "interrupt" + can_continue << true + thread.join + + ScratchPad.recorded + end + + before :each do + Thread.pending_interrupt?.should == false # sanity check + end + + it "with :never defers interrupts until exiting the handle_interrupt block" do + make_handle_interrupt_thread(RuntimeError => :never).should == [:deferred] + end + + it "with :on_blocking defers interrupts until the next blocking call" do + make_handle_interrupt_thread(RuntimeError => :on_blocking).should == [:interrupted] + make_handle_interrupt_thread({ RuntimeError => :on_blocking }, false).should == [:deferred] + end + + it "with :immediate handles interrupts immediately" do + make_handle_interrupt_thread(RuntimeError => :immediate).should == [:interrupted] + end + + it "with :immediate immediately runs pending interrupts, before the block" do + Thread.handle_interrupt(RuntimeError => :never) do + current = Thread.current + Thread.new { + current.raise "interrupt immediate" + }.join + + Thread.pending_interrupt?.should == true + -> { + Thread.handle_interrupt(RuntimeError => :immediate) { + flunk "not reached" + } + }.should raise_error(RuntimeError, "interrupt immediate") + Thread.pending_interrupt?.should == false + end + end + + it "also works with suspended Fibers and does not duplicate interrupts" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + + Thread.handle_interrupt(RuntimeError => :never) do + current = Thread.current + Thread.new { + current.raise "interrupt with fibers" + }.join + + Thread.pending_interrupt?.should == true + -> { + Thread.handle_interrupt(RuntimeError => :immediate) { + flunk "not reached" + } + }.should raise_error(RuntimeError, "interrupt with fibers") + Thread.pending_interrupt?.should == false + end + + fiber.resume + end + + it "runs pending interrupts at the end of the block, even if there was an exception raised in the block" do + executed = false + -> { + Thread.handle_interrupt(RuntimeError => :never) do + current = Thread.current + Thread.new { + current.raise "interrupt exception" + }.join + + Thread.pending_interrupt?.should == true + executed = true + raise "regular exception" + end + }.should raise_error(RuntimeError, "interrupt exception") + executed.should == true + end + + it "supports multiple pairs in the Hash" do + make_handle_interrupt_thread(ArgumentError => :never, RuntimeError => :never).should == [:deferred] + end +end diff --git a/spec/ruby/core/thread/ignore_deadlock_spec.rb b/spec/ruby/core/thread/ignore_deadlock_spec.rb new file mode 100644 index 0000000000..b48bc9f9b0 --- /dev/null +++ b/spec/ruby/core/thread/ignore_deadlock_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Thread.ignore_deadlock" do + it "returns false by default" do + Thread.ignore_deadlock.should == false + end +end + +describe "Thread.ignore_deadlock=" do + it "changes the value of Thread.ignore_deadlock" do + ignore_deadlock = Thread.ignore_deadlock + Thread.ignore_deadlock = true + begin + Thread.ignore_deadlock.should == true + ensure + Thread.ignore_deadlock = ignore_deadlock + end + end +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..4fca900cd8 --- /dev/null +++ b/spec/ruby/core/thread/initialize_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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 + -> { + @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..bd6e0c31fc --- /dev/null +++ b/spec/ruby/core/thread/inspect_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Thread#inspect" do + it_behaves_like :thread_to_s, :inspect +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..213fe2e505 --- /dev/null +++ b/spec/ruby/core/thread/join_spec.rb @@ -0,0 +1,70 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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) + end + + it "raises TypeError if the argument is not a valid timeout" do + t = Thread.new { } + t.join + -> { t.join(:foo) }.should raise_error TypeError + -> { t.join("bar") }.should raise_error TypeError + end + + it "returns nil if it is not finished when given a timeout" do + q = Queue.new + t = Thread.new { q.pop } + begin + t.join(0).should == nil + ensure + q << true + end + t.join.should == t + end + + it "accepts a floating point timeout length" do + q = Queue.new + t = Thread.new { q.pop } + begin + t.join(0.01).should == nil + ensure + q << true + end + t.join.should == t + end + + it "raises any exceptions encountered in the thread body" do + t = Thread.new { + Thread.current.report_on_exception = false + raise NotImplementedError.new("Just kidding") + } + -> { 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") } + -> { 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..339fa98f53 --- /dev/null +++ b/spec/ruby/core/thread/key_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#key?" do + before :each do + @th = Thread.new do + Thread.current[:oliver] = "a" + end + @th.join + end + + it "tests for existence 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 "converts a key that is neither String nor Symbol with #to_str" do + key = mock('key') + key.should_receive(:to_str).and_return('oliver') + + @th.key?(key).should == true + end + + it "raises exceptions on the wrong type of keys" do + -> { Thread.current.key? nil }.should raise_error(TypeError) + -> { 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..15efda51d6 --- /dev/null +++ b/spec/ruby/core/thread/keys_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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..4b62c686c7 --- /dev/null +++ b/spec/ruby/core/thread/kill_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exit' + +# This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/builds/19473223/job/f69derxnlo09xhuj +# TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard. +platform_is_not :mingw do + describe "Thread#kill" do + it_behaves_like :thread_exit, :kill + 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 +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..3c6f70c13e --- /dev/null +++ b/spec/ruby/core/thread/list_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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 + q = Queue.new + t = Thread.new { q.pop } + begin + Thread.pass while t.status and t.status != 'sleep' + Thread.list.should include(t) + ensure + q << nil + t.join + end + end + + it "returns instances of Thread and not null or nil values" do + spawner = Thread.new do + Array.new(100) do + Thread.new {} + end + end + + begin + Thread.list.each { |th| + th.should be_kind_of(Thread) + } + end while spawner.alive? + + threads = spawner.value + threads.each(&:join) + end +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..ec91709576 --- /dev/null +++ b/spec/ruby/core/thread/main_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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..9b3d2f4b09 --- /dev/null +++ b/spec/ruby/core/thread/name_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' + +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 + -> { + @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 diff --git a/spec/ruby/core/thread/native_thread_id_spec.rb b/spec/ruby/core/thread/native_thread_id_spec.rb new file mode 100644 index 0000000000..374cc59279 --- /dev/null +++ b/spec/ruby/core/thread/native_thread_id_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +platform_is :linux, :darwin, :windows, :freebsd do + describe "Thread#native_thread_id" do + it "returns an integer when the thread is alive" do + Thread.current.native_thread_id.should be_kind_of(Integer) + end + + it "returns nil when the thread is not running" do + t = Thread.new {} + t.join + t.native_thread_id.should == nil + end + + it "each thread has different native thread id" do + t = Thread.new { sleep } + Thread.pass until t.stop? + main_thread_id = Thread.current.native_thread_id + t_thread_id = t.native_thread_id + + if ruby_version_is "3.3" + # native_thread_id can be nil on a M:N scheduler + t_thread_id.should be_kind_of(Integer) if t_thread_id != nil + else + t_thread_id.should be_kind_of(Integer) + end + + main_thread_id.should_not == t_thread_id + + t.run + t.join + t.native_thread_id.should == nil + 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..47a836201c --- /dev/null +++ b/spec/ruby/core/thread/new_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread.new" do + it "creates a thread executing the given block" do + q = Queue.new + Thread.new { q << true }.join + q << false + q.pop.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 + -> { 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 + + -> { + 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 + + it "releases Mutexes held by the Thread when the Thread finishes" do + m1 = Mutex.new + m2 = Mutex.new + t = Thread.new { + m1.lock + m1.should.locked? + m2.lock + m2.should.locked? + } + t.join + m1.should_not.locked? + m2.should_not.locked? + end + + it "releases Mutexes held by the Thread when the Thread finishes, also with Mutex#synchronize" do + m = Mutex.new + t = Thread.new { + m.synchronize { + m.unlock + m.lock + } + m.lock + m.should.locked? + } + t.join + m.should_not.locked? + 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..a5ac11a58c --- /dev/null +++ b/spec/ruby/core/thread/pass_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread.pass" do + it "returns nil" do + Thread.pass.should == nil + end +end diff --git a/spec/ruby/core/thread/pending_interrupt_spec.rb b/spec/ruby/core/thread/pending_interrupt_spec.rb new file mode 100644 index 0000000000..cd565d92a4 --- /dev/null +++ b/spec/ruby/core/thread/pending_interrupt_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' + +describe "Thread.pending_interrupt?" do + it "returns false if there are no pending interrupts, e.g., outside any Thread.handle_interrupt block" do + Thread.pending_interrupt?.should == false + end + + it "returns true if there are pending interrupts, e.g., Thread#raise inside Thread.handle_interrupt" do + executed = false + -> { + Thread.handle_interrupt(RuntimeError => :never) do + Thread.pending_interrupt?.should == false + + current = Thread.current + Thread.new { + current.raise "interrupt" + }.join + + Thread.pending_interrupt?.should == true + executed = true + end + }.should raise_error(RuntimeError, "interrupt") + executed.should == true + Thread.pending_interrupt?.should == false + end +end + +describe "Thread#pending_interrupt?" do + it "returns whether the given threads has pending interrupts" do + Thread.current.pending_interrupt?.should == false + 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..e13ad478b5 --- /dev/null +++ b/spec/ruby/core/thread/priority_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#priority" do + before :each do + @current_priority = Thread.current.priority + ThreadSpecs.clear_state + @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit } + Thread.pass until @thread.alive? + end + + after :each 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 :each do + ThreadSpecs.clear_state + @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit } + Thread.pass until @thread.alive? + end + + after :each do + ThreadSpecs.state = :exit + @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 + ->{ @thread.priority = Object.new }.should raise_error(TypeError) + end + end + + it "sets priority even when the thread has died" do + thread = Thread.new {} + 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..b473eabd42 --- /dev/null +++ b/spec/ruby/core/thread/raise_spec.rb @@ -0,0 +1,235 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/raise' + +describe "Thread#raise" do + it_behaves_like :kernel_raise, :raise, ThreadSpecs::NewThreadToRaise + it_behaves_like :kernel_raise_across_contexts, :raise, ThreadSpecs::NewThreadToRaise + + it "ignores dead threads and returns nil" do + t = Thread.new { :dead } + Thread.pass while t.alive? + t.raise("Kill the thread").should == nil + t.join + 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 + Thread.current.report_on_exception = false + sleep + end + + ThreadSpecs.spin_until_sleeping(t) + + t.raise + -> { t.value }.should raise_error(RuntimeError) + end + + it "raises a RuntimeError when called with no arguments inside rescue" do + t = Thread.new do + Thread.current.report_on_exception = false + begin + 1/0 + rescue ZeroDivisionError + sleep + end + end + begin + raise RangeError + rescue + ThreadSpecs.spin_until_sleeping(t) + t.raise + end + -> { t.value }.should raise_error(RuntimeError) + end + + it "re-raises a previously rescued exception without overwriting the backtrace" do + t = Thread.new do + -> { # To make sure there is at least one entry in the call stack + begin + sleep + rescue => e + e + end + }.call + end + + ThreadSpecs.spin_until_sleeping(t) + + begin + initial_raise_line = __LINE__; raise 'raised' + rescue => raised + raise_again_line = __LINE__; t.raise raised + raised_again = t.value + + raised_again.backtrace.first.should include("#{__FILE__}:#{initial_raise_line}:") + raised_again.backtrace.first.should_not include("#{__FILE__}:#{raise_again_line}:") + end + end + + it "calls #exception in both the caller and in the target thread" do + cls = Class.new(Exception) do + attr_accessor :log + def initialize(*args) + @log = [] # This is shared because the super #exception uses a shallow clone + super + end + + def exception(*args) + @log << [self, Thread.current, args] + super + end + end + exc = cls.new + + @thr.raise exc, "Thread#raise #exception spec" + @thr.join + ScratchPad.recorded.should.is_a?(cls) + exc.log.should == [ + [exc, Thread.current, ["Thread#raise #exception spec"]], + [ScratchPad.recorded, @thr, []] + ] + 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 + q = Queue.new + t = Thread.new do + Thread.current.report_on_exception = false + q << true + loop { Thread.pass } + end + + q.pop # wait for `report_on_exception = false`. + t.raise + -> { 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 + Thread.current.report_on_exception = false + 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 + -> { 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 + Thread.current.report_on_exception = false + begin + 1/0 + rescue ZeroDivisionError + raised = true + loop { Thread.pass } + end + end + begin + raise RangeError + rescue + Thread.pass until raised + t.raise + end + -> { 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 + Thread.current.report_on_exception = false + begin + 1/0 + rescue ZeroDivisionError + Thread.current.raise + end + end + -> { t.value }.should raise_error(RuntimeError, '') + end +end diff --git a/spec/ruby/core/thread/report_on_exception_spec.rb b/spec/ruby/core/thread/report_on_exception_spec.rb new file mode 100644 index 0000000000..d9daa041cd --- /dev/null +++ b/spec/ruby/core/thread/report_on_exception_spec.rb @@ -0,0 +1,155 @@ +require_relative '../../spec_helper' + +describe "Thread.report_on_exception" do + it "defaults to true" do + ruby_exe("p Thread.report_on_exception").should == "true\n" + end +end + +describe "Thread.report_on_exception=" do + before :each do + @report_on_exception = Thread.report_on_exception + end + + after :each do + Thread.report_on_exception = @report_on_exception + end + + it "changes the default value for new threads" do + Thread.report_on_exception = true + Thread.report_on_exception.should == true + t = Thread.new {} + t.join + t.report_on_exception.should == true + end +end + +describe "Thread#report_on_exception" do + it "returns true for the main Thread" do + Thread.current.report_on_exception.should == true + end + + it "returns true for new Threads" do + Thread.new { Thread.current.report_on_exception }.value.should == true + end + + it "returns whether the Thread will print a backtrace if it exits with an exception" do + t = Thread.new { Thread.current.report_on_exception = true } + t.join + t.report_on_exception.should == true + + t = Thread.new { Thread.current.report_on_exception = false } + t.join + t.report_on_exception.should == false + end +end + +describe "Thread#report_on_exception=" do + describe "when set to true" do + it "prints a backtrace on $stderr if it terminates with an exception" do + t = nil + -> { + t = Thread.new { + Thread.current.report_on_exception = true + raise RuntimeError, "Thread#report_on_exception specs" + } + Thread.pass while t.alive? + }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception specs/m) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception specs") + end + + it "prints a backtrace on $stderr in the regular backtrace order" do + line_raise = __LINE__ + 2 + def foo + raise RuntimeError, "Thread#report_on_exception specs backtrace order" + end + + line_call_foo = __LINE__ + 5 + go = false + t = Thread.new { + Thread.current.report_on_exception = true + Thread.pass until go + foo + } + + -> { + go = true + Thread.pass while t.alive? + }.should output("", /\A +#{Regexp.quote(t.inspect)}\sterminated\swith\sexception\s\(report_on_exception\sis\strue\):\n +#{Regexp.quote(__FILE__)}:#{line_raise}:in\s[`']foo':\sThread\#report_on_exception\sspecs\sbacktrace\sorder\s\(RuntimeError\)\n +\tfrom\s#{Regexp.quote(__FILE__)}:#{line_call_foo}:in\s[`']block\s\(4\slevels\)\sin\s<top\s\(required\)>'\n +\z/x) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception specs backtrace order") + end + + it "prints the backtrace even if the thread was killed just after Thread#raise" do + t = nil + ready = false + -> { + t = Thread.new { + Thread.current.report_on_exception = true + ready = true + sleep + } + + Thread.pass until ready and t.stop? + t.raise RuntimeError, "Thread#report_on_exception before kill spec" + t.kill + Thread.pass while t.alive? + }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception before kill spec/m) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception before kill spec") + end + end + + describe "when set to false" do + it "lets the thread terminates silently with an exception" do + t = nil + -> { + t = Thread.new { + Thread.current.report_on_exception = false + raise RuntimeError, "Thread#report_on_exception specs" + } + Thread.pass while t.alive? + }.should output("", "") + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception specs") + end + end + + describe "when used in conjunction with Thread#abort_on_exception" do + it "first reports then send the exception back to the main Thread" do + t = nil + mutex = Mutex.new + mutex.lock + -> { + t = Thread.new { + Thread.current.abort_on_exception = true + Thread.current.report_on_exception = true + mutex.lock + mutex.unlock + raise RuntimeError, "Thread#report_on_exception specs" + } + + -> { + mutex.sleep(5) + }.should raise_error(RuntimeError, "Thread#report_on_exception specs") + }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception specs/m) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception specs") + end + 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..f86f793489 --- /dev/null +++ b/spec/ruby/core/thread/run_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +require_relative 'shared/wakeup' + +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..e5d8298ae0 --- /dev/null +++ b/spec/ruby/core/thread/set_trace_func_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +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..13e8832684 --- /dev/null +++ b/spec/ruby/core/thread/shared/exit.rb @@ -0,0 +1,219 @@ +describe :thread_exit, shared: true do + before :each do + ScratchPad.clear + end + + # This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/builds/19390874/job/wv1bsm8skd4e1pxl + # TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard. + platform_is_not :mingw do + + 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 "does not reset $!" do + ScratchPad.record [] + + exc = RuntimeError.new("foo") + thread = Thread.new do + begin + raise exc + ensure + ScratchPad << $! + begin + Thread.current.send(@method) + ensure + ScratchPad << $! + end + end + end + thread.join + ScratchPad.recorded.should == [exc, exc] + 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 + + 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 + + it "kills other fibers of that thread without running their ensure clauses" do + t = Thread.new do + f = Fiber.new do + ScratchPad.record :fiber_resumed + begin + Fiber.yield + ensure + ScratchPad.record :fiber_ensure + end + end + f.resume + sleep + end + Thread.pass until t.stop? + t.send(@method) + t.join + ScratchPad.recorded.should == :fiber_resumed + 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 "propagates inner exception to Thread.join if there is an outer ensure clause" do + thread = ThreadSpecs.dying_thread_with_outer_ensure(@method) { } + -> { 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 # platform_is_not :mingw +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..2ba926bf00 --- /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 + -> { + 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/to_s.rb b/spec/ruby/core/thread/shared/to_s.rb new file mode 100644 index 0000000000..43640deb33 --- /dev/null +++ b/spec/ruby/core/thread/shared/to_s.rb @@ -0,0 +1,53 @@ +require_relative '../fixtures/classes' + +describe :thread_to_s, shared: true do + it "returns a description including file and line number" do + thread, line = Thread.new { "hello" }, __LINE__ + thread.join + thread.send(@method).should =~ /^#<Thread:([^ ]*?) #{Regexp.escape __FILE__}:#{line} \w+>$/ + end + + it "has a binary encoding" do + ThreadSpecs.status_of_current_thread.send(@method).encoding.should == Encoding::BINARY + end + + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.send(@method).should include('run') + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.send(@method).should include('run') + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.send(@method).should include('sleep') + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.send(@method).should include('sleep') + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.send(@method).should include('dead') + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.send(@method).should include('dead') + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.send(@method).should include('dead') + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.send(@method).should include('sleep') + end + + it "reports aborting on a killed thread" do + ThreadSpecs.status_of_dying_running_thread.send(@method).should include('aborting') + end + + it "reports aborting on a killed thread after sleep" do + ThreadSpecs.status_of_dying_thread_after_sleep.send(@method).should include('aborting') + 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..6f010fea25 --- /dev/null +++ b/spec/ruby/core/thread/shared/wakeup.rb @@ -0,0 +1,62 @@ +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 + 10.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 + sleep 0.001 + 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 + -> { 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..3dd040f98b --- /dev/null +++ b/spec/ruby/core/thread/start_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/start' + +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..4fde663c91 --- /dev/null +++ b/spec/ruby/core/thread/status_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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..084ab46ef6 --- /dev/null +++ b/spec/ruby/core/thread/stop_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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.should_not.stop? + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.should_not.stop? + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.should.stop? + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.should.stop? + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.should.stop? + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.should.stop? + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.should.stop? + end + + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.should_not.stop? + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.should.stop? + end + + it "describes a dying thread after sleep" do + ThreadSpecs.status_of_dying_thread_after_sleep.should_not.stop? + 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..cf6cab472b --- /dev/null +++ b/spec/ruby/core/thread/terminate_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exit' + +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..1ea34cf2b3 --- /dev/null +++ b/spec/ruby/core/thread/thread_variable_get_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' + +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 #thread_variable_set" 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 + + it "accepts String and Symbol keys interchangeably" do + @t.thread_variable_set("a", 49) + @t.thread_variable_get("a").should == 49 + @t.thread_variable_get(:a).should == 49 + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('key') + key.should_receive(:to_str).and_return('a') + @t.thread_variable_set(:a, 49) + @t.thread_variable_get(key).should == 49 + end + + it "does not raise FrozenError if the thread is frozen" do + @t.freeze + @t.thread_variable_get(:a).should be_nil + end + + it "raises a TypeError if the key is neither Symbol nor String when thread variables are already set" do + @t.thread_variable_set(:a, 49) + -> { @t.thread_variable_get(123) }.should raise_error(TypeError, /123 is not a symbol/) + end + + ruby_version_is '3.4' do + it "raises a TypeError if the key is neither Symbol nor String when no thread variables are set" do + -> { @t.thread_variable_get(123) }.should raise_error(TypeError, /123 is not a symbol/) + end + + it "raises a TypeError if the key is neither Symbol nor String without calling #to_sym" do + key = mock('key') + key.should_not_receive(:to_sym) + -> { @t.thread_variable_get(key) }.should raise_error(TypeError, /#{Regexp.escape(key.inspect)} is not a symbol/) + end + 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..eadee76afb --- /dev/null +++ b/spec/ruby/core/thread/thread_variable_set_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' + +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 + + it "accepts String and Symbol keys interchangeably" do + @t.thread_variable_set('a', 49) + @t.thread_variable_get('a').should == 49 + + @t.thread_variable_set(:a, 50) + @t.thread_variable_get('a').should == 50 + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('key') + key.should_receive(:to_str).and_return('a') + @t.thread_variable_set(key, 49) + @t.thread_variable_get(:a).should == 49 + end + + it "removes a key if the value is nil" do + @t.thread_variable_set(:a, 52) + @t.thread_variable_set(:a, nil) + @t.thread_variable?(:a).should be_false + end + + it "raises a FrozenError if the thread is frozen" do + @t.freeze + -> { @t.thread_variable_set(:a, 1) }.should raise_error(FrozenError, "can't modify frozen thread locals") + end + + it "raises a TypeError if the key is neither Symbol nor String, nor responds to #to_str" do + -> { @t.thread_variable_set(123, 1) }.should raise_error(TypeError, /123 is not a symbol/) + end + + it "does not try to convert the key with #to_sym" do + key = mock('key') + key.should_not_receive(:to_sym) + -> { @t.thread_variable_set(key, 42) }.should raise_error(TypeError, /#{Regexp.quote(key.inspect)} is not a symbol/) + 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..1b021e9404 --- /dev/null +++ b/spec/ruby/core/thread/thread_variable_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' + +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 + + it "accepts String and Symbol keys interchangeably" do + @t.thread_variable?('a').should be_false + @t.thread_variable?(:a).should be_false + + @t.thread_variable_set(:a, 49) + + @t.thread_variable?('a').should be_true + @t.thread_variable?(:a).should be_true + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('key') + key.should_receive(:to_str).and_return('a') + @t.thread_variable_set(:a, 49) + @t.thread_variable?(key).should be_true + end + + it "does not raise FrozenError if the thread is frozen" do + @t.freeze + @t.thread_variable?(:a).should be_false + end + + it "raises a TypeError if the key is neither Symbol nor String when thread variables are already set" do + @t.thread_variable_set(:a, 49) + -> { @t.thread_variable?(123) }.should raise_error(TypeError, /123 is not a symbol/) + end + + ruby_version_is '3.4' do + it "raises a TypeError if the key is neither Symbol nor String when no thread variables are set" do + -> { @t.thread_variable?(123) }.should raise_error(TypeError, /123 is not a symbol/) + end + + it "raises a TypeError if the key is neither Symbol nor String without calling #to_sym" do + key = mock('key') + key.should_not_receive(:to_sym) + -> { @t.thread_variable?(key) }.should raise_error(TypeError, /#{Regexp.escape(key.inspect)} is not a symbol/) + end + 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..51ceef3376 --- /dev/null +++ b/spec/ruby/core/thread/thread_variables_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +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 "returns the keys private to self" do + @t.thread_variable_set(:a, 82) + @t.thread_variable_set(:b, 82) + Thread.current.thread_variables.should_not include(:a, :b) + end + + it "only contains user thread variables and is empty initially" do + Thread.current.thread_variables.should == [] + @t.thread_variables.should == [] + end + + it "returns keys as Symbols" do + key = mock('key') + key.should_receive(:to_str).and_return('a') + + @t.thread_variable_set(key, 49) + @t.thread_variable_set('b', 50) + @t.thread_variable_set(:c, 51) + @t.thread_variables.sort.should == [:a, :b, :c] + end +end diff --git a/spec/ruby/core/thread/to_s_spec.rb b/spec/ruby/core/thread/to_s_spec.rb new file mode 100644 index 0000000000..cb182a017f --- /dev/null +++ b/spec/ruby/core/thread/to_s_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Thread#to_s" do + it_behaves_like :thread_to_s, :to_s +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..30e43abd1a --- /dev/null +++ b/spec/ruby/core/thread/value_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +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 { + Thread.current.report_on_exception = false + raise "Hello" + } + -> { 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 + + it "returns when the thread finished" do + q = Queue.new + t = Thread.new { + q.pop + } + -> { t.value }.should block_caller + q.push :result + t.value.should == :result + 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..da5dfea377 --- /dev/null +++ b/spec/ruby/core/thread/wakeup_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/wakeup' + +describe "Thread#wakeup" do + it_behaves_like :thread_wakeup, :wakeup +end |
