diff options
Diffstat (limited to 'spec/ruby/core/thread')
21 files changed, 320 insertions, 85 deletions
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..26a87a806c --- /dev/null +++ b/spec/ruby/core/thread/backtrace/limit_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../../spec_helper' + +ruby_version_is "3.1" do + 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 +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 b0ae28beee..6e381e4868 100644 --- a/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb @@ -17,11 +17,30 @@ describe 'Thread::Backtrace::Location#absolute_path' do 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 filename" do - code = "caller_locations(0)[0].absolute_path" - eval(code, nil, "foo.rb").should == "foo.rb" - eval(code, nil, "foo/bar.rb").should == "foo/bar.rb" + code = "caller_locations(0)[0].absolute_path" + + ruby_version_is ""..."3.1" do + it "returns filename with absolute_path" do + eval(code, nil, "foo.rb").should == "foo.rb" + eval(code, nil, "foo/bar.rb").should == "foo/bar.rb" + end + end + + ruby_version_is "3.1" do + it "returns nil with absolute_path" do + eval(code, nil, "foo.rb").should == nil + eval(code, nil, "foo/bar.rb").should == nil + end end end @@ -40,7 +59,7 @@ describe 'Thread::Backtrace::Location#absolute_path' do it "returns nil" do location = nil tap { location = caller_locations(1, 1)[0] } - location.label.should == "tap" + location.label.should =~ /\A(?:Kernel#)?tap\z/ if location.path.start_with?("<internal:") location.absolute_path.should == nil else 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/label_spec.rb b/spec/ruby/core/thread/backtrace/location/label_spec.rb index 7312d017e5..85ddccc8e3 100644 --- a/spec/ruby/core/thread/backtrace/location/label_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/label_spec.rb @@ -7,11 +7,11 @@ 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 @@ -22,9 +22,9 @@ describe 'Thread::Backtrace::Location#label' do first_level_location, second_level_location, third_level_location = ThreadBacktraceLocationSpecs.locations_inside_nested_blocks - first_level_location.label.should == 'block in locations_inside_nested_blocks' - second_level_location.label.should == 'block (2 levels) in locations_inside_nested_blocks' - third_level_location.label.should == 'block (3 levels) in 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 diff --git a/spec/ruby/core/thread/backtrace/location/lineno_spec.rb b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb index d14cf17514..10457f80f0 100644 --- a/spec/ruby/core/thread/backtrace/location/lineno_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb @@ -7,7 +7,7 @@ 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 diff --git a/spec/ruby/core/thread/backtrace/location/path_spec.rb b/spec/ruby/core/thread/backtrace/location/path_spec.rb index 7863c055d3..75f76833a9 100644 --- a/spec/ruby/core/thread/backtrace/location/path_spec.rb +++ b/spec/ruby/core/thread/backtrace/location/path_spec.rb @@ -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 diff --git a/spec/ruby/core/thread/backtrace_locations_spec.rb b/spec/ruby/core/thread/backtrace_locations_spec.rb index 1f77e13378..09fe622e0d 100644 --- a/spec/ruby/core/thread/backtrace_locations_spec.rb +++ b/spec/ruby/core/thread/backtrace_locations_spec.rb @@ -43,20 +43,16 @@ describe "Thread#backtrace_locations" do Thread.current.backtrace_locations(2..-2).map(&:to_s).should == Thread.current.backtrace_locations[2..-2].map(&:to_s) end - ruby_version_is "2.6" do - 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 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 - ruby_version_is "2.7" do - it "can be called with an beginless range" do - locations1 = Thread.current.backtrace_locations(0) - locations2 = Thread.current.backtrace_locations(eval("(..5)")) - locations2.map(&:to_s)[eval("(2..)")].should == locations1[eval("(..5)")].map(&:to_s)[eval("(2..)")] - 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 @@ -74,7 +70,7 @@ describe "Thread#backtrace_locations" do end it "the first location reports the call to #backtrace_locations" do - Thread.current.backtrace_locations(0..0)[0].to_s.should == "#{__FILE__ }:#{__LINE__ }:in `backtrace_locations'" + 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 diff --git a/spec/ruby/core/thread/backtrace_spec.rb b/spec/ruby/core/thread/backtrace_spec.rb index 9001b1b7eb..15bb29a349 100644 --- a/spec/ruby/core/thread/backtrace_spec.rb +++ b/spec/ruby/core/thread/backtrace_spec.rb @@ -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 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..29c271789b --- /dev/null +++ b/spec/ruby/core/thread/each_caller_location_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' + +describe "Thread.each_caller_location" do + ruby_version_is "3.2" 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 +end diff --git a/spec/ruby/core/thread/exclusive_spec.rb b/spec/ruby/core/thread/exclusive_spec.rb deleted file mode 100644 index 37c4b19d1a..0000000000 --- a/spec/ruby/core/thread/exclusive_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require_relative '../../spec_helper' - -ruby_version_is ''...'3.0' do - describe "Thread.exclusive" do - before :each do - ScratchPad.clear - $VERBOSE, @verbose = nil, $VERBOSE - end - - after :each do - $VERBOSE = @verbose - 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 "blocks the caller if another thread is also in an exclusive block" do - m = Mutex.new - q1 = Queue.new - q2 = Queue.new - - t = Thread.new { - Thread.exclusive { - q1.push :ready - q2.pop - } - } - - q1.pop.should == :ready - - -> { Thread.exclusive { } }.should block_caller - - q2.push :done - t.join - end - - it "is not recursive" do - Thread.exclusive do - -> { Thread.exclusive { } }.should raise_error(ThreadError) - end - end - end -end diff --git a/spec/ruby/core/thread/fetch_spec.rb b/spec/ruby/core/thread/fetch_spec.rb index 6b37d4cfc5..85ffb71874 100644 --- a/spec/ruby/core/thread/fetch_spec.rb +++ b/spec/ruby/core/thread/fetch_spec.rb @@ -29,6 +29,36 @@ describe 'Thread#fetch' do 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) 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/kill_spec.rb b/spec/ruby/core/thread/kill_spec.rb index f932bf5232..4b62c686c7 100644 --- a/spec/ruby/core/thread/kill_spec.rb +++ b/spec/ruby/core/thread/kill_spec.rb @@ -9,10 +9,6 @@ platform_is_not :mingw do it_behaves_like :thread_exit, :kill end - describe "Thread#kill!" do - it "needs to be reviewed for spec completeness" - end - describe "Thread.kill" do it "causes the given thread to exit" do thread = Thread.new { sleep } 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..17a08c8a15 --- /dev/null +++ b/spec/ruby/core/thread/native_thread_id_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +ruby_version_is "3.1" do + 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 +end diff --git a/spec/ruby/core/thread/raise_spec.rb b/spec/ruby/core/thread/raise_spec.rb index 27de3cc627..49323cf270 100644 --- a/spec/ruby/core/thread/raise_spec.rb +++ b/spec/ruby/core/thread/raise_spec.rb @@ -102,6 +102,30 @@ describe "Thread#raise on a sleeping thread" do raised_again.backtrace.first.should_not include("#{__FILE__}:#{raise_again_line}:") end end + + it "calls #exception in both the caller and in the target thread" do + cls = Class.new(Exception) do + attr_accessor :log + def initialize(*args) + @log = [] # This is shared because the super #exception uses a shallow clone + super + end + + def exception(*args) + @log << [self, Thread.current, args] + super + end + end + exc = cls.new + + @thr.raise exc, "Thread#raise #exception spec" + @thr.join + ScratchPad.recorded.should.is_a?(cls) + exc.log.should == [ + [exc, Thread.current, ["Thread#raise #exception spec"]], + [ScratchPad.recorded, @thr, []] + ] + end end describe "Thread#raise on a running thread" do diff --git a/spec/ruby/core/thread/report_on_exception_spec.rb b/spec/ruby/core/thread/report_on_exception_spec.rb index bf50a167df..d9daa041cd 100644 --- a/spec/ruby/core/thread/report_on_exception_spec.rb +++ b/spec/ruby/core/thread/report_on_exception_spec.rb @@ -60,6 +60,55 @@ describe "Thread#report_on_exception=" do t.join }.should raise_error(RuntimeError, "Thread#report_on_exception specs") end + + it "prints a backtrace on $stderr in the regular backtrace order" do + line_raise = __LINE__ + 2 + def foo + raise RuntimeError, "Thread#report_on_exception specs backtrace order" + end + + line_call_foo = __LINE__ + 5 + go = false + t = Thread.new { + Thread.current.report_on_exception = true + Thread.pass until go + foo + } + + -> { + go = true + Thread.pass while t.alive? + }.should output("", /\A +#{Regexp.quote(t.inspect)}\sterminated\swith\sexception\s\(report_on_exception\sis\strue\):\n +#{Regexp.quote(__FILE__)}:#{line_raise}:in\s[`']foo':\sThread\#report_on_exception\sspecs\sbacktrace\sorder\s\(RuntimeError\)\n +\tfrom\s#{Regexp.quote(__FILE__)}:#{line_call_foo}:in\s[`']block\s\(4\slevels\)\sin\s<top\s\(required\)>'\n +\z/x) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception specs backtrace order") + end + + it "prints the backtrace even if the thread was killed just after Thread#raise" do + t = nil + ready = false + -> { + t = Thread.new { + Thread.current.report_on_exception = true + ready = true + sleep + } + + Thread.pass until ready and t.stop? + t.raise RuntimeError, "Thread#report_on_exception before kill spec" + t.kill + Thread.pass while t.alive? + }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception before kill spec/m) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception before kill spec") + end end describe "when set to false" do diff --git a/spec/ruby/core/thread/shared/exit.rb b/spec/ruby/core/thread/shared/exit.rb index 40dc478947..13e8832684 100644 --- a/spec/ruby/core/thread/shared/exit.rb +++ b/spec/ruby/core/thread/shared/exit.rb @@ -66,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 @@ -73,7 +93,7 @@ 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 @@ -93,6 +113,25 @@ describe :thread_exit, shared: true do ScratchPad.recorded.should == nil end + it "kills other fibers of that thread without running their ensure clauses" do + t = Thread.new do + f = Fiber.new do + ScratchPad.record :fiber_resumed + begin + Fiber.yield + ensure + ScratchPad.record :fiber_ensure + end + end + f.resume + sleep + end + Thread.pass until t.stop? + t.send(@method) + t.join + ScratchPad.recorded.should == :fiber_resumed + end + # This spec is a mess. It fails randomly, it hangs on MRI, it needs to be removed quarantine! do it "killing dying running does nothing" do diff --git a/spec/ruby/core/thread/shared/to_s.rb b/spec/ruby/core/thread/shared/to_s.rb index 45c04af627..43640deb33 100644 --- a/spec/ruby/core/thread/shared/to_s.rb +++ b/spec/ruby/core/thread/shared/to_s.rb @@ -1,12 +1,10 @@ require_relative '../fixtures/classes' describe :thread_to_s, shared: true do - sep = ruby_version_is("2.7") ? " " : "@" - it "returns a description including file and line number" do thread, line = Thread.new { "hello" }, __LINE__ thread.join - thread.send(@method).should =~ /^#<Thread:([^ ]*?)#{sep}#{Regexp.escape __FILE__}:#{line} \w+>$/ + thread.send(@method).should =~ /^#<Thread:([^ ]*?) #{Regexp.escape __FILE__}:#{line} \w+>$/ end it "has a binary encoding" do diff --git a/spec/ruby/core/thread/shared/wakeup.rb b/spec/ruby/core/thread/shared/wakeup.rb index f111edf0fd..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 diff --git a/spec/ruby/core/thread/thread_variable_get_spec.rb b/spec/ruby/core/thread/thread_variable_get_spec.rb index 38f90d5830..0ad19bfd88 100644 --- a/spec/ruby/core/thread/thread_variable_get_spec.rb +++ b/spec/ruby/core/thread/thread_variable_get_spec.rb @@ -13,7 +13,7 @@ describe "Thread#thread_variable_get" do @t.thread_variable_get(:a).should be_nil end - it "returns the value previously set by #[]=" do + 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 |