diff options
Diffstat (limited to 'test/-ext-')
21 files changed, 680 insertions, 100 deletions
diff --git a/test/-ext-/bug_reporter/test_bug_reporter.rb b/test/-ext-/bug_reporter/test_bug_reporter.rb index 6e44337d41..c7b2a5b370 100644 --- a/test/-ext-/bug_reporter/test_bug_reporter.rb +++ b/test/-ext-/bug_reporter/test_bug_reporter.rb @@ -2,16 +2,15 @@ require 'test/unit' require 'tmpdir' require_relative '../../lib/jit_support' +require_relative '../../lib/parser_support' class TestBugReporter < Test::Unit::TestCase - def yjit_enabled? - defined?(RubyVM::YJIT.enabled?) && RubyVM::YJIT.enabled? - end - def test_bug_reporter_add - omit if ENV['RUBY_ON_BUG'] + pend "macOS 15 beta is not working with this test" if macos?(15) + omit "flaky with RJIT" if JITSupport.rjit_enabled? description = RUBY_DESCRIPTION + description = description.sub(/\+PRISM /, '') unless ParserSupport.prism_enabled_in_subprocess? description = description.sub(/\+RJIT /, '') unless JITSupport.rjit_force_enabled? expected_stderr = [ :*, @@ -24,9 +23,9 @@ class TestBugReporter < Test::Unit::TestCase tmpdir = Dir.mktmpdir no_core = "Process.setrlimit(Process::RLIMIT_CORE, 0); " if defined?(Process.setrlimit) && defined?(Process::RLIMIT_CORE) - args = ["--disable-gems", "-r-test-/bug_reporter", - "-C", tmpdir] - args.push("--yjit") if yjit_enabled? # We want the printed description to match this process's RUBY_DESCRIPTION + args = ["-r-test-/bug_reporter", "-C", tmpdir] + args.push("--yjit") if JITSupport.yjit_enabled? # We want the printed description to match this process's RUBY_DESCRIPTION + args.unshift({"RUBY_ON_BUG" => nil}) stdin = "#{no_core}register_sample_bug_reporter(12345); Process.kill :SEGV, $$" assert_in_out_err(args, stdin, [], expected_stderr, encoding: "ASCII-8BIT") ensure diff --git a/test/-ext-/debug/test_debug.rb b/test/-ext-/debug/test_debug.rb index 8a351d74fa..b244eb41ea 100644 --- a/test/-ext-/debug/test_debug.rb +++ b/test/-ext-/debug/test_debug.rb @@ -29,7 +29,7 @@ class TestDebug < Test::Unit::TestCase # check same location assert_equal(loc.path, iseq.path, msg) assert_equal(loc.absolute_path, iseq.absolute_path, msg) - assert_equal(loc.label, iseq.label, msg) + #assert_equal(loc.label, iseq.label, msg) assert_operator(loc.lineno, :>=, iseq.first_lineno, msg) end diff --git a/test/-ext-/debug/test_profile_frames.rb b/test/-ext-/debug/test_profile_frames.rb index d6ae953dd2..bd819266df 100644 --- a/test/-ext-/debug/test_profile_frames.rb +++ b/test/-ext-/debug/test_profile_frames.rb @@ -14,6 +14,8 @@ class SampleClassForTestProfileFrames end class Sample2 + EVAL_LINE = __LINE__ + 3 + def baz(block) instance_eval "def zab(block) block.call end" [self, zab(block)] @@ -37,6 +39,20 @@ class SampleClassForTestProfileFrames end end +class SampleClassForTestProfileThreadFrames + def initialize(mutex) + @mutex = mutex + end + + def foo(block) + bar(block) + end + + def bar(block) + block.call + end +end + class TestProfileFrames < Test::Unit::TestCase def test_profile_frames obj, frames = Fiber.new{ @@ -112,7 +128,7 @@ class TestProfileFrames < Test::Unit::TestCase "SampleClassForTestProfileFrames#foo", "TestProfileFrames#test_profile_frames", ] - paths = [ nil, file=__FILE__, "(eval)", file, file, file, file, file, file, nil ] + paths = [ nil, file=__FILE__, "(eval at #{__FILE__}:#{SampleClassForTestProfileFrames::Sample2::EVAL_LINE})", file, file, file, file, file, file, nil ] absolute_paths = [ "<cfunc>", file, nil, file, file, file, file, file, file, nil ] assert_equal(labels.size, frames.size) @@ -137,13 +153,56 @@ class TestProfileFrames < Test::Unit::TestCase } end + def test_profile_thread_frames + mutex = Mutex.new + th = Thread.new do + mutex.lock + Thread.stop + SampleClassForTestProfileThreadFrames.new(mutex).foo(lambda { mutex.unlock; loop { sleep(1) } } ) + end + + # ensure execution has reached SampleClassForTestProfileThreadFrames#bar before running profile_thread_frames + loop { break if th.status == "sleep"; sleep 0.1 } + th.run + mutex.lock # wait until SampleClassForTestProfileThreadFrames#bar has been called + + frames = Bug::Debug.profile_thread_frames(th, 0, 10) + + full_labels = [ + "Kernel#sleep", + "TestProfileFrames#test_profile_thread_frames", + "Kernel#loop", + "TestProfileFrames#test_profile_thread_frames", + "SampleClassForTestProfileThreadFrames#bar", + "SampleClassForTestProfileThreadFrames#foo", + "TestProfileFrames#test_profile_thread_frames", + ] + + frames.each.with_index do |frame, i| + assert_equal(full_labels[i], frame) + end + + ensure + th.kill + th.join + end + + def test_matches_backtrace_locations_main_thread assert_equal(Thread.current, Thread.main) # Keep these in the same line, so the backtraces match exactly backtrace_locations, profile_frames = [Thread.current.backtrace_locations, Bug::Debug.profile_frames(0, 100)] - assert_equal(backtrace_locations.size, profile_frames.size) + errmsg = "backtrace_locations:\n " + backtrace_locations.map.with_index{|loc, i| "#{i} #{loc}"}.join("\n ") + errmsg += "\n\nprofile_frames:\n " + profile_frames.map.with_index{|(path, absolute_path, _, base_label, _, _, _, _, _, full_label, lineno), i| + if lineno + "#{i} #{absolute_path}:#{lineno} // #{full_label}" + else + "#{i} #{absolute_path} #{full_label}" + end + }.join("\n ") + assert_equal(backtrace_locations.size, profile_frames.size, errmsg) # The first entries are not going to match, since one is #backtrace_locations and the other #profile_frames backtrace_locations.shift @@ -177,4 +236,8 @@ class TestProfileFrames < Test::Unit::TestCase a end; end + + def test_start + assert_equal Bug::Debug.profile_frames(0, 10).tap(&:shift), Bug::Debug.profile_frames(1, 9) + end end diff --git a/test/-ext-/integer/test_my_integer.rb b/test/-ext-/integer/test_my_integer.rb index 1b6f8489f8..0dfa234921 100644 --- a/test/-ext-/integer/test_my_integer.rb +++ b/test/-ext-/integer/test_my_integer.rb @@ -8,19 +8,13 @@ class Test_MyInteger < Test::Unit::TestCase Bug::Integer::MyInteger.new.to_f end - begin - Bug::Integer::MyInteger.class_eval do - def to_f - end + int = Class.new(Bug::Integer::MyInteger) do + def to_f end + end - assert_nothing_raised do - Bug::Integer::MyInteger.new.to_f - end - ensure - Bug::Integer::MyInteger.class_eval do - remove_method :to_f - end + assert_nothing_raised do + int.new.to_f end end @@ -29,20 +23,14 @@ class Test_MyInteger < Test::Unit::TestCase Bug::Integer::MyInteger.new <=> 0 end - begin - Bug::Integer::MyInteger.class_eval do - def <=>(other) - 0 - end + int = Class.new(Bug::Integer::MyInteger) do + def <=>(other) + 0 end + end - assert_nothing_raised do - Bug::Integer::MyInteger.new <=> 0 - end - ensure - Bug::Integer::MyInteger.class_eval do - remove_method :<=> - end + assert_nothing_raised do + int.new <=> 0 end end end diff --git a/test/-ext-/load/test_resolve_symbol.rb b/test/-ext-/load/test_resolve_symbol.rb new file mode 100644 index 0000000000..471d3acebd --- /dev/null +++ b/test/-ext-/load/test_resolve_symbol.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +require 'test/unit' + +class Test_Load_ResolveSymbol < Test::Unit::TestCase + def test_load_resolve_symbol_resolver + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + feature = "Feature #20005" + assert_raise(LoadError, "resolve_symbol_target is not loaded") { + require '-test-/load/resolve_symbol_resolver' + } + require '-test-/load/resolve_symbol_target' + assert_nothing_raised(LoadError, "#{feature} resolver can be loaded") { + require '-test-/load/resolve_symbol_resolver' + } + assert_not_nil ResolveSymbolResolver + assert_equal "from target", ResolveSymbolResolver.any_method + + assert_raise(LoadError, "tries to resolve missing feature name, and it should raise LoadError") { + ResolveSymbolResolver.try_resolve_fname + } + assert_raise(LoadError, "tries to resolve missing symbol name, and it should raise LoadError") { + ResolveSymbolResolver.try_resolve_sname + } + end; + end +end diff --git a/test/-ext-/load/test_stringify_symbols.rb b/test/-ext-/load/test_stringify_symbols.rb new file mode 100644 index 0000000000..0d9736b591 --- /dev/null +++ b/test/-ext-/load/test_stringify_symbols.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +require 'test/unit' + +class Test_Load_stringify_symbols < Test::Unit::TestCase + def test_load_stringify_symbol_required_extensions + require '-test-/load/stringify_symbols' + require '-test-/load/stringify_target' + r1 = StringifySymbols.stringify_symbol("-test-/load/stringify_target", "stt_any_method") + assert_not_nil r1 + r2 = StringifySymbols.stringify_symbol("-test-/load/stringify_target.so", "stt_any_method") + assert_equal r1, r2, "resolved symbols should be equal even with or without .so suffix" + end + + def test_load_stringify_symbol_statically_linked + require '-test-/load/stringify_symbols' + # "complex.so" is actually not a statically linked extension. + # But it is registered in $LOADED_FEATURES, so it can be a target of this test. + r1 = StringifySymbols.stringify_symbol("complex", "rb_complex_minus") + assert_not_nil r1 + r2 = StringifySymbols.stringify_symbol("complex.so", "rb_complex_minus") + assert_equal r1, r2 + end + + def test_load_stringify_symbol_missing_target + require '-test-/load/stringify_symbols' + r1 = assert_nothing_raised { + StringifySymbols.stringify_symbol("something_missing", "unknown_method") + } + assert_nil r1 + r2 = assert_nothing_raised { + StringifySymbols.stringify_symbol("complex.so", "unknown_method") + } + assert_nil r2 + end +end diff --git a/test/-ext-/marshal/test_internal_ivar.rb b/test/-ext-/marshal/test_internal_ivar.rb index a32138f6e8..faabe14ab2 100644 --- a/test/-ext-/marshal/test_internal_ivar.rb +++ b/test/-ext-/marshal/test_internal_ivar.rb @@ -11,7 +11,7 @@ module Bug::Marshal assert_equal("hello", v.normal) assert_equal("world", v.internal) assert_equal("bye", v.encoding_short) - dump = assert_warn(/instance variable `E' on class \S+ is not dumped/) { + dump = assert_warn(/instance variable 'E' on class \S+ is not dumped/) { ::Marshal.dump(v) } v = assert_nothing_raised {break ::Marshal.load(dump)} diff --git a/test/-ext-/postponed_job/test_postponed_job.rb b/test/-ext-/postponed_job/test_postponed_job.rb index fee0172d11..8c2b3e95d1 100644 --- a/test/-ext-/postponed_job/test_postponed_job.rb +++ b/test/-ext-/postponed_job/test_postponed_job.rb @@ -2,34 +2,70 @@ require 'test/unit' require '-test-/postponed_job' -module Bug - def self.postponed_job_call_direct_wrapper(*args) - postponed_job_call_direct(*args) +class TestPostponed_job < Test::Unit::TestCase + def test_preregister_and_trigger + assert_separately([], __FILE__, __LINE__, <<-'RUBY') + require '-test-/postponed_job' + Bug.postponed_job_preregister_and_call_without_sleep(counters = []) + # i.e. rb_postponed_job_trigger performs coalescing + assert_equal([3], counters) + + # i.e. rb_postponed_job_trigger resets after interrupts are checked + Bug.postponed_job_preregister_and_call_with_sleep(counters = []) + assert_equal([1, 2, 3], counters) + RUBY end - def self.postponed_job_register_wrapper(*args) - postponed_job_register(*args) + def test_multiple_preregistration + assert_separately([], __FILE__, __LINE__, <<-'RUBY') + require '-test-/postponed_job' + handles = Bug.postponed_job_preregister_multiple_times + # i.e. rb_postponed_job_preregister returns the same handle if preregistered multiple times + assert_equal [handles[0]], handles.uniq + RUBY end -end -class TestPostponed_job < Test::Unit::TestCase - def test_register - direct, registered = [], [] + def test_multiple_preregistration_with_new_data + assert_separately([], __FILE__, __LINE__, <<-'RUBY') + require '-test-/postponed_job' + values = Bug.postponed_job_preregister_calls_with_last_argument + # i.e. the callback is called with the last argument it was preregistered with + assert_equal [3, 4], values + RUBY + end - Bug.postponed_job_call_direct_wrapper(direct) - Bug.postponed_job_register_wrapper(registered) + def test_legacy_register + assert_separately([], __FILE__, __LINE__, <<-'RUBY') + require '-test-/postponed_job' + direct, registered = [], [] - assert_equal([0], direct) - assert_equal([3], registered) + Bug.postponed_job_call_direct(direct) + Bug.postponed_job_register(registered) - Bug.postponed_job_register_one(ary = []) - assert_equal [1], ary + assert_equal([0], direct) + assert_equal([3], registered) + + Bug.postponed_job_register_one(ary = []) + assert_equal [1], ary + RUBY + end + + def test_legacy_register_one_same + assert_separately([], __FILE__, __LINE__, <<-'RUBY') + require '-test-/postponed_job' + # Registering the same job three times should result in three of the same handle + handles = Bug.postponed_job_register_one_same + assert_equal [handles[0]], handles.uniq + RUBY end if Bug.respond_to?(:postponed_job_register_in_c_thread) - def test_register_in_c_thread - assert Bug.postponed_job_register_in_c_thread(ary = []) - assert_equal [1], ary + def test_legacy_register_in_c_thread + assert_separately([], __FILE__, __LINE__, <<-'RUBY') + require '-test-/postponed_job' + assert Bug.postponed_job_register_in_c_thread(ary = []) + assert_equal [1], ary + RUBY end end end diff --git a/test/-ext-/required.rb b/test/-ext-/required.rb new file mode 100644 index 0000000000..70514355ff --- /dev/null +++ b/test/-ext-/required.rb @@ -0,0 +1,10 @@ +require 'continuation' +cont = nil +a = [*1..10].reject do |i| + callcc {|c| cont = c} if !cont and i == 10 + false +end +if a.size < 1000 + a.unshift(:x) + cont.call +end diff --git a/test/-ext-/string/test_capacity.rb b/test/-ext-/string/test_capacity.rb index 50f8c10f58..bcca64d85a 100644 --- a/test/-ext-/string/test_capacity.rb +++ b/test/-ext-/string/test_capacity.rb @@ -23,7 +23,7 @@ class Test_StringCapacity < Test::Unit::TestCase def test_s_new_capacity assert_equal("", String.new(capacity: 1000)) assert_equal(String, String.new(capacity: 1000).class) - assert_equal(10000, capa(String.new(capacity: 10000))) + assert_equal(10_000, capa(String.new(capacity: 10_000))) assert_equal("", String.new(capacity: -1000)) assert_equal(capa(String.new(capacity: -10000)), capa(String.new(capacity: -1000))) @@ -66,7 +66,7 @@ class Test_StringCapacity < Test::Unit::TestCase end def embed_header_size - 2 * RbConfig::SIZEOF['void*'] + RbConfig::SIZEOF['long'] + 3 * RbConfig::SIZEOF['void*'] end def max_embed_len diff --git a/test/-ext-/string/test_fstring.rb b/test/-ext-/string/test_fstring.rb index 5a3456c566..fcec6be543 100644 --- a/test/-ext-/string/test_fstring.rb +++ b/test/-ext-/string/test_fstring.rb @@ -15,19 +15,27 @@ class Test_String_Fstring < Test::Unit::TestCase def test_rb_enc_interned_str_autoloaded_encoding assert_separately([], <<~RUBY) require '-test-/string' - assert_include(Encoding::Windows_31J.inspect, 'autoload') - Bug::String.rb_enc_interned_str(Encoding::Windows_31J) + assert_include(Encoding::CESU_8.inspect, 'autoload') + Bug::String.rb_enc_interned_str(Encoding::CESU_8) RUBY end + def test_rb_enc_interned_str_null_encoding + assert_equal Encoding::ASCII_8BIT, Bug::String.rb_enc_interned_str(nil).encoding + end + def test_rb_enc_str_new_autoloaded_encoding assert_separately([], <<~RUBY) require '-test-/string' - assert_include(Encoding::Windows_31J.inspect, 'autoload') - Bug::String.rb_enc_str_new(Encoding::Windows_31J) + assert_include(Encoding::CESU_8.inspect, 'autoload') + Bug::String.rb_enc_str_new(Encoding::CESU_8) RUBY end + def test_rb_enc_str_new_null_encoding + assert_equal Encoding::ASCII_8BIT, Bug::String.rb_enc_str_new(nil).encoding + end + def test_instance_variable str = __method__.to_s * 3 str.instance_variable_set(:@test, 42) @@ -49,6 +57,10 @@ class Test_String_Fstring < Test::Unit::TestCase assert_raise(TypeError) {fstr.singleton_class} end + def test_fake_str + assert_equal([*"a".."z"].join(""), Bug::String.fstring_fake_str) + end + class S < String end diff --git a/test/-ext-/string/test_set_len.rb b/test/-ext-/string/test_set_len.rb index 67ba961194..1531d76167 100644 --- a/test/-ext-/string/test_set_len.rb +++ b/test/-ext-/string/test_set_len.rb @@ -34,4 +34,51 @@ class Test_StrSetLen < Test::Unit::TestCase assert_equal 128, Bug::String.capacity(str) assert_equal 127, str.set_len(127).bytesize, bug12757 end + + def test_coderange_after_append + u = -"\u3042" + str = Bug::String.new(encoding: Encoding::UTF_8) + bsize = u.bytesize + str.append(u) + assert_equal 0, str.bytesize + str.set_len(bsize) + assert_equal bsize, str.bytesize + assert_predicate str, :valid_encoding? + assert_not_predicate str, :ascii_only? + assert_equal u, str + end + + def test_coderange_after_trunc + u = -"\u3042" + bsize = u.bytesize + str = Bug::String.new(u) + str.set_len(bsize - 1) + assert_equal bsize - 1, str.bytesize + assert_not_predicate str, :valid_encoding? + assert_not_predicate str, :ascii_only? + str.append(u.byteslice(-1)) + str.set_len(bsize) + assert_equal bsize, str.bytesize + assert_predicate str, :valid_encoding? + assert_not_predicate str, :ascii_only? + assert_equal u, str + end + + def test_valid_encoding_after_resized + s = "\0\0".force_encoding(Encoding::UTF_16BE) + str = Bug::String.new(s) + assert_predicate str, :valid_encoding? + str.resize(1) + assert_not_predicate str, :valid_encoding? + str.resize(2) + assert_predicate str, :valid_encoding? + str.resize(3) + assert_not_predicate str, :valid_encoding? + + s = "\xDB\x00\xDC\x00".force_encoding(Encoding::UTF_16BE) + str = Bug::String.new(s) + assert_predicate str, :valid_encoding? + str.resize(2) + assert_not_predicate str, :valid_encoding? + end end diff --git a/test/-ext-/string/test_too_many_dummy_encodings.rb b/test/-ext-/string/test_too_many_dummy_encodings.rb index 4d71fd1d72..b96b40db7b 100644 --- a/test/-ext-/string/test_too_many_dummy_encodings.rb +++ b/test/-ext-/string/test_too_many_dummy_encodings.rb @@ -4,7 +4,7 @@ require "-test-/string" class Test_TooManyDummyEncodings < Test::Unit::TestCase def test_exceed_encoding_table_size - assert_separately(%w[--disable=gems], "#{<<~"begin;"}\n#{<<~'end;'}") + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; require "-test-/string" assert_raise_with_message(EncodingError, /too many encoding/) do diff --git a/test/-ext-/struct/test_data.rb b/test/-ext-/struct/test_data.rb new file mode 100644 index 0000000000..8dbc9113a5 --- /dev/null +++ b/test/-ext-/struct/test_data.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: false +require 'test/unit' +require "-test-/struct" + +class Bug::Struct::Test_Data < Test::Unit::TestCase + def test_data_new_default + klass = Bug::Struct.data_new(false) + assert_equal Data, klass.superclass + assert_equal %i[mem1 mem2], klass.members + end + + def test_data_new_superclass + superclass = Data.define + klass = Bug::Struct.data_new(superclass) + assert_equal superclass, klass.superclass + assert_equal %i[mem1 mem2], klass.members + end +end diff --git a/test/-ext-/symbol/test_type.rb b/test/-ext-/symbol/test_type.rb index fdee692fe4..2b0fbe5b79 100644 --- a/test/-ext-/symbol/test_type.rb +++ b/test/-ext-/symbol/test_type.rb @@ -134,5 +134,10 @@ module Test_Symbol Bug::Symbol.find(cx) } end + + def test_const_name_type + sym = "\xb5".force_encoding(Encoding::Windows_1253) + assert_not_operator Bug::Symbol, :const?, sym, sym.encode(Encoding::UTF_8) + end end end diff --git a/test/-ext-/test_bug-3571.rb b/test/-ext-/test_bug-3571.rb index c75d2e8523..5952ce2a33 100644 --- a/test/-ext-/test_bug-3571.rb +++ b/test/-ext-/test_bug-3571.rb @@ -13,8 +13,8 @@ end SRC out = [ "start() function is unimplemented on this machine", - "-:2:in `start'", - "-:2:in `<main>'", + "-:2:in 'Bug.start'", + "-:2:in '<main>'", ] assert_in_out_err(%w"-r-test-/bug_3571", src, [], out, bug3571) end diff --git a/test/-ext-/test_ensure_and_callcc.rb b/test/-ext-/test_ensure_and_callcc.rb new file mode 100644 index 0000000000..9303a094ea --- /dev/null +++ b/test/-ext-/test_ensure_and_callcc.rb @@ -0,0 +1,40 @@ +# -*- coding: us-ascii -*- +# frozen_string_literal: false +require 'test/unit' + +class TestEnsureAndCallcc < Test::Unit::TestCase + def test_bug20655_dir_chdir_using_rb_ensure + require 'tmpdir' + need_continuation + called = 0 + tmp = nil + Dir.mktmpdir do |tmpdir| + Dir.chdir(tmpdir) do + tmp = Dir.pwd + cont = nil + callcc{|c| cont = c} + assert_equal(tmp, Dir.pwd, "BUG #20655: ensure called and pwd was changed unexpectedly") + called += 1 + cont.call if called < 10 + end + end + end + + def test_bug20655_extension_using_rb_ensure + need_continuation + require '-test-/ensure_and_callcc' + EnsureAndCallcc.reset + assert_equal(0, EnsureAndCallcc.ensure_called) + EnsureAndCallcc.require_with_ensure(File.join(__dir__, 'required')) + assert_equal(1, EnsureAndCallcc.ensure_called, + "BUG #20655: ensure called unexpectedly in the required script even without exceptions") + end + + private + def need_continuation + unless respond_to?(:callcc, true) + EnvUtil.suppress_warning {require 'continuation'} + end + omit 'requires callcc support' unless respond_to?(:callcc, true) + end +end diff --git a/test/-ext-/thread/helper.rb b/test/-ext-/thread/helper.rb new file mode 100644 index 0000000000..3ea2057d15 --- /dev/null +++ b/test/-ext-/thread/helper.rb @@ -0,0 +1,51 @@ +module ThreadInstrumentation + module TestHelper + private + + def record + Bug::ThreadInstrumentation.register_callback(!ENV["GVL_DEBUG"]) + yield + ensure + timeline = Bug::ThreadInstrumentation.unregister_callback + if $! + raise + else + return timeline + end + end + + def timeline_for(thread, timeline) + timeline.select { |t, _| t == thread }.map(&:last) + end + + def assert_consistent_timeline(events) + refute_predicate events, :empty? + + previous_event = nil + events.each do |event| + refute_equal :exited, previous_event, "`exited` must be the final event: #{events.inspect}" + case event + when :started + assert_nil previous_event, "`started` must be the first event: #{events.inspect}" + when :ready + unless previous_event.nil? + assert %i(started suspended).include?(previous_event), "`ready` must be preceded by `started` or `suspended`: #{events.inspect}" + end + when :resumed + unless previous_event.nil? + assert_equal :ready, previous_event, "`resumed` must be preceded by `ready`: #{events.inspect}" + end + when :suspended + unless previous_event.nil? + assert_equal :resumed, previous_event, "`suspended` must be preceded by `resumed`: #{events.inspect}" + end + when :exited + unless previous_event.nil? + assert %i(resumed suspended).include?(previous_event), "`exited` must be preceded by `resumed` or `suspended`: #{events.inspect}" + end + end + previous_event = event + end + end + end +end diff --git a/test/-ext-/thread/test_instrumentation_api.rb b/test/-ext-/thread/test_instrumentation_api.rb index dd620e7380..663e41be53 100644 --- a/test/-ext-/thread/test_instrumentation_api.rb +++ b/test/-ext-/thread/test_instrumentation_api.rb @@ -1,91 +1,289 @@ # frozen_string_literal: false require 'envutil' +require_relative "helper" class TestThreadInstrumentation < Test::Unit::TestCase + include ThreadInstrumentation::TestHelper + def setup pend("No windows support") if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM require '-test-/thread/instrumentation' - Thread.list.each do |thread| - if thread != Thread.current - thread.kill - thread.join rescue nil - end - end - assert_equal [Thread.current], Thread.list - - Bug::ThreadInstrumentation.reset_counters - Bug::ThreadInstrumentation::register_callback + cleanup_threads end def teardown return if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM - Bug::ThreadInstrumentation::unregister_callback + Bug::ThreadInstrumentation.unregister_callback + cleanup_threads end THREADS_COUNT = 3 - def test_thread_instrumentation - threads = threaded_cpu_work - assert_equal [false] * THREADS_COUNT, threads.map(&:status) - counters = Bug::ThreadInstrumentation.counters - assert_join_counters(counters) - assert_global_join_counters(counters) + def test_single_thread_timeline + thread = nil + full_timeline = record do + thread = Thread.new { 1 + 1 } + thread.join + end + assert_equal %i(started ready resumed suspended exited), timeline_for(thread, full_timeline) + ensure + thread&.kill + end + + def test_thread_pass_single_thread + full_timeline = record do + Thread.pass + end + assert_equal [], timeline_for(Thread.current, full_timeline) + end + + def test_thread_pass_multi_thread + thread = Thread.new do + cpu_bound_work(0.5) + end + + full_timeline = record do + Thread.pass + end + + assert_equal %i(suspended ready resumed), timeline_for(Thread.current, full_timeline) + ensure + thread&.kill + thread&.join + end + + def test_multi_thread_timeline + threads = nil + full_timeline = record do + threads = threaded_cpu_bound_work(1.0) + results = threads.map(&:value) + results.each do |r| + refute_equal false, r + end + assert_equal [false] * THREADS_COUNT, threads.map(&:status) + end + + threads.each do |thread| + timeline = timeline_for(thread, full_timeline) + assert_consistent_timeline(timeline) + assert_operator timeline.count(:suspended), :>=, 1, "Expected threads to yield suspended at least once: #{timeline.inspect}" + end + + timeline = timeline_for(Thread.current, full_timeline) + assert_consistent_timeline(timeline) + ensure + threads&.each(&:kill) + end + + def test_join_suspends # Bug #18900 + thread = other_thread = nil + full_timeline = record do + other_thread = Thread.new { sleep 0.3 } + thread = Thread.new { other_thread.join } + thread.join + end + + timeline = timeline_for(thread, full_timeline) + assert_consistent_timeline(timeline) + assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline + ensure + other_thread&.kill + thread&.kill end - def test_join_counters # Bug #18900 - thr = Thread.new { fib(30) } - Bug::ThreadInstrumentation.reset_counters - thr.join - assert_join_counters(Bug::ThreadInstrumentation.local_counters) + def test_io_release_gvl + r, w = IO.pipe + thread = nil + full_timeline = record do + thread = Thread.new do + w.write("Hello\n") + end + thread.join + end + + timeline = timeline_for(thread, full_timeline) + assert_consistent_timeline(timeline) + assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline + ensure + r&.close + w&.close + end + + def test_queue_releases_gvl + queue1 = Queue.new + queue2 = Queue.new + + thread = nil + + full_timeline = record do + thread = Thread.new do + queue1 << true + queue2.pop + end + + queue1.pop + queue2 << true + thread.join + end + + timeline = timeline_for(thread, full_timeline) + assert_consistent_timeline(timeline) + assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline + end + + def test_blocking_on_ractor + assert_ractor(<<-"RUBY", require_relative: "helper", require: "-test-/thread/instrumentation") + include ThreadInstrumentation::TestHelper + + ractor = Ractor.new { + Ractor.receive # wait until woke + Thread.current + } + + # Wait for the main thread to block, then wake the ractor + Thread.new do + while Thread.main.status != "sleep" + Thread.pass + end + ractor.send true + end + + full_timeline = record do + ractor.take + end + + timeline = timeline_for(Thread.current, full_timeline) + assert_consistent_timeline(timeline) + assert_equal %i(suspended ready resumed), timeline + RUBY + end + + def test_sleeping_inside_ractor + assert_ractor(<<-"RUBY", require_relative: "helper", require: "-test-/thread/instrumentation") + include ThreadInstrumentation::TestHelper + + thread = nil + + full_timeline = record do + thread = Ractor.new{ + sleep 0.1 + Thread.current + }.take + sleep 0.1 + end + + timeline = timeline_for(thread, full_timeline) + assert_consistent_timeline(timeline) + assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline + RUBY + end + + def test_thread_blocked_forever_on_mutex + mutex = Mutex.new + mutex.lock + thread = nil + + full_timeline = record do + thread = Thread.new do + mutex.lock + end + 10.times { Thread.pass } + sleep 0.1 + end + + mutex.unlock + thread.join + + timeline = timeline_for(thread, full_timeline) + assert_consistent_timeline(timeline) + assert_equal %i(started ready resumed suspended), timeline + end + + def test_thread_blocked_temporarily_on_mutex + mutex = Mutex.new + mutex.lock + thread = nil + + full_timeline = record do + thread = Thread.new do + mutex.lock + end + 10.times { Thread.pass } + sleep 0.1 + mutex.unlock + 10.times { Thread.pass } + sleep 0.1 + end + + thread.join + + timeline = timeline_for(thread, full_timeline) + assert_consistent_timeline(timeline) + assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline end def test_thread_instrumentation_fork_safe skip "No fork()" unless Process.respond_to?(:fork) - thread_statuses = counters = nil + thread_statuses = full_timeline = nil IO.popen("-") do |read_pipe| if read_pipe thread_statuses = Marshal.load(read_pipe) - counters = Marshal.load(read_pipe) + full_timeline = Marshal.load(read_pipe) else - Bug::ThreadInstrumentation.reset_counters - threads = threaded_cpu_work + threads = threaded_cpu_bound_work.each(&:join) Marshal.dump(threads.map(&:status), STDOUT) - Marshal.dump(Bug::ThreadInstrumentation.counters, STDOUT) + full_timeline = Bug::ThreadInstrumentation.unregister_callback.map { |t, e| [t.to_s, e ] } + Marshal.dump(full_timeline, STDOUT) end end assert_predicate $?, :success? assert_equal [false] * THREADS_COUNT, thread_statuses - assert_join_counters(counters) - assert_global_join_counters(counters) + thread_names = full_timeline.map(&:first).uniq + thread_names.each do |thread_name| + assert_consistent_timeline(timeline_for(thread_name, full_timeline)) + end end def test_thread_instrumentation_unregister - Bug::ThreadInstrumentation::unregister_callback assert Bug::ThreadInstrumentation::register_and_unregister_callbacks end private - def fib(n = 20) + def fib(n = 30) return n if n <= 1 fib(n-1) + fib(n-2) end - def threaded_cpu_work(size = 20) - THREADS_COUNT.times.map { Thread.new { fib(size) } }.each(&:join) + def cpu_bound_work(duration) + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration + i = 0 + while deadline > Process.clock_gettime(Process::CLOCK_MONOTONIC) + fib(25) + i += 1 + end + i > 0 ? i : false end - def assert_join_counters(counters) - counters.each_with_index do |c, i| - assert_operator c, :>, 0, "Call counters[#{i}]: #{counters.inspect}" + def threaded_cpu_bound_work(duration = 0.5) + THREADS_COUNT.times.map do + Thread.new do + cpu_bound_work(duration) + end end end - def assert_global_join_counters(counters) - assert_equal THREADS_COUNT, counters.first + def cleanup_threads + Thread.list.each do |thread| + if thread != Thread.current + thread.kill + thread.join rescue nil + end + end + assert_equal [Thread.current], Thread.list end end diff --git a/test/-ext-/thread/test_lock_native_thread.rb b/test/-ext-/thread/test_lock_native_thread.rb new file mode 100644 index 0000000000..8a5ba78838 --- /dev/null +++ b/test/-ext-/thread/test_lock_native_thread.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: false + +require 'envutil' + +mn_supported_p = -> do + out, *_ = EnvUtil.invoke_ruby([{'RUBY_MN_THREADS' => '1'}, '-v'], '', true) + return /\+MN/ =~ out +end + +if mn_supported_p.call + # test only on MN threads +else + return +end + +class TestThreadLockNativeThread < Test::Unit::TestCase + def test_lock_native_thread + assert_separately([{'RUBY_MN_THREADS' => '1'}], <<-RUBY) + require '-test-/thread/lock_native_thread' + + Thread.new{ + assert_equal true, Thread.current.lock_native_thread + }.join + + # main thread already has DNT + assert_equal false, Thread.current.lock_native_thread + RUBY + end + + def test_lock_native_thread_tls + assert_separately([{'RUBY_MN_THREADS' => '1'}], <<-RUBY) + require '-test-/thread/lock_native_thread' + tn = 10 + ln = 1_000 + + ts = tn.times.map{|i| + Thread.new(i){|i| + Thread.current.set_tls i + assert_equal true, Thread.current.lock_native_thread + + ln.times{ + assert_equal i, Thread.current.get_tls + Thread.pass + } + } + } + ts.each(&:join) + RUBY + end +end diff --git a/test/-ext-/tracepoint/test_tracepoint.rb b/test/-ext-/tracepoint/test_tracepoint.rb index 9d1679602a..48ffe2605c 100644 --- a/test/-ext-/tracepoint/test_tracepoint.rb +++ b/test/-ext-/tracepoint/test_tracepoint.rb @@ -11,6 +11,7 @@ class TestTracepointObj < Test::Unit::TestCase def test_tracks_objspace_events result = EnvUtil.suppress_warning {eval(<<-EOS, nil, __FILE__, __LINE__+1)} + # frozen_string_literal: false Bug.tracepoint_track_objspace_events { 99 'abc' |