diff options
Diffstat (limited to 'spec/ruby/core/thread')
69 files changed, 1521 insertions, 430 deletions
diff --git a/spec/ruby/core/thread/abort_on_exception_spec.rb b/spec/ruby/core/thread/abort_on_exception_spec.rb index e424b2fd26..49be84ea9f 100644 --- a/spec/ruby/core/thread/abort_on_exception_spec.rb +++ b/spec/ruby/core/thread/abort_on_exception_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread#abort_on_exception" do before do @@ -35,7 +35,7 @@ describe :thread_abort_on_exception, shared: true do ScratchPad << :before @thread.abort_on_exception = true if @object - lambda do + -> do ThreadSpecs.state = :run # Wait for the main thread to be interrupted sleep @@ -72,7 +72,7 @@ describe "Thread.abort_on_exception" do end after do - Thread.abort_on_exception = @abort_on_exception + Thread.abort_on_exception = @abort_on_exception end it "is false by default" do diff --git a/spec/ruby/core/thread/add_trace_func_spec.rb b/spec/ruby/core/thread/add_trace_func_spec.rb index c2010ef317..0abae81a78 100644 --- a/spec/ruby/core/thread/add_trace_func_spec.rb +++ b/spec/ruby/core/thread/add_trace_func_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' describe "Thread#add_trace_func" do it "needs to be reviewed for spec completeness" diff --git a/spec/ruby/core/thread/alive_spec.rb b/spec/ruby/core/thread/alive_spec.rb index c1459ac693..c2f5f5371d 100644 --- a/spec/ruby/core/thread/alive_spec.rb +++ b/spec/ruby/core/thread/alive_spec.rb @@ -1,41 +1,41 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +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.alive?.should == true + ThreadSpecs.status_of_current_thread.should.alive? end it "describes a running thread" do - ThreadSpecs.status_of_running_thread.alive?.should == true + ThreadSpecs.status_of_running_thread.should.alive? end it "describes a sleeping thread" do - ThreadSpecs.status_of_sleeping_thread.alive?.should == true + ThreadSpecs.status_of_sleeping_thread.should.alive? end it "describes a blocked thread" do - ThreadSpecs.status_of_blocked_thread.alive?.should == true + ThreadSpecs.status_of_blocked_thread.should.alive? end it "describes a completed thread" do - ThreadSpecs.status_of_completed_thread.alive?.should == false + ThreadSpecs.status_of_completed_thread.should_not.alive? end it "describes a killed thread" do - ThreadSpecs.status_of_killed_thread.alive?.should == false + 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.alive?.should == false + ThreadSpecs.status_of_thread_with_uncaught_exception.should_not.alive? end it "describes a dying running thread" do - ThreadSpecs.status_of_dying_running_thread.alive?.should == true + ThreadSpecs.status_of_dying_running_thread.should.alive? end it "describes a dying sleeping thread" do - ThreadSpecs.status_of_dying_sleeping_thread.alive?.should == true + ThreadSpecs.status_of_dying_sleeping_thread.should.alive? end it "returns true for a killed but still running thread" do @@ -51,7 +51,7 @@ describe "Thread#alive?" do ThreadSpecs.spin_until_sleeping(t) t.kill - t.alive?.should == true + t.should.alive? exit = true t.join end diff --git a/spec/ruby/core/thread/allocate_spec.rb b/spec/ruby/core/thread/allocate_spec.rb index 1db05878ba..cfd556812f 100644 --- a/spec/ruby/core/thread/allocate_spec.rb +++ b/spec/ruby/core/thread/allocate_spec.rb @@ -1,8 +1,8 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' describe "Thread.allocate" do it "raises a TypeError" do - lambda { + -> { Thread.allocate }.should raise_error(TypeError) 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 index 6810bdcd78..68a69049d9 100644 --- a/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' describe 'Thread::Backtrace::Location#absolute_path' do before :each do @@ -9,4 +9,85 @@ describe 'Thread::Backtrace::Location#absolute_path' do 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 index cba7e3f34c..739f62f42f 100644 --- a/spec/ruby/core/thread/backtrace/location/base_label_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/base_label_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' describe 'Thread::Backtrace::Location#base_label' do before :each do @@ -9,4 +9,41 @@ describe 'Thread::Backtrace::Location#base_label' do 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 index 3e42d8cf81..e903c3e450 100644 --- a/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb +++ b/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb @@ -14,4 +14,22 @@ module ThreadBacktraceLocationSpecs 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 index d2d14ac957..bde208a059 100644 --- a/spec/ruby/core/thread/backtrace/location/fixtures/main.rb +++ b/spec/ruby/core/thread/backtrace/location/fixtures/main.rb @@ -1,5 +1,5 @@ -def example +def backtrace_location_example caller_locations[0].path end -print example +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 index 56d440c04a..20e477a5a6 100644 --- a/spec/ruby/core/thread/backtrace/location/inspect_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/inspect_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' describe 'Thread::Backtrace::Location#inspect' do before :each do diff --git a/spec/ruby/core/thread/backtrace/location/label_spec.rb b/spec/ruby/core/thread/backtrace/location/label_spec.rb index 4e67509d0f..85ddccc8e3 100644 --- a/spec/ruby/core/thread/backtrace/location/label_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/label_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' describe 'Thread::Backtrace::Location#label' do it 'returns the base label of the call frame' do @@ -7,14 +7,31 @@ describe 'Thread::Backtrace::Location#label' do end it 'returns the method name for a method location' do - ThreadBacktraceLocationSpecs.method_location[0].label.should == "method_location" + 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 == "block in block_location" + 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 index 7d203008e5..10457f80f0 100644 --- a/spec/ruby/core/thread/backtrace/location/lineno_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' describe 'Thread::Backtrace::Location#lineno' do before :each do @@ -7,7 +7,17 @@ describe 'Thread::Backtrace::Location#lineno' do @line = __LINE__ - 1 end - it 'returns the absolute path of the call frame' do + 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 index c2f2058990..75f76833a9 100644 --- a/spec/ruby/core/thread/backtrace/location/path_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/path_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' describe 'Thread::Backtrace::Location#path' do context 'outside a main script' do @@ -41,7 +41,7 @@ describe 'Thread::Backtrace::Location#path' do context 'when using a relative script path' do it 'returns a path relative to the working directory' do path = 'fixtures/main.rb' - directory = File.dirname(__FILE__) + directory = __dir__ Dir.chdir(directory) { ruby_exe(path) }.should == path @@ -56,7 +56,7 @@ describe 'Thread::Backtrace::Location#path' do end context 'when the script is outside of the working directory' do - before do + before :each do @parent_dir = tmp('path_outside_pwd') @sub_dir = File.join(@parent_dir, 'sub') @script = File.join(@parent_dir, 'main.rb') @@ -67,9 +67,7 @@ describe 'Thread::Backtrace::Location#path' do cp(source, @script) end - after do - rm_r(@script) - rm_r(@sub_dir) + after :each do rm_r(@parent_dir) end @@ -88,4 +86,39 @@ describe 'Thread::Backtrace::Location#path' do 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 index 486d7da4c9..5911cdced0 100644 --- a/spec/ruby/core/thread/backtrace/location/to_s_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/to_s_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' describe 'Thread::Backtrace::Location#to_s' do before :each do 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 index a20fdee956..15bb29a349 100644 --- a/spec/ruby/core/thread/backtrace_spec.rb +++ b/spec/ruby/core/thread/backtrace_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' describe "Thread#backtrace" do it "returns the current backtrace of a thread" do @@ -13,7 +13,7 @@ describe "Thread#backtrace" do backtrace = t.backtrace backtrace.should be_kind_of(Array) - backtrace.first.should =~ /`sleep'/ + backtrace.first.should =~ /[`'](?:Kernel#)?sleep'/ t.raise 'finish the thread' t.join @@ -24,4 +24,46 @@ describe "Thread#backtrace" do 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 index cc969b71c4..f5ed1d95cd 100644 --- a/spec/ruby/core/thread/current_spec.rb +++ b/spec/ruby/core/thread/current_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread.current" do it "returns a thread" do @@ -12,4 +12,20 @@ describe "Thread.current" do 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 index 81b11d2c09..fde9d1f440 100644 --- a/spec/ruby/core/thread/element_reference_spec.rb +++ b/spec/ruby/core/thread/element_reference_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread#[]" do it "gives access to thread local values" do @@ -37,8 +37,19 @@ describe "Thread#[]" do 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 - lambda { Thread.current[nil] }.should raise_error(TypeError) - lambda { Thread.current[5] }.should raise_error(TypeError) + -> { 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 index ed92a84fa3..f205177304 100644 --- a/spec/ruby/core/thread/element_set_spec.rb +++ b/spec/ruby/core/thread/element_set_spec.rb @@ -1,24 +1,47 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread#[]=" do after :each do Thread.current[:value] = nil end - it "raises a RuntimeError if the thread is frozen" do + it "raises a FrozenError if the thread is frozen" do Thread.new do th = Thread.current th.freeze -> { th[:foo] = "bar" - }.should raise_error(RuntimeError, /frozen/) + }.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 - lambda { Thread.current[nil] = true }.should raise_error(TypeError) - lambda { Thread.current[5] = true }.should raise_error(TypeError) + -> { Thread.current[nil] = true }.should raise_error(TypeError) + -> { Thread.current[5] = true }.should raise_error(TypeError) end it "is not shared across fibers" do diff --git a/spec/ruby/core/thread/exclusive_spec.rb b/spec/ruby/core/thread/exclusive_spec.rb deleted file mode 100644 index 66c87f4713..0000000000 --- a/spec/ruby/core/thread/exclusive_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require File.expand_path('../../../spec_helper', __FILE__) - -describe "Thread.exclusive" do - before :each do - ScratchPad.clear - end - - it "yields to the block" do - Thread.exclusive { ScratchPad.record true } - ScratchPad.recorded.should == true - end - - it "returns the result of yielding" do - Thread.exclusive { :result }.should == :result - end - - it "needs to be reviewed for spec completeness" -end diff --git a/spec/ruby/core/thread/exit_spec.rb b/spec/ruby/core/thread/exit_spec.rb index 0fb329e66f..c3f710920e 100644 --- a/spec/ruby/core/thread/exit_spec.rb +++ b/spec/ruby/core/thread/exit_spec.rb @@ -1,6 +1,6 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) -require File.expand_path('../shared/exit', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exit' describe "Thread#exit!" do it "needs to be reviewed for spec completeness" 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 index 601e515e3e..7c485660a8 100644 --- a/spec/ruby/core/thread/fixtures/classes.rb +++ b/spec/ruby/core/thread/fixtures/classes.rb @@ -1,10 +1,3 @@ -unless defined? Channel - require 'thread' - class Channel < Queue - alias receive shift - end -end - module ThreadSpecs class SubThread < Thread @@ -13,12 +6,38 @@ module ThreadSpecs 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 + 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 @@ -188,7 +207,7 @@ module ThreadSpecs def self.join_dying_thread_with_outer_ensure(kill_method_name=:kill) t = dying_thread_with_outer_ensure(kill_method_name) { yield } - lambda { t.join }.should raise_error(RuntimeError, "In dying thread") + -> { t.join }.should raise_error(RuntimeError, "In dying thread") return t end diff --git a/spec/ruby/core/thread/fork_spec.rb b/spec/ruby/core/thread/fork_spec.rb index d321230812..a2f4181298 100644 --- a/spec/ruby/core/thread/fork_spec.rb +++ b/spec/ruby/core/thread/fork_spec.rb @@ -1,6 +1,6 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) -require File.expand_path('../shared/start', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/start' describe "Thread.fork" do describe "Thread.start" do diff --git a/spec/ruby/core/thread/group_spec.rb b/spec/ruby/core/thread/group_spec.rb index aecc1422ba..d0d4704b66 100644 --- a/spec/ruby/core/thread/group_spec.rb +++ b/spec/ruby/core/thread/group_spec.rb @@ -1,5 +1,16 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' + describe "Thread#group" do - it "needs to be reviewed for spec completeness" + 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 index b6345f03de..4fca900cd8 100644 --- a/spec/ruby/core/thread/initialize_spec.rb +++ b/spec/ruby/core/thread/initialize_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread#initialize" do @@ -15,7 +15,7 @@ describe "Thread#initialize" do end it "raises a ThreadError" do - lambda { + -> { @t.instance_eval do initialize {} end diff --git a/spec/ruby/core/thread/inspect_spec.rb b/spec/ruby/core/thread/inspect_spec.rb index 95e598eb6a..bd6e0c31fc 100644 --- a/spec/ruby/core/thread/inspect_spec.rb +++ b/spec/ruby/core/thread/inspect_spec.rb @@ -1,44 +1,6 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'shared/to_s' describe "Thread#inspect" do - it "can check it's own status" do - ThreadSpecs.status_of_current_thread.inspect.should include('run') - end - - it "describes a running thread" do - ThreadSpecs.status_of_running_thread.inspect.should include('run') - end - - it "describes a sleeping thread" do - ThreadSpecs.status_of_sleeping_thread.inspect.should include('sleep') - end - - it "describes a blocked thread" do - ThreadSpecs.status_of_blocked_thread.inspect.should include('sleep') - end - - it "describes a completed thread" do - ThreadSpecs.status_of_completed_thread.inspect.should include('dead') - end - - it "describes a killed thread" do - ThreadSpecs.status_of_killed_thread.inspect.should include('dead') - end - - it "describes a thread with an uncaught exception" do - ThreadSpecs.status_of_thread_with_uncaught_exception.inspect.should include('dead') - end - - it "describes a dying sleeping thread" do - ThreadSpecs.status_of_dying_sleeping_thread.inspect.should include('sleep') - end - - it "reports aborting on a killed thread" do - ThreadSpecs.status_of_dying_running_thread.inspect.should include('aborting') - end - - it "reports aborting on a killed thread after sleep" do - ThreadSpecs.status_of_dying_thread_after_sleep.inspect.should include('aborting') - end + 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 index 249b3d333e..213fe2e505 100644 --- a/spec/ruby/core/thread/join_spec.rb +++ b/spec/ruby/core/thread/join_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread#join" do it "returns the thread when it is finished" do @@ -19,28 +19,33 @@ describe "Thread#join" do t.join(0).should equal(t) t.join(0.0).should equal(t) t.join(nil).should equal(t) - lambda { t.join(:foo) }.should raise_error TypeError - lambda { t.join("bar") }.should raise_error TypeError + end + + it "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 - c = Channel.new - t = Thread.new { c.receive } + q = Queue.new + t = Thread.new { q.pop } begin t.join(0).should == nil ensure - c << true + q << true end t.join.should == t end it "accepts a floating point timeout length" do - c = Channel.new - t = Thread.new { c.receive } + q = Queue.new + t = Thread.new { q.pop } begin t.join(0.01).should == nil ensure - c << true + q << true end t.join.should == t end @@ -50,7 +55,7 @@ describe "Thread#join" do Thread.current.report_on_exception = false raise NotImplementedError.new("Just kidding") } - lambda { t.join }.should raise_error(NotImplementedError) + -> { t.join }.should raise_error(NotImplementedError) end it "returns the dead thread" do @@ -60,6 +65,6 @@ describe "Thread#join" do it "raises any uncaught exception encountered in ensure block" do t = ThreadSpecs.dying_thread_ensures { raise NotImplementedError.new("Just kidding") } - lambda { t.join }.should raise_error(NotImplementedError) + -> { 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 index d82a21ab39..339fa98f53 100644 --- a/spec/ruby/core/thread/key_spec.rb +++ b/spec/ruby/core/thread/key_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread#key?" do before :each do @@ -16,9 +16,16 @@ describe "Thread#key?" do @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 - lambda { Thread.current.key? nil }.should raise_error(TypeError) - lambda { Thread.current.key? 5 }.should raise_error(TypeError) + -> { Thread.current.key? nil }.should raise_error(TypeError) + -> { Thread.current.key? 5 }.should raise_error(TypeError) end it "is not shared across fibers" do diff --git a/spec/ruby/core/thread/keys_spec.rb b/spec/ruby/core/thread/keys_spec.rb index 0fc8184e06..15efda51d6 100644 --- a/spec/ruby/core/thread/keys_spec.rb +++ b/spec/ruby/core/thread/keys_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +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 diff --git a/spec/ruby/core/thread/kill_spec.rb b/spec/ruby/core/thread/kill_spec.rb index cf71307af5..4b62c686c7 100644 --- a/spec/ruby/core/thread/kill_spec.rb +++ b/spec/ruby/core/thread/kill_spec.rb @@ -1,21 +1,21 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) -require File.expand_path('../shared/exit', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exit' -describe "Thread#kill" do - it_behaves_like :thread_exit, :kill -end - -describe "Thread#kill!" do - it "needs to be reviewed for spec completeness" -end +# 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 + 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 index b8deb98260..3c6f70c13e 100644 --- a/spec/ruby/core/thread/list_spec.rb +++ b/spec/ruby/core/thread/list_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread.list" do it "includes the current and main thread" do @@ -25,18 +25,31 @@ describe "Thread.list" do end it "includes waiting threads" do - c = Channel.new - t = Thread.new { c.receive } + q = Queue.new + t = Thread.new { q.pop } begin Thread.pass while t.status and t.status != 'sleep' Thread.list.should include(t) ensure - c << nil + q << nil t.join end end -end -describe "Thread.list" do - it "needs to be reviewed for spec completeness" + 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 index 0cada8f59d..ec91709576 100644 --- a/spec/ruby/core/thread/main_spec.rb +++ b/spec/ruby/core/thread/main_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread.main" do it "returns the main thread" do diff --git a/spec/ruby/core/thread/name_spec.rb b/spec/ruby/core/thread/name_spec.rb index 0417d7a500..9b3d2f4b09 100644 --- a/spec/ruby/core/thread/name_spec.rb +++ b/spec/ruby/core/thread/name_spec.rb @@ -1,56 +1,54 @@ -require File.expand_path('../../../spec_helper', __FILE__) - -ruby_version_is '2.3' do - describe "Thread#name" do - before :each do - @thread = Thread.new {} - end - - after :each do - @thread.join - end - - it "is nil initially" do - @thread.name.should == nil - end - - it "returns the thread name" do - @thread.name = "thread_name" - @thread.name.should == "thread_name" - end - end - - describe "Thread#name=" do - before :each do - @thread = Thread.new {} - end - - after :each do - @thread.join - end - - it "can be set to a String" do - @thread.name = "new thread name" - @thread.name.should == "new thread name" - end - - it "raises an ArgumentError if the name includes a null byte" do - lambda { - @thread.name = "new thread\0name" - }.should raise_error(ArgumentError) - end - - it "can be reset to nil" do - @thread.name = nil - @thread.name.should == nil - end - - it "calls #to_str to convert name to String" do - name = mock("Thread#name") - name.should_receive(:to_str).and_return("a thread name") - - @thread.name = name - @thread.name.should == "a thread name" - end +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 index b1ed5560a1..47a836201c 100644 --- a/spec/ruby/core/thread/new_spec.rb +++ b/spec/ruby/core/thread/new_spec.rb @@ -1,12 +1,12 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread.new" do it "creates a thread executing the given block" do - c = Channel.new - Thread.new { c << true }.join - c << false - c.receive.should == true + q = Queue.new + Thread.new { q << true }.join + q << false + q.pop.should == true end it "can pass arguments to the thread block" do @@ -18,7 +18,7 @@ describe "Thread.new" do end it "raises an exception when not given a block" do - lambda { Thread.new }.should raise_error(ThreadError) + -> { Thread.new }.should raise_error(ThreadError) end it "creates a subclass of thread calls super with a block in initialize" do @@ -34,7 +34,7 @@ describe "Thread.new" do end end - lambda { + -> { c.new }.should raise_error(ThreadError) end @@ -53,4 +53,31 @@ describe "Thread.new" do 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 index 128de934ac..a5ac11a58c 100644 --- a/spec/ruby/core/thread/pass_spec.rb +++ b/spec/ruby/core/thread/pass_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread.pass" do it "returns nil" do 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 index b986fb7a0d..e13ad478b5 100644 --- a/spec/ruby/core/thread/priority_spec.rb +++ b/spec/ruby/core/thread/priority_spec.rb @@ -1,14 +1,15 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread#priority" do - before 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 do + after :each do ThreadSpecs.state = :exit @thread.join end @@ -31,12 +32,14 @@ describe "Thread#priority" do end describe "Thread#priority=" do - before do + before :each do ThreadSpecs.clear_state - @thread = Thread.new {} + @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit } + Thread.pass until @thread.alive? end - after do + after :each do + ThreadSpecs.state = :exit @thread.join end @@ -56,13 +59,14 @@ describe "Thread#priority=" do describe "when set with a non-integer" do it "raises a type error" do - lambda{ @thread.priority = Object.new }.should raise_error(TypeError) + ->{ @thread.priority = Object.new }.should raise_error(TypeError) end end it "sets priority even when the thread has died" do - @thread.join - @thread.priority = 3 - @thread.priority.should == 3 + 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 index 8724d26202..b473eabd42 100644 --- a/spec/ruby/core/thread/raise_spec.rb +++ b/spec/ruby/core/thread/raise_spec.rb @@ -1,13 +1,16 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) -require File.expand_path('../../../shared/kernel/raise', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/raise' describe "Thread#raise" do - it "ignores dead threads" 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? - lambda {t.raise("Kill the thread")}.should_not raise_error - lambda {t.value}.should_not raise_error + t.raise("Kill the thread").should == nil + t.join end end @@ -58,7 +61,7 @@ describe "Thread#raise on a sleeping thread" do ThreadSpecs.spin_until_sleeping(t) t.raise - lambda { t.value }.should raise_error(RuntimeError) + -> { t.value }.should raise_error(RuntimeError) end it "raises a RuntimeError when called with no arguments inside rescue" do @@ -76,7 +79,55 @@ describe "Thread#raise on a sleeping thread" do ThreadSpecs.spin_until_sleeping(t) t.raise end - lambda {t.value}.should raise_error(RuntimeError) + -> { 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 @@ -114,13 +165,16 @@ describe "Thread#raise on a running thread" do 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 - lambda {t.value}.should raise_error(RuntimeError) + -> { t.value }.should raise_error(RuntimeError) end it "raises the given argument even when there is an active exception" do @@ -139,7 +193,7 @@ describe "Thread#raise on a running thread" do rescue Thread.pass until raised t.raise RangeError - lambda {t.value}.should raise_error(RangeError) + -> { t.value }.should raise_error(RangeError) end end @@ -151,7 +205,7 @@ describe "Thread#raise on a running thread" do 1/0 rescue ZeroDivisionError raised = true - loop { } + loop { Thread.pass } end end begin @@ -160,7 +214,7 @@ describe "Thread#raise on a running thread" do Thread.pass until raised t.raise end - lambda {t.value}.should raise_error(RuntimeError) + -> { t.value }.should raise_error(RuntimeError) end end @@ -176,6 +230,6 @@ describe "Thread#raise on same thread" do Thread.current.raise end end - lambda {t.value}.should raise_error(RuntimeError) + -> { 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 index d8400c080a..d9daa041cd 100644 --- a/spec/ruby/core/thread/report_on_exception_spec.rb +++ b/spec/ruby/core/thread/report_on_exception_spec.rb @@ -1,120 +1,155 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' -ruby_version_is "2.4" do - describe "Thread.report_on_exception" do - ruby_version_is "2.4"..."2.5" do - it "defaults to false" do - ruby_exe("p Thread.report_on_exception").should == "false\n" - end - end +describe "Thread.report_on_exception" do + it "defaults to true" do + ruby_exe("p Thread.report_on_exception").should == "true\n" + end +end - ruby_version_is "2.5" 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 - 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 - 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 - 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 +describe "Thread#report_on_exception" do + it "returns true for the main Thread" do + Thread.current.report_on_exception.should == true end - describe "Thread#report_on_exception" do - ruby_version_is "2.5" 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 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 "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 + 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 - t = Thread.new { Thread.current.report_on_exception = false } - t.join - t.report_on_exception.should == false + 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 - 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) + 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 - -> { - t.join - }.should raise_error(RuntimeError, "Thread#report_on_exception specs") - 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 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("", "") + 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" + } -> { - t.join + mutex.sleep(5) }.should raise_error(RuntimeError, "Thread#report_on_exception specs") - end - end + }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception specs/m) - ruby_bug "#13163", "2.4"..."2.5" do - 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 + -> { + 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 index 26ed9ed961..f86f793489 100644 --- a/spec/ruby/core/thread/run_spec.rb +++ b/spec/ruby/core/thread/run_spec.rb @@ -1,9 +1,8 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' -require File.expand_path('../shared/wakeup', __FILE__) +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 index 6dd5448d79..e5d8298ae0 100644 --- a/spec/ruby/core/thread/set_trace_func_spec.rb +++ b/spec/ruby/core/thread/set_trace_func_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' describe "Thread#set_trace_func" do it "needs to be reviewed for spec completeness" diff --git a/spec/ruby/core/thread/shared/exit.rb b/spec/ruby/core/thread/shared/exit.rb index 0c9198c538..13e8832684 100644 --- a/spec/ruby/core/thread/shared/exit.rb +++ b/spec/ruby/core/thread/shared/exit.rb @@ -3,6 +3,10 @@ describe :thread_exit, shared: true 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 @@ -62,6 +66,26 @@ describe :thread_exit, shared: true do 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 @@ -69,26 +93,43 @@ describe :thread_exit, shared: true do rescue Exception ScratchPad.record :in_rescue end - ScratchPad.record :end_of_thread_block + ScratchPad.record :end_of_thread_block end thread.join ScratchPad.recorded.should == nil end - with_feature :fiber do - it "kills the entire thread when a fiber is active" do - t = Thread.new do - Fiber.new do - sleep - end.resume + 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 - Thread.pass while t.status and t.status != "sleep" - t.send(@method) - t.join - ScratchPad.recorded.should == nil + 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 @@ -114,7 +155,7 @@ describe :thread_exit, shared: true do it "propagates inner exception to Thread.join if there is an outer ensure clause" do thread = ThreadSpecs.dying_thread_with_outer_ensure(@method) { } - lambda { thread.join }.should raise_error(RuntimeError, "In dying thread") + -> { thread.join }.should raise_error(RuntimeError, "In dying thread") end it "runs all outer ensure clauses even if inner ensure clause raises exception" do @@ -173,4 +214,6 @@ describe :thread_exit, shared: true do 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 index 80ce063a0e..2ba926bf00 100644 --- a/spec/ruby/core/thread/shared/start.rb +++ b/spec/ruby/core/thread/shared/start.rb @@ -4,7 +4,7 @@ describe :thread_start, shared: true do end it "raises an ArgumentError if not passed a block" do - lambda { + -> { Thread.send(@method) }.should raise_error(ArgumentError) 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 index 71838d88e5..6f010fea25 100644 --- a/spec/ruby/core/thread/shared/wakeup.rb +++ b/spec/ruby/core/thread/shared/wakeup.rb @@ -36,7 +36,7 @@ describe :thread_wakeup, shared: true do it "does not result in a deadlock" do t = Thread.new do - 100.times { Thread.stop } + 10.times { Thread.stop } end while t.status @@ -47,6 +47,7 @@ describe :thread_wakeup, shared: true do t.status.should == false end Thread.pass + sleep 0.001 end t.status.should == false @@ -56,6 +57,6 @@ describe :thread_wakeup, shared: true do it "raises a ThreadError when trying to wake up a dead thread" do t = Thread.new { 1 } t.join - lambda { t.send @method }.should raise_error(ThreadError) + -> { 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 index 932e782382..3dd040f98b 100644 --- a/spec/ruby/core/thread/start_spec.rb +++ b/spec/ruby/core/thread/start_spec.rb @@ -1,6 +1,6 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) -require File.expand_path('../shared/start', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/start' describe "Thread.start" do describe "Thread.start" do diff --git a/spec/ruby/core/thread/status_spec.rb b/spec/ruby/core/thread/status_spec.rb index 6cfdf0be40..4fde663c91 100644 --- a/spec/ruby/core/thread/status_spec.rb +++ b/spec/ruby/core/thread/status_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread#status" do it "can check it's own status" do diff --git a/spec/ruby/core/thread/stop_spec.rb b/spec/ruby/core/thread/stop_spec.rb index 0bc99487fd..084ab46ef6 100644 --- a/spec/ruby/core/thread/stop_spec.rb +++ b/spec/ruby/core/thread/stop_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread.stop" do it "causes the current thread to sleep indefinitely" do @@ -13,42 +13,42 @@ end describe "Thread#stop?" do it "can check it's own status" do - ThreadSpecs.status_of_current_thread.stop?.should == false + ThreadSpecs.status_of_current_thread.should_not.stop? end it "describes a running thread" do - ThreadSpecs.status_of_running_thread.stop?.should == false + ThreadSpecs.status_of_running_thread.should_not.stop? end it "describes a sleeping thread" do - ThreadSpecs.status_of_sleeping_thread.stop?.should == true + ThreadSpecs.status_of_sleeping_thread.should.stop? end it "describes a blocked thread" do - ThreadSpecs.status_of_blocked_thread.stop?.should == true + ThreadSpecs.status_of_blocked_thread.should.stop? end it "describes a completed thread" do - ThreadSpecs.status_of_completed_thread.stop?.should == true + ThreadSpecs.status_of_completed_thread.should.stop? end it "describes a killed thread" do - ThreadSpecs.status_of_killed_thread.stop?.should == true + ThreadSpecs.status_of_killed_thread.should.stop? end it "describes a thread with an uncaught exception" do - ThreadSpecs.status_of_thread_with_uncaught_exception.stop?.should == true + ThreadSpecs.status_of_thread_with_uncaught_exception.should.stop? end it "describes a dying running thread" do - ThreadSpecs.status_of_dying_running_thread.stop?.should == false + ThreadSpecs.status_of_dying_running_thread.should_not.stop? end it "describes a dying sleeping thread" do - ThreadSpecs.status_of_dying_sleeping_thread.stop?.should == true + ThreadSpecs.status_of_dying_sleeping_thread.should.stop? end it "describes a dying thread after sleep" do - ThreadSpecs.status_of_dying_thread_after_sleep.stop?.should == false + 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 index bb89d87762..cf6cab472b 100644 --- a/spec/ruby/core/thread/terminate_spec.rb +++ b/spec/ruby/core/thread/terminate_spec.rb @@ -1,6 +1,6 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) -require File.expand_path('../shared/exit', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exit' describe "Thread#terminate" do it_behaves_like :thread_exit, :terminate diff --git a/spec/ruby/core/thread/thread_variable_get_spec.rb b/spec/ruby/core/thread/thread_variable_get_spec.rb index 0e02c30fad..1ea34cf2b3 100644 --- a/spec/ruby/core/thread/thread_variable_get_spec.rb +++ b/spec/ruby/core/thread/thread_variable_get_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' describe "Thread#thread_variable_get" do before :each do @@ -13,13 +13,48 @@ describe "Thread#thread_variable_get" do @t.thread_variable_get(:a).should be_nil end - it "returns the value previously set by #[]=" do - @t.thread_variable_set :a, 49 + 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 + @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 index 0f55341132..eadee76afb 100644 --- a/spec/ruby/core/thread/thread_variable_set_spec.rb +++ b/spec/ruby/core/thread/thread_variable_set_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' describe "Thread#thread_variable_set" do before :each do @@ -10,17 +10,53 @@ describe "Thread#thread_variable_set" do end it "returns the value set" do - (@t.thread_variable_set :a, 2).should == 2 + @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_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_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 index b409b3abfc..1b021e9404 100644 --- a/spec/ruby/core/thread/thread_variable_spec.rb +++ b/spec/ruby/core/thread/thread_variable_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' describe "Thread#thread_variable?" do before :each do @@ -10,12 +10,51 @@ describe "Thread#thread_variable?" do end it "returns false if the thread variables do not contain 'key'" do - @t.thread_variable_set :a, 2 + @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_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 index 39299cf20e..51ceef3376 100644 --- a/spec/ruby/core/thread/thread_variables_spec.rb +++ b/spec/ruby/core/thread/thread_variables_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' describe "Thread#thread_variables" do before :each do @@ -10,15 +10,15 @@ describe "Thread#thread_variables" do 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_variable_set(:a, 2) + @t.thread_variable_set(:b, 4) + @t.thread_variable_set(:c, 6) @t.thread_variables.sort.should == [:a, :b, :c] end - it "sets a value private to self" do - @t.thread_variable_set :a, 82 - @t.thread_variable_set :b, 82 + 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 @@ -26,4 +26,14 @@ describe "Thread#thread_variables" 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 index 3d900959df..30e43abd1a 100644 --- a/spec/ruby/core/thread/value_spec.rb +++ b/spec/ruby/core/thread/value_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Thread#value" do it "returns the result of the block" do @@ -11,11 +11,21 @@ describe "Thread#value" do Thread.current.report_on_exception = false raise "Hello" } - lambda { t.value }.should raise_error(RuntimeError, "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 index 5197a03a35..da5dfea377 100644 --- a/spec/ruby/core/thread/wakeup_spec.rb +++ b/spec/ruby/core/thread/wakeup_spec.rb @@ -1,6 +1,6 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) -require File.expand_path('../shared/wakeup', __FILE__) +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/wakeup' describe "Thread#wakeup" do it_behaves_like :thread_wakeup, :wakeup |
